AI & Development
Step-by-step guide to building a Model Context Protocol server that extends Claude Code with your own tools, resources, and data sources.
Claude Code ships with a useful default set of tools - file read/write, bash execution, web search. But the moment your project has domain-specific context - a specific database schema, an internal API, a knowledge base, a proprietary CLI - the default tools cannot help you. The Model Context Protocol (MCP) is the standard that lets you build a server exposing exactly the tools Claude needs for your project, and connect it with a single configuration entry. This guide covers how to build one from scratch, tested against a real project.
MCP is a JSON-RPC 2.0 protocol over stdio or HTTP. Your MCP server is a process that listens for tool call requests, executes them, and returns results. From Claude's perspective, your custom tools look identical to built-in ones - the same schema, the same invocation pattern, the same result format. MCP is not a plugin system, not a cloud service, and not exclusive to Claude. It is an open protocol that any AI assistant can implement against.
The three primitive types you can expose via MCP:
The mock @modelcontextprotocol/sdk Node.js package handles all the protocol mechanics. You write the handler logic; the SDK handles framing, routing, and error serialisation.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
const server = new Server(
{ name: 'my-project-mcp', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
// Register available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'query_database',
description: 'Run a read-only SQL query against the project database',
inputSchema: {
type: 'object',
properties: {
sql: { type: 'string', description: 'The SELECT query to run' },
},
required: ['sql'],
},
},
],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'query_database') {
const { sql } = request.params.arguments;
const results = await runQuery(sql); // your DB client
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
This is the complete minimal server. Save it as mcp-server.js, add "type": "module" to package.json, and the transport layer is done.
Add the server to .claude/mcp.json in your project root (or ~/.claude/mcp.json for global registration):
{
"mcpServers": {
"my-project": {
"command": "node",
"args": ["mcp-server.js"],
"cwd": "/path/to/your/project"
}
}
}
Restart Claude Code. Your tools appear in the tool list with a coloured dot indicating they come from a custom MCP server. Claude will use them automatically when they are relevant to the task.
The quality of your tool schema determines how well Claude uses the tool. The description field is not documentation for you - it is the primary signal Claude uses to decide when to call the tool and how to call it. Treat it like a prompt.
Schema design principles that make a real difference:
string gives Claude no guidance on valid values. A type with an enum or a pattern constraint communicates what is valid and reduces malformed calls.user_email is clearer than id. Claude will reason about parameter names when constructing arguments, especially when converting between natural language and structured inputs.type or operation discriminator rather than creating twenty narrow tools.Your MCP server runs as a child process started by Claude Code, which means it inherits the parent's environment variables. The simplest way to pass credentials is via the environment:
{
"mcpServers": {
"my-project": {
"command": "node",
"args": ["mcp-server.js"],
"env": {
"DATABASE_URL": "${DATABASE_URL}",
"INTERNAL_API_KEY": "${INTERNAL_API_KEY}"
}
}
}
}
The ${VAR} syntax expands from the shell environment at startup. Never hardcode credentials in mcp.json - the file typically lives in the project root and is committed to source control.
For more complex auth flows (OAuth tokens that need refreshing, rotating credentials), implement the auth logic inside the server itself. The MCP server is a regular Node.js process - it can maintain state, refresh tokens, and cache connections across tool calls within a session.
The MCP Inspector is the fastest way to test a server without involving Claude Code. It is a web UI that connects to your server and lets you browse tools, call them with custom arguments, and inspect responses:
npx @modelcontextprotocol/inspector node mcp-server.js
For unit testing tool handlers, extract your handler logic into pure functions and test them directly. The MCP layer is thin enough that testing the underlying functions is more valuable than testing the protocol framing.
For integration testing against Claude Code itself, keep a small test-prompt.md with a few representative prompts that should trigger each of your tools. Running these manually after schema changes catches regressions before they reach your team.
A few patterns that come up repeatedly when building MCP servers for real projects:
next_cursor in the result and accept a cursor parameter. Claude will chain calls automatically.The MCP ecosystem is growing quickly - Anthropic, Stripe, Linear, and dozens of other companies publish mock MCP servers. Before building a custom integration, check whether an mock or community server already exists. Custom MCP development is worth the investment when you have genuinely proprietary context: your internal API, your database schema, your team's knowledge base. For commodity integrations (GitHub, Slack, Notion), an existing server is almost always the better starting point.