Building MCP Servers: Giving AI Agents Real Tools
The Problem: AI That Only Talks
LLMs are impressive. But they're trapped in a text box. You ask Claude to help with your codebase, and it can:
- Explain your code
- Suggest improvements
- Write new code
But it can't:
- Actually read your files
- Run your tests
- Deploy your changes
- Query your database
Every interaction is copy-paste between your terminal and the chat. That's not automation. That's assisted manual labor.
MCP: The HTTP for AI Agents
Model Context Protocol (MCP) is Anthropic's open standard for connecting AI models to external systems. Think of it as:
| Protocol | Purpose | What It Enables |
|---|---|---|
| HTTP | Web apps communicating | The entire web ecosystem |
| MCP | AI agents communicating | The entire AI ecosystem |
MCP servers expose three things to AI models:
- Resources - Data the model can read (files, database records, API responses)
- Tools - Actions the model can take (create file, run test, send email)
- Prompts - Pre-defined prompt templates for common tasks
The Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AI Application β
β (Claude Desktop, Continue, etc.) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β MCP Protocol
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MCP Server β
β β
β Resources Tools Prompts β
β βββββββββββ βββββββββββ βββββββββββ β
β β Files β β Execute β β Review β β
β β DB Rows β β Query β β Debug β β
β β API Dataβ β Create β β Explain β β
β βββββββββββ βββββββββββ βββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your Systems β
β FileSystem Database APIs Git Cloud Services β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Building Your First MCP Server
Let's build a server that gives Claude access to a SQLite database.
Project Setup
mkdir mcp-sqlite && cd mcp-sqlite
npm init -y
npm install @modelcontextprotocol/sdk better-sqlite3
npm install -D @types/better-sqlite3 typescriptThe Server Structure
// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import Database from 'better-sqlite3';
const db = new Database('./mydb.sqlite');
const server = new Server(
{ name: 'sqlite-server', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {} } }
);Defining Tools
Tools are actions the AI can take:
// src/index.ts (continued)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'query',
description: 'Execute a SQL query on the database. Returns results as JSON.',
inputSchema: {
type: 'object',
properties: {
sql: {
type: 'string',
description: 'The SQL query to execute. Use ? for parameters.',
},
params: {
type: 'array',
items: { type: 'string' },
description: 'Parameters for the SQL query.',
},
},
required: ['sql'],
},
},
{
name: 'list_tables',
description: 'List all tables in the database.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'describe_table',
description: 'Get the schema for a specific table.',
inputSchema: {
type: 'object',
properties: {
table: {
type: 'string',
description: 'The table name to describe.',
},
},
required: ['table'],
},
},
],
}));Implementing Tool Handlers
// src/index.ts (continued)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'query': {
try {
const stmt = db.prepare(args.sql as string);
const results = args.params
? stmt.all(...(args.params as any[]))
: stmt.all();
return {
content: [{
type: 'text',
text: JSON.stringify(results, null, 2),
}],
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: `Error: ${error.message}`,
}],
isError: true,
};
}
}
case 'list_tables': {
const tables = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
`).all();
return {
content: [{
type: 'text',
text: tables.map((t: any) => t.name).join('\n'),
}],
};
}
case 'describe_table': {
const info = db.pragma(`table_info(${args.table})`);
return {
content: [{
type: 'text',
text: JSON.stringify(info, null, 2),
}],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});Defining Resources
Resources are data the AI can read:
// src/index.ts (continued)
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'sqlite://tables',
name: 'Database Tables',
description: 'List of all tables in the database',
mimeType: 'application/json',
},
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'sqlite://tables') {
const tables = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`).all();
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(tables, null, 2),
}],
};
}
throw new Error(`Unknown resource: ${uri}`);
});Starting the Server
// src/index.ts (continued)
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('SQLite MCP server running on stdio');
}
main().catch(console.error);Configuring Claude Desktop
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"sqlite": {
"command": "node",
"args": ["/path/to/mcp-sqlite/dist/index.js"]
}
}
}Restart Claude Desktop. Now when you chat with Claude:
You: What tables are in my database?
Claude: I'll check your database tables.
[Claude calls list_tables tool]
Claude: Your database has these tables:
- users
- posts
- comments
- tags
Would you like me to describe any of them?
Security Considerations
MCP gives AI real power. Use it responsibly.
1. Never Expose Destructive Operations Without Confirmation
// DON'T do this
{
name: 'delete_all_users',
description: 'Delete all users from the database',
inputSchema: { type: 'object', properties: {} }
}
// DO this instead
{
name: 'delete_user',
description: 'Delete a specific user. Requires confirmation.',
inputSchema: {
type: 'object',
properties: {
userId: { type: 'string', description: 'The user ID to delete' },
confirm: { type: 'boolean', description: 'Set to true to confirm deletion' }
},
required: ['userId', 'confirm']
}
}2. Validate and Sanitize All Inputs
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'query') {
// Basic SQL injection prevention
const forbidden = ['DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE'];
const sql = (args.sql as string).toUpperCase();
if (forbidden.some(word => sql.includes(word))) {
return {
content: [{
type: 'text',
text: 'Error: Destructive operations not allowed through this tool.',
}],
isError: true,
};
}
// ... rest of handler
}
});3. Log Everything
const log = {
timestamp: new Date().toISOString(),
tool: name,
arguments: args,
result: undefined as any,
error: undefined as any,
};
try {
const result = await executeTool(name, args);
log.result = result;
return result;
} catch (error: any) {
log.error = error.message;
throw error;
} finally {
// Send to your logging system
auditLogger.info(log);
}Real-World MCP Servers I've Built
1. Code Review Agent
// MCP server that reviews PRs
const tools = [
{
name: 'get_pr_diff',
description: 'Get the diff for a pull request',
inputSchema: {
type: 'object',
properties: {
repo: { type: 'string', description: 'Repository name (owner/repo)' },
prNumber: { type: 'number', description: 'PR number' },
},
required: ['repo', 'prNumber'],
},
},
{
name: 'run_tests',
description: 'Run tests for changed files',
inputSchema: {
type: 'object',
properties: {
files: {
type: 'array',
items: { type: 'string' },
description: 'Files to test'
},
},
},
},
{
name: 'post_comment',
description: 'Post a review comment on the PR',
inputSchema: {
type: 'object',
properties: {
repo: { type: 'string' },
prNumber: { type: 'number' },
file: { type: 'string', description: 'File to comment on' },
line: { type: 'number', description: 'Line number' },
body: { type: 'string', description: 'Comment text' },
},
required: ['repo', 'prNumber', 'body'],
},
},
];Claude can now review PRs autonomously:
- Fetches the diff
- Identifies potential issues
- Runs relevant tests
- Posts comments with suggestions
2. Infrastructure Agent
// MCP server for infrastructure queries
const tools = [
{
name: 'query_prometheus',
description: 'Query Prometheus metrics',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'PromQL query' },
timeRange: { type: 'string', description: 'Time range (1h, 24h, 7d)' },
},
required: ['query'],
},
},
{
name: 'get_pods',
description: 'List Kubernetes pods with issues',
inputSchema: {
type: 'object',
properties: {
namespace: { type: 'string', description: 'Kubernetes namespace' },
status: { type: 'string', enum: ['Running', 'Error', 'CrashLoopBackOff', 'Pending'] },
},
},
},
{
name: 'suggest_fix',
description: 'Suggest fixes for infrastructure issues',
inputSchema: {
type: 'object',
properties: {
issue: { type: 'string', description: 'Description of the issue' },
context: { type: 'string', description: 'Additional context' },
},
required: ['issue'],
},
},
];Usage:
You: Check why my API is slow
Claude: I'll investigate your API performance.
[Queries Prometheus for latency metrics]
[Checks pod resource usage]
[Analyzes log patterns]
Claude: Your API latency spiked at 2pm. The cause:
1. Pod memory is at 95% - triggering GC pauses
2. Database connection pool is exhausted
Recommendations:
1. Increase pod memory from 512Mi to 1Gi
2. Increase connection pool from 10 to 20
3. Add connection timeout of 5s
Want me to prepare the kubectl commands?
3. Documentation Agent
// MCP server that generates docs from code
const tools = [
{
name: 'read_file',
description: 'Read a file from the repository',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'File path' },
},
required: ['path'],
},
},
{
name: 'generate_docs',
description: 'Generate documentation for a module',
inputSchema: {
type: 'object',
properties: {
module: { type: 'string', description: 'Module path' },
format: { type: 'string', enum: ['markdown', 'jsdoc', 'tsdoc'] },
},
required: ['module'],
},
},
{
name: 'update_wiki',
description: 'Update the project wiki with new docs',
inputSchema: {
type: 'object',
properties: {
page: { type: 'string', description: 'Wiki page name' },
content: { type: 'string', description: 'Markdown content' },
},
required: ['page', 'content'],
},
},
];The Future of AI Agents
MCP is early. But the pattern is clear:
- Standardized interfaces enable ecosystems (HTTP, USB, now MCP)
- AI agents become first-class citizens in your architecture
- The gap between "can think" and "can do" closes
Imagine:
- Agents that maintain your infrastructure 24/7
- Agents that review every PR within minutes
- Agents that document your codebase as you write it
- Agents that debug production issues before you wake up
MCP makes this possible by giving models real tools, not just text.
Resources
- MCP Specification - Official docs
- MCP TypeScript SDK - Reference implementation
- Awesome MCP Servers - Community servers
- Claude Desktop - First MCP client
Conclusion
MCP is to AI agents what HTTP was to web appsβa standard that enables an ecosystem.
The servers you build today are primitive compared to what's coming. But building them now teaches you how AI agents will integrate with real systems.
That knowledge will be valuable whether you're building agents, deploying them, or designing systems for them to interact with.
Building MCP servers? I'd love to see what you're working on. Find me on Twitter.