The Model Context Protocol (MCP) is an open standard that enables AI assistants to interact with external tools and data sources. By building your own MCP server, you can extend AI capabilities with custom functionality like weather data fetching, browser automation, API integrations, and more. I built my custom MCP server using the official TypeScript SDK, which provides a robust foundation for implementing the protocol. This guide walks through creating a production-ready MCP server using TypeScript.
Creating the Server
The main server file initializes the MCP server, registers request handlers, and manages the transport layer. Here is the complete server setup:
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { tools } from './tools/index.js';
function createServer(): Server {
const server = new Server(
{
name: 'custom-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Handle tool listing requests
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// Handle tool execution requests
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = tools.find((t) => t.name === name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
return await tool.handler(args);
});
return server;
}
async function main(): Promise<void> {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
// Graceful shutdown handling
process.on('SIGINT', async () => {
await server.close();
process.exit(0);
});
}
main().catch((err) => {
console.error('Failed to start server:', err);
process.exit(1);
});
Building Tools with Zod Validation
Each tool needs three components: input schema validation using Zod, a handler function, and a tool definition export. Here is a weather tool example:
import { z } from 'zod';
// Define input schema with validation
const WeatherToolInputSchema = z.object({
city: z.string().min(1, 'City name is required'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
});
// Tool handler function
async function weatherToolHandler(input: unknown) {
const validated = WeatherToolInputSchema.parse(input);
// Call weather API or service
const weatherData = {
city: validated.city,
temperature: 22,
condition: 'Partly Cloudy',
humidity: 65,
units: validated.units,
};
return {
content: [{
type: 'text',
text: JSON.stringify(weatherData, null, 2),
}],
};
}
// Export tool definition
export const weatherTool = {
name: 'fetch_weather',
description: 'Get current weather information for a specified city',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'Name of the city to get weather for',
},
units: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'Temperature units (default: celsius)',
},
},
required: ['city'],
},
handler: weatherToolHandler,
};
Centralized Tool Registry
Maintain a single registry for all tools to simplify server setup:
import { weatherTool } from './weather-tool.js';
import { browserUrlTool } from './browser-url-tool.js';
export const tools = [
weatherTool,
browserUrlTool,
];
export function getToolByName(name: string) {
return tools.find((tool) => tool.name === name);
}
export function getToolNames(): string[] {
return tools.map((tool) => tool.name);
}
Deploying the MCP Server
Build the TypeScript project:
npm run build
Configure the MCP client (like LM Studio) to use your server:
{
"mcpServers": {
"custom-mcp": {
"command": "node",
"args": ["/path/to/my-mcp/dist/index.js"],
"cwd": "/path/to/my-mcp"
}
}
}
Best Practices
- Use Zod for validation: Catch invalid inputs before tool execution
- Implement proper error handling: Return structured error responses
- Add logging: Track tool calls and failures for debugging
- Validate configuration: Ensure required environment variables are present
- Handle graceful shutdown: Clean up resources on SIGINT/SIGTERM
- Keep tools focused: Each tool should do one thing well
- Document input schemas: Clear descriptions help AI clients use tools correctly
Building custom MCP servers opens up endless possibilities for extending AI assistant capabilities. Whether you need weather data, browser automation, database queries, or API integrations, the MCP protocol provides a standardized way to make these resources available to AI clients.
I use this custom MCP with Claude Code to enhance my development workflow. I've built multiple tools and custom MCPs for different workflows. The best part about this approach is that I can filter and enable only the tools I need for a specific workflow, preventing unnecessary context bloat. This selective tool loading keeps the AI assistant focused and responsive while still having access to powerful capabilities when needed.
For more tips on getting the most out of Claude Code, check out my guide on Claude Code best practices.