Tutorial de servidor MCP em TypeScript: exemplo do SDK e template para copiar

Um servidor MCP funcionando em TypeScript, do npm install ao Claude Code. Copie o template do SDK, registre uma ferramenta, um recurso e um prompt, escolha stdio ou Streamable HTTP, teste no Inspector e depois adicione ferramentas de imagem e vídeo que chamam uma API real sem bloquear o cliente.

Tutorial de servidor MCP em TypeScript: exemplo do SDK e template para copiar
Cristian Da Conceicao
Fundador do Picasso IA

Você pode conectar um modelo de linguagem ao seu próprio código em cerca de quarenta linhas. Este tutorial de servidor MCP em TypeScript faz exatamente isso: um servidor funcionando com o SDK oficial, um template de projeto para copiar e os dois transportes que importam, stdio para clientes locais e Streamable HTTP para clientes remotos. Você terminará com uma ferramenta, um recurso e um prompt, testados no Inspector e registrados no Claude Code. Depois, adicionamos ferramentas de imagem e vídeo, porque é aí que um servidor Model Context Protocol deixa de ser uma demonstração e passa a fazer trabalho de verdade. Todos os trechos de código rodam no Node.js 20 ou mais recente com o pacote @modelcontextprotocol/sdk.

Um desenvolvedor digitando TypeScript em um notebook sobre uma mesa de madeira, com luz suave de manhã

O que um servidor MCP faz

O Model Context Protocol (MCP) é um padrão aberto que permite a um cliente de IA, como o Claude Code, o Claude Desktop ou um agente de IDE, chamar funções e ler dados que vivem dentro do seu processo. As mensagens trafegam como JSON-RPC 2.0. Seu servidor anuncia o que oferece, o cliente lista essas capacidades e o modelo decide quando usá-las. Seu código nunca conversa diretamente com o modelo. Ele responde às solicitações, e é por isso que o servidor continua pequeno.

Três blocos de construção

Todo servidor MCP é feito de alguma combinação de três primitivas:

BlocoQuem decide usarUso típico
FerramentaO modeloConsultar um banco de dados, chamar uma API, gerar uma imagem
RecursoA aplicação ou o usuárioExpor um documento, um arquivo ou uma configuração como contexto legível
PromptO usuárioUm template reutilizável, como "revise este pull request"

Na prática, as ferramentas fazem a maior parte do trabalho. Uma ferramenta é uma função com nome, um esquema de entrada tipado e um resultado de texto ou imagem. Recursos e prompts são opcionais, mas custam quase nada para adicionar depois que o servidor existe.

Cliente, servidor e transporte

O transporte é apenas o canal por onde as mensagens JSON-RPC passam. O mesmo objeto McpServer funciona com stdio ou HTTP, então a estrutura certa é construir o servidor em uma única função e conectar um transporte em um arquivo de entrada separado. O template abaixo segue essa regra, o que mantém os testes simples e permite publicar os dois transportes a partir de uma única base de código.

Configurar o projeto TypeScript

Instalar o SDK e o zod

Crie uma pasta e instale as dependências. O SDK usa o zod para os esquemas de entrada: ele os converte em JSON Schema para o cliente e valida cada argumento recebido antes do seu handler rodar.

mkdir mcp-notes-server && cd mcp-notes-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

Configurar package.json e tsconfig

Mude o projeto para módulos ES e adicione um script de build:

{
  "type": "module",
  "bin": { "mcp-notes-server": "dist/index.js" },
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Em seguida, adicione um tsconfig.json que corresponda à forma como o Node resolve os módulos:

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

A estrutura de pastas é propositalmente simples:

mcp-notes-server/
  src/
    index.ts      stdio transport and startup
    http.ts       Streamable HTTP transport
    server.ts     buildServer() factory
  package.json
  tsconfig.json

💡 Os imports do SDK terminam com .js, mesmo em arquivos TypeScript. Com a resolução Node16, o compilador exige extensões explícitas, e uma extensão faltando aparece em tempo de execução como ERR_MODULE_NOT_FOUND.

Vista de cima de uma mesa organizada com um caderno onde há um esboço da árvore de pastas de um projeto

Construir o template do servidor

A fábrica do servidor

Coloque tudo o que o servidor oferece em src/server.ts. Esta versão registra uma ferramenta que salva uma nota e outra que busca uma nota:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const notes = new Map<string, string>();

export function buildServer(): McpServer {
  const server = new McpServer({ name: "notes-server", version: "1.0.0" });

  server.registerTool(
    "add_note",
    {
      title: "Add note",
      description: "Save a short note under a unique id. Overwrites an existing id.",
      inputSchema: {
        id: z.string().min(1).describe("Unique id, for example 'standup-0612'"),
        text: z.string().min(1).max(2000).describe("The note body"),
      },
    },
    async ({ id, text }) => {
      notes.set(id, text);
      return { content: [{ type: "text", text: `Saved note ${id}` }] };
    }
  );

  server.registerTool(
    "get_note",
    {
      title: "Get note",
      description: "Return the text of a saved note by id.",
      inputSchema: { id: z.string().min(1).describe("The note id") },
    },
    async ({ id }) => {
      const text = notes.get(id);
      if (text === undefined) {
        return { isError: true, content: [{ type: "text", text: `No note with id ${id}` }] };
      }
      return { content: [{ type: "text", text }] };
    }
  );

  // resources and prompts go here (next section)

  return server;
}

Dois detalhes importam mais do que parecem. Primeiro, a descrição é o que o modelo lê ao decidir se chama a ferramenta, então escreva-a como documentação para um colega. Segundo, quando algo falha, retorne isError: true com uma mensagem legível em vez de lançar uma exceção. O modelo pode então tentar de novo com um argumento corrigido ou explicar o problema ao usuário.

Uma mão desenhando caixas conectadas e setas em um quadro branco em uma sala de reunião iluminada

Adicionar um recurso e um prompt

Substitua o comentário de placeholder por este código:

server.registerResource(
  "all-notes",
  "notes://all",
  {
    title: "All notes",
    description: "Every saved note as JSON",
    mimeType: "application/json",
  },
  async (uri) => ({
    contents: [{ uri: uri.href, text: JSON.stringify([...notes.entries()]) }],
  })
);

server.registerPrompt(
  "summarize-notes",
  {
    title: "Summarize notes",
    description: "Ask for a short summary of the saved notes",
    argsSchema: { tone: z.string().optional() },
  },
  ({ tone }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Summarize my saved notes in a ${tone ?? "neutral"} tone.`,
        },
      },
    ],
  })
);

Os recursos são endereçados por URI (notes://all), e os clientes normalmente os mostram em um seletor para que o usuário os anexe como contexto. Os prompts aparecem como comandos de barra ou itens de menu, dependendo do cliente.

Deixe um modelo de código ajudar

Depois que este template roda, um modelo de código pode adicionar ferramentas em minutos. Cole src/server.ts em um chat e peça uma nova ferramenta que siga o mesmo padrão: esquema primeiro, isError em caso de falha. Claude Sonnet 5, Kimi K2.6 e GPT 5.6 Sol são todos feitos para trabalho com código e disponíveis no PicassoIA. Leia o esquema gerado antes de aceitá-lo: um modelo faz campos obrigatórios virarem opcionais sem a menor cerimônia.

Escolher um transporte

stdio para clientes locais

O stdio é o transporte mais simples. O cliente inicia seu servidor como um processo filho e se comunica com ele por stdin e stdout. Crie src/index.ts:

#!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { buildServer } from "./server.js";

const server = buildServer();
await server.connect(new StdioServerTransport());
console.error("notes-server ready on stdio");

💡 Nunca use console.log em um servidor stdio. A saída padrão transporta o protocolo, então uma linha de log solta corrompe o fluxo e o cliente se desconecta com um erro de parsing. Envie os logs para stderr com console.error.

Streamable HTTP para clientes remotos

Para um servidor que roda em um host e não em um notebook, use o Streamable HTTP. Ele substituiu o antigo transporte HTTP mais SSE e precisa de um único endpoint. Instale o Express com npm install express e npm install -D @types/express, depois salve isto como src/http.ts:

import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { buildServer } from "./server.js";

const app = express();
app.use(express.json());

app.post("/mcp", async (req, res) => {
  const server = buildServer();
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  res.on("close", () => {
    transport.close();
    server.close();
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000, () => console.error("MCP endpoint on http://localhost:3000/mcp"));

Definir sessionIdGenerator: undefined faz o transporte rodar no modo stateless: um servidor novo por requisição, sem sessão para acompanhar e escala horizontal como qualquer outra API. Se você precisar de notificações iniciadas pelo servidor ou de streams retomáveis, mude para o modo stateful com ids de sessão.

stdioStreamable HTTP
Roda ondeProcesso filho do clienteQualquer host acessível por HTTP
AutenticaçãoHerda o ambiente do usuárioVocê a adiciona (OAuth ou tokens bearer)
Melhor paraFerramentas pessoais e locais para desenvolvedoresServidores compartilhados e hospedados
EscalaUm processo por clienteStateless, escala como uma API

Vista de baixo para cima de um corredor estreito com racks de servidores e cabos de rede bem organizados

Testar antes de conectar

Rodar o MCP Inspector

O Inspector é a interface oficial de depuração. Faça o build do projeto e inicie seu servidor por ele:

npm run build
npx @modelcontextprotocol/inspector node dist/index.js

Abra a URL local que ele mostrar, clique em Connect e use a aba Tools para listar as ferramentas e executar add_note com um argumento JSON. O painel de histórico mostra o tráfego JSON-RPC bruto, que é a forma mais rápida de identificar um esquema que não corresponde ao que você pretendia.

Registrar no Claude Code e no Claude Desktop

O Claude Code registra um servidor local com um único comando, e um servidor HTTP com uma flag de transporte:

claude mcp add notes -- node /absolute/path/mcp-notes-server/dist/index.js
claude mcp add --transport http notes-remote http://localhost:3000/mcp

O Claude Desktop lê um arquivo JSON (claude_desktop_config.json):

{
  "mcpServers": {
    "notes": {
      "command": "node",
      "args": ["/absolute/path/mcp-notes-server/dist/index.js"]
    }
  }
}

💡 Use caminhos absolutos nos dois. Um caminho relativo é resolvido a partir do diretório de trabalho do cliente, não do seu, e a mensagem de erro raramente deixa isso claro.

Um desenvolvedor em uma mesa em pé com dois monitores, concentrado na depuração em um terminal

Adicionar ferramentas de imagem e vídeo

Um servidor de notas comprova o padrão. Ferramentas de mídia mostram por que ele compensa: trabalhos lentos, saídas grandes e uma API externa com seus próprios limites. O PicassoIA expõe uma API de desenvolvedor no estilo Replicate em https://api.picassoia.com/v1, autenticada com um token Bearer que começa com pia_sk_. Quatro modelos ficam acessíveis pela API e pelo conector MCP: PicassoIA Image para texto para imagem, PicassoIA Image Editor Pro para edições, PicassoIA Video para vídeo e Seedance 2.5 Lite para vídeo com áudio. Os trabalhos são assíncronos: você cria uma predição, consulta o status e depois lê o resultado.

Encapsular um endpoint de imagem

Dois pequenos auxiliares atendem a todos os modelos, porque o formato da API é o mesmo:

const API = "https://api.picassoia.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
  "Content-Type": "application/json",
};

export async function createPrediction(model: string, input: Record<string, unknown>) {
  const res = await fetch(`${API}/models/${model}/predictions`, {
    method: "POST",
    headers,
    body: JSON.stringify({ input }),
    signal: AbortSignal.timeout(15_000),
  });
  if (!res.ok) throw new Error(`Create failed: HTTP ${res.status}`);
  return (await res.json()) as { id: string };
}

export async function getPrediction(id: string) {
  const res = await fetch(`${API}/predictions/${id}`, { headers });
  if (!res.ok) throw new Error(`Status failed: HTTP ${res.status}`);
  return (await res.json()) as { status: string; output?: unknown; error?: string };
}

A ferramenta de imagem usa esses auxiliares e espera até dois minutos por um resultado:

server.registerTool(
  "generate_image",
  {
    title: "Generate image",
    description: "Create an image from a text prompt and return its URL.",
    inputSchema: { prompt: z.string().min(10).max(4000) },
  },
  async ({ prompt }) => {
    try {
      const { id } = await createPrediction("picassoia/picassoia-image", { prompt });
      for (let i = 0; i < 60; i++) {
        const job = await getPrediction(id);
        if (job.status === "succeeded") {
          return { content: [{ type: "text", text: JSON.stringify(job.output) }] };
        }
        if (job.status === "failed") throw new Error(job.error ?? "Generation failed");
        await new Promise((r) => setTimeout(r, 2000));
      }
      throw new Error("Timed out waiting for the image");
    } catch (err) {
      return { isError: true, content: [{ type: "text", text: String(err) }] };
    }
  }
);

O limite de 4.000 caracteres no esquema corresponde ao limite de prompt da API, e cada página de modelo lista os campos de entrada exatos e o formato de saída. A API também permite 5 predições simultâneas por conta, compartilhadas entre tokens e conexões MCP, então enfileire as chamadas de ferramenta em paralelo em vez de disparar todas de uma vez.

Mesa de um designer com fotos impressas de paisagens de montanha, um tablet e amostras de cor

Lidar com trabalhos lentos de vídeo

O vídeo leva muito mais tempo que uma imagem, e uma chamada de ferramenta que bloqueia por minutos pode atingir o timeout próprio do cliente. Divida o trabalho em duas ferramentas: uma inicia o trabalho e devolve o id imediatamente, a outra consulta o status.

server.registerTool(
  "start_video",
  {
    title: "Start video",
    description: "Start a video job from a prompt. Returns a prediction id to check later.",
    inputSchema: { prompt: z.string().min(10).max(4000) },
  },
  async ({ prompt }) => {
    const { id } = await createPrediction("picassoia/picassoia-video", { prompt });
    return { content: [{ type: "text", text: JSON.stringify({ predictionId: id }) }] };
  }
);

server.registerTool(
  "check_video",
  {
    title: "Check video",
    description: "Return the status and output of a video job by prediction id.",
    inputSchema: { predictionId: z.string().min(1) },
  },
  async ({ predictionId }) => {
    const job = await getPrediction(predictionId);
    return { content: [{ type: "text", text: JSON.stringify(job) }] };
  }
);

O modelo chama start_video, faz outras coisas e consulta check_video até que o status diga succeeded. Nada bloqueia, e um trabalho que falhou é só mais um status para reportar. Para vídeo com áudio, troque o modelo para picassoia/seedance-2.5-lite (Seedance 2.5 Lite); os auxiliares não mudam.

Estação de trabalho de um editor de vídeo com uma linha do tempo desfocada de imagens de pôr do sol e uma claquete simples

Publicar com segurança

Validar entradas e proteger segredos

Trate cada argumento de ferramenta como não confiável. Um modelo pode ser direcionado por textos que lê na web ou em um arquivo, então uma página com instruções injetadas pode pedir à sua ferramenta que faça algo que você nunca pretendeu. Três hábitos reduzem a maior parte do risco:

  • Limite cada campo no zod: min, max, enum e regex para ids.
  • Nunca passe argumentos para um comando de shell ou para uma string SQL. Use consultas parametrizadas e restrinja os caminhos de arquivo a um único diretório base.
  • Leia os tokens a partir de variáveis de ambiente, nunca os escreva diretamente no código, e nunca os devolva em um resultado de ferramenta.

Para clientes stdio, defina os segredos no bloco env da configuração do cliente. Para servidores HTTP, exija um cabeçalho Authorization e verifique-o antes de handleRequest rodar.

Uma mão inserindo um token de segurança de hardware de aço escovado em um notebook

Corrija os três erros mais comuns

  1. console.log em stdio. Troque por console.error.
  2. Extensões .js ausentes. Um import como ./server falha em tempo de execução sob Node16.
  3. Descrições vagas. Uma ferramenta chamada run com a descrição "faz coisas" nunca é escolhida, ou é escolhida com os argumentos errados. Dê a ela um nome que diga a ação e explique quando usá-la.

Para publicar, mantenha a linha shebang no topo de src/index.ts, rode npm run build e depois npm publish. Qualquer pessoa pode registrá-lo com claude mcp add notes -- npx -y mcp-notes-server. Servidores HTTP são distribuídos como contêiner ou rodam em qualquer host Node.

Quatro colegas revisando juntos um notebook em volta de uma mesa iluminada pelo sol em um espaço de coworking

Experimente no Picasso IA

Agora você tem um template que roda localmente, roda remotamente e pode chamar modelos de imagem e vídeo. A forma mais rápida de ver o que essas ferramentas devolvem é testar os modelos manualmente antes. Abra o PicassoIA Image e escreva um prompt tão específico quanto os que você enviaria a partir de generate_image: sujeito, lente, luz, cenário. Depois, experimente o PicassoIA Video ou o Seedance 2.5 Lite para animar a ideia, e anote qual formulação dá o resultado que você quer antes de fixar valores padrão no seu servidor. Escolha qualquer modelo do catálogo completo em picassoia.com/en/all-models e conecte-o com os mesmos dois auxiliares. Crie suas próprias imagens no Picasso IA hoje e deixe sua primeira chamada de ferramenta MCP entregar o resultado.

Compartilhe este artigo

Escolha seu idioma