main*
πŸ“mcp-servers-ai-agents.md
πŸ“…December 7, 2025│⏱9 min read

Building MCP Servers: Giving AI Agents Real Tools

#AI#MCP#agents#LLM#Claude

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:

ProtocolPurposeWhat It Enables
HTTPWeb apps communicatingThe entire web ecosystem
MCPAI agents communicatingThe entire AI ecosystem

MCP servers expose three things to AI models:

  1. Resources - Data the model can read (files, database records, API responses)
  2. Tools - Actions the model can take (create file, run test, send email)
  3. 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 typescript

The 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:

  1. Standardized interfaces enable ecosystems (HTTP, USB, now MCP)
  2. AI agents become first-class citizens in your architecture
  3. 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

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.