Back

Como construir servidores MCP em TypeScript: Guia completo 2026

Todo IDE com IA que presta em 2026 (Claude Desktop, Cursor, Windsurf, VS Code com Copilot) fala MCP. O Model Context Protocol virou o padrão universal pra conectar assistentes de IA com ferramentas externas, bancos de dados e APIs. Mas aqui tá o ponto: a maioria dos devs continua só consumindo servidores MCP, sem construir os próprios.

Isso é desperdiçar uma oportunidade. Construir seu próprio servidor MCP significa dar ao seu assistente de IA acesso a qualquer coisa: as APIs internas da empresa, o pipeline de deploy, o banco de dados, os dashboards de monitoramento, o CMS. A partir do momento que você constrói o primeiro servidor MCP, o assistente deixa de ser um chatbot genérico e vira uma extensão real do seu ambiente de desenvolvimento.

Este guia te leva por tudo que precisa pra construir servidores MCP de qualidade produção em TypeScript. Do zero ao production-ready, cobrindo os internos do protocolo, o SDK oficial, padrões do mundo real, segurança e estratégias de deploy.

O que é MCP (versão de 30 segundos)

Se você já usou servidores MCP mas nunca construiu um, o modelo mental é esse:

MCP é um protocolo JSON-RPC 2.0 que padroniza como clientes de IA (Claude, Cursor, etc.) se comunicam com provedores de capacidades externas (seu servidor). Pense no LSP (Language Server Protocol), só que pra assistentes de IA em vez de editores de código.

Seu servidor MCP pode expor três tipos de capacidades:

  1. Tools : Funções que a IA pode chamar (ex: "consultar o banco", "deployar pra staging", "criar ticket no Jira")
  2. Resources : Dados que a IA pode ler (ex: "o schema atual", "os error logs de hoje", "o status do deploy")
  3. Prompts : Templates de prompt pré-montados (ex: "analisa esse PR", "gera um plano de migração")

O cliente (Claude, Cursor) descobre o que seu servidor oferece, e o modelo decide quando e como usar essas capacidades baseado no contexto da conversa.

┌─────────────────┐         JSON-RPC 2.0         ┌─────────────────┐
│   Cliente IA    │ ◄──────────────────────────► │   Servidor MCP  │
│  (Claude, etc.) │    stdio / SSE / HTTP        │   (Seu código)  │
│                 │                               │                 │
│  - Descobre     │                               │  - Tools        │
│    capacidades  │                               │  - Resources    │
│  - Chama tools  │                               │  - Prompts      │
│  - Lê dados    │                               │  - Auth/Security│
└─────────────────┘                               └─────────────────┘

Montando o projeto

Vamos construir um servidor MCP de verdade. Um servidor que dá acesso a um PostgreSQL pro assistente de IA : caso de uso prático que mostra todos os padrões fundamentais.

Pré-requisitos

node --version # v20+ recomendado npm --version # v10+

Inicialização

mkdir mcp-database-server cd mcp-database-server npm init -y npm install @modelcontextprotocol/sdk@latest zod # v1.27+ em março 2026 npm install -D typescript @types/node tsx

Configuração TypeScript:

// tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "declaration": true }, "include": ["src/**/*"] }

Atualiza o package.json:

{ "type": "module", "bin": { "mcp-database-server": "./dist/index.js" }, "scripts": { "build": "tsc", "dev": "tsx src/index.ts", "start": "node dist/index.js" } }

Primeiro servidor MCP

O esqueleto. Todo servidor MCP começa igual:

// src/index.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; const server = new McpServer({ name: 'mcp-database-server', version: '1.0.0', capabilities: { tools: {}, resources: {}, prompts: {}, }, }); // Aqui vamos adicionar tools, resources e prompts const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Database Server running on stdio');

Repara que o log vai pra stderr, não stdout. O canal stdout é reservado pra comunicação JSON-RPC com o cliente. Se escrever qualquer coisa no stdout que não seja JSON-RPC válido, quebra o protocolo. É um erro clássico de primeira tentativa.

Adicionando Tools

Tools são a primitiva mais poderosa do MCP. Deixam a IA executar ações no seu nome.

Definição básica de tool

import { z } from 'zod'; server.tool( 'query', 'Execute a read-only SQL query against the database', { sql: z.string().describe('The SQL query to execute (SELECT only)'), params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])) .optional() .describe('Parameterized query values'), }, async ({ sql, params }) => { const normalized = sql.trim().toUpperCase(); if (!normalized.startsWith('SELECT') && !normalized.startsWith('WITH') && !normalized.startsWith('EXPLAIN')) { return { content: [{ type: 'text', text: 'Error: Only SELECT, WITH, and EXPLAIN queries are allowed.', }], isError: true, }; } try { const result = await pool.query(sql, params); return { content: [{ type: 'text', text: JSON.stringify(result.rows, null, 2) }], }; } catch (error) { return { content: [{ type: 'text', text: `Query error: ${error instanceof Error ? error.message : 'Unknown error'}`, }], isError: true, }; } } );

A anatomia:

  1. Nome ('query') : Identificador único do tool
  2. Descrição : Crucial. A IA lê isso pra decidir quando usar o tool. Escreve como se tivesse explicando pra um dev junior competente.
  3. Schema : Schema Zod que define os parâmetros. A IA usa pra montar chamadas válidas.
  4. Handler : Função async que roda quando o tool é chamado.

Boas práticas pra descrições

Descrições de tools são prompt engineering. O modelo lê pra decidir qual tool usar e como. Descrições ruins = IA escolhe o tool errado ou passa parâmetros incorretos.

// ❌ Ruim: Muito vago server.tool('query', 'Run a query', { sql: z.string() }, handler); // ❌ Ruim: Muito curto, sem restrições server.tool('query', 'SQL query', { sql: z.string() }, handler); // ✅ Bom: Específico, com restrições e exemplos server.tool( 'query', 'Execute a read-only SQL query against the PostgreSQL database. ' + 'Only SELECT, WITH (CTE), and EXPLAIN queries are allowed. ' + 'Use parameterized queries ($1, $2, ...) for user-provided values. ' + 'Returns results as JSON array of objects. ' + 'Example: SELECT id, name FROM users WHERE active = $1', { sql: z.string().describe('PostgreSQL query (SELECT/WITH/EXPLAIN only)'), params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])) .optional() .describe('Values for parameterized query placeholders ($1, $2, ...)'), }, handler );

Padrão multi-tool

Servidores reais expõem múltiplos tools relacionados com guardrails de segurança:

// Só leitura: Sem confirmação server.tool( 'list_tables', 'List all tables in the database with column count and row count estimates', {}, async () => { const result = await pool.query(` SELECT schemaname, tablename, (SELECT count(*) FROM information_schema.columns c WHERE c.table_schema = t.schemaname AND c.table_name = t.tablename) as column_count, n_live_tup as estimated_rows FROM pg_stat_user_tables t ORDER BY schemaname, tablename `); return { content: [{ type: 'text', text: JSON.stringify(result.rows, null, 2) }] }; } ); // Só leitura com parâmetros server.tool( 'describe_table', 'Get detailed schema information for a specific table including columns, types, constraints, and indexes', { table: z.string().describe('Table name (can include schema prefix like "public.users")'), }, async ({ table }) => { const [schema, tableName] = table.includes('.') ? table.split('.') : ['public', table]; const columns = await pool.query(` SELECT column_name, data_type, is_nullable, column_default, character_maximum_length FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position `, [schema, tableName]); const indexes = await pool.query(` SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 `, [schema, tableName]); return { content: [{ type: 'text', text: JSON.stringify({ columns: columns.rows, indexes: indexes.rows }, null, 2), }], }; } ); // Escrita: Resultado claro pra IA confirmar com o usuário server.tool( 'execute_migration', 'Execute a SQL migration statement (CREATE, ALTER, DROP). ' + 'WARNING: Modifies the database schema. ' + 'The AI should always confirm with the user before calling this tool.', { sql: z.string().describe('The DDL statement to execute'), description: z.string().describe('Human-readable description of what this migration does'), }, async ({ sql, description }) => { try { await pool.query('BEGIN'); await pool.query(sql); await pool.query('COMMIT'); return { content: [{ type: 'text', text: `Migration successful: ${description}\n\nExecuted:\n${sql}` }], }; } catch (error) { await pool.query('ROLLBACK'); return { content: [{ type: 'text', text: `Migration failed: ${error instanceof Error ? error.message : 'Unknown error'}\n\nRolled back.` }], isError: true, }; } } );

Adicionando Resources

Resources deixam a IA ler dados sem executar uma ação. Ótimos pra dar contexto : informação de fundo que ajuda a IA a tomar decisões melhores.

Resources estáticos

server.resource( 'schema', 'db://schema', { description: 'Current database schema including all tables, columns, and relationships', mimeType: 'application/json', }, async () => { const result = await pool.query(` SELECT t.table_schema, t.table_name, json_agg(json_build_object( 'column', c.column_name, 'type', c.data_type, 'nullable', c.is_nullable = 'YES', 'default', c.column_default ) ORDER BY c.ordinal_position) as columns FROM information_schema.tables t JOIN information_schema.columns c ON c.table_schema = t.table_schema AND c.table_name = t.table_name WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE' GROUP BY t.table_schema, t.table_name ORDER BY t.table_name `); return { contents: [{ uri: 'db://schema', mimeType: 'application/json', text: JSON.stringify(result.rows, null, 2) }], }; } );

Resource Templates (Resources dinâmicos)

Expõem dados parametrizados:

server.resource( 'table-data', 'db://tables/{tableName}/sample', { description: 'Sample data (first 10 rows) from a specific table', mimeType: 'application/json' }, async (uri) => { const tableName = uri.pathname.split('/')[2]; // Validar nome da tabela (prevenir SQL injection) const validTable = await pool.query( `SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename = $1`, [tableName] ); if (validTable.rows.length === 0) throw new Error(`Table '${tableName}' not found`); const result = await pool.query(`SELECT * FROM "${tableName}" LIMIT 10`); return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result.rows, null, 2) }], }; } );

Adicionando Prompts

Prompts são templates pré-montados que ajudam a IA com tarefas específicas. Pouca gente usa, mas são muito poderosos pra codificar conhecimento de domínio.

server.prompt( 'analyze-query-performance', 'Analyze a slow SQL query and suggest optimizations', { query: z.string().describe('The SQL query to analyze') }, async ({ query }) => { // Pegar o plano de execução const explainResult = await pool.query(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query}`); const plan = JSON.stringify(explainResult.rows[0], null, 2); // Pegar estatísticas das tabelas relevantes const tables = extractTableNames(query); const stats = await Promise.all( tables.map(async (table) => { const result = await pool.query(` SELECT relname, reltuples::bigint as estimated_rows, pg_size_pretty(pg_total_relation_size(oid)) as total_size FROM pg_class WHERE relname = $1 `, [table]); return result.rows[0]; }) ); return { messages: [{ role: 'user', content: { type: 'text', text: [ `Analyze this PostgreSQL query for performance:`, '', '```sql', query, '```', '', 'Execution Plan:', '```json', plan, '```', '', 'Table Stats:', '```json', JSON.stringify(stats, null, 2), '```', '', 'Provide: bottlenecks, CREATE INDEX suggestions, query rewrites, expected improvement.', ].join('\n'), }, }], }; } );

Camadas de transporte: stdio, SSE e Streamable HTTP

MCP suporta múltiplos mecanismos de transporte. Escolher certo importa pro modelo de deploy.

stdio (Standard I/O)

O default pra servidores locais. O cliente spawna o servidor como processo filho e se comunica via stdin/stdout.

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; const transport = new StdioServerTransport(); await server.connect(transport);

Quando usar: Ferramentas de dev locais, integrações CLI, servidores na mesma máquina que o cliente.

Vantagens: Simples, sem setup de rede, lifecycle automático.
Desvantagens: Só local, um cliente por instância.

SSE (Server-Sent Events) : Deprecado

⚠️ Nota: O transporte SSE está deprecado desde a especificação MCP 2025-11-25. Projetos novos devem usar Streamable HTTP. Tá aqui porque muitos servidores existentes ainda usam.

Pra servidores remotos acessíveis via HTTP:

import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import express from 'express'; const app = express(); const transports = new Map<string, SSEServerTransport>(); app.get('/sse', (req, res) => { const transport = new SSEServerTransport('/messages', res); transports.set(transport.sessionId, transport); res.on('close', () => transports.delete(transport.sessionId)); server.connect(transport); }); app.post('/messages', (req, res) => { const sessionId = req.query.sessionId as string; const transport = transports.get(sessionId); if (transport) transport.handlePostMessage(req, res); else res.status(404).send('Session not found'); }); app.listen(3001, () => console.error('MCP SSE server on port 3001'));

Quando usar: Servidores legados, compatibilidade com clientes antigos. Pra projetos novos, use Streamable HTTP.

Streamable HTTP (Recomendado)

O transporte recomendado pra todos os servidores MCP remotos novos em 2026. Substituiu o SSE como transporte web padrão, oferecendo endpoint único, suporte pra respostas batch e streaming, gerenciamento de sessão e escalabilidade horizontal:

import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; const app = express(); app.use(express.json()); app.post('/mcp', async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // modo stateless }); res.on('close', () => transport.close()); await server.connect(transport); await transport.handleRequest(req, res); }); app.listen(3001);

Quando usar: Deploy serverless (AWS Lambda, Vercel Edge), APIs stateless, ambientes onde conexões de longa duração são problemáticas.

Segurança: Não shippa um servidor RCE

Aqui é onde a maioria dos tutoriais para, e é exatamente onde começa o trabalho de verdade. Um servidor MCP inseguro é basicamente um endpoint de execução remota de código que um modelo de IA pode disparar.

Validação de inputs

Nunca confie nos inputs do modelo. Valide tudo:

server.tool( 'query', 'Execute a read-only SQL query', { sql: z.string() .max(10000) // Limitar tamanho do query .refine( (sql) => { const upper = sql.trim().toUpperCase(); return upper.startsWith('SELECT') || upper.startsWith('WITH') || upper.startsWith('EXPLAIN'); }, { message: 'Only SELECT, WITH, and EXPLAIN queries are allowed' } ) .refine( (sql) => { const upper = sql.toUpperCase(); const dangerous = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'ALTER', 'TRUNCATE', 'GRANT', 'REVOKE', 'CREATE']; return !dangerous.some(kw => new RegExp(`\\b${kw}\\b`).test(upper)); }, { message: 'Query contains disallowed DDL/DML keywords' } ), }, handler );

Segurança da conexão com o banco

Use conexão read-only pros tools de consulta:

import pg from 'pg'; // Pool read-only const readPool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 5, options: '-c default_transaction_read_only=on', }); // Pool separado pra escrita (se precisar) const writePool = new pg.Pool({ connectionString: process.env.DATABASE_WRITE_URL, max: 2, });

Rate limiting

Impedir que loops de IA descontrolados martelar seu servidor:

class RateLimiter { private calls: Map<string, number[]> = new Map(); check(toolName: string, maxCalls: number, windowMs: number): boolean { const now = Date.now(); const timestamps = this.calls.get(toolName) || []; const recent = timestamps.filter(t => now - t < windowMs); if (recent.length >= maxCalls) return false; recent.push(now); this.calls.set(toolName, recent); return true; } }

Timeouts

function withTimeout<T>(promise: Promise<T>, ms: number, op: string): Promise<T> { return Promise.race([ promise, new Promise<never>((_, reject) => setTimeout(() => reject(new Error(`${op} timed out after ${ms}ms`)), ms) ), ]); } // No handler do tool: const result = await withTimeout(pool.query(sql, params), 30_000, 'Database query');

Tratamento de erros

O que separa servidor de brinquedo de servidor de produção.

function handleToolError(error: unknown, context: string) { console.error(`[${context}]`, error); if (error instanceof z.ZodError) { return { content: [{ type: 'text' as const, text: `Validation error: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }], isError: true, }; } if (error instanceof pg.DatabaseError) { return { content: [{ type: 'text' as const, text: `Database error (${error.code}): ${error.message}` + (error.detail ? `\nDetail: ${error.detail}` : '') + (error.hint ? `\nHint: ${error.hint}` : ''), }], isError: true, }; } if (error instanceof Error && error.message.includes('timed out')) { return { content: [{ type: 'text' as const, text: `Operation timed out. The query may be too complex or the database may be under heavy load.`, }], isError: true, }; } return { content: [{ type: 'text' as const, text: `Error in ${context}: ${error instanceof Error ? error.message : 'Unknown error'}`, }], isError: true, }; }

O ponto chave: mensagens de erro são prompts. A IA lê e usa pra ajustar a abordagem. Inclua informação acionável:

  • "Error: 42P01" : Inútil pra IA
  • "Table 'userz' not found. Did you mean 'users'? Available tables: users, posts, comments" : IA consegue se auto-corrigir

Testando

MCP Inspector

npx @modelcontextprotocol/inspector tsx src/index.ts

Abre um UI no browser onde dá pra:

  • Ver tools/resources/prompts registrados
  • Testar com inputs
  • Inspecionar mensagens JSON-RPC
  • Debugar problemas de transporte

Integração com Claude Desktop

Adiciona o servidor no ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):

{ "mcpServers": { "database": { "command": "tsx", "args": ["/absolute/path/to/src/index.ts"], "env": { "DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb" } } } }

Reinicia o Claude Desktop e seus tools aparecem no ícone de ferramentas.

Testes automatizados

// test/server.test.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; describe('MCP Database Server', () => { let server: McpServer; let client: Client; beforeEach(async () => { server = createServer(); client = new Client({ name: 'test-client', version: '1.0.0' }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await server.connect(serverTransport); await client.connect(clientTransport); }); test('list_tables returns table information', async () => { const result = await client.callTool({ name: 'list_tables', arguments: {} }); expect(result.isError).toBeFalsy(); const tables = JSON.parse(result.content[0].text); expect(tables).toBeInstanceOf(Array); expect(tables.length).toBeGreaterThan(0); }); test('query rejects INSERT', async () => { const result = await client.callTool({ name: 'query', arguments: { sql: "INSERT INTO users (name) VALUES ('hack')" }, }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Only SELECT'); }); test('query handles parameterized queries', async () => { const result = await client.callTool({ name: 'query', arguments: { sql: 'SELECT * FROM users WHERE id = $1', params: [1] }, }); expect(result.isError).toBeFalsy(); }); });

O servidor completo

Juntando tudo num servidor pronto pra produção:

// src/index.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import pg from 'pg'; // ── Configuração ─────────────────────────────────────────── const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { console.error('DATABASE_URL environment variable is required'); process.exit(1); } const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 5, options: '-c default_transaction_read_only=on -c statement_timeout=30000', }); pool.query('SELECT 1').catch((err) => { console.error('Failed to connect to database:', err.message); process.exit(1); }); // ── Server ───────────────────────────────────────────────── const server = new McpServer({ name: 'mcp-database-server', version: '1.0.0', capabilities: { tools: {}, resources: {}, prompts: {} }, }); // ── Tools ────────────────────────────────────────────────── server.tool( 'list_tables', 'List all tables in the public schema with column count and estimated row count', {}, async () => { const result = await pool.query(` SELECT tablename, (SELECT count(*) FROM information_schema.columns c WHERE c.table_schema = 'public' AND c.table_name = t.tablename) as columns, n_live_tup as estimated_rows FROM pg_stat_user_tables t WHERE schemaname = 'public' ORDER BY tablename `); return { content: [{ type: 'text', text: JSON.stringify(result.rows, null, 2) }] }; } ); server.tool( 'describe_table', 'Get detailed column info, types, constraints, and indexes for a table', { table: z.string().describe('Table name in the public schema') }, async ({ table }) => { const columns = await pool.query(` SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position `, [table]); if (columns.rows.length === 0) { return { content: [{ type: 'text', text: `Table '${table}' not found.` }], isError: true }; } const indexes = await pool.query(` SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = $1 `, [table]); const fks = await pool.query(` SELECT kcu.column_name, ccu.table_name AS foreign_table, ccu.column_name AS foreign_column FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = $1 `, [table]); return { content: [{ type: 'text', text: JSON.stringify({ table, columns: columns.rows, indexes: indexes.rows, foreignKeys: fks.rows }, null, 2), }], }; } ); server.tool( 'query', 'Execute a read-only SQL query. Only SELECT, WITH (CTE), and EXPLAIN allowed. ' + 'Use $1, $2, ... for parameterized values. Returns JSON array.', { sql: z.string().max(10000).describe('PostgreSQL SELECT query'), params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])) .optional().describe('Parameter values for $1, $2, ... placeholders'), }, async ({ sql, params }) => { const upper = sql.trim().toUpperCase(); const disallowed = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE', 'CREATE', 'GRANT', 'REVOKE']; for (const kw of disallowed) { if (new RegExp(`\\b${kw}\\b`).test(upper)) { return { content: [{ type: 'text', text: `Blocked: '${kw}' statements are not allowed.` }], isError: true }; } } try { const result = await pool.query(sql, params || []); const truncated = result.rows.length > 100 ? { rows: result.rows.slice(0, 100), note: `Showing 100 of ${result.rows.length} rows` } : { rows: result.rows, total: result.rows.length }; return { content: [{ type: 'text', text: JSON.stringify(truncated, null, 2) }] }; } catch (err) { const pgErr = err as pg.DatabaseError; return { content: [{ type: 'text', text: `Query failed: ${pgErr.message}${pgErr.hint ? `\nHint: ${pgErr.hint}` : ''}` }], isError: true, }; } } ); // ── Resources ────────────────────────────────────────────── server.resource( 'schema-overview', 'db://schema', { description: 'Complete database schema overview', mimeType: 'application/json' }, async () => { const result = await pool.query(` SELECT t.tablename, json_agg(json_build_object('column', c.column_name, 'type', c.data_type, 'nullable', c.is_nullable = 'YES') ORDER BY c.ordinal_position) as columns FROM pg_stat_user_tables t JOIN information_schema.columns c ON c.table_schema = 'public' AND c.table_name = t.tablename WHERE t.schemaname = 'public' GROUP BY t.tablename ORDER BY t.tablename `); return { contents: [{ uri: 'db://schema', mimeType: 'application/json', text: JSON.stringify(result.rows, null, 2) }] }; } ); // ── Prompts ──────────────────────────────────────────────── server.prompt( 'optimize-query', 'Analyze a SQL query and suggest performance optimizations', { query: z.string().describe('The SQL query to optimize') }, async ({ query }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Analyze this PostgreSQL query for performance:\n\n\`\`\`sql\n${query}\n\`\`\`\n\nSuggest index optimizations, query rewrites, and explain the reasoning.`, }, }], }) ); // ── Lifecycle ────────────────────────────────────────────── process.on('SIGINT', async () => { await pool.end(); process.exit(0); }); // ── Start ────────────────────────────────────────────────── const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Database Server running');

Padrões de deploy

Padrão 1: Pacote npm (distribuição local)

npm run build npm pack # Distribuir o .tgz ou publicar no npm # Usuários instalam e configuram: npm install -g mcp-database-server

Configuração no Claude Desktop:

{ "mcpServers": { "database": { "command": "mcp-database-server", "env": { "DATABASE_URL": "postgresql://..." } } } }

Padrão 2: Docker (time/remoto)

FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY dist/ ./dist/ EXPOSE 3001 CMD ["node", "dist/sse-server.js"]

Padrão 3: Serverless (Streamable HTTP)

Pra Vercel, AWS Lambda ou Cloudflare Workers:

// api/mcp.ts (função serverless Vercel) import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; export default async function handler(req: Request): Promise<Response> { const server = createServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await server.connect(transport); return transport.handleRequest(req); }

Erros comuns e como evitar

1. Logar no stdout

// ❌ Quebra o protocolo JSON-RPC console.log('Server started'); // ✅ Use stderr console.error('Server started');

2. Não tratar lifecycle da conexão

// ❌ Recursos vazam na desconexão const server = new McpServer(config); // ✅ Limpe corretamente server.onclose = async () => { await pool.end(); console.error('Connection closed, resources cleaned up'); };

3. Devolver dados demais

// ❌ Despeja tabela inteira no context window return { content: [{ type: 'text', text: JSON.stringify(allRows) }] }; // ✅ Pagine e resuma const rows = result.rows.slice(0, 50); return { content: [{ type: 'text', text: JSON.stringify({ rows, total: result.rowCount, note: result.rowCount > 50 ? `Showing 50 of ${result.rowCount} rows. Use LIMIT/OFFSET for pagination.` : undefined, }, null, 2), }], };

4. Mensagens de erro sem contexto

// ❌ IA não consegue se auto-corrigir return { content: [{ type: 'text', text: 'Error' }], isError: true }; // ✅ IA consegue ajustar a abordagem return { content: [{ type: 'text', text: `Column "user_name" not found in table "users". Available columns: id, email, full_name, created_at. Did you mean "full_name"?`, }], isError: true, };

5. Sem timeout nos queries

// ❌ Query complexo trava a conexão pra sempre await pool.query(complexSql); // ✅ Setar statement_timeout no nível do pool const pool = new pg.Pool({ connectionString: DATABASE_URL, options: '-c statement_timeout=30000', // 30 segundos });

O futuro do MCP

O protocolo tá evoluindo rápido. O que já saiu e o que tá vindo:

Já na especificação (2025-11-25):

  • OAuth 2.1 : Servidores MCP agora são classificados como OAuth Resource Servers com descoberta padronizada de Protected Resource Metadata. Acabaram os padrões de auth improvisados.
  • Structured tool output : Tools podem retornar dados estruturados além de texto, facilitando o processamento programático de resultados.
  • Elicitation : Servidores podem pedir input adicional dos usuários durante interações (ex: "insira sua API key" ou "autorize esse fluxo OAuth").
  • Icon metadata : Tools, resources e prompts podem incluir URLs de ícones, retornados nas respostas list pra UIs de cliente mais ricas.
  • Campo title : Um novo campo title fornece nomes de exibição amigáveis, enquanto name fica como identificador programático.

Em breve (SDK v2 e além):

  • MCP agent-to-agent : Servidores MCP que são clientes de outros servidores, habilitando cadeias de ferramentas componíveis.
  • Tasks (experimental) : Nova primitiva pra tracking de estado durável e recuperação diferida de resultados, útil pra operações longas.
  • Execução em sandbox : Isolamento baseado em containers com permissões por manifesto.

O ecossistema já tem centenas de servidores comunitários pra GitHub, Slack, Jira, Kubernetes, AWS, Terraform e mais. Construir o seu é o melhor jeito de entender o protocolo : e de dar superpoderes pro seu assistente de IA.

Conclusão

Construir um servidor MCP não é difícil. O SDK cuida da complexidade do protocolo e você foca no que importa: definir os tools, resources e prompts certos pro seu caso de uso.

Pontos chave:

  1. Comece pelos tools. São a primitiva mais impactante. Faça um funcionar no Claude Desktop e itere a partir dali.
  2. Descrições são prompt engineering. Literalmente. A IA usa pra decidir quando e como usar seus tools.
  3. Segurança não é opcional. Valide inputs, use conexões read-only, implemente rate limiting e timeouts.
  4. Erros são prompts. Retorne contexto acionável pra IA conseguir se auto-corrigir.
  5. Escolha o transporte certo. stdio pra local, Streamable HTTP pra remoto (SSE tá deprecado).

A distância entre "IA que conversa sobre código" e "IA que opera sua infraestrutura" é exatamente um servidor MCP. Hora de construir o seu.

MCPTypeScriptAIClaudeModel Context ProtocolNode.jsAI agentsdeveloper tools

Explore ferramentas relacionadas

Experimente estas ferramentas gratuitas do Pockit