MCP Server Starter

MCP Server Starter

A starter template for building MCP servers that enable AI assistants to interact with custom tools and data.

Category
访问服务器

README

🚀 MCP Server Starter

Build your own AI tools in minutes!

This is a starter template for building MCP (Model Context Protocol) servers. MCP lets you create tools that AI assistants like Claude, VS Code Copilot, and other AI apps can use.

TypeScript MCP SDK License: MIT


📖 Table of Contents


🤔 What is MCP?

Model Context Protocol (MCP) is an open protocol that lets AI assistants interact with external tools and data. Think of it as a USB port for AI—a standard way to plug in new capabilities.

How It Works

┌─────────────────┐         ┌─────────────────┐         ┌─────────────────┐
│   AI Assistant  │  MCP    │   Your Server   │         │  External APIs  │
│  (Claude, etc)  │◄───────►│  (This repo!)   │◄───────►│  Databases, etc │
└─────────────────┘         └─────────────────┘         └─────────────────┘
  1. AI assistant connects to your MCP server
  2. Your server tells the AI what tools are available
  3. AI decides when to use your tools based on user requests
  4. Your tools execute and return results to the AI

What Can MCP Servers Provide?

Feature Description Example
🔧 Tools Functions the AI can execute Calculate, search, API calls
📄 Resources Data the AI can read Files, configs, documentation
💬 Prompts Pre-built prompt templates Code review, debugging help

⚡ Quick Start (2 minutes)

Prerequisites

Installation

# 1. Clone this repository
git clone https://github.com/your-username/mcp-server-starter.git
cd mcp-server-starter

# 2. Install dependencies
npm install

# 3. Build the project
npm run build

# 4. Test your server!
npm run inspector

The inspector opens a web UI at http://localhost:5173 where you can interactively test all your tools.

Verify It's Working

In the inspector:

  1. Click on "Tools" in the sidebar
  2. Select add from the tool list
  3. Enter values for a and b
  4. Click "Run" - you should see the result!

📁 Project Structure

mcp-server-starter/
├── src/
│   ├── index.ts              # 🏠 Main entry point - starts the server
│   ├── tools/
│   │   ├── _template.ts      # 📋 COPY THIS to create new tools!
│   │   ├── calculator.ts     # ➕ Example: math operations
│   │   └── greeting.ts       # 👋 Example: greeting & utility tools
│   ├── resources/
│   │   └── index.ts          # 📄 Data sources AI can read
│   └── prompts/
│       └── index.ts          # 💬 Pre-built prompt templates
├── build/                    # 📦 Compiled JavaScript (generated)
├── package.json              # 📋 Dependencies and scripts
├── tsconfig.json             # ⚙️ TypeScript configuration
└── README.md                 # 📖 You are here!

Key Files Explained

File Purpose
src/index.ts Creates the MCP server, imports all tools/resources/prompts, starts listening
src/tools/_template.ts Copy this file to create new tools - has examples and comments
src/tools/calculator.ts Example showing basic math tools with error handling
src/tools/greeting.ts Example showing string tools, enums, optional params
src/resources/index.ts Example showing how to expose data to AI
src/prompts/index.ts Example showing reusable prompt templates

🧠 Core Concepts

1. Tools

Tools are functions that AI can execute. They have:

  • Name: Unique identifier (e.g., search_files)
  • Description: Explains what the tool does (AI reads this!)
  • Parameters: Input schema validated with Zod
  • Handler: Async function that does the work
server.tool(
  "tool_name",           // Name (lowercase_with_underscores)
  "What this tool does", // Description (be specific!)
  { /* parameters */ },  // Zod schema for inputs
  async (params) => {    // Handler function
    // Do work here
    return { content: [{ type: "text", text: "result" }] };
  }
);

2. Resources

Resources are data sources that AI can read. Unlike tools, they don't perform actions—they just provide information.

server.resource(
  "mcp://server/config",        // URI (unique identifier)
  "Application configuration",  // Description
  { mimeType: "application/json" },
  async (uri) => ({
    contents: [{ uri: uri.href, text: JSON.stringify(config) }]
  })
);

3. Prompts

Prompts are reusable templates that structure AI interactions.

server.prompt(
  "code-review",                  // Name
  "Review code for issues",       // Description
  { code: z.string() },           // Arguments
  async ({ code }) => ({
    messages: [{
      role: "user",
      content: { type: "text", text: `Review this code:\n${code}` }
    }]
  })
);

🛠️ Adding Your First Tool

Step 1: Create a new file

cp src/tools/_template.ts src/tools/my-tools.ts

Step 2: Write your tool

Edit src/tools/my-tools.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export function registerMyTools(server: McpServer): void {
  
  // A simple tool
  server.tool(
    "hello_world",
    "Say hello to someone by name",
    {
      name: z.string().describe("The person's name to greet"),
    },
    async ({ name }) => {
      return {
        content: [{ 
          type: "text", 
          text: `Hello, ${name}! Welcome to MCP!` 
        }],
      };
    }
  );

  // Add more tools here...
}

Step 3: Register your tools

Edit src/index.ts and add:

// Add this import at the top
import { registerMyTools } from "./tools/my-tools.js";

// Add this line after other registrations (before server.connect)
registerMyTools(server);

Step 4: Build and test

npm run build
npm run inspector

Your hello_world tool should now appear in the inspector!


📚 Tool Examples Cookbook

🔤 String Operations

// Reverse a string
server.tool(
  "reverse_string",
  "Reverse the characters in a string",
  { text: z.string().describe("Text to reverse") },
  async ({ text }) => ({
    content: [{ type: "text", text: text.split("").reverse().join("") }],
  })
);

// Count words
server.tool(
  "count_words",
  "Count the number of words in text",
  { text: z.string().describe("Text to analyze") },
  async ({ text }) => {
    const count = text.trim().split(/\s+/).filter(w => w.length > 0).length;
    return { content: [{ type: "text", text: `Word count: ${count}` }] };
  }
);

🔢 Math Operations

// Calculate percentage
server.tool(
  "calculate_percentage",
  "Calculate what percentage A is of B",
  {
    part: z.number().describe("The part value"),
    whole: z.number().describe("The whole value"),
  },
  async ({ part, whole }) => {
    if (whole === 0) {
      return { content: [{ type: "text", text: "Error: Cannot divide by zero" }], isError: true };
    }
    const percentage = (part / whole) * 100;
    return { content: [{ type: "text", text: `${percentage.toFixed(2)}%` }] };
  }
);

📋 Working with Enums

// Format text in different styles
server.tool(
  "format_text",
  "Format text in various styles",
  {
    text: z.string().describe("Text to format"),
    style: z.enum(["uppercase", "lowercase", "title", "sentence"])
      .describe("Formatting style to apply"),
  },
  async ({ text, style }) => {
    let result: string;
    switch (style) {
      case "uppercase": result = text.toUpperCase(); break;
      case "lowercase": result = text.toLowerCase(); break;
      case "title": result = text.replace(/\w\S*/g, t => 
        t.charAt(0).toUpperCase() + t.slice(1).toLowerCase()
      ); break;
      case "sentence": result = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); break;
    }
    return { content: [{ type: "text", text: result }] };
  }
);

❓ Optional Parameters

// Greet with optional title
server.tool(
  "formal_greeting",
  "Generate a formal greeting",
  {
    name: z.string().describe("Person's name"),
    title: z.enum(["Mr", "Ms", "Mrs", "Dr", "Prof"]).optional()
      .describe("Optional title/honorific"),
    includeTime: z.boolean().optional().default(false)
      .describe("Include time-based greeting"),
  },
  async ({ name, title, includeTime }) => {
    const fullName = title ? `${title}. ${name}` : name;
    let greeting = `Hello, ${fullName}`;
    
    if (includeTime) {
      const hour = new Date().getHours();
      const timeGreeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
      greeting = `${timeGreeting}, ${fullName}`;
    }
    
    return { content: [{ type: "text", text: greeting }] };
  }
);

🌐 API Calls

// Fetch data from an API
server.tool(
  "get_github_user",
  "Get information about a GitHub user",
  { username: z.string().describe("GitHub username") },
  async ({ username }) => {
    try {
      const response = await fetch(`https://api.github.com/users/${username}`);
      
      if (!response.ok) {
        return { 
          content: [{ type: "text", text: `User '${username}' not found` }], 
          isError: true 
        };
      }
      
      const user = await response.json();
      const info = [
        `Name: ${user.name || 'N/A'}`,
        `Bio: ${user.bio || 'N/A'}`,
        `Public Repos: ${user.public_repos}`,
        `Followers: ${user.followers}`,
      ].join('\n');
      
      return { content: [{ type: "text", text: info }] };
    } catch (error) {
      return { 
        content: [{ type: "text", text: `Error: ${error.message}` }], 
        isError: true 
      };
    }
  }
);

📂 File System Operations

import * as fs from "fs/promises";
import * as path from "path";

// Read a file
server.tool(
  "read_file",
  "Read contents of a file",
  { 
    filePath: z.string().describe("Path to the file to read"),
    encoding: z.enum(["utf8", "base64"]).optional().default("utf8"),
  },
  async ({ filePath, encoding }) => {
    try {
      const content = await fs.readFile(filePath, encoding as BufferEncoding);
      return { content: [{ type: "text", text: content }] };
    } catch (error) {
      return { 
        content: [{ type: "text", text: `Error reading file: ${error.message}` }], 
        isError: true 
      };
    }
  }
);

// List directory contents
server.tool(
  "list_directory",
  "List files and folders in a directory",
  { dirPath: z.string().describe("Path to the directory") },
  async ({ dirPath }) => {
    try {
      const entries = await fs.readdir(dirPath, { withFileTypes: true });
      const listing = entries.map(e => 
        `${e.isDirectory() ? '📁' : '📄'} ${e.name}`
      ).join('\n');
      return { content: [{ type: "text", text: listing }] };
    } catch (error) {
      return { 
        content: [{ type: "text", text: `Error: ${error.message}` }], 
        isError: true 
      };
    }
  }
);

🗄️ Database Operations

// Example with a hypothetical database
server.tool(
  "query_users",
  "Search for users in the database",
  {
    searchTerm: z.string().describe("Name or email to search for"),
    limit: z.number().min(1).max(100).optional().default(10)
      .describe("Maximum number of results"),
  },
  async ({ searchTerm, limit }) => {
    try {
      // Replace with your actual database query
      const results = await db.query(
        "SELECT * FROM users WHERE name LIKE ? OR email LIKE ? LIMIT ?",
        [`%${searchTerm}%`, `%${searchTerm}%`, limit]
      );
      
      if (results.length === 0) {
        return { content: [{ type: "text", text: "No users found" }] };
      }
      
      const formatted = results.map(u => `- ${u.name} (${u.email})`).join('\n');
      return { content: [{ type: "text", text: `Found ${results.length} users:\n${formatted}` }] };
    } catch (error) {
      return { 
        content: [{ type: "text", text: `Database error: ${error.message}` }], 
        isError: true 
      };
    }
  }
);

⚠️ Comprehensive Error Handling

server.tool(
  "safe_divide",
  "Safely divide two numbers with comprehensive error handling",
  {
    dividend: z.number().describe("Number to be divided"),
    divisor: z.number().describe("Number to divide by"),
  },
  async ({ dividend, divisor }) => {
    // Validate inputs
    if (!Number.isFinite(dividend) || !Number.isFinite(divisor)) {
      return {
        content: [{ type: "text", text: "Error: Both inputs must be finite numbers" }],
        isError: true,
      };
    }

    // Check for division by zero
    if (divisor === 0) {
      return {
        content: [{ type: "text", text: "Error: Cannot divide by zero" }],
        isError: true,
      };
    }

    // Perform calculation
    const result = dividend / divisor;

    // Check for overflow
    if (!Number.isFinite(result)) {
      return {
        content: [{ type: "text", text: "Error: Result is too large" }],
        isError: true,
      };
    }

    return {
      content: [{ 
        type: "text", 
        text: `${dividend} ÷ ${divisor} = ${result}` 
      }],
    };
  }
);

📄 Adding Resources

Resources let AI read data from your server. Add them in src/resources/index.ts:

// Static text resource
server.resource(
  "mcp://server/readme",
  "Project README file",
  { mimeType: "text/plain" },
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: "This is the readme content..."
    }]
  })
);

// Dynamic JSON resource
server.resource(
  "mcp://server/status",
  "Current server status",
  { mimeType: "application/json" },
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: JSON.stringify({
        status: "healthy",
        uptime: process.uptime(),
        timestamp: new Date().toISOString()
      }, null, 2)
    }]
  })
);

// Resource from external source
server.resource(
  "mcp://server/config",
  "Application configuration",
  { mimeType: "application/json" },
  async (uri) => {
    const config = await loadConfigFromDatabase();
    return {
      contents: [{
        uri: uri.href,
        text: JSON.stringify(config, null, 2)
      }]
    };
  }
);

💬 Adding Prompts

Prompts are templates that help structure AI interactions. Add them in src/prompts/index.ts:

// Simple prompt
server.prompt(
  "explain-code",
  "Explain what code does in simple terms",
  { code: z.string().describe("Code to explain") },
  async ({ code }) => ({
    messages: [{
      role: "user",
      content: { 
        type: "text", 
        text: `Please explain what this code does in simple terms:\n\n\`\`\`\n${code}\n\`\`\`` 
      }
    }]
  })
);

// Prompt with multiple parameters
server.prompt(
  "code-review",
  "Review code for potential issues",
  {
    code: z.string().describe("Code to review"),
    language: z.string().optional().describe("Programming language"),
    focus: z.enum(["security", "performance", "style", "all"]).optional().default("all")
      .describe("What to focus on"),
  },
  async ({ code, language, focus }) => ({
    messages: [{
      role: "user",
      content: { 
        type: "text", 
        text: `Review this ${language || ''} code for ${focus} issues:\n\n\`\`\`${language || ''}\n${code}\n\`\`\`\n\nProvide specific suggestions for improvement.`
      }
    }]
  })
);

// Multi-message prompt
server.prompt(
  "debug-assistant",
  "Help debug an issue",
  {
    error: z.string().describe("Error message or description"),
    code: z.string().optional().describe("Relevant code"),
    context: z.string().optional().describe("Additional context"),
  },
  async ({ error, code, context }) => ({
    messages: [
      {
        role: "user",
        content: { 
          type: "text", 
          text: `I'm encountering this error: ${error}` 
        }
      },
      ...(code ? [{
        role: "user" as const,
        content: { 
          type: "text" as const, 
          text: `Here's the relevant code:\n\`\`\`\n${code}\n\`\`\`` 
        }
      }] : []),
      ...(context ? [{
        role: "user" as const,
        content: { 
          type: "text" as const, 
          text: `Additional context: ${context}` 
        }
      }] : []),
      {
        role: "user",
        content: { 
          type: "text", 
          text: "Please help me understand and fix this issue." 
        }
      }
    ]
  })
);

📐 Zod Schema Reference

Zod is used to validate tool inputs. Here's a quick reference:

Basic Types

z.string()                    // Any string
z.number()                    // Any number
z.boolean()                   // true or false
z.null()                      // null
z.undefined()                 // undefined
z.any()                       // Any type (avoid if possible)

String Validations

z.string().min(1)             // At least 1 character
z.string().max(100)           // At most 100 characters
z.string().length(5)          // Exactly 5 characters
z.string().email()            // Valid email format
z.string().url()              // Valid URL format
z.string().uuid()             // Valid UUID
z.string().regex(/pattern/)   // Matches regex
z.string().startsWith("Hi")   // Starts with "Hi"
z.string().endsWith("!")      // Ends with "!"

Number Validations

z.number().min(0)             // >= 0
z.number().max(100)           // <= 100
z.number().positive()         // > 0
z.number().negative()         // < 0
z.number().int()              // Integer only
z.number().multipleOf(5)      // Divisible by 5

Enums & Literals

z.enum(["a", "b", "c"])       // One of these values
z.literal("exact")            // Exactly this value
z.union([z.string(), z.number()])  // String OR number

Optional & Defaults

z.string().optional()         // String or undefined
z.string().nullable()         // String or null
z.string().default("hi")      // Default if not provided
z.string().optional().default("hi")  // Optional with default

Arrays & Objects

z.array(z.string())           // Array of strings
z.array(z.number()).min(1)    // Non-empty number array
z.array(z.string()).max(10)   // At most 10 strings

z.object({                    // Object shape
  name: z.string(),
  age: z.number().optional(),
})

Descriptions (Important!)

// Always add descriptions - AI uses these!
z.string().describe("The user's email address")
z.number().min(1).max(5).describe("Rating from 1 to 5 stars")
z.enum(["asc", "desc"]).describe("Sort order")

🔌 Connect to AI Apps

VS Code (Copilot)

The .vscode/mcp.json file is already included. Update the path:

{
  "servers": {
    "mcp-server-starter": {
      "type": "stdio",
      "command": "node",
      "args": ["/absolute/path/to/mcp-server-starter/build/index.js"]
    }
  }
}

Then reload VS Code and your tools will be available to Copilot!

Claude Desktop (macOS)

Edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-server-starter/build/index.js"]
    }
  }
}

Claude Desktop (Windows)

Edit %APPDATA%\Claude\claude_desktop_config.json:

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["C:\\path\\to\\mcp-server-starter\\build\\index.js"]
    }
  }
}

With Environment Variables

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["/path/to/build/index.js"],
      "env": {
        "API_KEY": "your-api-key",
        "DATABASE_URL": "postgres://..."
      }
    }
  }
}

🔄 Development Workflow

Daily Development

# Start watch mode (auto-rebuilds on changes)
npm run dev

# In another terminal, run the inspector
npm run inspector

Testing Changes

  1. Make changes to your TypeScript files
  2. Watch mode automatically rebuilds
  3. In inspector, click "Reconnect" or refresh
  4. Test your changes

Before Committing

# Full build to catch any issues
npm run build

# Test in inspector
npm run inspector

🐛 Debugging

Enable Debug Logging

Add to your code:

// Use console.error for logging (stdout is for MCP protocol)
console.error("[DEBUG] Tool called with:", params);

Common Issues

Problem Solution
"Tool not found" Did you register it in index.ts? Did you rebuild?
"Cannot find module" Use .js extension in imports (even for .ts files)
Server won't start Check for syntax errors with npm run build
Inspector won't connect Make sure no other server is running on the port
Changes not showing Rebuild with npm run build and reconnect inspector

Viewing Protocol Messages

Run with debug output:

DEBUG=mcp* node build/index.js

✅ Best Practices

Tool Design

  1. Naming: Use lowercase_with_underscores (e.g., search_files, send_email)
  2. Single Purpose: Each tool should do one thing well
  3. Clear Descriptions: AI reads these to decide when to use your tool
  4. Parameter Descriptions: Always use .describe() on every parameter

Error Handling

  1. Always validate inputs even though Zod helps
  2. Return isError: true for failures so AI knows something went wrong
  3. Provide helpful error messages that explain what went wrong
  4. Handle edge cases (empty strings, zero values, null, etc.)

Performance

  1. Keep tools fast - AI waits for responses
  2. Use timeouts for network requests
  3. Cache when appropriate for repeated operations
  4. Stream large responses if supported

Security

  1. Never expose secrets through tool responses
  2. Validate file paths to prevent directory traversal
  3. Sanitize user input before using in commands/queries
  4. Use environment variables for sensitive configuration

❓ FAQ

Q: Can I use JavaScript instead of TypeScript?

Yes! Remove tsconfig.json, rename files to .js, and update package.json:

"scripts": {
  "start": "node src/index.js"
}

Q: How do I add npm packages?

npm install package-name
npm install -D @types/package-name  # For TypeScript types

Then import and use in your tools.

Q: Can one tool call another tool?

Yes, but it's better to extract shared logic into a regular function:

// Shared logic
function calculateTax(amount: number, rate: number): number {
  return amount * rate;
}

// Tool 1 uses it
server.tool("calc_sales_tax", ..., async ({ amount }) => {
  const tax = calculateTax(amount, 0.08);
  return { content: [{ type: "text", text: `Tax: $${tax}` }] };
});

// Tool 2 also uses it  
server.tool("calc_income_tax", ..., async ({ income }) => {
  const tax = calculateTax(income, 0.25);
  return { content: [{ type: "text", text: `Tax: $${tax}` }] };
});

Q: How do I return structured data?

Return JSON as a string:

server.tool("get_user", ..., async ({ id }) => {
  const user = await fetchUser(id);
  return { 
    content: [{ 
      type: "text", 
      text: JSON.stringify(user, null, 2)  // Pretty-printed JSON
    }] 
  };
});

Q: Can I return images or files?

Yes, using base64 encoding:

server.tool("get_chart", ..., async (params) => {
  const imageBuffer = await generateChart(params);
  return {
    content: [{
      type: "image",
      data: imageBuffer.toString("base64"),
      mimeType: "image/png"
    }]
  };
});

Q: How do I handle long-running operations?

MCP supports progress notifications for long operations:

server.tool("long_task", ..., async (params, { sendProgress }) => {
  for (let i = 0; i <= 100; i += 10) {
    await doPartialWork();
    await sendProgress({ progress: i, total: 100 });
  }
  return { content: [{ type: "text", text: "Done!" }] };
});

📖 Commands Reference

Command Description
npm install Install dependencies
npm run build Compile TypeScript to JavaScript
npm run dev Watch mode - auto-rebuild on changes
npm run start Run the compiled server directly
npm run inspector Open interactive testing UI

🔗 Resources

Official Documentation

Examples & Inspiration

Related Tools


🤝 Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Submit a pull request

📄 License

MIT - Use this however you want!

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选