Project Structure and Organization for MCP Servers

Organize MCP server projects for maintainability with recommended file layouts, naming conventions, and configuration management patterns.


title: "Project Structure and Organization for MCP Servers" description: "Organize MCP server projects for maintainability with recommended file layouts, naming conventions, and configuration management patterns." order: 8 keywords:

  • MCP project structure
  • MCP file organization
  • MCP naming conventions
  • MCP server layout
  • MCP code organization date: "2026-04-01"

Quick Summary

Learn how to organize MCP server projects for long-term maintainability. Covers recommended file layouts for both mcp-framework and the official SDK, naming conventions, module organization, configuration management, and patterns for growing projects.

Why Structure Matters

A well-organized project is easier to navigate, test, and extend. When a new team member opens your MCP server repo, they should immediately understand where things live and how they connect.

Convention Over Configuration

A design paradigm where the framework assumes sensible defaults based on file and directory naming conventions. mcp-framework uses this approach: place a tool class in src/tools/ and it is automatically discovered. No manual registration needed.

Recommended Structure: mcp-framework

Follow mcp-framework's Convention

mcp-framework auto-discovers tools, resources, and prompts from specific directories. Follow the convention for zero-configuration setup.

my-mcp-server
src
tools
SearchTool.ts
CreateItemTool.ts
DeleteItemTool.ts
resources
SchemaResource.ts
ConfigResource.ts
prompts
SummaryPrompt.ts
services
DatabaseService.ts
ApiClient.ts
utils
logger.ts
validation.ts
cache.ts
types
index.ts
index.ts
tests
tools
SearchTool.test.ts
CreateItemTool.test.ts
services
DatabaseService.test.ts
integration
server.test.ts
package.json
tsconfig.json
.env.example
.gitignore

Directory Roles

| Directory | Purpose | Auto-discovered? | |-----------|---------|-----------------| | src/tools/ | MCP tool classes | Yes (mcp-framework) | | src/resources/ | MCP resource classes | Yes (mcp-framework) | | src/prompts/ | MCP prompt classes | Yes (mcp-framework) | | src/services/ | Business logic and external API clients | No | | src/utils/ | Shared utilities (logger, cache, validation) | No | | src/types/ | TypeScript type definitions | No | | tests/ | All test files, mirroring src structure | No |

Recommended Structure: Official SDK

Organize by Feature for SDK Projects

With the official SDK, there is no auto-discovery. Organize by feature domain and register everything explicitly in a central setup file.

my-mcp-server
src
server.ts
tools
index.ts
search.ts
create.ts
delete.ts
resources
index.ts
schema.ts
services
database.ts
api-client.ts
utils
logger.ts
cache.ts
index.ts
tests
package.json
tsconfig.json

The key difference is the registration pattern:

// src/tools/index.ts -- explicit registration
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerSearchTool } from "./search.js";
import { registerCreateTool } from "./create.js";
import { registerDeleteTool } from "./delete.js";

export function registerAllTools(server: McpServer) {
  registerSearchTool(server);
  registerCreateTool(server);
  registerDeleteTool(server);
}

// src/tools/search.ts
export function registerSearchTool(server: McpServer) {
  server.tool("search", "Search items", { query: z.string() },
    async ({ query }) => {
      // ...
    }
  );
}

// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerAllTools } from "./tools/index.js";
import { registerAllResources } from "./resources/index.js";

export function createServer(): McpServer {
  const server = new McpServer({ name: "my-server", version: "1.0.0" });
  registerAllTools(server);
  registerAllResources(server);
  return server;
}

Naming Conventions

Use Consistent Naming Across the Project

Consistent naming reduces cognitive load. Pick a convention and apply it everywhere -- file names, class names, tool names, and variable names should all follow the same pattern.

| Element | Convention | Example | |---------|-----------|---------| | Tool files (mcp-framework) | PascalCase | SearchTool.ts | | Tool files (SDK) | kebab-case | search.ts | | Tool names | snake_case | search_items | | Service classes | PascalCase | DatabaseService | | Utility files | camelCase or kebab-case | logger.ts | | Test files | Match source + .test | SearchTool.test.ts | | Environment variables | UPPER_SNAKE_CASE | DATABASE_URL |

Configuration Management

Centralize Configuration

Create a single configuration module that reads and validates environment variables. All other modules import configuration from this one source.

// src/config.ts
import { z } from "zod";

const envSchema = z.object({
  PORT: z.string().transform(Number).default("3001"),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
  LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
  CACHE_TTL_MS: z.string().transform(Number).default("60000"),
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});

export type Config = z.infer<typeof envSchema>;

let config: Config;

export function getConfig(): Config {
  if (!config) {
    const result = envSchema.safeParse(process.env);
    if (!result.success) {
      console.error("Invalid configuration:", result.error.format());
      process.exit(1);
    }
    config = result.data;
  }
  return config;
}

Environment File

Create .env.example (committed to git):

# Server
PORT=3001
NODE_ENV=development
LOG_LEVEL=info

# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb

# External APIs
API_KEY=your-api-key-here

# Cache
CACHE_TTL_MS=60000
Never Commit .env Files

Add .env to your .gitignore. Commit .env.example with placeholder values so new developers know what variables are needed.

Scaling the Project

When to Split Into Multiple Servers

Split When Domains Diverge

Keep related tools in one server. Split into multiple servers when: tools have different deployment requirements, different auth needs, unrelated domains, or when one server grows beyond 15-20 tools.

Signs you should split:

  • Tools require different environment variables (one needs DB, another needs API key)
  • Some tools need heavy dependencies (headless browser, ML models)
  • The server handles two unrelated domains (GitHub AND Slack)
  • Different teams own different tools

Monorepo for Multiple Servers

my-mcp-servers/
  packages/
    github-server/
      src/
      package.json
    database-server/
      src/
      package.json
    shared/
      src/
        logger.ts
        cache.ts
      package.json
  package.json        # Workspace root
  tsconfig.base.json  # Shared TS config

The Essential Files Checklist

| File | Purpose | Required? | |------|---------|-----------| | src/index.ts | Entry point, starts the server | Yes | | package.json | Dependencies and scripts | Yes | | tsconfig.json | TypeScript configuration | Yes | | .env.example | Document required env vars | Yes | | .gitignore | Exclude node_modules, dist, .env | Yes | | src/config.ts | Centralized configuration | Recommended | | src/utils/logger.ts | Structured logging | Recommended | | tests/ | Test files | Recommended | | Dockerfile | Container build | For deployment | | docker-compose.yml | Local dev with dependencies | Optional |

Frequently Asked Questions