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.
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.
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.
En qué se diferencian las configuraciones
El servidor es idéntico. El registro no lo es. Esta es toda la diferencia en una tabla:
Ajuste
Claude Code
GitHub 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íz
mcpServers
servers
Añadir desde la terminal
claude mcp add
Paleta de comandos: MCP: Add Server
Campo de transporte
type (stdio, http, sse)
type es obligatorio (stdio o http)
Secretos
Marca --env o expansión ${VAR}
Bloque inputs con ${input:id}
Dónde se ejecutan las herramientas
Cualquier sesión
Solo 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.
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.
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
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:
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
Escribe .vscode/mcp.json
Crea .vscode/mcp.json en tu espacio de trabajo. Recuerda la propiedad raíz distinta y el type obligatorio:
${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:
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
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:
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íntoma
Causa probable
Solución
El servidor nunca se conecta
Un console.log escribió en stdout
Cambia a console.error
"Command not found"
Ruta relativa o compilación que falta
Usa una ruta absoluta y ejecuta npm run build
Herramientas que faltan en Copilot
El chat está en modo Ask
Cambia a modo agente
La herramienta existe pero nunca se elige
Descripción vaga
Reescríbela con frases que activen su uso
Variable de entorno vacía
No declarada en la configuración
Añádela al bloque env
Las llamadas de imagen fallan con carga alta
Más de 5 trabajos a la vez
Pon 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.
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:
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.
Abre la página de Claude Sonnet 5 en la colección de modelos de lenguaje.
Pega las definiciones de tus herramientas, incluidos nombres, descripciones y esquemas, en el prompt.
Pídele que reescriba cada descripción como una instrucción breve que indique cuándo llamar a la herramienta y qué devuelve.
Pide diez entradas de casos límite por herramienta, como cadenas vacías, textos muy largos y etiquetas poco habituales.
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.
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.