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/sdkStep 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 mcpCreate 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
| Practice | Description |
|---|---|
| Validate input | Always validate tool arguments |
| Limit operations | Restrict to safe operations |
| Use env vars | Never hardcode credentials |
| Log carefully | Don't log sensitive data |
Performance
| Practice | Description |
|---|---|
| Cache results | Cache expensive operations |
| Timeout handlers | Set reasonable timeouts |
| Batch requests | Combine multiple queries |
| Lazy load | Load 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
- Enable server in Cursor
- Open Agent Chat
- Ask a question that uses your tool
- 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:
npm package:
bashnpm publishUsers install with:
json{ "mcpServers": { "your-server": { "command": "npx", "args": ["-y", "your-package-name"] } } }GitHub: Users clone and configure path