Introduction: Organizing a Large-Scale MCP Project
When developing a single MCP server, the project structure is simple: a package.json,
a few TypeScript files, and you're ready to go. But as the number of servers grows, organizational
complexity explodes. How do you share common code without publishing to npm? How do you ensure all
servers compile in the correct order? How do you maintain consistency in structure and patterns?
The answer to these questions is the monorepo: a single repository that contains all servers and shared packages. In this article, we analyze the monorepo architecture of the Tech-MCP project, the technological choices at its foundation (TypeScript, SQLite, STDIO/HTTP), and the 4-layer pattern that every server follows to ensure uniformity and maintainability.
What You'll Learn in This Article
- How to structure an MCP monorepo with pnpm workspaces and Turborepo
- The complete directory structure with
packages/andservers/ - Why TypeScript, SQLite, and STDIO/HTTP are the optimal choices for MCP servers
- The 4-layer architectural pattern:
index.ts,server.ts,tools/,services/ - The
McpSuiteServerinterface and thecreateMcpServer()factory - The build pipeline with Turborepo and the dependency graph
Why a Monorepo for MCP Servers?
A monorepo collects all project components in a single Git repository. For a suite of MCP servers, the advantages are significant:
-
Code sharing without publishing: packages in
packages/are available to all servers through pnpm'sworkspace:protocol, without needing to publish them to npm - Atomic and ordered builds: Turborepo analyzes the dependency graph and compiles packages in the correct order, parallelizing where possible
- Unified versioning: all packages and servers evolve together, eliminating version compatibility issues
-
Superior Developer Experience: a single
pnpm installand a singlepnpm buildfor the entire project -
Safe refactoring: modifying an interface in
@mcp-suite/coreimmediately highlights all servers that need updating
Complete Monorepo Structure
The Tech-MCP project is organized into three main directories: the root with configuration files,
packages/ for shared libraries, and servers/ for independent MCP servers.
mcp-suite/
├── package.json # Root: global scripts, engine constraints
├── pnpm-workspace.yaml # Defines packages/* and servers/* as workspaces
├── turbo.json # Build pipeline with Turborepo
├── tsconfig.base.json # Shared TypeScript configuration
│
├── packages/ # Shared libraries (6 packages)
│ ├── core/ # Server factory, config, logger, errors, types
│ ├── event-bus/ # Typed EventBus with 29 events
│ ├── database/ # SQLite connection + migrations
│ ├── testing/ # Test harness + MockEventBus
│ ├── cli/ # CLI for managing servers
│ └── client-manager/ # MCP client pool for server-to-server communication
│
├── servers/ # 22 independent MCP servers
│ ├── scrum-board/ # Sprint, story, and task management
│ ├── standup-notes/ # Daily standup notes
│ ├── time-tracking/ # Time tracking
│ ├── agile-metrics/ # Agile metrics (velocity, burndown)
│ ├── code-review/ # Code analysis and review
│ ├── test-generator/ # Automatic test generation
│ ├── cicd-monitor/ # CI/CD pipeline monitoring
│ ├── docker-compose/ # Docker Compose management
│ ├── db-schema-explorer/ # Database schema exploration
│ ├── dependency-manager/ # Project dependency management
│ ├── api-documentation/ # API documentation
│ ├── codebase-knowledge/ # Code knowledge base
│ ├── data-mock-generator/ # Mock data generation
│ ├── environment-manager/ # Environment variable management
│ ├── http-client/ # HTTP client for API testing
│ ├── log-analyzer/ # Application log analysis
│ ├── performance-profiler/ # Performance profiling
│ ├── project-economics/ # Project economics (budget, costs)
│ ├── project-scaffolding/ # New project scaffolding
│ ├── regex-builder/ # Regular expression builder
│ ├── retrospective-manager/# Agile retrospective management
│ └── snippet-manager/ # Code snippet library
│
└── docs/ # Project documentation
This structure implements the principle of server independence: each server is a self-contained
folder with its own package.json, tsconfig.json, and source code. At the same time,
optional collaboration is enabled through the shared packages in packages/.
The 6 Shared Packages
The packages in packages/ provide the common foundation for all servers:
Shared Packages and Their Responsibilities
| Package | Responsibility | Dependencies |
|---|---|---|
@mcp-suite/core |
Server factory, configuration, logger, errors, shared types | event-bus |
@mcp-suite/event-bus |
Typed EventBus with 29 events and pub/sub pattern | None |
@mcp-suite/database |
SQLite connection with WAL mode and migration system | core |
@mcp-suite/testing |
Test harness and MockEventBus for unit testing | core, event-bus |
@mcp-suite/cli |
CLI for managing, starting, and monitoring servers | core, event-bus, client-manager |
@mcp-suite/client-manager |
MCP client pool for server-to-server communication | core |
Design Decisions: Why TypeScript, SQLite, and STDIO
The technologies underpinning the project were not chosen randomly. Each decision addresses specific requirements of a distributed MCP system.
Why TypeScript?
TypeScript is the ideal language for MCP servers for four fundamental reasons:
-
End-to-end type-safety: types defined in
@mcp-suite/coreare shared across all servers, ensuring compile-time consistency. An interface change immediately propagates as an error in all servers that use it -
Native ESM: target ES2022 with
"module": "Node16"for native compatibility with modern Node.js, without needing additional bundlers or transpilers -
Declaration maps: each package generates
.d.tsand.d.ts.map, enabling "Go to Definition" across the entire monorepo directly in the IDE -
Strict mode:
"strict": trueintsconfig.base.jsonfor maximum type safety and runtime error prevention
Why SQLite (better-sqlite3)?
For the local persistence of each server, SQLite is the optimal choice:
-
Zero configuration: no database server to install or manage. The
.dbfile is created automatically on first execution -
File-based: each server has its own database file in
~/.mcp-suite/data/, completely independent from other servers -
Synchronous:
better-sqlite3uses synchronous C++ bindings, ideal for fast local operations without the complexity of Promises for local I/O - WAL mode: Write-Ahead Logging enabled for optimal concurrent read performance, essential when multiple tools access the store simultaneously
- Portable: the database moves simply by copying the file, facilitating backup and migration
Transports: STDIO and HTTP
MCP Suite supports two transports, selectable via the MCP_SUITE_TRANSPORT environment variable:
MCP Transport Comparison
| Feature | STDIO (default) | HTTP (Streamable HTTP) |
|---|---|---|
| Use case | Local use with Claude Desktop, Cursor, VS Code | Remote deployments, inter-server communication |
| Configuration | No ports, no network conflicts | Dedicated port per server |
| Security | Local communication, no exposure | Stateful protocol with session UUID |
| Scalability | One process per connection | Containers, horizontal scaling |
| Routes | stdin/stdout | POST/GET/DELETE /mcp + GET /health |
STDIO is the default transport, ideal for local development. HTTP becomes necessary when servers need to communicate with each other through the Client Manager or when deploying to remote servers.
Monorepo Configuration
Three files in the root directory define the monorepo structure: the pnpm workspace,
the Turborepo pipeline, and the root package.json.
pnpm-workspace.yaml
This file declares which directories contain the monorepo workspaces:
# pnpm-workspace.yaml
packages:
- "packages/*"
- "servers/*"
With this configuration, pnpm treats each subfolder of packages/ and servers/
as an independent workspace. Internal dependencies are resolved through the workspace:*
protocol instead of being fetched from npm.
turbo.json
Turborepo manages the build pipeline, defining compilation order and task dependencies:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"clean": {
"cache": false
}
}
}
The key directive is "dependsOn": ["^build"]. The ^ symbol indicates that each
workspace's build task depends on the build of its internal dependencies. This
ensures that @mcp-suite/core is compiled before any server that uses it.
Server package.json with Internal Dependencies
Each server declares its own dependencies, both external (npm) and internal (workspace):
{
"name": "@mcp-suite/scrum-board",
"version": "0.1.0",
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"clean": "rm -rf dist"
},
"dependencies": {
"@mcp-suite/core": "workspace:*",
"@mcp-suite/event-bus": "workspace:*",
"@mcp-suite/database": "workspace:*",
"@modelcontextprotocol/sdk": "^1.0.0",
"better-sqlite3": "^11.0.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.0",
"typescript": "^5.7.0"
}
}
The "workspace:*" syntax tells pnpm to resolve the dependency from the local workspace
instead of fetching it from npm. This means changes to shared packages are immediately available
to all servers after recompilation.
The Build Pipeline
When running pnpm build from the monorepo root, Turborepo analyzes the dependency graph
and plans the optimal build:
pnpm build
│
├── @mcp-suite/event-bus (no internal dependencies)
├── @mcp-suite/core (depends on event-bus)
├── @mcp-suite/database (depends on core)
├── @mcp-suite/testing (depends on core, event-bus)
├── @mcp-suite/client-manager (depends on core)
├── @mcp-suite/cli (depends on core, event-bus, client-manager)
│
└── servers/* (all depend on core, event-bus, database)
├── scrum-board
├── standup-notes
├── time-tracking
└── ... (other 19 servers compiled in parallel)
Turborepo compiles event-bus first (no dependencies), then core (depends on event-bus),
then database and other packages, and finally all servers in parallel. Turborepo's built-in cache
skips recompilation of unchanged packages, making incremental builds extremely fast.
The 4-Layer Server Pattern
Every MCP server in the suite follows a rigorous 4-layer architectural pattern. This pattern ensures uniformity, maintainability, and testability across all 22 servers. This is not an optional convention but an architectural contract: every new server must adhere to this structure.
servers/server-name/
├── package.json # Dependencies and scripts
├── tsconfig.json # Extends tsconfig.base.json
└── src/
├── index.ts # Layer 1: Entry point, bootstrap
├── server.ts # Layer 2: Factory, tool registration
├── tools/ # Layer 3: One file per tool
│ ├── create-sprint.ts
│ ├── get-sprint.ts
│ └── ...
├── services/ # Layer 4: SQLite store, business logic
│ └── scrum-store.ts
└── collaboration.ts # Cross-server event handlers (optional)
The 4 Layers and Their Responsibilities
| Layer | File | Responsibility | Dependencies |
|---|---|---|---|
| 1. Entry Point | index.ts |
Bootstrap: create EventBus, call factory, start transport | @mcp-suite/core, @mcp-suite/event-bus |
| 2. Factory | server.ts |
Create McpSuiteServer, instantiate store, register tools |
@mcp-suite/core, services, tools |
| 3. Tools | tools/*.ts |
Single tool definition: Zod schema + async handler | @modelcontextprotocol/sdk, services |
| 4. Services | services/*.ts |
SQLite persistence, migrations, domain logic | @mcp-suite/database |
Layer 1: Entry Point (index.ts)
The entry point is the simplest file in the entire server. It has exactly three responsibilities:
create a LocalEventBus instance, call the server factory, and start the transport.
#!/usr/bin/env node
import { startServer } from '@mcp-suite/core';
import { LocalEventBus } from '@mcp-suite/event-bus';
import { createScrumBoardServer } from './server.js';
const eventBus = new LocalEventBus();
const suite = createScrumBoardServer(eventBus);
startServer(suite).catch((error) => {
console.error('Failed to start scrum-board server:', error);
process.exit(1);
});
The shebang #!/usr/bin/env node enables direct execution as a binary.
The EventBus is created here and injected into the server via Dependency Injection.
The startServer() function automatically selects the transport (STDIO or HTTP) based
on the configuration. Fatal errors terminate the process with process.exit(1).
Layer 2: Factory (server.ts)
The factory is the heart of the server. It creates the McpSuiteServer object, instantiates
persistence services, registers all tools, and configures collaboration with other servers.
import { createMcpServer, type McpSuiteServer, type EventBus } from '@mcp-suite/core';
import { ScrumStore } from './services/scrum-store.js';
import { registerCreateSprint } from './tools/create-sprint.js';
import { registerGetSprint } from './tools/get-sprint.js';
import { registerCreateStory } from './tools/create-story.js';
import { registerCreateTask } from './tools/create-task.js';
import { registerUpdateTaskStatus } from './tools/update-task-status.js';
import { registerSprintBoard } from './tools/sprint-board.js';
import { registerGetBacklog } from './tools/get-backlog.js';
import { setupCollaborationHandlers } from './collaboration.js';
export function createScrumBoardServer(eventBus?: EventBus): McpSuiteServer {
const suite = createMcpServer({
name: 'scrum-board',
version: '0.1.0',
description: 'MCP server for managing sprints, user stories, tasks',
eventBus,
});
const store = new ScrumStore();
// Register all tools
registerCreateSprint(suite.server, store, suite.eventBus);
registerGetSprint(suite.server, store);
registerCreateStory(suite.server, store);
registerCreateTask(suite.server, store);
registerUpdateTaskStatus(suite.server, store, suite.eventBus);
registerSprintBoard(suite.server, store);
registerGetBacklog(suite.server, store);
// Cross-server collaboration (only if EventBus is present)
if (suite.eventBus) {
setupCollaborationHandlers(suite.eventBus, store);
}
suite.logger.info('All scrum-board tools registered');
return suite;
}
Notice how createMcpServer() returns a fully configured McpSuiteServer object.
The store is created once and shared among all tools through the registration functions.
The eventBus is optional: if not provided, the server operates in standalone mode.
Layer 3: Tool Registration (tools/)
Each tool is a separate file that exports a registerXxx() function. This function receives
the McpServer, the store, and optionally the EventBus, and registers a single
tool with its Zod validation schema and async handler.
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import type { EventBus } from '@mcp-suite/core';
import type { ScrumStore } from '../services/scrum-store.js';
export function registerCreateSprint(
server: McpServer,
store: ScrumStore,
eventBus?: EventBus,
): void {
server.tool(
'create-sprint', // Tool name
'Create a new sprint with a name, date range, and goals', // LLM description
{ // Zod schema
name: z.string().describe('Sprint name (e.g. "Sprint 12")'),
startDate: z.string().describe('Start date ISO (YYYY-MM-DD)'),
endDate: z.string().describe('End date ISO (YYYY-MM-DD)'),
goals: z.array(z.string()).describe('Sprint goals'),
},
async ({ name, startDate, endDate, goals }) => { // Handler
try {
const sprint = store.createSprint({ name, startDate, endDate, goals });
// Fire-and-forget event publishing
eventBus?.publish('scrum:sprint-started', {
sprintId: String(sprint.id),
name: sprint.name,
startDate: sprint.startDate,
endDate: sprint.endDate,
});
return {
content: [{ type: 'text' as const, text: JSON.stringify(sprint, null, 2) }],
};
} catch (error) {
return {
content: [{
type: 'text' as const,
text: `Failed to create sprint: 






