Cómo crear un servidor MCP en local y conectarlo a Claude (con código que funciona)

Crea un servidor MCP local desde una carpeta vacía: instala el SDK de TypeScript, registra dos herramientas que funcionen, pruébalas en el MCP Inspector y conecta el servidor a Claude Desktop y Claude Code. Incluye archivos de configuración, correcciones de rutas en Windows y una lista de comprobación para los fallos que rompen la mayoría de las configuraciones.

Cómo crear un servidor MCP en local y conectarlo a Claude (con código que funciona)
Cristian Da Conceicao
Fundador de Picasso IA

La mayoría de los tutoriales de MCP se quedan en el "hola mundo" y te dejan mirando una insignia roja de Disconnected. Este termina con un servidor ejecutándose en tu propio equipo, Claude llamando a sus herramientas y una lista breve con los fallos que más afectan a la gente. Escribirás unas 70 líneas de TypeScript, las probarás en un inspector que funciona en el navegador y conectarás el resultado tanto a Claude Desktop como a Claude Code.

El servidor es una pequeña herramienta de notas: Claude puede guardar una nota en un archivo JSON de tu disco y buscarla después. Es deliberadamente sencillo, porque la infraestructura es la misma tanto si tus herramientas leen un archivo de notas, consultan una base de datos o llaman a un modelo de imagen. He compilado y ejecutado el archivo del servidor que aparece más abajo con la versión 1.32 del SDK de TypeScript, así que el código se compila exactamente como se muestra.

Desarrollador escribiendo en una terminal en un equipo portátil sobre un escritorio de roble con luz de la mañana

Qué estás construyendo realmente

MCP en dos párrafos

El Model Context Protocol (MCP) es un estándar abierto, presentado por Anthropic en noviembre de 2024, que permite a una aplicación de IA comunicarse con herramientas externas de una forma coherente. En lugar de que cada aplicación invente su propio formato de plugins, un servidor MCP expone capacidades y un cliente MCP, como Claude Desktop o Claude Code, las encuentra y las llama. Los mensajes son JSON-RPC 2.0 simple, así que un servidor puede escribirse en cualquier lenguaje.

Un servidor puede ofrecer tres tipos de cosas:

  • Tools (herramientas): funciones que el modelo puede llamar, como "guardar una nota" o "ejecutar una consulta".
  • Resources (recursos): datos de solo lectura que la aplicación puede cargar como contexto, como un archivo o un registro de una base de datos.
  • Prompts: plantillas de prompt reutilizables que el usuario activa a propósito.

Este tutorial se centra en las herramientas, porque son las más sencillas de probar y las más útiles desde el primer día.

Vista cenital de una libreta con un diagrama dibujado a mano de tres cajas unidas por flechas

Por qué ejecutarlo en local

Un servidor local se ejecuta como proceso hijo del cliente, en tu equipo, con tus archivos y tus permisos. No queda expuesto a internet, no hay factura de alojamiento y la iteración es rápida: editas un archivo, compilas y reinicias. El transporte que usa es stdio: el cliente lanza tu programa y se comunica con él a través de la entrada y la salida estándar.

stdio (local)Streamable HTTP (remoto)
Dónde se ejecutaProceso hijo en tu equipoUn servidor web que alojas tú u otra persona
Quién puede accederSolo la aplicación que lo lanzóCualquiera con la URL y las credenciales
AutenticaciónNinguna, hereda tu cuenta de usuarioObligatoria (OAuth o tokens)
Ideal paraHerramientas personales, acceso a archivos, desarrolloHerramientas de equipo compartidas, integraciones SaaS

💡 Nota: Streamable HTTP sustituyó al transporte HTTP+SSE anterior en la revisión de la especificación 2025-03-26. Y como un navegador no puede lanzar un proceso en tu equipo, un servidor stdio no se puede añadir a claude.ai en el navegador. Ahí solo funcionan los servidores remotos.

Vista desde abajo de un mini PC gris grafito junto a un equipo portátil plateado unidos por un cable USB-C trenzado

Prepara el proyecto

Lo que necesitas instalado

HerramientaVersiónCómo comprobarlo
Node.js20 LTS o más recientenode --version
npmIncluido con Nodenpm --version
Claude DesktopÚltima versión, macOS o WindowsAjustes, luego Desarrollador
Claude Code (opcional)Última versiónclaude --version

Claude Desktop está disponible para macOS y Windows. En Linux, usa la ruta de Claude Code de la sección de conexión más abajo; el servidor es exactamente el mismo.

Crea y configura el proyecto

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

Abre package.json y añade tres cosas junto a las dependencias que creó npm: la marca de módulos ES y dos scripts.

{
  "name": "local-notes-mcp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  }
}

Después crea tsconfig.json en la raíz del proyecto:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "build",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src"]
}

⚠️ Cuidado: TypeScript 7, la versión que npm instala hoy, ya no carga por sí solo los paquetes @types. Sin la línea "types": ["node"] obtendrás Cannot find name 'process' y otros errores similares en cada importación de Node.

Primer plano de las manos de un desarrollador apoyadas mientras escribe en un escritorio iluminado por una lámpara cálida

Escribe tus dos primeras herramientas

El archivo completo del servidor

Guárdalo como src/index.ts. Expone save_note y search_notes, y guarda todo en un único archivo JSON.

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 os from "node:os";
import path from "node:path";

const NOTES_DIR = process.env.NOTES_DIR ?? path.join(os.homedir(), "mcp-notes");
const NOTES_FILE = path.join(NOTES_DIR, "notes.json");

type Note = { id: number; title: string; body: 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: "local-notes", version: "1.0.0" });

server.registerTool(
  "save_note",
  {
    title: "Save note",
    description: "Save a short note with a title and a body to the local notes file.",
    inputSchema: {
      title: z.string().min(1).max(120).describe("Short title for the note"),
      body: z.string().min(1).describe("The text of the note"),
    },
  },
  async ({ title, body }) => {
    const notes = await readNotes();
    const note: Note = {
      id: notes.length + 1,
      title,
      body,
      createdAt: new Date().toISOString(),
    };
    await fs.mkdir(NOTES_DIR, { recursive: true });
    await fs.writeFile(NOTES_FILE, JSON.stringify([...notes, note], null, 2));
    return { content: [{ type: "text", text: `Saved note #${note.id}: ${title}` }] };
  }
);

server.registerTool(
  "search_notes",
  {
    title: "Search notes",
    description: "Find saved notes whose title or body contains a word or phrase.",
    inputSchema: {
      query: z.string().min(1).describe("Word or phrase to look for"),
    },
  },
  async ({ query }) => {
    const q = query.toLowerCase();
    const hits = (await readNotes()).filter((n) =>
      `${n.title} ${n.body}`.toLowerCase().includes(q)
    );
    if (hits.length === 0) {
      return { content: [{ type: "text", text: `No notes match "${query}".` }] };
    }
    const text = hits.map((n) => `#${n.id} ${n.title}\n${n.body}`).join("\n\n");
    return { content: [{ type: "text", text }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("local-notes MCP server running on stdio");

Qué hace cada parte

  • McpServer es la clase de alto nivel. El name y el version que le pasas aparecen en la lista de servidores del cliente y en los registros.
  • registerTool recibe el nombre de una herramienta, un objeto de configuración (title, description, inputSchema) y un manejador asíncrono.
  • El esquema de Zod se valida antes de que se ejecute tu manejador y después se convierte a JSON Schema para que Claude lo pueda leer. Cada cadena .describe() llega al modelo.
  • El valor de retorno es siempre { content: [...] }. El texto plano es el tipo de contenido más sencillo; también se admiten imágenes y enlaces a recursos.
  • StdioServerTransport lee las peticiones de stdin y escribe las respuestas en stdout.

💡 Tip: Escribe las descripciones para el modelo, no para las personas. "Busca notas guardadas cuyo título o cuerpo contenga una palabra o frase" le indica a Claude cuándo llamar a la herramienta. "Buscar" no. Claude elige las herramientas sobre todo por sus nombres y descripciones.

Como search_notes nunca modifica nada, márcalo así en su configuración: annotations: { readOnlyHint: true }. Las pistas son orientativas, y cada cliente decide por sí mismo cuánto confiar en ellas, pero permiten que los clientes bien diseñados traten las herramientas de solo lectura con más suavidad.

Desarrolladora de pie frente a un escritorio regulable revisando código en un monitor ancho

Nunca imprimas en stdout

Con stdio, stdout es el canal del protocolo. Un console.log("started") fuera de lugar introduce en el flujo una línea que no es JSON, y la mayoría de los clientes cortará la conexión o marcará el servidor como fallido. Recuerda esta regla y evitarás el fallo más común del primer día:

  • Sí: console.error("message"), que escribe en stderr, donde los clientes recogen los registros.
  • No: console.log(...) ni process.stdout.write(...) en ningún punto de tu servidor, incluidas las bibliotecas que importes.

Pruébalo antes que Claude

Ejecuta el MCP Inspector

El MCP Inspector es la herramienta oficial de depuración. Lanza tu servidor de la misma forma que lo haría un cliente y te ofrece botones en lugar de prompts.

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

Se abre una página en tu navegador. Haz clic en Connect, abre la pestaña Tools y pulsa List Tools. Deberías ver save_note y search_notes con sus esquemas. Ejecuta save_note con un título y un cuerpo, y luego ejecuta search_notes con una palabra de ese cuerpo. La primera llamada devuelve Saved note #1: Standup, y el archivo de notas aparece en una carpeta mcp-notes dentro de tu directorio personal (o en NOTES_DIR si lo configuraste).

Dos compañeros inclinados hacia la pantalla de un equipo portátil en una mesa de madera compartida

Envía mensajes JSON-RPC en bruto

Si quieres ver el protocolo en sí, stdio usa un mensaje JSON por línea. Pon estas tres líneas en requests.jsonl:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}

Después envíaselas al servidor y mantén stdin abierto un momento para que las respuestas tengan tiempo de salir:

(cat requests.jsonl; sleep 2) | node build/index.js

La primera respuesta confirma el handshake, con la versión del protocolo que acordó el servidor y tu serverInfo:

{"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"local-notes","version":"1.0.0"}},"jsonrpc":"2.0","id":1}

La segunda respuesta enumera las dos herramientas con sus JSON Schemas. Ese intercambio es todo lo que hace Claude al conectarse: handshake, listar herramientas, llamar a herramientas.

Conéctalo a Claude

Edita la configuración de Claude Desktop

En Claude Desktop, abre Ajustes, luego Desarrollador, luego Editar configuración. Eso revela claude_desktop_config.json:

SOUbicación del archivo de configuración
Windows%APPDATA%\Claude\claude_desktop_config.json
macOS~/Library/Application Support/Claude/claude_desktop_config.json

Añade tu servidor en mcpServers. Usa rutas absolutas, porque Claude Desktop lanza tu proceso desde su propio directorio de trabajo, no desde la carpeta de tu proyecto.

{
  "mcpServers": {
    "local-notes": {
      "command": "node",
      "args": ["C:/Users/you/projects/local-notes-mcp/build/index.js"],
      "env": {
        "NOTES_DIR": "C:/Users/you/mcp-notes"
      }
    }
  }
}

En macOS, la ruta tiene este aspecto: /Users/you/projects/local-notes-mcp/build/index.js. Guarda el archivo, luego cierra por completo Claude Desktop (en Windows, desde la bandeja del sistema, no solo con el botón de cerrar de la ventana) y vuelve a abrirlo. Tus herramientas aparecen en el menú de herramientas del campo de chat, y Claude pide permiso antes de ejecutar una.

Desarrollador trabajando en una mesa de mármol de una cafetería con un equipo portátil plateado y un flat white

Añádelo a Claude Code

Claude Code no necesita editar archivos. Un solo comando registra el servidor:

claude mcp add --transport stdio --env NOTES_DIR=/home/you/mcp-notes local-notes -- node /home/you/projects/local-notes-mcp/build/index.js

Todo lo que va después del doble guion es el comando que lanza tu servidor. Compruébalo con claude mcp list, o escribe /mcp dentro de una sesión para ver su estado. Un parámetro de ámbito decide quién tiene acceso al servidor:

ÁmbitoSe guarda enQuién lo ve
local (por defecto)Tus ajustes privados para este proyectoSolo tú, en este proyecto
project (--scope project).mcp.json en el repositorioTodos los que lo clonen, después de aprobarlo
user (--scope user)Tu configuración de usuarioSolo tú, en todos los proyectos

Prueba un prompt real

Pídele a Claude algo que obligue a llamar a una herramienta:

Guarda una nota titulada "Standup" que diga "Publicar el post de MCP el viernes". Luego busca en mis notas "viernes".

Claude llama a save_note, después a search_notes, y te cita el resultado. Abre notes.json para confirmar que los datos se han guardado en tu disco. Si es así, tienes un servidor MCP local funcionando.

Corrige los fallos y protégelo

Corrige los fallos más comunes

SíntomaCausa probableSolución
El servidor aparece como fallido o desconectadoSalida por stdout, o un fallo al arrancarEjecuta node build/index.js a mano y lee stderr; elimina todos los console.log
No hay herramientas después de editar la configuraciónEl cliente sigue abierto, o JSON no válidoCiérralo del todo; revisa las comas finales
spawn node ENOENTLa aplicación no encuentra node en su PATHUsa la ruta absoluta del binario node como command
Funciona en el Inspector, falla en ClaudeRutas relativas o variables de entorno que faltanRutas absolutas, y pon las variables en env
Los cambios en el código no tienen efectoNo compilaste ni reiniciasteEjecuta npm run build y luego reinicia el cliente

Dos detalles de Windows causan la mitad de los problemas restantes. Las barras invertidas dentro de las cadenas JSON deben duplicarse (C:\\Users\\you\\...), o puedes usar simplemente barras normales, como en el ejemplo de arriba. Y en Windows nativo, los servidores lanzados mediante npx suelen necesitar un envoltorio cmd /c en command; un comando node simple no basta.

Cuando algo sigue fallando, lee los registros. Claude Desktop escribe un registro por servidor, en ~/Library/Logs/Claude en macOS y en %APPDATA%\Claude\logs en Windows. Tus propias líneas de console.error acaban ahí.

Ingeniero reclinado en una silla de escritorio con una sonrisa de alivio bajo la luz cálida de una lámpara de latón

Valores seguros que conviene mantener

Un servidor stdio hereda tus permisos, así que trata cada herramienta como código que puede actuar en tu nombre.

  • Limita el alcance del daño. Mantén el acceso a archivos dentro de una sola carpeta. Si una herramienta acepta una ruta, resuélvela y rechaza cualquier cosa fuera del directorio permitido.
  • Valida cada entrada. Las reglas min, max y enum de Zod no cuestan nada y bloquean las llamadas mal formadas antes de que se ejecute tu manejador.
  • Mantén los secretos fuera del código. Pon los tokens en el bloque env de tu configuración, y mantén ese archivo fuera del control de versiones.
  • Lee antes de instalar. Añade solo servidores de terceros cuyo código fuente hayas revisado. Se ejecutan con el acceso de tu cuenta.
  • Trata la salida de las herramientas como no confiable. El texto que tu herramienta obtiene de páginas web o correos puede contener instrucciones dirigidas al modelo. Devuélvelo como datos y mantén las acciones de escritura detrás de una confirmación.

Pasar de stdio a HTTP

Cuando tus compañeros necesiten las mismas herramientas, cambia el transporte. El SDK incluye StreamableHTTPServerTransport, que sirve las mismas McpServer por HTTP detrás de tu propia autenticación. Tus llamadas registerTool no cambian. Solo cambian el transporte y el registro en el cliente, por ejemplo claude mcp add --transport http notes https://your-host/mcp.

Ya puedes ver este patrón en la práctica. El conector de PicassoIA en claude.ai es un servidor MCP remoto que enumera la generación de imágenes, la edición de imágenes y la generación de video como herramientas, y Claude las llama sin ningún proceso local.

Primer plano de un armario de servidores negro mate con cables ethernet grises ordenadamente recogidos

Redacta especificaciones de herramientas en PicassoIA

Las buenas herramientas empiezan por buenos nombres y buenas descripciones, y un modelo de lenguaje es una forma rápida de redactarlos antes de escribir código. PicassoIA aloja 75 modelos de texto en su categoría Large Language Models, entre ellos Claude Sonnet 5, pensado para tareas de programación. Así se usa para diseñar herramientas.

Cómo usar Claude Sonnet 5 en PicassoIA

  1. Abre la página de Claude Sonnet 5 en PicassoIA.
  2. Describe la herramienta en lenguaje sencillo: qué hace, qué recibe, qué devuelve y si modifica algo.
  3. Pide un formato de salida fijo: un nombre de herramienta en snake_case, una descripción de máximo dos frases escrita para un modelo, un esquema de Zod con .describe() en cada campo y tres casos límite que deberían fallar la validación.
  4. Pega el resultado en una llamada a registerTool, compila y pruébalo en el Inspector.
  5. Itera sobre la descripción, no sobre el esquema, cuando Claude elija la herramienta equivocada. El problema suele estar en la redacción.

Un prompt que funciona bien:

Estoy creando una herramienta MCP llamada list_overdue_tasks. Lee tasks.json, devuelve las tareas cuyo dueDate sea anterior a hoy y no cambia nada. Escribe el nombre de la herramienta, una descripción de dos frases para un modelo de IA, un esquema de entrada de Zod con un describe() en cada campo y tres entradas no válidas que debería rechazar.

Para refactorizaciones más grandes, como dividir un servidor de 600 líneas en módulos, prueba Claude Fable 5 o Claude Opus 4.7 pegando tu archivo completo.

Haz tu primera imagen en PicassoIA

Tu servidor de notas es una plantilla. Sustituye el archivo JSON por una llamada a un modelo de imagen y Claude podrá generar imágenes cuando se lo pidas. No hace falta que construyas eso primero para ver el resultado. Cada foto de este artículo se generó con P-Image, uno de los modelos de texto a imagen de Picasso IA.

Abre Picasso IA, escribe una frase que describa una escena y genera. Luego prueba tres experimentos:

  • Cambia la lente. Reescribe el mismo prompt con "35mm" y después con "85mm" y compara el encuadre.
  • Cambia la luz. Sustituye "morning window light" por "warm desk lamp" y observa cómo cambia el ambiente.
  • Cambia el ángulo. Pide una toma cenital y luego una toma desde abajo del mismo sujeto.

Construye una herramienta que te gustaría que Claude tuviera, conéctala con los pasos de arriba y dedica diez minutos a Picasso IA para crear las imágenes de tu proyecto. El servidor lleva una tarde. Las imágenes, segundos.

Compartir este artículo

Elige tu idioma