If your AI coding agent seems to fumble with project-specific conventions, a custom MCP server is the fix. Instead of generic context, you expose live, structured data about your codebase—like your custom hooks, API endpoints, or style configurations. This tutorial builds a simple MCP server in Node.js that serves project metadata to any MCP-compatible client (e.g., Claude Desktop, Cursor, VS Code extensions).

What You'll Build

A minimal MCP server that provides two tools: get-architecture (returns folder structure) and get-conventions (returns coding conventions from a config file).

Prerequisites

Step 1: Initialize the Project

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

Create a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

Step 2: Write the MCP Server

Create src/index.ts:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

// --- Tool definitions ---
const server = new Server(
  {
    name: "my-codebase-mcp",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Tool: get-architecture
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get-architecture",
      description: "Returns the folder structure of the project.",
      inputSchema: {
        type: "object",
        properties: {
          basePath: { type: "string", description: "Root folder path" },
        },
      },
    },
    {
      name: "get-conventions",
      description: "Returns coding conventions from a .conventions.json file.",
      inputSchema: {
        type: "object",
        properties: {},
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "get-architecture") {
    const basePath = (args as any)?.basePath || ".";
    const structure = await getFolderStructure(basePath);
    return {
      content: [{ type: "text", text: JSON.stringify(structure, null, 2) }],
    };
  }

  if (name === "get-conventions") {
    const conventions = await readConventions();
    return {
      content: [{ type: "text", text: JSON.stringify(conventions, null, 2) }],
    };
  }

  throw new Error(`Tool not found: ${name}`);
});

// --- Helper functions ---
async function getFolderStructure(path: string): Promise {
  const fs = await import("fs/promises");
  const pathModule = await import("path");

  async function walk(dir: string): Promise {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    const result: any = {};
    for (const entry of entries) {
      if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
      const fullPath = pathModule.join(dir, entry.name);
      if (entry.isDirectory()) {
        result[entry.name] = await walk(fullPath);
      } else {
        result[entry.name] = "file";
      }
    }
    return result;
  }

  return walk(path);
}

async function readConventions(): Promise {
  const fs = await import("fs/promises");
  try {
    const data = await fs.readFile(".conventions.json", "utf-8");
    return JSON.parse(data);
  } catch {
    return { message: "No .conventions.json file found in root." };
  }
}

// --- Start server ---
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP server running on stdio");
}

main().catch((err) => {
  console.error("Server error:", err);
  process.exit(1);
});


Step 3: Create a Conventions File

In your project root, create .conventions.json:

{
  "style": "Use named exports for components",
  "testing": "Place test files next to source files",
  "api": "All API routes must be under /api/*",
  "hooks": "Custom hooks should start with 'use' and be in /hooks"
}

Step 4: Build and Run

npx tsc
node dist/index.js

You should see: "MCP server running on stdio".

Step 5: Connect Your MCP Client

For Claude Desktop: Edit claude_desktop_config.json (found in ~/Library/Application Support/Claude/ on macOS) to add:
{
  "mcpServers": {
    "my-codebase": {
      "command": "node",
      "args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
    }
  }
}

Restart Claude Desktop. Now when you ask Claude about your project's architecture or conventions, it will call your tools.

Security: The MCP server runs with your user privileges. Never expose it to untrusted inputs. For production, add validation and rate limiting.

Next Steps

  • Add a tool to query your database schema
  • Expose your Git history with git log commands
  • Serve your project's README or inline documentation
You now have a working MCP server that gives AI agents superpowers over your codebase. Customization is limited only by your imagination—and what you decide to expose.

Comments

No comments yet

Connect with Google to comment or reply.

Connect with Google