Skip to content

Building an MCP Server

Learn how to create a custom Model Context Protocol (MCP) server to extend Cursor's capabilities.


Overview

MCP servers allow you to:

  • Connect external data sources
  • Add custom tools for agents
  • Integrate with internal systems
  • Provide specialized context

Prerequisites

  • Node.js 18+ or Python 3.8+
  • Understanding of MCP protocol
  • Cursor with MCP enabled

Quick Start: Node.js Server

Step 1: Create Project

bash
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk

Step 2: Create Server

Create index.js:

javascript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

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

// Define a tool
server.setRequestHandler("tools/list", async () => ({
  tools: [{
    name: "get_weather",
    description: "Get weather for a location",
    inputSchema: {
      type: "object",
      properties: {
        location: {
          type: "string",
          description: "City name"
        }
      },
      required: ["location"]
    }
  }]
}));

// Handle tool calls
server.setRequestHandler("tools/call", async (request) => {
  if (request.params.name === "get_weather") {
    const location = request.params.arguments.location;
    // Your logic here
    return {
      content: [{
        type: "text",
        text: `Weather in ${location}: Sunny, 72°F`
      }]
    };
  }
  throw new Error("Unknown tool");
});

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

Step 3: Configure in Cursor

Add to .cursor/mcp.json:

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

Step 4: Restart Cursor

Restart Cursor to load the MCP server.


Python Server Example

Setup

bash
pip install mcp

Create Server

Create server.py:

python
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

server = Server("my-python-server")

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="search_database",
            description="Search the internal database",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string"}
                },
                "required": ["query"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "search_database":
        query = arguments["query"]
        # Your database logic
        results = f"Results for: {query}"
        return [TextContent(type="text", text=results)]
    raise ValueError(f"Unknown tool: {name}")

async def main():
    async with stdio_server() as (read, write):
        await server.run(read, write)

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

Configure

json
{
  "mcpServers": {
    "python-server": {
      "command": "python",
      "args": ["path/to/server.py"]
    }
  }
}

Adding Resources

Resources provide read-only data:

javascript
server.setRequestHandler("resources/list", async () => ({
  resources: [{
    uri: "config://settings",
    name: "Application Settings",
    description: "Current app configuration"
  }]
}));

server.setRequestHandler("resources/read", async (request) => {
  if (request.params.uri === "config://settings") {
    return {
      contents: [{
        uri: "config://settings",
        mimeType: "application/json",
        text: JSON.stringify({ theme: "dark", version: "1.0" })
      }]
    };
  }
  throw new Error("Unknown resource");
});

Adding Prompts

Prompts are reusable templates:

javascript
server.setRequestHandler("prompts/list", async () => ({
  prompts: [{
    name: "review_code",
    description: "Review code for best practices",
    arguments: [{
      name: "file_path",
      description: "Path to file to review",
      required: true
    }]
  }]
}));

server.setRequestHandler("prompts/get", async (request) => {
  if (request.params.name === "review_code") {
    const filePath = request.params.arguments?.file_path;
    return {
      messages: [{
        role: "user",
        content: {
          type: "text",
          text: `Review this file for best practices: ${filePath}`
        }
      }]
    };
  }
  throw new Error("Unknown prompt");
});

Database Integration Example

Connect to PostgreSQL:

javascript
import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL
});

server.setRequestHandler("tools/call", async (request) => {
  if (request.params.name === "query_database") {
    const query = request.params.arguments.query;

    // Safety: Only allow SELECT queries
    if (!query.trim().toLowerCase().startsWith("select")) {
      throw new Error("Only SELECT queries allowed");
    }

    const result = await pool.query(query);
    return {
      content: [{
        type: "text",
        text: JSON.stringify(result.rows, null, 2)
      }]
    };
  }
});

HTTP Transport

For remote servers, use SSE transport:

json
{
  "mcpServers": {
    "remote-server": {
      "url": "http://localhost:3001/sse"
    }
  }
}

Server setup with Express:

javascript
import express from "express";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";

const app = express();

app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/message", res);
  await server.connect(transport);
});

app.post("/message", async (req, res) => {
  // Handle incoming messages
});

app.listen(3001);

Best Practices

Security

PracticeDescription
Validate inputAlways validate tool arguments
Limit operationsRestrict to safe operations
Use env varsNever hardcode credentials
Log carefullyDon't log sensitive data

Performance

PracticeDescription
Cache resultsCache expensive operations
Timeout handlersSet reasonable timeouts
Batch requestsCombine multiple queries
Lazy loadLoad resources on demand

Error Handling

javascript
server.setRequestHandler("tools/call", async (request) => {
  try {
    // Your logic
  } catch (error) {
    console.error("Tool error:", error);
    return {
      content: [{
        type: "text",
        text: `Error: ${error.message}`
      }],
      isError: true
    };
  }
});

Testing Your Server

Manual Testing

  1. Enable server in Cursor
  2. Open Agent Chat
  3. Ask a question that uses your tool
  4. Verify the tool is called

Debug Logging

Add logging to your server:

javascript
console.error("Tool called:", request.params);

View logs in Cursor's MCP panel.


Publishing

Share your server:

  1. npm package:

    bash
    npm publish

    Users install with:

    json
    {
      "mcpServers": {
        "your-server": {
          "command": "npx",
          "args": ["-y", "your-package-name"]
        }
      }
    }
  2. GitHub: Users clone and configure path


See Also

Built with VitePress | Powered by Claude AI