07-01-04 — Construindo um MCP Server
TL;DR
Construir um MCP server é surpreendentemente simples com os SDKs oficiais. Em Python: pip install mcp; em TypeScript: npm install @modelcontextprotocol/sdk. O exemplo deste tópico é um server de notas: ferramentas para criar, ler e listar notas, com um resource que expõe o conteúdo de uma nota específica. Leva menos de 50 linhas de código funcional em cada linguagem.
Os SDKs oficiais
A Anthropic mantém SDKs oficiais para Python e TypeScript. Ambos abstraem o protocolo JSON-RPC e o transporte, expondo uma API declarativa para registrar tools, resources e prompts.
- Python: github.com/modelcontextprotocol/python-sdk
- TypeScript: github.com/modelcontextprotocol/typescript-sdk
- C# (.NET): github.com/modelcontextprotocol/csharp-sdk (comunidade, sendo absorvido pelo SDK oficial da Microsoft)
Exemplo completo em Python: Notes API
Vamos construir um MCP server que expõe uma API simples de notas. O server terá três tools (create_note, list_notes, delete_note) e um resource (note://{id}) para ler o conteúdo de uma nota.
Instalação
pip install mcp
Código completo (Python)
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, Resource, TextContent
import json
# Estado em memória (em produção: banco de dados)
notes: dict[str, str] = {}
# Criar o server
server = Server("notes-server")
# ---- TOOLS ----
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="create_note",
description="Cria uma nova nota com título e conteúdo.",
inputSchema={
"type": "object",
"properties": {
"title": {"type": "string", "description": "Título da nota"},
"content": {"type": "string", "description": "Conteúdo da nota"}
},
"required": ["title", "content"]
}
),
Tool(
name="list_notes",
description="Lista todas as notas disponíveis.",
inputSchema={"type": "object", "properties": {}}
),
Tool(
name="delete_note",
description="Remove uma nota pelo título.",
inputSchema={
"type": "object",
"properties": {
"title": {"type": "string", "description": "Título da nota a remover"}
},
"required": ["title"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "create_note":
title = arguments["title"]
notes[title] = arguments["content"]
return [TextContent(type="text", text=f"Nota '{title}' criada com sucesso.")]
elif name == "list_notes":
if not notes:
return [TextContent(type="text", text="Nenhuma nota encontrada.")]
listing = "\n".join(f"- {title}" for title in notes.keys())
return [TextContent(type="text", text=f"Notas disponíveis:\n{listing}")]
elif name == "delete_note":
title = arguments["title"]
if title in notes:
del notes[title]
return [TextContent(type="text", text=f"Nota '{title}' removida.")]
return [TextContent(type="text", text=f"Nota '{title}' não encontrada.")]
raise ValueError(f"Tool desconhecida: {name}")
# ---- RESOURCES ----
@server.list_resources()
async def list_resources():
from mcp.types import Resource as McpResource
return [
McpResource(
uri=f"note://{title}",
name=f"Nota: {title}",
mimeType="text/plain"
)
for title in notes.keys()
]
@server.read_resource()
async def read_resource(uri: str) -> str:
title = uri.replace("note://", "")
if title in notes:
return notes[title]
raise ValueError(f"Recurso não encontrado: {uri}")
# ---- ENTRY POINT ----
if __name__ == "__main__":
import asyncio
asyncio.run(stdio_server(server))
O exemplo usa stdio_server — ideal para rodar localmente como processo filho do host (Claude Desktop, VS Code). Para expor via HTTP (ex: Azure Functions), substitua por streamable_http_server da mesma biblioteca.
Exemplo completo em TypeScript
Instalação
npm install @modelcontextprotocol/sdk
Código completo (TypeScript)
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";
// Estado em memória
const notes = new Map();
// Criar o server
const server = new Server(
{ name: "notes-server", version: "1.0.0" },
{ capabilities: { tools: {}, resources: {} } }
);
// ---- TOOLS ----
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "create_note",
description: "Cria uma nova nota com título e conteúdo.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Título da nota" },
content: { type: "string", description: "Conteúdo da nota" },
},
required: ["title", "content"],
},
},
{
name: "list_notes",
description: "Lista todas as notas disponíveis.",
inputSchema: { type: "object", properties: {} },
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "create_note") {
notes.set(args!.title as string, args!.content as string);
return {
content: [{ type: "text", text: `Nota '${args!.title}' criada com sucesso.` }],
};
}
if (name === "list_notes") {
if (notes.size === 0) {
return { content: [{ type: "text", text: "Nenhuma nota encontrada." }] };
}
const listing = Array.from(notes.keys()).map((t) => `- ${t}`).join("\n");
return { content: [{ type: "text", text: `Notas:\n${listing}` }] };
}
throw new Error(`Tool desconhecida: ${name}`);
});
// ---- RESOURCES ----
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: Array.from(notes.keys()).map((title) => ({
uri: `note://${title}`,
name: `Nota: ${title}`,
mimeType: "text/plain",
})),
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const title = request.params.uri.replace("note://", "");
const content = notes.get(title);
if (!content) throw new Error(`Recurso não encontrado: ${request.params.uri}`);
return { contents: [{ uri: request.params.uri, mimeType: "text/plain", text: content }] };
});
// ---- ENTRY POINT ----
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Notes MCP server rodando via stdio");
Configurando no Claude Desktop para testar
// claude_desktop_config.json
{
"mcpServers": {
"notes": {
"command": "python",
"args": ["/caminho/para/notes_server.py"]
}
}
}
// Ou para TypeScript:
// "command": "node",
// "args": ["/caminho/para/notes_server.js"]
Erros no MCP server costumam aparecer silenciosamente. Use console.error() (TypeScript) ou print(..., file=sys.stderr) (Python) para logs — o stdout é reservado para o protocolo JSON-RPC. Para debugging avançado, use o MCP Inspector: npx @modelcontextprotocol/inspector python notes_server.py
Boas práticas para produção
- Descriptions importam: o LLM decide quando chamar uma tool baseado na description. Seja preciso e inclua exemplos de quando usar.
- Schemas rigorosos: defina
requiredcorretamente — o LLM tentará preencher todos os campos obrigatórios. - Tratamento de erros: retorne mensagens de erro informativas no
contentda tool — o LLM vai usar essa informação para tomar decisões. - Idempotência: tools invocadas por LLMs podem ser chamadas múltiplas vezes. Projete para isso.
- Autenticação: para servers remotos, use OAuth 2.0 ou API keys via headers — nunca exponha endpoints MCP sem autenticação.