Crea un servidor MCP para Claude Code y GitHub Copilot

Crea un único servidor MCP en TypeScript y regístralo en Claude Code y en GitHub Copilot. Obtienes código de herramientas que funciona, la configuración exacta de cada cliente, una rutina de depuración con el MCP Inspector y una herramienta de imágenes que llama a la API de PicassoIA.

Crea un servidor MCP para Claude Code y GitHub Copilot
Cristian Da Conceicao
Fundador de Picasso IA

Escribiste un script que te ahorra diez minutos al día, y ahora quieres que tu asistente de IA lo ejecute sin el rodeo de copiar y pegar. Constrúyelo una vez como servidor de Model Context Protocol y tanto Claude Code como GitHub Copilot podrán llamar a las mismas herramientas, porque MCP es el lenguaje común que usan para comunicarse con cualquier cosa fuera del editor. Este tutorial crea un servidor pequeño en TypeScript, lo registra en Claude Code, lo registra en Copilot dentro de VS Code y termina con una herramienta de imágenes real que llama a la API de PicassoIA. Cuenta con unos 40 minutos y unas 100 líneas de código.

Por qué un solo servidor gana a dos

Antes de MCP, cada asistente exigía su propio formato de plugin, su propio manifiesto y sus propias reglas de empaquetado. Un servidor MCP sustituye todo eso por un único proceso que anuncia lo que puede hacer. El cliente arranca el proceso, pide su lista de capacidades y pasa esa lista al modelo. Después, el modelo decide, a mitad de la conversación, cuándo merece la pena hacer una llamada.

Vista cenital de un escritorio con un diagrama dibujado a mano de cajas y flechas junto a un equipo portátil y una taza de café

El mismo protocolo, dos clientes

Un servidor puede exponer tres tipos de capacidades:

  • Herramientas: funciones que el modelo puede llamar, como add_note o generate_image.
  • Recursos: datos de solo lectura que el cliente puede adjuntar a una conversación, como un archivo de registro o un esquema.
  • Prompts: plantillas reutilizables que el usuario activa a propósito.

Hoy en día, casi todo el valor está en las herramientas, así que este artículo se centra en ellas. Tanto Claude Code como Copilot hablan los mismos mensajes JSON-RPC a través de los mismos transportes, lo que significa que un servidor que funciona con uno funcionará con el otro con casi ningún cambio.

Desarrollador de pie junto a una pizarra blanca llena de notas adhesivas organizadas en tres columnas

En qué se diferencian las configuraciones

El servidor es idéntico. El registro no lo es. Esta es toda la diferencia en una tabla:

AjusteClaude CodeGitHub Copilot en VS Code
Archivo de configuración.mcp.json en el proyecto, o ~/.claude.json.vscode/mcp.json, o tu perfil de usuario
Propiedad raízmcpServersservers
Añadir desde la terminalclaude mcp addPaleta de comandos: MCP: Add Server
Campo de transportetype (stdio, http, sse)type es obligatorio (stdio o http)
SecretosMarca --env o expansión ${VAR}Bloque inputs con ${input:id}
Dónde se ejecutan las herramientasCualquier sesiónSolo en modo agente

💡 Consejo: La propiedad raíz es la trampa clásica. Si pegas una configuración de Claude Code en VS Code sin cambiarla, no se carga nada, porque Copilot busca servers, no mcpServers.

Prepara el proyecto

Elige una carpeta fuera de tu repositorio principal para que el servidor pueda atender varios proyectos más adelante. Necesitas Node.js 20 o una versión posterior y una terminal.

Primer plano de las manos de un desarrollador escribiendo, con un editor de código desenfocado detrás

Instala el SDK

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

Abre package.json y añade "type": "module" más dos scripts, "build": "tsc" y "dev": "tsx src/index.ts". Después crea un tsconfig.json:

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

Los ejemplos usan la API 1.x de @modelcontextprotocol/sdk con McpServer y registerTool. Si una versión mayor nueva cambia una ruta de importación, los conceptos de abajo siguen siendo los mismos.

Empieza con stdio

MCP define dos transportes principales. stdio significa que el cliente lanza tu servidor como proceso hijo e intercambia mensajes por la entrada y salida estándar. HTTP en streaming significa que el servidor se ejecuta por su cuenta y los clientes se conectan por URL. Empieza con stdio. No necesita puerto, ni capa de autenticación ni alojamiento, y ambos clientes lo admiten de serie. Pasa a HTTP solo cuando varias personas deban compartir una misma instancia en ejecución.

Escribe el servidor

Nuestro ejemplo es un servidor minúsculo de notas de equipo con dos herramientas: una guarda una nota y otra las busca. Es lo bastante pequeño para leerlo en un minuto y lo bastante real como para ser útil.

Dos desarrolladores sentados uno junto al otro mientras uno señala la pantalla de un equipo portátil

Registra una herramienta

Crea 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");

Ejecuta npm run build. Ahora tienes dist/index.js, y ese archivo es lo único que los dos clientes necesitan saber.

Devuelve resultados limpios

El modelo lee todo lo que devuelves, así que trata el valor de retorno como una interfaz. Mantén los resultados breves, estructurados y honestos. Cuando algo falle, no lances una excepción que muera en la capa de transporte. Devuelve un error que el modelo pueda leer y al que pueda reaccionar:

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

Un resultado isError permite que el asistente te explique el problema o vuelva a intentarlo con otra entrada. Un fallo solo muestra un vago aviso de "servidor desconectado".

Mantén stdout en silencio

Esta es la razón más habitual por la que falla un primer servidor. Con stdio, la salida estándar pertenece al protocolo. Un solo console.log suelto inyecta texto plano en el flujo JSON-RPC y el cliente cierra la conexión. Registra con console.error, que escribe en stderr, y ambos clientes lo capturarán como salida de diagnóstico.

💡 Consejo: Escribe las descripciones de las herramientas como instrucciones para el modelo, no como documentación para personas. "Úsala cuando el usuario pida recordar una decisión" consigue que la herramienta se elija en el momento adecuado. "Utilidad de notas" no.

Conecta Claude Code

Primer plano de una ventana de terminal en la pantalla de un equipo portátil reflejada en unas gafas

Añádelo con la CLI

Un solo comando registra el servidor. Las opciones van antes del nombre, y un doble guion separa el nombre del comando que Claude Code lanzará:

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

Usa una ruta absoluta. Claude Code inicia el proceso desde el directorio que use tu sesión, así que las rutas relativas se rompen en cuanto abres otro proyecto. Después, compruébalo:

claude mcp list
claude mcp get team-notes

Dentro de una sesión, escribe /mcp para ver el estado de la conexión y la lista de herramientas. Pide algo natural, como "Recuerda que desplegamos los jueves, etiquétalo como release", y observa cómo Claude Code pide permiso para llamar a add_note.

Compártelo mediante .mcp.json

La marca de ámbito decide quién tiene acceso al servidor. local lo mantiene privado para ti en un proyecto, user lo deja disponible en todos los proyectos, y project escribe un archivo .mcp.json que puedes subir al repositorio para que todo el equipo lo tenga. Aquí tienes una configuración compartida que evita rutas fijas:

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

Cada compañero define TEAM_NOTES_PATH una vez en su shell. La forma ${NOTES_DIR:-.notes} proporciona un valor por defecto cuando falta la variable. Claude Code pide aprobación la primera vez que ve un servidor con ámbito de proyecto, una salvaguarda sensata para cualquier cosa que se descargue de un repositorio.

Conecta GitHub Copilot

Desarrollador recostado en una silla de oficina sonriendo ante dos monitores con la luz de última hora de la tarde

Escribe .vscode/mcp.json

Crea .vscode/mcp.json en tu espacio de trabajo. Recuerda la propiedad raíz distinta y el type obligatorio:

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

${workspaceFolder} hace que el archivo sea portable, así que puedes subirlo al repositorio. Para los secretos, añade una matriz inputs. VS Code pide el valor una vez, lo guarda de forma segura y lo inyecta:

{
  "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}" }
    }
  }
}

Cambia al modo agente

Copilot Chat se abre en modo Ask por defecto, y las herramientas de MCP solo se activan en modo agente. Cambia el modo en el panel del chat, abre el selector de herramientas y confirma que team-notes aparece con ambas herramientas marcadas. Si no aparece, ejecuta MCP: List Servers desde la paleta de comandos, elige el servidor y lee su salida. Reinícialo desde el mismo menú después de cada recompilación.

Copilot también te permite elegir entre los modelos que incluye tu plan, así que el mismo servidor se pone a prueba con modelos distintos. Es una forma barata de comprobar si las descripciones de tus herramientas son lo bastante claras para todos ellos.

Prueba y depura antes de publicar

Perfil lateral de un desarrollador con barba y gorro de lana frente a un escritorio de pie junto a una ventana con lluvia

Ejecuta el MCP Inspector

Antes de culpar a cualquiera de los dos clientes, prueba el servidor por separado. El Inspector oficial abre una página web local donde puedes listar herramientas, rellenar argumentos y ver las respuestas sin procesar:

npx @modelcontextprotocol/inspector node dist/index.js

Llama a add_note con un text vacío. Tu esquema de Zod debería rechazarlo con un mensaje de validación legible. Después llama a search_notes con una etiqueta que acabas de guardar. Si ambas pruebas funcionan aquí, cualquier problema que quede está en la configuración del cliente, no en tu código.

Soluciona los fallos habituales

SíntomaCausa probableSolución
El servidor nunca se conectaUn console.log escribió en stdoutCambia a console.error
"Command not found"Ruta relativa o compilación que faltaUsa una ruta absoluta y ejecuta npm run build
Herramientas que faltan en CopilotEl chat está en modo AskCambia a modo agente
La herramienta existe pero nunca se eligeDescripción vagaReescríbela con frases que activen su uso
Variable de entorno vacíaNo declarada en la configuraciónAñádela al bloque env
Las llamadas de imagen fallan con carga altaMás de 5 trabajos a la vezPon las llamadas en cola dentro de la herramienta

Dale a tu servidor una herramienta de imágenes

Las notas están bien, pero la mejor demostración de MCP es una herramienta que haga algo que el asistente no puede hacer por sí solo. La generación de imágenes encaja bien: el modelo escribe un prompt preciso, tu servidor lo convierte en un archivo y la URL vuelve directamente a la conversación.

Vista cenital de cuatro personas señalando diagramas impresos alrededor de una larga mesa de roble

Llama a la API de PicassoIA

La API para desarrolladores de PicassoIA está en https://api.picassoia.com/v1 y usa un token Bearer que empieza por pia_sk_. Las predicciones son asíncronas, al estilo de Replicate: creas una y luego consultas su estado hasta que figure como succeeded. El modelo PicassoIA Image acepta un prompt de hasta 4000 caracteres y un aspect_ratio, y devuelve una lista de URL de imágenes. Añade esta herramienta antes de la línea 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] }] };
  }
);

Una cuenta permite 5 predicciones simultáneas, compartidas entre todos los tokens y conexiones, así que un bucle que lance diez imágenes a la vez llegará a ese límite. Genéralas una tras otra dentro de la herramienta o mantén una cola pequeña.

💡 Consejo: Revisa los precios actuales y los requisitos del plan en la página de la API de PicassoIA antes de publicar un servidor para otras personas. La documentación y la página de precios describen el acceso de forma distinta, así que confirma qué incluye tu propio plan.

Cómo usar Sonnet 5 en PicassoIA

Las descripciones de tus herramientas son prompts, y un modelo de lenguaje es el mejor editor para ellas. Claude Sonnet 5 es una opción sólida para esta tarea, y puedes ejecutarlo en PicassoIA sin salir del navegador.

  1. Abre la página de Claude Sonnet 5 en la colección de modelos de lenguaje.
  2. Pega las definiciones de tus herramientas, incluidos nombres, descripciones y esquemas, en el prompt.
  3. Pídele que reescriba cada descripción como una instrucción breve que indique cuándo llamar a la herramienta y qué devuelve.
  4. Pide diez entradas de casos límite por herramienta, como cadenas vacías, textos muy largos y etiquetas poco habituales.
  5. Pasa esas entradas por el MCP Inspector, corrige cada fallo y pega las descripciones mejoradas de nuevo en tu código.

Para una segunda opinión sobre la lógica compleja, Claude Fable 5 y GPT 5.6 Sol aparecen ambos en la lista para tareas de programación. Comparar sus reescrituras de una misma descripción suele revelar qué formulación es ambigua.

Pruébalo tú mismo en PicassoIA

Ahora tienes un servidor que funciona en dos asistentes: notas para la memoria, una herramienta de imágenes para la salida y una rutina de pruebas que los mantiene fiables. El mismo patrón escala a cualquier cosa que puedas envolver en una función, desde scripts de despliegue hasta consultas a bases de datos.

Equipo portátil y café con leche sobre la mesa de una cafetería con un teléfono inteligente al lado

Empieza por la herramienta de imágenes, porque da una respuesta inmediata. Escribe un prompt, llama a generate_image desde Claude Code o Copilot y mira lo que vuelve en segundos. Después abre la página de PicassoIA Image y experimenta con las relaciones de aspecto y los estilos de prompt directamente, o explora todos los modelos disponibles en picassoia.com/en/all-models. Tu primera imagen está a un solo prompt de distancia.

Compartir este artículo

Elige tu idioma