Tutorial de servidor MCP en TypeScript: ejemplo del SDK y plantilla lista para copiar

Un servidor MCP funcional en TypeScript, desde npm install hasta Claude Code. Copia la plantilla del SDK, registra una herramienta, un recurso y un prompt, elige stdio o Streamable HTTP, prueba todo en el Inspector y añade herramientas de imagen y video que llamen a una API real sin bloquear al cliente.

Tutorial de servidor MCP en TypeScript: ejemplo del SDK y plantilla lista para copiar
Cristian Da Conceicao
Fundador de Picasso IA

Puedes conectar un modelo de lenguaje a tu propio código en unas cuarenta líneas. Este tutorial de servidor MCP en TypeScript construye exactamente eso: un servidor funcional con el SDK oficial, una plantilla de proyecto que puedes copiar y los dos transportes que importan, stdio para clientes locales y Streamable HTTP para remotos. Al final tendrás una herramienta, un recurso y un prompt, probados en el Inspector y registrados en Claude Code. Después añadimos herramientas de imagen y video, porque ahí es donde un servidor de Model Context Protocol deja de ser una demo y empieza a hacer trabajo real. Cada fragmento de código funciona con Node.js 20 o superior y el paquete @modelcontextprotocol/sdk.

Un desarrollador escribiendo TypeScript en un equipo portátil sobre un escritorio de madera con luz suave de la mañana

Qué hace un servidor MCP

El Model Context Protocol (MCP) es un estándar abierto que permite a un cliente de IA, como Claude Code, Claude Desktop o un agente de un IDE, llamar a funciones y leer datos que viven dentro de tu proceso. Los mensajes viajan como JSON-RPC 2.0. Tu servidor anuncia lo que ofrece, el cliente lista esas capacidades y el modelo decide cuándo usarlas. Tu código nunca habla directamente con el modelo. Responde a las peticiones, y por eso el servidor se mantiene pequeño.

Tres bloques de construcción

Todo servidor MCP se compone de alguna combinación de tres primitivas:

BloqueQuién decide usarloUso típico
HerramientaEl modeloConsultar una base de datos, llamar a una API, generar una imagen
RecursoLa aplicación o el usuarioExponer un documento, un archivo o una configuración como contexto legible
PromptEl usuarioUna plantilla reutilizable, como "revisa este pull request"

En la práctica, las herramientas hacen la mayor parte del trabajo. Una herramienta es una función con nombre, un esquema de entrada tipado y un resultado de texto o imagen. Los recursos y los prompts son opcionales, pero cuestan casi nada añadirlos una vez que el servidor existe.

Cliente, servidor y transporte

El transporte es solo la tubería por la que circulan los mensajes JSON-RPC. El mismo objeto McpServer funciona con stdio o con HTTP, así que la estructura correcta es construir el servidor en una sola función y conectar el transporte en un archivo de entrada aparte. La plantilla de abajo sigue esa regla, lo que simplifica las pruebas y te permite publicar ambos transportes desde una sola base de código.

Preparar el proyecto de TypeScript

Instalar el SDK y zod

Crea una carpeta e instala las dependencias. El SDK usa zod para los esquemas de entrada: los convierte a JSON Schema para el cliente y valida cada argumento que llega antes de que se ejecute tu handler.

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 y tsconfig

Cambia el proyecto a módulos ES y añade un script de compilación:

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

Después añade un tsconfig.json que coincida con la forma en que Node resuelve los módulos:

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

La estructura de carpetas es deliberadamente sencilla:

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

💡 Las importaciones del SDK terminan en .js, incluso en archivos TypeScript. Con la resolución Node16, el compilador exige extensiones explícitas, y si falta una, el error aparece en tiempo de ejecución como ERR_MODULE_NOT_FOUND.

Vista cenital de un escritorio ordenado con una libreta donde se dibuja el árbol de carpetas de un proyecto

Construir la plantilla del servidor

La fábrica del servidor

Pon todo lo que ofrece el servidor en src/server.ts. Esta versión registra una herramienta que guarda una nota y otra que busca una 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;
}

Hay dos detalles más importantes de lo que parecen. Primero, la descripción es lo que lee el modelo para decidir si llama a la herramienta, así que escríbela como si fuera documentación para un compañero. Segundo, cuando algo falla, devuelve isError: true con un mensaje legible en lugar de lanzar una excepción. Así el modelo puede reintentar con un argumento corregido o explicarle el problema al usuario.

Una mano dibujando cajas conectadas y flechas en una pizarra blanca dentro de una sala de reuniones luminosa

Añadir un recurso y un prompt

Sustituye el comentario de marcador de posición por esto:

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.`,
        },
      },
    ],
  })
);

Los recursos se identifican por URI (notes://all), y los clientes suelen mostrarlos en un selector para que el usuario los adjunte como contexto. Los prompts aparecen como comandos de barra o entradas de menú, según el cliente.

Deja que un modelo de programación te ayude

Una vez que esta plantilla funciona, un modelo de programación puede añadir herramientas en minutos. Pega src/server.ts en un chat y pide una herramienta nueva que siga el mismo patrón: primero el esquema, isError en caso de fallo. Claude Sonnet 5, Kimi K2.6 y GPT 5.6 Sol están diseñados para trabajo de código y están disponibles en PicassoIA. Lee el esquema generado antes de aceptarlo: un modelo hará opcional sin pensarlo un campo que debería ser obligatorio.

Elegir un transporte

stdio para clientes locales

stdio es el transporte más sencillo. El cliente lanza tu servidor como proceso hijo y se comunica con él por stdin y stdout. Crea 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 uses console.log en un servidor stdio. La salida estándar transporta el protocolo, así que una sola línea de log suelta corrompe el flujo y el cliente se desconecta con un error de análisis. Envía los logs a stderr con console.error.

Streamable HTTP para clientes remotos

Para un servidor que vive en un host en lugar de en un equipo portátil, usa Streamable HTTP. Sustituyó al antiguo transporte HTTP plus SSE y necesita un único endpoint. Instala Express con npm install express y npm install -D @types/express, y luego guarda esto 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"));

Al establecer sessionIdGenerator: undefined, el transporte funciona en modo stateless: un servidor nuevo por petición, sin sesión que seguir y escalado horizontal como cualquier otra API. Si necesitas notificaciones iniciadas por el servidor o flujos reanudables, cambia al modo stateful con ids de sesión.

stdioStreamable HTTP
Se ejecuta enProceso hijo del clienteCualquier host accesible por HTTP
AutenticaciónHereda el entorno del usuarioLa añades tú (OAuth o tokens bearer)
Ideal paraHerramientas personales y de desarrollo localServidores compartidos y alojados
EscaladoUn proceso por clienteSin estado, escala como una API

Vista desde abajo de un pasillo estrecho con racks de servidores y cables de red bien ordenados

Probar antes de conectar

Ejecutar el MCP Inspector

El Inspector es la interfaz de depuración oficial. Compila el proyecto y lanza tu servidor a través de él:

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

Abre la URL local que muestra, pulsa Connect y luego usa la pestaña Tools para listar las herramientas y ejecutar add_note con un argumento JSON. El panel de historial muestra el tráfico JSON-RPC sin procesar, que es la forma más rápida de detectar un esquema que no coincide con lo que pretendías.

Registrar en Claude Code y Claude Desktop

Claude Code registra un servidor local con un solo comando, y un servidor HTTP con una opción 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

Claude Desktop lee un archivo JSON (claude_desktop_config.json):

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

💡 Usa rutas absolutas en ambos casos. Una ruta relativa se resuelve desde el directorio de trabajo del cliente, no desde el tuyo, y el mensaje de error rara vez lo indica.

Un desarrollador en un escritorio de pie con dos monitores, concentrado en depurar una terminal

Añadir herramientas de imagen y video

Un servidor de notas demuestra el patrón. Las herramientas multimedia muestran por qué compensa: trabajos lentos, salidas grandes y una API externa con sus propios límites. PicassoIA expone una API para desarrolladores al estilo de Replicate en https://api.picassoia.com/v1, autenticada con un token Bearer que empieza por pia_sk_. Cuatro modelos son accesibles a través de la API y del conector MCP: PicassoIA Image para texto a imagen, PicassoIA Image Editor Pro para ediciones, PicassoIA Video para video y Seedance 2.5 Lite para video con audio. Los trabajos son asíncronos: creas una predicción, la consultas repetidamente y después lees el resultado.

Envolver un endpoint de imagen

Dos pequeños helpers sirven para todos los modelos, porque la forma de la API es la misma:

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 };
}

La herramienta de imagen los usa y espera hasta dos minutos por un 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) }] };
    }
  }
);

El límite de 4000 caracteres del esquema coincide con el límite de prompt de la API, y la página de cada modelo enumera sus campos de entrada exactos y su formato de salida. La API también permite 5 predicciones simultáneas por cuenta, compartidas entre tokens y conexiones MCP, así que pon en cola las llamadas paralelas a herramientas en lugar de lanzarlas todas a la vez.

Escritorio de un diseñador con fotos impresas de paisajes de montaña, una tableta y muestras de color

Gestionar los trabajos lentos de video

El video tarda mucho más que una imagen, y una llamada a herramienta que se bloquea durante minutos puede superar el tiempo de espera propio del cliente. Divide el trabajo en dos herramientas: una inicia el trabajo y devuelve su id de inmediato, la otra consulta su estado.

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) }] };
  }
);

El modelo llama a start_video, hace otras cosas y consulta check_video hasta que el estado indique succeeded. Nada se bloquea, y un trabajo fallido es solo otro estado que reportar. Para video con audio, cambia el modelo a picassoia/seedance-2.5-lite (Seedance 2.5 Lite); los helpers no cambian.

Estación de trabajo de un editor de video con una línea de tiempo desenfocada de imágenes de atardecer y una claqueta sin decoración

Publicarlo con seguridad

Validar entradas y proteger los secretos

Trata cada argumento de herramienta como no confiable. Un modelo puede ser dirigido por texto que lee en la web o en un archivo, así que una página manipulada puede pedir a tu herramienta que haga algo que nunca pretendiste. Tres hábitos reducen la mayor parte del riesgo:

  • Acota cada campo en zod: min, max, enum y regex para los ids.
  • Nunca pases argumentos a un comando de shell ni a una cadena SQL. Usa consultas parametrizadas y limita las rutas de archivo a un único directorio base.
  • Lee los tokens desde variables de entorno, nunca los escribas en el código, y nunca los devuelvas en el resultado de una herramienta.

Para los clientes stdio, define los secretos en el bloque env de la configuración del cliente. Para los servidores HTTP, exige una cabecera Authorization y verifícala antes de que se ejecute handleRequest.

Una mano insertando un token de seguridad de hardware de acero cepillado en un equipo portátil

Corregir los tres errores habituales

  1. console.log en stdio. Cámbialo por console.error.
  2. Extensiones .js que faltan. Una importación como ./server falla en tiempo de ejecución con Node16.
  3. Descripciones vagas. Una herramienta llamada run con la descripción "hace cosas" nunca se elige, o se elige con los argumentos equivocados. Nómbrala según la acción y di cuándo usarla.

Para publicarlo, mantén la línea shebang al principio de src/index.ts, ejecuta npm run build y después npm publish. Cualquiera puede registrarlo con claude mcp add notes -- npx -y mcp-notes-server. Los servidores HTTP se distribuyen como contenedor o se ejecutan en cualquier host de Node.

Cuatro colegas revisando juntos un equipo portátil alrededor de una mesa iluminada por el sol en un espacio de coworking

Pruébalo en Picasso IA

Ahora tienes una plantilla que funciona en local, funciona en remoto y puede llamar a modelos de imagen y video. La forma más rápida de ver lo que devuelven esas herramientas es probar primero los modelos a mano. Abre PicassoIA Image y escribe un prompt tan específico como los que enviarías desde generate_image: sujeto, lente, luz, escenario. Después prueba PicassoIA Video o Seedance 2.5 Lite para animar la idea, y anota qué formulación da el resultado que quieres antes de fijar valores por defecto en tu servidor. Elige cualquier modelo del catálogo completo en picassoia.com/en/all-models y conéctalo con los mismos dos helpers. Crea tus propias imágenes en Picasso IA hoy, y deja que tu primera llamada a una herramienta MCP te entregue el resultado.

Compartir este artículo

Elige tu idioma