How to Set Up a Custom MCP Server
Step-by-step guide to creating, testing, and deploying your own Model Context Protocol server from scratch.
The Model Context Protocol (MCP) lets AI clients like Claude Desktop, Cursor, and Windsurf interact with your own tools and data sources. This guide walks you through building a custom MCP server from scratch.
Prerequisites
- Node.js 18+ or Python 3.10+
- An MCP-compatible client (Claude Desktop, Cursor, Windsurf, or the MCP Inspector)
- Basic familiarity with JSON and command-line tools
Choose Your SDK
MCP provides official SDKs for TypeScript and Python:
| Language | Package | Install |
|---|---|---|
| TypeScript | @modelcontextprotocol/sdk | npm install @modelcontextprotocol/sdk |
| Python | mcp | pip install mcp |
This guide uses the TypeScript SDK. The concepts translate directly to Python.
Step 1: Scaffold the Project
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
Create a server.ts file:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server(
{
name: 'my-custom-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
This creates an MCP server that communicates over standard input/output (stdio).
Step 2: Add a Tool
Tools are the core of MCP — they let AI clients perform actions. Here’s a simple tool that fetches weather data:
import { z } from 'zod';
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_weather',
description: 'Get current weather for a city',
inputSchema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' },
},
required: ['city'],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'get_weather') {
const city = request.params.arguments?.city;
// In production, call a real weather API here
return {
content: [
{
type: 'text',
text: `Weather for ${city}: 22°C, partly cloudy`,
},
],
};
}
throw new Error('Tool not found');
});
Make sure to import the request schemas:
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
Step 3: Add Resources
Resources expose data that the AI client can read. Here’s an example that serves documentation:
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'docs://getting-started',
name: 'Getting Started Guide',
mimeType: 'text/markdown',
description: 'How to get started with this server',
},
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === 'docs://getting-started') {
return {
contents: [
{
uri: request.params.uri,
mimeType: 'text/markdown',
text: '# Getting Started\n\nThis is my custom MCP server.',
},
],
};
}
throw new Error('Resource not found');
});
Step 4: Test with MCP Inspector
The MCP Inspector is a browser-based debugging tool:
npx @modelcontextprotocol/inspector node dist/server.js
This opens a web UI where you can:
- List all tools and resources
- Call tools with custom arguments
- Read resource contents
- Inspect the raw JSON-RPC messages
Step 5: Configure in Your Client
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"my-custom-server": {
"command": "node",
"args": ["path/to/dist/server.js"]
}
}
}
Cursor
In Cursor settings > Features > MCP Servers, add:
- Name:
my-custom-server - Type:
command - Command:
node path/to/dist/server.js
Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"my-custom-server": {
"command": "node",
"args": ["path/to/dist/server.js"]
}
}
}
Step 6: Add Environment Variables
Secure configuration values like API keys should be passed via environment variables:
const API_KEY = process.env.MY_API_KEY;
if (!API_KEY) {
console.error('MY_API_KEY environment variable is required');
process.exit(1);
}
Configure them in your client config:
{
"mcpServers": {
"my-custom-server": {
"command": "node",
"args": ["dist/server.js"],
"env": {
"MY_API_KEY": "sk-..."
}
}
}
}
Complete Example
Here’s a full MCP server that combines tools and resources:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{ name: 'my-custom-server', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'greet',
description: 'Greet a user by name',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
required: ['name'],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === 'greet') {
return {
content: [{ type: 'text', text: `Hello, ${req.params.arguments?.name}!` }],
};
}
throw new Error(`Unknown tool: ${req.params.name}`);
});
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'info://version',
name: 'Server Version',
mimeType: 'text/plain',
},
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
if (req.params.uri === 'info://version') {
return { contents: [{ uri: req.params.uri, mimeType: 'text/plain', text: '1.0.0' }] };
}
throw new Error(`Unknown resource: ${req.params.uri}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
Troubleshooting
| Problem | Solution |
|---|---|
| Server crashes on start | Check that all env variables are provided |
| Tool not found | Verify the tool name matches exactly in your handler |
| Connection refused | Ensure the server binary path is correct |
| JSON parse errors | Check for trailing commas or unquoted keys in your config |
Next Steps
- Use the MCPConfig Builder to generate your server configuration visually
- Explore the server templates for pre-built configurations
- Browse the server directory for community-built MCP servers