Kaique Mitsuo Silva Yamamoto
Arquitetura softwareCursor + MCP

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 typescript

tsconfig.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 stderr

Regra 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.js

Abre 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() — apenas console.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 build sem erros TypeScript
  • Testado no MCP Inspector antes de conectar ao Cursor
  • tsconfig.json com "module": "Node16" e "type": "module" no package.json

On this page