Introduction
The Model Context Protocol (MCP) has emerged as the standard for connecting AI assistants to external tools and data sources. While pre-built MCP servers exist, building custom servers allows you to integrate any tool, API, or data source with AI models.
This comprehensive guide teaches you how to build MCP servers from scratch, covering architecture, implementation patterns, and deployment strategies.
Understanding MCP Architecture
Protocol Overview
MCP uses a client-server architecture:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MCP Host โ
โ (Cursor, Claude, ChatGPT) โ
โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ JSON-RPC 2.0
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MCP Server โ
โ - Resources โ
โ - Tools โ
โ - Prompts โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Core Concepts
| Component | Purpose |
|---|---|
| Resources | Data sources AI can read |
| Tools | Actions AI can execute |
| Prompts | Pre-defined prompt templates |
Setting Up Your Environment
Installation
# Create project
mkdir my-mcp-server
cd my-mcp-server
# Initialize
npm init -y
# Install MCP SDK
npm install @modelcontextprotocol/sdk
# TypeScript support
npm install -D typescript @types/node
Project Structure
my-mcp-server/
โโโ src/
โ โโโ index.ts # Main entry
โ โโโ resources.ts # Resource handlers
โ โโโ tools.ts # Tool handlers
โ โโโ prompts.ts # Prompt templates
โโโ package.json
โโโ tsconfig.json
Building Your First MCP Server
Basic Server Implementation
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
class MyServer {
constructor() {
this.server = new Server(
{
name: 'my-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
this.setupHandlers();
}
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
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
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'get_weather':
return await this.getWeather(args.location);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
}
private async getWeather(location: string) {
// Implement weather API call
const response = await fetch(
`https://api.weather.com/v3/wx/conditions/current?location=${location}`
);
const data = await response.json();
return {
content: [
{
type: 'text',
text: `Weather in ${location}: ${data.temperature}ยฐF, ${data.description}`
}
]
};
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('MCP Server running on stdio');
}
}
const server = new MyServer();
server.start();
Creating Tools
Tool Patterns
1. API Integration
// tools/api.ts
export const apiTools = [
{
name: 'search_github',
description: 'Search GitHub repositories',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
language: { type: 'string', description: 'Programming language' }
}
}
},
{
name: 'get_github_user',
description: 'Get GitHub user info',
inputSchema: {
type: 'object',
properties: {
username: { type: 'string' }
},
required: ['username']
}
}
];
export async function handleGitHubTool(
name: string,
args: Record<string, any>
) {
const headers = {
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json'
};
switch (name) {
case 'search_github': {
const response = await fetch(
`https://api.github.com/search/repositories?q=${encodeURIComponent(args.query)}`,
{ headers }
);
const data = await response.json();
return formatResults(data.items);
}
case 'get_github_user': {
const response = await fetch(
`https://api.github.com/users/${args.username}`,
{ headers }
);
const user = await response.json();
return formatUser(user);
}
}
}
function formatResults(items: any[]) {
return {
content: [{
type: 'text',
text: items.slice(0, 10).map(item =>
`- ${item.full_name}: ${item.description || 'No description'}`
).join('\n')
}]
};
}
2. Database Operations
// tools/database.ts
export const dbTools = [
{
name: 'query_users',
description: 'Query users from database',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', default: 10 },
offset: { type: 'number', default: 0 }
}
}
},
{
name: 'create_user',
description: 'Create a new user',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string' }
},
required: ['name', 'email']
}
}
];
export async function handleDbTool(name: string, args: any) {
const db = getDbConnection();
switch (name) {
case 'query_users': {
const users = await db.user.findMany({
take: args.limit,
skip: args.offset
});
return { content: [{ type: 'text', text: JSON.stringify(users) }] };
}
case 'create_user': {
const user = await db.user.create({
data: { name: args.name, email: args.email }
});
return { content: [{ type: 'text', text: JSON.stringify(user) }] };
}
}
}
Resources
File System Resources
// resources/filesystem.ts
export const fileResources = [
{
uri: 'file://config',
name: 'Configuration',
description: 'App configuration file',
mimeType: 'application/json'
},
{
uri: 'file://logs',
name: 'Application Logs',
description: 'Recent application logs',
mimeType: 'text/plain'
}
];
export async function handleResourceRequest(uri: string) {
const path = uri.replace('file://', '');
switch (uri) {
case 'file://config':
const config = await fs.readFile('./config.json', 'utf-8');
return {
contents: [{
uri,
mimeType: 'application/json',
text: config
}]
};
case 'file://logs':
const logs = await fs.readFile('./logs/app.log', 'utf-8');
const recent = logs.split('\n').slice(-100).join('\n');
return {
contents: [{
uri,
mimeType: 'text/plain',
text: recent
}]
};
}
}
Prompts
Template Prompts
// prompts.ts
export const prompts = [
{
name: 'analyze_code',
description: 'Analyze code for issues',
arguments: [
{
name: 'code',
description: 'Code to analyze',
required: true
},
{
name: 'language',
description: 'Programming language',
required: false
}
]
},
{
name: 'review_pr',
description: 'Review a pull request',
arguments: [
{
name: 'pr_url',
description: 'URL of the PR',
required: true
}
]
}
];
export function getPrompt(name: string, args: Record<string, string>) {
switch (name) {
case 'analyze_code':
return `Analyze the following ${args.language || 'code'} for issues, bugs, and improvements:\n\n${args.code}`;
case 'review_pr':
return `Review the pull request at ${args.pr_url} for:\n- Code quality\n- Potential bugs\n- Security issues\n- Performance concerns`;
}
}
Testing MCP Servers
Using the MCP Inspector
# Install inspector
npm install -g @modelcontextprotocol/inspector
# Run inspector
mcp-inspector node ./dist/index.js
Unit Testing
import { describe, it, expect, vi } from 'vitest';
describe('MCP Server Tools', () => {
it('should return weather data', async () => {
// Mock fetch
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({
temperature: 72,
description: 'Sunny'
})
});
const result = await handleGitHubTool('get_weather', {
location: 'New York'
});
expect(result.content[0].text).toContain('72');
});
});
Best Practices
Security
// Always validate inputs
function validateToolInput(args: any, schema: any) {
if (schema.required) {
for (const field of schema.required) {
if (!args[field]) {
throw new Error(`Missing required field: ${field}`);
}
}
}
// Validate types
for (const [key, value] of Object.entries(args)) {
const expectedType = schema.properties[key]?.type;
if (expectedType && typeof value !== expectedType) {
throw new Error(`Invalid type for ${key}: expected ${expectedType}`);
}
}
}
// Use environment variables for secrets
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error('API_KEY environment variable required');
}
Error Handling
async function safeToolHandler(name: string, args: any) {
try {
return await handleTool(name, args);
} catch (error) {
return {
content: [{
type: 'text',
text: `Error: ${error.message}`
}],
isError: true
};
}
}
External Resources
Documentation
Examples
Conclusion
Building MCP servers enables powerful integrations between AI models and your tools, data, and services. By following this guide, you can create custom servers that expose any capability to AI assistants.
Key takeaways:
- Start with the SDK - Simplifies protocol handling
- Define clear tools - Well-documented tools work best
- Handle errors gracefully - Return meaningful error messages
- Test thoroughly - Use inspector and unit tests
- Secure your server - Validate inputs, protect secrets
Comments