MCP Server Custom Tool Template

MCP

Template for building custom MCP servers with TypeScript FastMCP.

MCP Server Custom Tool Template

Overview

This template provides a foundation for building custom MCP (Model Context Protocol) servers using TypeScript FastMCP. It enables you to create MCP servers that expose custom tools, resources, and prompts to AI agents.

MCP servers allow AI agents to interact with external systems, APIs, and data sources in a standardized way.

Prerequisites

  • Node.js 18+
  • Basic TypeScript knowledge
  • An MCP-compatible AI client (Claude Desktop, Cursor, etc.)

Project Structure

mcp-custom-server/
├── src/
│   ├── index.ts              # Server entry point
│   ├── tools/
│   │   ├── index.ts          # Tool exports
│   │   └── calculator.ts     # Example tool
│   ├── resources/
│   │   ├── index.ts          # Resource exports
│   │   └── docs.ts           # Example resource
│   └── prompts/
│       ├── index.ts          # Prompt exports
│       └── code-review.ts    # Example prompt
├── package.json
├── tsconfig.json
└── README.md

Installation

# Create project
mkdir mcp-custom-server
cd mcp-custom-server

# Initialize npm
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk zod

# Install dev dependencies
npm install -D typescript @types/node tsx

# Initialize TypeScript
npx tsc --init

Package Configuration

package.json:

{
  "name": "mcp-custom-server",
  "version": "1.0.0",
  "description": "Custom MCP server with FastMCP",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx src/index.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "tsx": "^4.0.0"
  }
}

TypeScript Configuration

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Core Server Implementation

Using FastMCP (Recommended)

FastMCP is a simplified API for building MCP servers:

src/index.ts:

import { FastMCP } from '@modelcontextprotocol/sdk';

// Create the server
const mcp = new FastMCP({
  name: 'custom-tools-server',
  version: '1.0.0',
});

// Add a simple tool
mcp.tool(
  'add',
  'Add two numbers together',
  {
    a: { type: 'number', description: 'First number' },
    b: { type: 'number', description: 'Second number' },
  },
  async ({ a, b }) => ({
    content: [{ type: 'text', text: String(a + b) }],
  })
);

// Add a tool with complex input
mcp.tool(
  'calculate',
  'Perform a calculation',
  {
    expression: { type: 'string', description: 'Math expression to evaluate' },
    precision: { type: 'number', description: 'Decimal places', optional: true },
  },
  async ({ expression, precision = 2 }) => {
    try {
      // WARNING: In production, use a proper expression parser
      // This is for demonstration only
      const result = Function('"use strict";return (' + expression + ')')();
      
      return {
        content: [
          {
            type: 'text',
            text: `Result: ${Number(result).toFixed(precision)}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Error: ${error.message}`,
          },
        ],
      };
    }
  }
);

// Add a resource
mcp.resource(
  'docs://readme',
  'This server\'s README',
  async () => ({
    contents: [
      {
        uri: 'docs://readme',
        mimeType: 'text/markdown',
        text: '# Custom Tools Server\n\nA simple MCP server with custom tools.',
      },
    ],
  })
);

// Add a prompt
mcp.prompt(
  'code-review',
  'Generate a code review prompt',
  {
    code: { type: 'string', description: 'Code to review' },
    language: { type: 'string', description: 'Programming language', optional: true },
  },
  ({ code, language = 'unknown' }) => ({
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: `Please review this ${language} code for bugs, security issues, and best practices:\n\n\`\`\`${language}\n${code}\n\`\`\``,
        },
      },
    ],
  })
);

// Start the server
await mcp.run();

Using Full SDK

For more control, use the full MCP SDK:

src/index-full.ts:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';

// Create server
const server = new Server(
  {
    name: 'custom-tools-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
      resources: {},
      prompts: {},
    },
  }
);

// Tool definitions
const tools = [
  {
    name: 'add',
    description: 'Add two numbers together',
    inputSchema: {
      type: 'object',
      properties: {
        a: { type: 'number', description: 'First number' },
        b: { type: 'number', description: 'Second number' },
      },
      required: ['a', 'b'],
    },
  },
  {
    name: 'multiply',
    description: 'Multiply two numbers',
    inputSchema: {
      type: 'object',
      properties: {
        a: { type: 'number', description: 'First number' },
        b: { type: 'number', description: 'Second number' },
      },
      required: ['a', 'b'],
    },
  },
];

// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return { tools };
});

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

  if (name === 'add') {
    const schema = z.object({
      a: z.number(),
      b: z.number(),
    });
    const { a, b } = schema.parse(args);
    return {
      content: [{ type: 'text', text: String(a + b) }],
    };
  }

  if (name === 'multiply') {
    const schema = z.object({
      a: z.number(),
      b: z.number(),
    });
    const { a, b } = schema.parse(args);
    return {
      content: [{ type: 'text', text: String(a * b) }],
    };
  }

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

// Resource definitions
const resources = [
  {
    uri: 'docs://readme',
    name: 'README',
    description: 'This server\'s documentation',
    mimeType: 'text/markdown',
  },
];

// List resources handler
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return { resources };
});

// Read resource handler
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri === 'docs://readme') {
    return {
      contents: [
        {
          uri,
          mimeType: 'text/markdown',
          text: '# Custom Tools Server\n\nA simple MCP server with custom tools.',
        },
      ],
    };
  }

  throw new Error(`Resource not found: ${uri}`);
});

// Prompt definitions
const prompts = [
  {
    name: 'code-review',
    description: 'Generate a code review prompt',
    arguments: [
      {
        name: 'code',
        description: 'Code to review',
        required: true,
      },
      {
        name: 'language',
        description: 'Programming language',
        required: false,
      },
    ],
  },
];

// List prompts handler
server.setRequestHandler(ListPromptsRequestSchema, async () => {
  return { prompts };
});

// Get prompt handler
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === 'code-review') {
    const code = args?.code as string;
    const language = (args?.language as string) || 'unknown';
    
    return {
      messages: [
        {
          role: 'user',
          content: {
            type: 'text',
            text: `Please review this ${language} code:\n\n\`\`\`${language}\n${code}\n\`\`\``,
          },
        },
      ],
    };
  }

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

// Start server
const transport = new StdioServerTransport();
await server.connect(transport);

Organizing Tools in Separate Files

src/tools/calculator.ts:

import { z } from 'zod';

export const calculatorTools = [
  {
    name: 'add',
    description: 'Add two numbers together',
    inputSchema: {
      type: 'object',
      properties: {
        a: { type: 'number', description: 'First number' },
        b: { type: 'number', description: 'Second number' },
      },
      required: ['a', 'b'],
    },
    execute: async ({ a, b }: { a: number; b: number }) => ({
      content: [{ type: 'text', text: String(a + b) }],
    }),
  },
  {
    name: 'multiply',
    description: 'Multiply two numbers',
    inputSchema: {
      type: 'object',
      properties: {
        a: { type: 'number', description: 'First number' },
        b: { type: 'number', description: 'Second number' },
      },
      required: ['a', 'b'],
    },
    execute: async ({ a, b }: { a: number; b: number }) => ({
      content: [{ type: 'text', text: String(a * b) }],
    }),
  },
  {
    name: 'divide',
    description: 'Divide two numbers',
    inputSchema: {
      type: 'object',
      properties: {
        a: { type: 'number', description: 'Numerator' },
        b: { type: 'number', description: 'Denominator' },
      },
      required: ['a', 'b'],
    },
    execute: async ({ a, b }: { a: number; b: number }) => {
      if (b === 0) {
        return {
          content: [{ type: 'text', text: 'Error: Division by zero' }],
        };
      }
      return {
        content: [{ type: 'text', text: String(a / b) }],
      };
    },
  },
];

src/tools/index.ts:

import { calculatorTools } from './calculator';

export const allTools = [...calculatorTools];

export { calculatorTools };

Configuration for Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):

{
  "mcpServers": {
    "custom-tools": {
      "command": "node",
      "args": ["/path/to/mcp-custom-server/dist/index.js"]
    }
  }
}

Or for development:

{
  "mcpServers": {
    "custom-tools": {
      "command": "npx",
      "args": ["-y", "tsx", "/path/to/mcp-custom-server/src/index.ts"]
    }
  }
}

Testing Your Server

Using mcp-cli

npm install -g @modelcontextprotocol/cli

# List available servers
mcp list

# Test a tool
mcp call custom-tools add --args '{"a": 5, "b": 3}'

Using a Python Client

from mcp import ClientSession, StdioServerParameters
import asyncio

async def test_server():
    server_params = StdioServerParameters(
        command="node",
        args=["/path/to/mcp-custom-server/dist/index.js"],
    )
    
    async with ClientSession(server_params) as session:
        await session.initialize()
        
        # List tools
        tools = await session.list_tools()
        print(f"Available tools: {[t.name for t in tools.tools]}")
        
        # Call a tool
        result = await session.call_tool("add", arguments={"a": 5, "b": 3})
        print(f"5 + 3 = {result}")

asyncio.run(test_server())

Best Practices

  1. Validate inputs: Use Zod or similar for input validation
  2. Handle errors gracefully: Return meaningful error messages
  3. Document tools: Clear descriptions help AI understand how to use them
  4. Keep tools focused: Each tool should do one thing well
  5. Test thoroughly: MCP servers run in production environments

Resources

View on GitHub

Related Templates