Crie um servidor MCP para Claude Code e GitHub Copilot

Crie um único servidor MCP em TypeScript e registre-o no Claude Code e no GitHub Copilot. Você leva o código funcional da ferramenta, a configuração exata de cada cliente, uma rotina de depuração com o MCP Inspector e uma ferramenta de imagem que chama a API do PicassoIA.

Crie um servidor MCP para Claude Code e GitHub Copilot
Cristian Da Conceicao
Fundador do Picasso IA

Você escreveu um script que economiza dez minutos por dia, e agora quer que seu assistente de IA o execute sem ter que copiar e colar. Construa-o uma vez como um servidor Model Context Protocol e tanto o Claude Code quanto o GitHub Copilot podem chamar as mesmas ferramentas, porque o MCP é a linguagem comum que eles usam para se comunicar com qualquer coisa fora do editor. Este passo a passo cria um pequeno servidor em TypeScript, registra-o no Claude Code, registra-o no Copilot dentro do VS Code e termina com uma ferramenta de imagem real que chama a API do PicassoIA. Reserve cerca de 40 minutos e cerca de 100 linhas de código.

Por que um servidor é melhor que dois

Antes do MCP, cada assistente exigia seu próprio formato de plugin, seu próprio manifesto e suas próprias regras de empacotamento. Um servidor MCP substitui todo esse acúmulo por um único processo que anuncia o que pode fazer. O cliente inicia o processo, pede a lista de capacidades e entrega essa lista ao modelo. O modelo então decide, no meio da conversa, quando uma chamada vale a pena.

Vista aérea de uma mesa com um diagrama desenhado à mão, com caixas e setas, ao lado de um notebook e um café

Mesmo protocolo, dois clientes

Um servidor pode expor três tipos de capacidade:

  • Ferramentas: funções que o modelo pode chamar, como add_note ou generate_image.
  • Recursos: dados somente leitura que o cliente pode anexar a uma conversa, como um arquivo de log ou um schema.
  • Prompts: modelos reutilizáveis que o usuário aciona de propósito.

As ferramentas concentram quase todo o valor hoje, então este artigo se concentra nelas. Tanto o Claude Code quanto o Copilot falam as mesmas mensagens JSON-RPC pelos mesmos transportes, o que significa que um servidor que funciona com um cliente vai funcionar com o outro praticamente sem mudanças.

Desenvolvedor de pé ao lado de um quadro branco cheio de notas adesivas organizadas em três colunas

Onde as configurações diferem

O servidor é idêntico. O registro não é. Aqui está toda a diferença em uma tabela:

ConfiguraçãoClaude CodeGitHub Copilot no VS Code
Arquivo de configuração.mcp.json no projeto, ou ~/.claude.json.vscode/mcp.json, ou seu perfil de usuário
Propriedade raizmcpServersservers
Adicionar pelo terminalclaude mcp addPaleta de Comandos: MCP: Add Server
Campo de transportetype (stdio, http, sse)type é obrigatório (stdio ou http)
SegredosFlag --env ou expansão de ${VAR}Bloco inputs com ${input:id}
Onde as ferramentas rodamQualquer sessãomodo Agent apenas

💡 Dica: A propriedade raiz é a armadilha clássica. Cole uma configuração do Claude Code no VS Code sem alterações e nada é carregado, porque o Copilot procura servers, não mcpServers.

Preparar o projeto

Escolha uma pasta fora do seu repositório principal para que o servidor possa atender vários projetos depois. Você precisa do Node.js 20 ou mais recente e de um terminal.

Close de mãos de um desenvolvedor digitando, com um editor de código desfocado ao fundo

Instalar o SDK

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

Abra package.json e adicione "type": "module" mais dois scripts, "build": "tsc" e "dev": "tsx src/index.ts". Depois crie um tsconfig.json:

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

Os exemplos usam a API 1.x do @modelcontextprotocol/sdk com McpServer e registerTool. Se uma versão principal mais nova alterar um caminho de importação, os conceitos abaixo continuam os mesmos.

Começar com stdio

O MCP define dois transportes principais. stdio significa que o cliente inicia seu servidor como um processo filho e troca mensagens pela entrada e saída padrão. HTTP em streaming significa que o servidor roda por conta própria e os clientes se conectam por URL. Comece com stdio. Ele não precisa de porta, de camada de autenticação nem de hospedagem, e os dois clientes o suportam de imediato. Passe para HTTP apenas quando várias pessoas precisarem compartilhar uma mesma instância em execução.

Escrever o servidor

Nosso exemplo é um pequeno servidor de notas de equipe com duas ferramentas: uma salva uma nota, a outra pesquisa nelas. Ele é pequeno o bastante para ler em um minuto e real o bastante para ser útil.

Dois desenvolvedores sentados lado a lado enquanto um aponta para a tela de um notebook

Registrar uma ferramenta

Crie src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { promises as fs } from "node:fs";
import path from "node:path";

const NOTES_FILE = path.join(process.env.NOTES_DIR ?? process.cwd(), "notes.json");

type Note = { id: number; text: string; tags: string[]; createdAt: string };

async function readNotes(): Promise<Note[]> {
  try {
    return JSON.parse(await fs.readFile(NOTES_FILE, "utf8"));
  } catch {
    return [];
  }
}

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

server.registerTool(
  "add_note",
  {
    title: "Add note",
    description:
      "Save a short engineering note with optional tags. Use it when the user asks to remember a decision, a command or a bug.",
    inputSchema: {
      text: z.string().min(3).max(2000),
      tags: z.array(z.string()).default([]),
    },
  },
  async ({ text, tags }) => {
    const notes = await readNotes();
    const note: Note = {
      id: notes.length + 1,
      text,
      tags,
      createdAt: new Date().toISOString(),
    };
    await fs.writeFile(NOTES_FILE, JSON.stringify([...notes, note], null, 2));
    return { content: [{ type: "text", text: `Saved note #${note.id}` }] };
  }
);

server.registerTool(
  "search_notes",
  {
    title: "Search notes",
    description: "Find saved notes whose text or tags contain the query.",
    inputSchema: { query: z.string().min(1) },
  },
  async ({ query }) => {
    const q = query.toLowerCase();
    const hits = (await readNotes()).filter(
      (n) =>
        n.text.toLowerCase().includes(q) ||
        n.tags.some((t) => t.toLowerCase().includes(q))
    );
    const text = hits.length
      ? hits.map((n) => `#${n.id} [${n.tags.join(", ")}] ${n.text}`).join("\n")
      : "No notes matched.";
    return { content: [{ type: "text", text }] };
  }
);

await server.connect(new StdioServerTransport());
console.error("team-notes MCP server running on stdio");

Execute npm run build. Agora você tem dist/index.js, e esse arquivo é a única coisa que os dois clientes precisam saber.

Retornar resultados limpos

O modelo lê tudo o que você retorna, então trate o valor de retorno como uma interface. Mantenha os resultados curtos, estruturados e exatos. Quando algo falhar, não lance uma exceção que morra na camada de transporte. Retorne um erro que o modelo possa ler e reagir a ele:

return {
  isError: true,
  content: [{ type: "text", text: "notes.json is not valid JSON. Fix or delete it." }],
};

Um resultado isError permite que o assistente explique o problema para você ou tente de novo com outra entrada. Uma falha só mostra um vago aviso de "servidor desconectado".

Mantenha o stdout em silêncio

Este é o motivo mais comum de um primeiro servidor falhar. Com stdio, a saída padrão pertence ao protocolo. Um único console.log perdido injeta texto simples no fluxo JSON-RPC e o cliente derruba a conexão. Registre logs em console.error, que escreve no stderr, e os dois clientes vão capturá-lo como saída de diagnóstico.

💡 Dica: Escreva as descrições das ferramentas como instruções para o modelo, não como documentação para humanos. "Use quando o usuário pedir para lembrar de uma decisão" faz a ferramenta ser escolhida no momento certo. "Utilitário de notas" não faz.

Conectar o Claude Code

Foto macro de uma janela de terminal numa tela de notebook refletida em um par de óculos

Adicionar com a CLI

Um comando registra o servidor. As opções vêm antes do nome, e um duplo hífen separa o nome do comando que o Claude Code vai iniciar:

claude mcp add --transport stdio --scope user \
  --env NOTES_DIR=/home/dev/notes \
  team-notes -- node /absolute/path/to/team-notes-mcp/dist/index.js

Use um caminho absoluto. O Claude Code inicia o processo a partir do diretório que sua sessão usa, então caminhos relativos quebram assim que você abre outro projeto. Depois confirme:

claude mcp list
claude mcp get team-notes

Dentro de uma sessão, digite /mcp para ver o status da conexão e a lista de ferramentas. Faça uma pergunta natural, como "Lembre que fazemos deploy às quintas, marque como release", e observe o Claude Code pedir permissão para chamar add_note.

Compartilhar pelo .mcp.json

A flag de escopo decide quem recebe o servidor. local mantém ele privado para você em um projeto, user o torna disponível em todos os projetos, e project grava um arquivo .mcp.json que você pode versionar para que a equipe inteira o receba. Aqui está uma configuração compartilhada que evita caminhos fixos no código:

{
  "mcpServers": {
    "team-notes": {
      "type": "stdio",
      "command": "node",
      "args": ["${TEAM_NOTES_PATH}/dist/index.js"],
      "env": { "NOTES_DIR": "${NOTES_DIR:-.notes}" }
    }
  }
}

Cada colega define TEAM_NOTES_PATH uma vez no shell. A forma ${NOTES_DIR:-.notes} fornece um valor padrão quando a variável não existe. O Claude Code pede aprovação na primeira vez que vê um servidor com escopo de projeto, o que é uma salvaguarda sensata para qualquer coisa baixada de um repositório.

Conectar o GitHub Copilot

Desenvolvedor recostado em uma cadeira de escritório, sorrindo para dois monitores na luz do fim de tarde

Escrever o .vscode/mcp.json

Crie .vscode/mcp.json no seu workspace. Lembre-se da propriedade raiz diferente e do type obrigatório:

{
  "servers": {
    "team-notes": {
      "type": "stdio",
      "command": "node",
      "args": ["${workspaceFolder}/tools/team-notes-mcp/dist/index.js"],
      "env": { "NOTES_DIR": "${workspaceFolder}/.notes" }
    }
  }
}

${workspaceFolder} torna o arquivo portável, para que você possa versioná-lo. Para segredos, adicione um array inputs. O VS Code pede uma vez, armazena o valor com segurança e o injeta:

{
  "inputs": [
    { "type": "promptString", "id": "picassoia-token", "description": "PicassoIA API token", "password": true }
  ],
  "servers": {
    "team-notes": {
      "type": "stdio",
      "command": "node",
      "args": ["${workspaceFolder}/tools/team-notes-mcp/dist/index.js"],
      "env": { "PICASSOIA_API_TOKEN": "${input:picassoia-token}" }
    }
  }
}

Mudar para o modo Agent

O Copilot Chat abre no modo Ask por padrão, e as ferramentas MCP só disparam no modo Agent. Troque o modo no painel de chat, abra o seletor de ferramentas e confirme que team-notes aparece com as duas ferramentas marcadas. Se não aparecer, execute MCP: List Servers na Paleta de Comandos, escolha o servidor e leia a saída dele. Reinicie-o pelo mesmo menu após cada recompilação.

O Copilot também permite escolher entre os modelos que seu plano oferece, então o mesmo servidor é testado por modelos diferentes. É uma forma barata de verificar se as descrições das suas ferramentas são claras o bastante para todos eles.

Testar e depurar antes de publicar

Perfil lateral de um desenvolvedor barbudo de gorro em uma mesa em pé ao lado de uma janela com chuva

Executar o MCP Inspector

Antes de culpar qualquer um dos clientes, teste o servidor sozinho. O Inspector oficial abre uma página web local onde você pode listar ferramentas, preencher argumentos e ver as respostas brutas:

npx @modelcontextprotocol/inspector node dist/index.js

Chame add_note com um text vazio. Seu schema Zod deve rejeitá-lo com uma mensagem de validação legível. Depois chame search_notes com uma tag que você acabou de salvar. Se os dois se comportarem aqui, qualquer problema restante está na configuração do cliente, não no seu código.

Corrigir as falhas mais comuns

SintomaCausa provávelCorreção
Servidor nunca conectaUm console.log escreveu no stdoutTroque por console.error
"Command not found"Caminho relativo ou build ausenteUse um caminho absoluto e execute npm run build
Ferramentas ausentes no CopilotO chat está no modo AskMude para o modo Agent
Ferramenta existe, mas nunca é escolhidaDescrição vagaReescreva com frases de gatilho
Variável de ambiente vaziaNão declarada na configuraçãoAdicione-a ao bloco env
Chamadas de imagem falham sob cargaMais de 5 jobs ao mesmo tempoEnfileire as chamadas dentro da ferramenta

Dê ao seu servidor uma ferramenta de imagem

Notas são úteis, mas a melhor demonstração do MCP é uma ferramenta que faz algo que o assistente não consegue fazer sozinho. A geração de imagens é uma boa escolha: o modelo escreve um prompt preciso, seu servidor o transforma em um arquivo, e a URL volta direto para a conversa.

Vista aérea de quatro pessoas apontando para diagramas impressos ao redor de uma mesa longa de carvalho

Chamar a API do PicassoIA

A API para desenvolvedores do PicassoIA fica em https://api.picassoia.com/v1 e usa um token Bearer que começa com pia_sk_. As previsões são assíncronas, no estilo Replicate: você cria uma e depois consulta até que o status mostre succeeded. O modelo PicassoIA Image aceita um prompt de até 4.000 caracteres, um aspect_ratio e retorna uma lista de URLs de imagens. Adicione esta ferramenta antes da linha server.connect:

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

server.registerTool(
  "generate_image",
  {
    title: "Generate image",
    description: "Create an image from a text prompt with PicassoIA and return its URL.",
    inputSchema: {
      prompt: z.string().min(1).max(4000),
      aspect_ratio: z.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"]).default("16:9"),
    },
  },
  async ({ prompt, aspect_ratio }) => {
    const created = await fetch(`${API}/models/picassoia/picassoia-image/predictions`, {
      method: "POST",
      headers,
      body: JSON.stringify({ input: { prompt, aspect_ratio } }),
    }).then((r) => r.json());

    let prediction = created;
    while (["starting", "processing"].includes(prediction.status)) {
      const wait = prediction.eta?.next_poll_in_seconds ?? 2;
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      prediction = await fetch(`${API}/predictions/${created.id}`, { headers }).then((r) => r.json());
    }

    if (prediction.status !== "succeeded") {
      return {
        isError: true,
        content: [{ type: "text", text: `Generation ${prediction.status ?? "request failed"}` }],
      };
    }
    return { content: [{ type: "text", text: prediction.output[0] }] };
  }
);

Uma conta permite 5 previsões simultâneas, compartilhadas entre todos os tokens e conexões, então um loop que dispara dez imagens de uma vez vai bater nesse teto. Gere-as uma após a outra dentro da ferramenta, ou mantenha uma pequena fila.

💡 Dica: Verifique os preços e os requisitos de plano atuais na página da API do PicassoIA antes de publicar um servidor para outras pessoas. A documentação e a página de preços descrevem o acesso de formas diferentes, então confirme o que o seu próprio plano inclui.

Como usar o Sonnet 5 no PicassoIA

Suas descrições de ferramentas são prompts, e um modelo de linguagem é o melhor editor para elas. O Claude Sonnet 5 é uma ótima escolha para essa tarefa, e você pode executá-lo no PicassoIA sem sair do navegador.

  1. Abra a página do Claude Sonnet 5 na coleção de modelos de linguagem.
  2. Cole as definições das suas ferramentas, incluindo nomes, descrições e schemas, no prompt.
  3. Peça para reescrever cada descrição como uma instrução curta que diga quando chamar a ferramenta e o que ela retorna.
  4. Peça dez entradas de casos extremos por ferramenta, como strings vazias, textos muito longos e tags incomuns.
  5. Execute essas entradas no MCP Inspector, corrija todas as falhas e cole as descrições melhoradas de volta no seu código.

Para uma segunda opinião sobre lógica complicada, o Claude Fable 5 e o GPT 5.6 Sol estão ambos listados para tarefas de programação. Comparar as reescritas deles de uma mesma descrição costuma revelar qual formulação é ambígua.

Experimente você mesmo no PicassoIA

Agora você tem um servidor rodando em dois assistentes: notas para memória, uma ferramenta de imagem para saída e uma rotina de testes que mantém os dois sob controle. O mesmo padrão escala para qualquer coisa que você possa encapsular em uma função, de scripts de deploy a consultas em banco de dados.

Notebook e café branco em uma mesa de cafeteria, com um smartphone ao lado

Comece pela ferramenta de imagem, porque ela dá retorno instantâneo. Escreva um prompt, chame generate_image a partir do Claude Code ou do Copilot e veja o resultado em segundos. Depois abra a página PicassoIA Image e teste proporções e estilos de prompt diretamente, ou navegue por todos os modelos disponíveis em picassoia.com/en/all-models. Sua primeira imagem está a um prompt de distância.

Compartilhe este artigo

Escolha seu idioma