Skip to main content

Building an MCP Server

4.1 Development Environment Setup

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

Python

mkdir my-mcp-server && cd my-mcp-server
pip install mcp

4.2 Complete TypeScript Example

An MCP Server providing a weather query tool:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// Create Server instance
const server = new McpServer({
  name: "weather-server",
  version: "1.0.0"
});

// Define a tool
server.tool(
  "get_weather",              // Tool name
  "Get current weather for a specified city",  // Description
  {                            // Input parameter schema
    city: {
      type: "string",
      description: "City name"
    }
  },
  async ({ city }) => {        // Execution function
    // Call your weather API here
    const weather = await fetchWeather(city);
    return {
      content: [{
        type: "text",
        text: `Current weather in ${city}: ${weather.temp} C, ${weather.condition}`
      }]
    };
  }
);

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

Multi-Tool Server (with outputSchema and structuredContent)

Protocol version 2025-11-25 supports outputSchema for defining structured output format, with structuredContent for returning structured data:
const server = new McpServer({
  name: "commerce-server",
  version: "1.0.0"
});

// Tool 1: Search products (with outputSchema)
server.tool(
  "search_products",
  "Search the product catalog",
  {
    query: { type: "string", description: "Search keyword" },
    category: { type: "string", description: "Product category (optional)" },
    limit: { type: "number", description: "Number of results, default 10" }
  },
  async ({ query, category, limit = 10 }) => {
    const results = await db.products.search(query, { category, limit });
    const data = {
      products: results.map(p => ({
        name: p.name, sku: p.sku, price: p.price, inStock: p.inventory > 0
      })),
      total: results.totalCount
    };
    return {
      content: [{
        type: "text",
        text: JSON.stringify(data, null, 2)
      }],
      // structuredContent: for programmatic processing (when outputSchema is defined)
      structuredContent: data
    };
  }
);

// Tool 2: Check inventory
server.tool(
  "check_inventory",
  "Query real-time product inventory",
  {
    sku: { type: "string", description: "Product SKU" }
  },
  async ({ sku }) => {
    const inventory = await db.inventory.getBySku(sku);
    if (!inventory) {
      return {
        content: [{ type: "text", text: `SKU ${sku} does not exist` }],
        isError: true
      };
    }
    return {
      content: [{
        type: "text",
        text: `SKU ${sku}: ${inventory.quantity} units (${inventory.warehouse} warehouse)`
      }]
    };
  }
);

// Tool 3: Get order
server.tool(
  "get_order",
  "Query order status",
  {
    orderId: { type: "string", description: "Order ID" }
  },
  async ({ orderId }) => {
    const order = await db.orders.getById(orderId);
    if (!order) {
      return {
        content: [{ type: "text", text: `Order ${orderId} not found` }],
        isError: true
      };
    }
    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          id: order.id,
          status: order.status,
          items: order.items.length,
          total: order.total,
          tracking: order.trackingNumber
        }, null, 2)
      }]
    };
  }
);

4.3 Complete Python Example

from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types

server = Server("commerce-server")

@server.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="search_products",
            description="Search the product catalog",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search keyword"},
                    "limit": {"type": "number", "description": "Number of results"}
                },
                "required": ["query"]
            }
        ),
        types.Tool(
            name="check_inventory",
            description="Query real-time product inventory",
            inputSchema={
                "type": "object",
                "properties": {
                    "sku": {"type": "string", "description": "Product SKU"}
                },
                "required": ["sku"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "search_products":
        results = await search_products(
            arguments["query"],
            arguments.get("limit", 10)
        )
        return [types.TextContent(
            type="text",
            text=json.dumps(results, ensure_ascii=False)
        )]
    elif name == "check_inventory":
        inventory = await get_inventory(arguments["sku"])
        return [types.TextContent(
            type="text",
            text=f"SKU {arguments['sku']}: {inventory['quantity']} units"
        )]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

4.4 Exposing Resources

In addition to tools, Servers can also expose data resources:
// Expose product categories as a resource
server.resource(
  "commerce://catalog/categories",
  "Product Categories",
  "application/json",
  async () => {
    const categories = await db.categories.getAll();
    return JSON.stringify(categories, null, 2);
  }
);

// Expose company information as a resource
server.resource(
  "commerce://company/info",
  "Company Information",
  "text/markdown",
  async () => {
    return `# Company Name\n\n> One-line description\n\n## Main Business\n...`;
  }
);

4.5 Streamable HTTP Server

Use Streamable HTTP transport for remote deployment:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";

const app = express();
app.use(express.json());

const server = new McpServer({
  name: "my-remote-server",
  version: "1.0.0"
});

// Register tools (same as above)...

// Create Streamable HTTP transport
const transport = new StreamableHTTPServerTransport({
  endpoint: "/mcp"
});

// Mount to Express
app.post("/mcp", transport.handlePost.bind(transport));
app.get("/mcp", transport.handleGet.bind(transport));

await server.connect(transport);

app.listen(3000, () => {
  console.error("MCP Server (Streamable HTTP) running on port 3000");
});

4.6 Testing and Debugging

MCP Inspector

The official interactive debugging tool:
npx @modelcontextprotocol/inspector node dist/index.js
The Inspector opens a web interface where you can:
  • View all tools, resources, and prompts exposed by the Server
  • Manually enter parameters and execute tools
  • Inspect the raw JSON-RPC requests and responses
  • Validate inputSchema and outputSchema

Testing in Claude Desktop

Edit the Claude Desktop configuration file: macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["D:/path/to/my-server/dist/index.js"]
    }
  }
}
Restart Claude Desktop and try using your tools in a conversation.

4.7 Common Errors

ErrorCauseFix
Server starts but does not respondconsole.log output to stdout interferes with the protocolSend logs to stderr instead
Tools do not appeartools/list returns emptyCheck tool registration code
Parameter validation failsinputSchema and actual parameters mismatchCheck JSON Schema definition
Connection timeoutServer process did not start correctlyCheck command and args configuration
structuredContent is ignoredNo outputSchema definedAdd outputSchema or use content only
Initialization failsProtocol version mismatchConfirm Client and Server use compatible versions
stdio Servers must never use console.log. stdout is the protocol channel; any non-JSON-RPC output will break communication. Use console.error or write to a log file instead.

Next Chapter: Commerce MCP Server — MCP Server design patterns for e-commerce scenarios