Criando MCP server com TypeScript
Guia completo para construir um servidor MCP customizado do zero com TypeScript, o SDK oficial da Anthropic e validação de schema com Zod.
Criando MCP server com TypeScript
Criar um servidor MCP próprio significa transformar qualquer API, banco de dados ou sistema interno em ferramentas que o Cursor pode chamar diretamente. Este guia cobre o setup completo do zero.
Setup do projeto
mkdir meu-mcp-server
cd meu-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescripttsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}package.json — campos obrigatórios
{
"type": "module",
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "tsc --watch"
}
}O campo "type": "module" é obrigatório para que os imports do SDK funcionem corretamente com ESM.
Servidor mínimo funcional
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "meu-servidor",
version: "1.0.0",
});
// Registrar uma tool simples
server.tool(
"somar",
"Soma dois números e retorna o resultado",
{
a: z.number().describe("Primeiro número"),
b: z.number().describe("Segundo número"),
},
async ({ a, b }) => ({
content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
})
);
// Conectar via STDIO
const transport = new StdioServerTransport();
await server.connect(transport);
// CRÍTICO: nunca use console.log() em servidores STDIO
// stdout é reservado para as mensagens JSON-RPC do protocolo.
// Use sempre console.error() para logs e debug — vai para stderr.
console.error("Servidor iniciado");Build e teste:
npm run build
node dist/server.js
# Deve aparecer: "Servidor iniciado" no stderrRegra de ouro: nunca use console.log()
Este é o erro mais comum em servidores STDIO. O protocolo MCP usa stdout para comunicação JSON-RPC. Qualquer console.log() que você escrever vai corromper as mensagens e fazer o Cursor marcar o servidor como inativo.
// ❌ ERRADO — corrompe o protocolo
console.log("processando...");
// ✅ CORRETO — vai para stderr, não interfere
console.error("processando...");Tools com chamadas a APIs externas
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "api-server",
version: "1.0.0",
});
// Tool com chamada HTTP
server.tool(
"buscar_usuario",
"Busca dados de um usuário pela ID",
{
userId: z.string().describe("ID do usuário na API"),
},
async ({ userId }) => {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
return {
content: [{ type: "text", text: `Erro HTTP ${response.status}` }],
isError: true,
};
}
const user = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(user, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: "text", text: `Falha na requisição: ${error}` }],
isError: true,
};
}
}
);
const transport = new StdioServerTransport();
await server.connect(transport);O campo isError: true sinaliza para o Cursor que algo deu errado sem encerrar o servidor. O agente vê o erro e pode tentar outra abordagem.
Tool com variáveis de ambiente
Chaves de API e URLs nunca devem ficar hardcoded no código. Leia-as das variáveis de ambiente configuradas no mcp.json.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Ler config no startup — falha rápido se faltar algo crítico
const API_KEY = process.env.MINHA_API_KEY;
const API_BASE = process.env.API_BASE_URL ?? "https://api.exemplo.com";
if (!API_KEY) {
console.error("ERRO: variável MINHA_API_KEY não definida");
process.exit(1);
}
const server = new McpServer({ name: "api-autenticada", version: "1.0.0" });
server.tool(
"buscar_dado",
"Busca um dado autenticado",
{ id: z.string() },
async ({ id }) => {
const response = await fetch(`${API_BASE}/dados/${id}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const data = await response.json();
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);Configuração no mcp.json:
{
"mcpServers": {
"api-autenticada": {
"command": "node",
"args": ["/caminho/absoluto/dist/server.js"],
"env": {
"MINHA_API_KEY": "sk-...",
"API_BASE_URL": "https://api.producao.com"
}
}
}
}Exemplo real: servidor para integração com Notion
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const NOTION_TOKEN = process.env.NOTION_TOKEN!;
const NOTION_DB_ID = process.env.NOTION_DB_ID!;
const server = new McpServer({ name: "notion-tasks", version: "1.0.0" });
server.tool(
"criar_tarefa",
"Cria uma nova tarefa no Notion",
{
titulo: z.string().describe("Título da tarefa"),
prioridade: z.enum(["alta", "media", "baixa"]).describe("Prioridade"),
descricao: z.string().optional().describe("Descrição opcional"),
},
async ({ titulo, prioridade, descricao }) => {
const response = await fetch("https://api.notion.com/v1/pages", {
method: "POST",
headers: {
Authorization: `Bearer ${NOTION_TOKEN}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
body: JSON.stringify({
parent: { database_id: NOTION_DB_ID },
properties: {
Nome: { title: [{ text: { content: titulo } }] },
Prioridade: { select: { name: prioridade } },
Descrição: descricao
? { rich_text: [{ text: { content: descricao } }] }
: undefined,
},
}),
});
const page = await response.json();
if (!response.ok) {
return {
content: [{ type: "text", text: `Erro: ${JSON.stringify(page)}` }],
isError: true,
};
}
return {
content: [{ type: "text", text: `Tarefa criada: ${page.url}` }],
};
}
);
server.tool(
"listar_tarefas",
"Lista tarefas do banco de dados do Notion",
{
filtro_prioridade: z
.enum(["alta", "media", "baixa"])
.optional()
.describe("Filtrar por prioridade"),
},
async ({ filtro_prioridade }) => {
const body: Record<string, unknown> = {
page_size: 20,
sorts: [{ property: "Prioridade", direction: "descending" }],
};
if (filtro_prioridade) {
body.filter = {
property: "Prioridade",
select: { equals: filtro_prioridade },
};
}
const response = await fetch(
`https://api.notion.com/v1/databases/${NOTION_DB_ID}/query`,
{
method: "POST",
headers: {
Authorization: `Bearer ${NOTION_TOKEN}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
const tarefas = data.results.map((p: { properties: { Nome: { title: { plain_text: string }[] }; Prioridade: { select: { name: string } } } }) => ({
titulo: p.properties.Nome.title[0]?.plain_text ?? "(sem título)",
prioridade: p.properties.Prioridade?.select?.name ?? "—",
}));
return {
content: [
{ type: "text", text: JSON.stringify(tarefas, null, 2) },
],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Notion MCP iniciado");Testando com o MCP Inspector
O SDK oficial inclui um inspector interativo que permite testar ferramentas sem precisar abrir o Cursor:
npx @modelcontextprotocol/inspector node dist/server.jsAbre uma interface em http://127.0.0.1:6274 onde você pode:
- Ver todas as tools registradas
- Executar cada tool com parâmetros customizados
- Inspecionar as respostas JSON-RPC brutas
- Verificar logs em tempo real
Configurando no Cursor após o build
{
"mcpServers": {
"meu-servidor": {
"command": "node",
"args": ["/caminho/absoluto/meu-mcp-server/dist/server.js"],
"env": {
"MINHA_API_KEY": "valor"
}
}
}
}Sempre use caminho absoluto. Caminhos relativos falham porque o Cursor inicia o processo a partir de um diretório diferente do seu projeto.
Checklist antes de publicar
- Nenhum
console.log()— apenasconsole.error() - Variáveis sensíveis lidas de
process.env, não hardcoded - Validação de schema com Zod em todos os parâmetros
- Tratamento de erro em cada tool com
isError: true - Build testado com
npm run buildsem erros TypeScript - Testado no MCP Inspector antes de conectar ao Cursor
-
tsconfig.jsoncom"module": "Node16"e"type": "module"no package.json
Configurando MCPs no Cursor
Como configurar o mcp.json global e por projeto, todos os campos disponíveis, e exemplos prontos dos MCPs mais úteis para desenvolvedores.
Criando MCP server com Python e FastMCP
Como construir servidores MCP com Python usando FastMCP — o framework com menos boilerplate, decorators Pythônicos e suporte nativo a async.