Free developer tools and practical guides for SQL, data workflows, and debugging.
AAskDBSQL & Data Toolkit

Model Context Protocol (MCP) Guide: Build AI Tool Integrations

·12 min read

The Model Context Protocol (MCP) is an open standard by Anthropic that defines how AI models connect to external tools, databases, and data sources. It replaces one-off custom integrations with a universal adapter pattern — build once, connect to any MCP-compatible AI client.

MCP architecture

// MCP defines 3 core primitives:
//
// Tools    — functions the AI can call (read/write actions)
// Resources — data the AI can read (files, databases, APIs)
// Prompts  — reusable prompt templates with parameters
//
// Communication flow:
//
// AI Client (Claude, Cursor, VS Code)
//   ↕ JSON-RPC over stdio / SSE / WebSocket
// MCP Server (your integration)
//   ↕ native SDK / API calls
// External system (database, GitHub, file system, etc.)

Setup

npm install @modelcontextprotocol/sdk

Build a simple MCP server

// server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

const server = new Server(
  { name: 'askdb-tools', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

// Register available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'run_sql',
      description: 'Execute a read-only SQL query against the analytics database',
      inputSchema: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'A valid SELECT SQL query' },
          database: { type: 'string', enum: ['analytics', 'reporting'], description: 'Target database' },
        },
        required: ['query'],
      },
    },
    {
      name: 'get_schema',
      description: 'Get the schema for a specific database table',
      inputSchema: {
        type: 'object',
        properties: {
          tableName: { type: 'string', description: 'Table name to inspect' },
        },
        required: ['tableName'],
      },
    },
  ],
}));

// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === 'run_sql') {
    const { query, database = 'analytics' } = args as { query: string; database?: string };

    // Safety: only allow SELECT
    if (!query.trim().toUpperCase().startsWith('SELECT')) {
      return { content: [{ type: 'text', text: 'Error: Only SELECT queries are permitted.' }], isError: true };
    }

    try {
      const results = await executeQuery(database, query);
      return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
    } catch (err) {
      return { content: [{ type: 'text', text: `SQL error: ${err}` }], isError: true };
    }
  }

  if (name === 'get_schema') {
    const { tableName } = args as { tableName: string };
    const schema = await getTableSchema(tableName);
    return { content: [{ type: 'text', text: schema }] };
  }

  throw new Error(`Unknown tool: ${name}`);
});

// Start server (stdio transport for Claude Desktop)
const transport = new StdioServerTransport();
await server.connect(transport);

Resources (read-only data)

import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: 'file:///docs/api-reference.md',
      name: 'API Reference',
      description: 'Complete API documentation',
      mimeType: 'text/markdown',
    },
    {
      uri: 'db://analytics/metrics/daily',
      name: 'Daily Metrics',
      description: 'Yesterday's key metrics',
      mimeType: 'application/json',
    },
  ],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri === 'file:///docs/api-reference.md') {
    const content = await fs.readFile('./docs/api-reference.md', 'utf-8');
    return { contents: [{ uri, mimeType: 'text/markdown', text: content }] };
  }

  if (uri === 'db://analytics/metrics/daily') {
    const metrics = await fetchDailyMetrics();
    return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(metrics) }] };
  }

  throw new Error(`Resource not found: ${uri}`);
});

Prompt templates

import {
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

server.setRequestHandler(ListPromptsRequestSchema, async () => ({
  prompts: [
    {
      name: 'analyze_sql_query',
      description: 'Analyze a SQL query for performance issues',
      arguments: [
        { name: 'query', description: 'The SQL query to analyze', required: true },
        { name: 'dialect', description: 'SQL dialect (postgresql, mysql)', required: false },
      ],
    },
  ],
}));

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === 'analyze_sql_query') {
    const dialect = (args?.dialect as string) ?? 'postgresql';
    return {
      messages: [
        {
          role: 'user',
          content: {
            type: 'text',
            text: `Analyze this ${dialect} query for performance issues, missing indexes, and improvements:

\`\`\`sql
${args?.query}
\`\`\``,
          },
        },
      ],
    };
  }

  throw new Error(`Unknown prompt: ${name}`);
});

Connect to Claude Desktop

// ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "askdb-tools": {
      "command": "node",
      "args": ["/path/to/your/server.js"],
      "env": {
        "DATABASE_URL": "postgresql://localhost/analytics"
      }
    }
  }
}

HTTP/SSE transport for remote servers

// For web-accessible MCP servers (instead of stdio)
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';

const app = express();
const transports = new Map<string, SSEServerTransport>();

app.get('/sse', async (req, res) => {
  const sessionId = crypto.randomUUID();
  const transport = new SSEServerTransport('/messages', res);
  transports.set(sessionId, transport);

  res.on('close', () => transports.delete(sessionId));

  await server.connect(transport);
});

app.post('/messages', async (req, res) => {
  const sessionId = req.query.sessionId as string;
  const transport = transports.get(sessionId);
  if (!transport) return res.status(404).end();
  await transport.handlePostMessage(req, res);
});

app.listen(3001);

Security checklist

  • Allowlist tool names — reject any call to an unregistered tool.
  • Validate all tool arguments with Zod before execution.
  • Use read-only database connections for data tools.
  • Log every tool call with args, duration, and result size.
  • Rate-limit tool calls per session to prevent runaway agents.
  • Never expose internal service credentials via tool outputs.

Takeaway

MCP turns your internal tools into AI-accessible capabilities without building custom integrations per model. Build your MCP server once, then connect it to Claude Desktop, Cursor, VS Code Copilot, or any other MCP-compatible client. Focus security investment on tool argument validation and read-only data access.