FastMCP

Un marco de trabajo en TypeScript para construir servidores MCP con manejo de sesiones de cliente.

Documentación

FastMCP

Un framework TypeScript para construir servidores MCP capaces de manejar sesiones de clientes.

[!IMPORTANT]

FastMCP implementa las revisiones heredadas de MCP basadas en handshake (2025-11-25 y anteriores). No soporta la especificación actual, 2026-07-28, que hizo el protocolo sin estado — sin handshake initialize y sin Mcp-Session-Id. Para un framework orientado a la especificación actual, usa ViteMCP.

Características

¿Cuándo usar FastMCP en lugar del SDK oficial?

FastMCP está construido sobre el SDK oficial.

El SDK oficial proporciona bloques fundamentales para construir MCPs, pero deja muchos detalles de implementación a tu cargo:

FastMCP elimina esta complejidad proporcionando un framework con opiniones que:

  • Maneja todo el código repetitivo automáticamente
  • Proporciona APIs simples e intuitivas para tareas comunes
  • Incluye mejores prácticas integradas y manejo de errores
  • Te permite centrarte en la funcionalidad principal de tu MCP

Cuándo elegir FastMCP: Quieres construir servidores MCP rápidamente sin lidiar con detalles de implementación de bajo nivel.

Cuándo usar el SDK oficial: Necesitas máximo control o tienes requisitos arquitectónicos específicos. En este caso, te animamos a consultar la implementación de FastMCP para evitar errores comunes.

Instalación

npm install fastmcp

Inicio rápido

[!NOTE]

Hay muchos ejemplos del mundo real de uso de FastMCP. Consulta el Escaparate para ver ejemplos.

import { FastMCP } from "fastmcp";
import { z } from "zod"; // Or any validation library that supports Standard Schema

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
});

server.addTool({
  name: "add",
  description: "Add two numbers",
  parameters: z.object({
    a: z.number(),
    b: z.number(),
  }),
  execute: async (args) => {
    return String(args.a + args.b);
  },
});

server.start({
  transportType: "stdio",
});

¡Eso es todo! Ya tienes un servidor MCP funcionando.

Puedes probar el servidor en la terminal con:

git clone https://github.com/punkpeye/fastmcp.git
cd fastmcp

pnpm install
pnpm build

# Test the addition server example using CLI:
npx fastmcp dev src/examples/addition.ts
# Test the addition server example using MCP Inspector:
npx fastmcp inspect src/examples/addition.ts

Si buscas un repositorio plantilla para construir tu propio servidor MCP, consulta fastmcp-boilerplate.

Opciones de servidor remoto

FastMCP soporta múltiples opciones de transporte para comunicación remota, permitiendo que un MCP alojado en una máquina remota sea accesible a través de la red.

HTTP Streaming

HTTP streaming proporciona una alternativa más eficiente a SSE en entornos que lo soportan, con potencialmente mejor rendimiento para cargas útiles más grandes.

Puedes ejecutar el servidor con soporte de HTTP streaming:

server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8080,
  },
});

Esto iniciará el servidor y escuchará conexiones de HTTP streaming en http://localhost:8080/mcp.

Nota: También puedes personalizar la ruta del endpoint usando la opción httpStream.endpoint (el valor predeterminado es /mcp).

Nota: Para servir HTTP streaming y rutas OAuth integradas bajo una ruta de emisor, establece httpStream.basePath (por ejemplo, /issuer1). Esto expone los metadatos del servidor de autorización en /.well-known/oauth-authorization-server/issuer1 según RFC 8414.

Nota: Esto también inicia un servidor SSE en http://localhost:8080/sse.

Puedes conectarte a estos servidores usando el transporte de cliente apropiado.

Para conexiones HTTP streaming:

import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const client = new Client(
  {
    name: "example-client",
    version: "1.0.0",
  },
  {
    capabilities: {},
  },
);

const transport = new StreamableHTTPClientTransport(
  new URL(`http://localhost:8080/mcp`),
);

await client.connect(transport);

Para conexiones SSE:

import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

const client = new Client(
  {
    name: "example-client",
    version: "1.0.0",
  },
  {
    capabilities: {},
  },
);

const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`));

await client.connect(transport);
Soporte HTTPS

FastMCP soporta HTTPS para conexiones seguras proporcionando opciones de certificado SSL:

server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8443,
    sslCert: "./path/to/cert.pem",
    sslKey: "./path/to/key.pem",
    sslCa: "./path/to/ca.pem", // Optional: for client certificate authentication
  },
});

Esto iniciará el servidor con HTTPS en https://localhost:8443/mcp.

Opciones SSL:

  • sslCert - Ruta al archivo de certificado SSL
  • sslKey - Ruta al archivo de clave privada SSL
  • sslCa - (Opcional) Ruta al certificado CA para autenticación mutua TLS

Para pruebas, puedes generar certificados autofirmados:

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"

Para producción, obtén certificados de una CA de confianza como Let's Encrypt.

Consulta el ejemplo de servidor https para una demostración completa.

Configuración CORS

Por defecto, FastMCP habilita CORS con un conjunto estándar de cabeceras permitidas. Puedes personalizar el comportamiento de CORS pasando una opción cors:

server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8080,
    cors: {
      origin: "http://localhost:3000",
      allowedHeaders: [
        "Content-Type",
        "Authorization",
        "Accept",
        "Mcp-Session-Id",
        "Mcp-Protocol-Version",
        "Last-Event-Id",
        "X-Custom-Header",
      ],
      credentials: true,
    },
  },
});

La opción cors acepta:

  • true (predeterminado) - habilita CORS con configuración predeterminada
  • false - deshabilita CORS por completo
  • Un objeto con estos campos:
    • origin - una cadena, un array de cadenas o una función (origin: string) => boolean
    • allowedHeaders - una cadena o un array de cadenas
    • methods - array de métodos HTTP permitidos
    • exposedHeaders - array de cabeceras a exponer
    • credentials - booleano para permitir credenciales
    • maxAge - duración de caché de preflight en segundos

El tipo CorsOptions se exporta desde fastmcp por conveniencia.

Rutas HTTP personalizadas

FastMCP te permite añadir rutas HTTP personalizadas junto a los endpoints MCP, permitiéndote construir servicios HTTP completos que incluyan APIs REST, webhooks, interfaces de administración y más, todo dentro del mismo proceso de servidor.

const app = server.getApp();

// Add REST API endpoints with Hono's native API
app.get("/api/users", async (c) => {
  return c.json({ users: [] });
});

// Handle path parameters
app.get("/api/users/:id", async (c) => {
  return c.json({
    userId: c.req.param("id"),
    query: c.req.query(), // Access query parameters
  });
});

// Handle POST requests with body parsing
app.post("/api/users", async (c) => {
  const body = await c.req.json();
  return c.json({ created: body }, 201);
});

// Serve HTML content
app.get("/admin", async (c) => {
  return c.html("<html><body><h1>Admin Panel</h1></body></html>");
});

// Handle webhooks
app.post("/webhook/github", async (c) => {
  const payload = await c.req.json();
  const event = c.req.header("x-github-event");

  // Process webhook...
  return c.json({ received: true });
});

Las rutas personalizadas usan la aplicación Hono subyacente devuelta por server.getApp() y soportan:

  • Métodos HTTP de Hono: get, post, put, delete, patch, options y más
  • Parámetros de ruta (:param) y comodines (*)
  • Análisis de cadenas de consulta
  • JSON, texto, formularios y otros ayudantes de cuerpo de c.req
  • Códigos de estado y cabeceras personalizados
  • Middleware y grupos de rutas a través de Hono

Las rutas se comparan en el orden en que se registran, permitiéndote definir rutas específicas antes de patrones de captura general.

Rutas públicas y protegidas

Las rutas Hono personalizadas son públicas a menos que añadas tu propio middleware de ruta o comprobaciones de autenticación. Para rutas personalizadas protegidas, coloca tu lógica de autenticación en un ayudante reutilizable y llámalo tanto desde la opción authenticate de FastMCP como desde tus manejadores de ruta Hono:

import type { IncomingMessage } from "node:http";
import type { Context } from "hono";
import { FastMCP } from "fastmcp";

async function authenticateRequest(request: IncomingMessage) {
  const apiKey = request.headers["x-api-key"];
  return apiKey === "123" ? { userId: "123" } : undefined;
}

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  authenticate: authenticateRequest,
});

const app = server.getApp();

async function requireAuth(c: Context) {
  const auth = await authenticateRequest(c.env.incoming);

  if (!auth) {
    return c.json({ error: "Authentication required" }, 401);
  }

  return auth;
}

// Public route - no authentication required
app.get("/.well-known/openid-configuration", async (c) => {
  return c.json({
    issuer: "https://example.com",
    authorization_endpoint: "https://example.com/auth",
    token_endpoint: "https://example.com/token",
  });
});

// Private route - requires authentication
app.get("/api/users", async (c) => {
  const auth = await requireAuth(c);
  if (auth instanceof Response) {
    return auth;
  }

  return c.json({ users: [] });
});

// Public static files
app.get("/public/*", async (c) => {
  return c.text(`File: ${c.req.path}`);
});

Las rutas públicas son perfectas para:

  • Endpoints de descubrimiento OAuth (.well-known/*)
  • Verificaciones de salud y páginas de estado
  • Activos estáticos y documentación
  • Endpoints de webhook de servicios externos
  • APIs públicas que no requieren autenticación de usuario

Consulta el ejemplo de rutas personalizadas para una demostración completa.

Soporte de runtime perimetral

FastMCP soporta runtimes perimetrales como Cloudflare Workers, permitiendo el despliegue de servidores MCP en el borde con latencia mínima a nivel mundial.

Elegir entre FastMCP y EdgeFastMCP
Caso de usoClaseImportación
Node.js, Express, BunFastMCPimport { FastMCP } from "fastmcp"
Cloudflare Workers, Deno DeployEdgeFastMCPimport { EdgeFastMCP } from "fastmcp/edge"
CaracterísticaFastMCPEdgeFastMCP
RuntimeNode.jsEdge (aislamientos V8)
Método de inicioserver.start({ port })export default server
Transportestdio, httpStream, SSESolo HTTP Streamable
SesionesCon estado o sin estadoSolo sin estado
Sistema de archivosSíNo
OAuth/AutenticaciónOpción authenticate integradaUsa middleware Hono (integrado planificado)
Rutas personalizadasserver.getApp()server.getApp()

Nota: La autenticación integrada para EdgeFastMCP está planificada para una versión futura. Tanto FastMCP como EdgeFastMCP usan Hono internamente, por lo que no hay barrera técnica: EdgeFastMCP simplemente se escribió antes de que se añadiera OAuth a FastMCP. Se aceptan PRs para añadir una opción authenticate que acepte Request web en lugar de http.IncomingMessage de Node.js.

Mientras tanto, usa middleware Hono:

const app = server.getApp();
app.use("/api/*", async (c, next) => {
  if (c.req.header("authorization") !== "Bearer secret") {
    return c.json({ error: "Unauthorized" }, 401);
  }
  await next();
});
Cloudflare Workers

Para desplegar FastMCP en Cloudflare Workers, usa la clase EdgeFastMCP de la subruta /edge:

import { EdgeFastMCP } from "fastmcp/edge";
import { z } from "zod";

const server = new EdgeFastMCP({
  name: "My Edge Server",
  version: "1.0.0",
  description: "MCP server running on Cloudflare Workers",
});

// Add tools, resources, prompts as usual
server.addTool({
  name: "greet",
  description: "Greet someone",
  parameters: z.object({
    name: z.string(),
  }),
  execute: async ({ name }) => {
    return `Hello, ${name}! Served from the edge.`;
  },
});

// Export the server as the default (required for Cloudflare Workers)
export default server;
Diferencias de runtime perimetral

Al ejecutarse en runtimes perimetrales:

  • Sin estado por defecto: Cada solicitud se maneja de forma independiente
  • Sin acceso al sistema de archivos: Usa APIs fetch para datos externos
  • Aislamientos V8: Inicios en frío rápidos y uso eficiente de recursos
  • Despliegue global: Distribución automática a ubicaciones perimetrales
Rutas personalizadas en el borde

Puedes acceder a la aplicación Hono subyacente para añadir rutas HTTP personalizadas:

const app = server.getApp();

// Add a landing page
app.get("/", (c) => c.html("<h1>Welcome to my MCP server</h1>"));

// Add REST API endpoints
app.get("/api/status", (c) => c.json({ status: "ok" }));
Despliegue

Configura tu wrangler.toml:

name = "my-mcp-server"
main = "src/index.ts"
compatibility_date = "2024-01-01"

Despliega con:

wrangler deploy

Consulta el ejemplo edge-cloudflare-worker para una demostración completa.

Modo sin estado

FastMCP soporta operación sin estado para HTTP streaming, donde cada solicitud se maneja de forma independiente sin mantener sesiones persistentes. Esto es ideal para entornos serverless, despliegues con balanceo de carga o cuando no se requiere estado de sesión.

En modo sin estado:

  • No se rastrean sesiones en el servidor
  • Cada solicitud crea una sesión temporal que se descarta después de la respuesta
  • Menor uso de memoria y mejor escalabilidad
  • Perfecto para entornos de despliegue sin estado

Puedes habilitar el modo sin estado añadiendo la opción stateless: true:

server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8080,
    stateless: true,
  },
});

Nota: El modo sin estado solo está disponible con transporte HTTP streaming. Las características que dependen de sesiones persistentes (como estado específico de sesión) no estarán disponibles en modo sin estado.

También puedes habilitar el modo sin estado usando argumentos CLI o variables de entorno:

# Via CLI argument
npx fastmcp dev src/server.ts --transport http-stream --port 8080 --stateless true

# Via environment variable
FASTMCP_STATELESS=true npx fastmcp dev src/server.ts

El endpoint de verificación de salud /ready indicará cuando el servidor esté ejecutándose en modo sin estado:

{
  "mode": "stateless",
  "ready": 1,
  "status": "ready",
  "total": 1
}

Conceptos principales

Herramientas

Las herramientas en MCP permiten a los servidores exponer funciones ejecutables que pueden ser invocadas por clientes y usadas por LLMs para realizar acciones.

FastMCP usa la especificación Standard Schema para definir parámetros de herramientas. Esto te permite usar tu biblioteca de validación de esquemas preferida (como Zod, ArkType o Valibot) siempre que implemente la especificación.

Ejemplo con Zod:

import { z } from "zod";

server.addTool({
  name: "fetch-zod",
  description: "Fetch the content of a url (using Zod)",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return await fetchWebpageContent(args.url);
  },
});

Ejemplo con ArkType:

import { type } from "arktype";

server.addTool({
  name: "fetch-arktype",
  description: "Fetch the content of a url (using ArkType)",
  parameters: type({
    url: "string",
  }),
  execute: async (args) => {
    return await fetchWebpageContent(args.url);
  },
});

Ejemplo con Valibot:

Valibot requiere la dependencia par @valibot/to-json-schema.

import * as v from "valibot";

server.addTool({
  name: "fetch-valibot",
  description: "Fetch the content of a url (using Valibot)",
  parameters: v.object({
    url: v.string(),
  }),
  execute: async (args) => {
    return await fetchWebpageContent(args.url);
  },
});

Ejemplo con JSON Schema plano:

Si ya tienes un JSON Schema — de un documento OpenAPI, un archivo de configuración u otro servidor — jsonSchemaAdapter lo envuelve para que pueda usarse directamente, sin ninguna librería de esquemas de por medio.

Requiere la dependencia par ajv, que realiza la validación, además de ajv-formats si tu esquema usa palabras clave format como email o uri. Ambas se importan la primera vez que se llama a una herramienta, por lo que los servidores que no usan esto no pagan nada por ello.

npm install ajv ajv-formats
import { jsonSchemaAdapter } from "fastmcp";

server.addTool({
  name: "fetch-json-schema",
  description: "Fetch the content of a url (using plain JSON Schema)",
  parameters: jsonSchemaAdapter({
    type: "object",
    properties: {
      url: { type: "string", format: "uri" },
    },
    required: ["url"],
  }),
  execute: async (args) => {
    const { url } = args as { url: string };
    return await fetchWebpageContent(url);
  },
});

Funciona también para outputSchema. Ten en cuenta que FastMCP anuncia los esquemas de entrada con additionalProperties: false, sin importar lo que dijera tu esquema — el mismo tratamiento que reciben los esquemas de Zod y Valibot. La excepción es un objeto compuesto solo por propiedades adicionales: un diccionario (sin properties y con un esquema additionalProperties, como z.record()) conserva su esquema de valores, y un argumento de forma libre ({ type: "object" }, o additionalProperties: true) permanece abierto. Los esquemas de salida conservan la regla de propiedades adicionales que tu esquema declaró.

A diferencia de las librerías de esquemas anteriores, un JSON Schema plano no incluye tipos de TypeScript, por lo que execute recibe argumentos unknown. Haz el cast o la reducción de tipos tú mismo.

Herramientas sin parámetros

Al crear herramientas que no requieren parámetros, tienes dos opciones:

  1. Omitir la propiedad de parámetros por completo:

    server.addTool({
      name: "sayHello",
      description: "Say hello",
      // No parameters property
      execute: async () => {
        return "Hello, world!";
      },
    });
    
  2. Definir explícitamente parámetros vacíos:

    import { z } from "zod";
    
    server.addTool({
      name: "sayHello",
      description: "Say hello",
      parameters: z.object({}), // Empty object
      execute: async () => {
        return "Hello, world!";
      },
    });
    

[!NOTE]

Ambos enfoques son totalmente compatibles con todos los clientes MCP, incluido Cursor. FastMCP genera automáticamente el esquema adecuado en ambos casos.

Salida estructurada de herramientas

Las herramientas pueden declarar un outputSchema y devolver datos estructurados. FastMCP expone ese valor como structuredContent de MCP, al mismo tiempo que devuelve un respaldo de texto JSON para clientes que solo renderizan contenido de texto.

server.addTool({
  name: "get-weather",
  description: "Get weather for a city",
  parameters: z.object({
    city: z.string(),
  }),
  outputSchema: z.object({
    temperature: z.number(),
    humidity: z.number(),
  }),
  execute: async ({ city }) => {
    const weather = await getWeather(city);

    return {
      temperature: weather.temperature,
      humidity: weather.humidity,
    };
  },
});

También puedes devolver contenido de texto explícito y contenido estructurado juntos:

server.addTool({
  name: "get-weather",
  description: "Get weather for a city",
  parameters: z.object({
    city: z.string(),
  }),
  outputSchema: z.object({
    temperature: z.number(),
    humidity: z.number(),
  }),
  execute: async ({ city }) => {
    const weather = await getWeather(city);

    return {
      content: [
        {
          type: "text",
          text: `${city}: ${weather.temperature}F`,
        },
      ],
      structuredContent: {
        temperature: weather.temperature,
        humidity: weather.humidity,
      },
    };
  },
});

Cuando se proporciona outputSchema, FastMCP valida structuredContent antes de enviar el resultado de la herramienta. La salida estructurada no válida se devuelve al cliente como un error de herramienta en lugar de violar silenciosamente el esquema anunciado.

Autorización de herramientas

Puedes controlar qué herramientas están disponibles para los usuarios autenticados añadiendo una función canAccess opcional a la definición de una herramienta. Esta función recibe el contexto de autenticación y debe devolver true si el usuario tiene permitido acceder a la herramienta.

server.addTool({
  name: "admin-tool",
  description: "An admin-only tool",
  canAccess: (auth) => auth?.role === "admin",
  execute: async () => "Welcome, admin!",
});

Un servidor sin una función authenticate no tiene autenticación que verificar, por lo que cada sesión ve todas las herramientas. Con una, una sesión que termina sin autenticación — en stdio, donde el servidor continúa cuando authenticate lanza una excepción o no devuelve nada — no ve ninguna de las herramientas que tienen canAccess.

Devolver una cadena

execute puede devolver una cadena:

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return "Hello, world!";
  },
});

La última es equivalente a:

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return {
      content: [
        {
          type: "text",
          text: "Hello, world!",
        },
      ],
    };
  },
});

Devolver una lista

Si quieres devolver una lista de mensajes, puedes devolver un objeto con una propiedad content:

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return {
      content: [
        { type: "text", text: "First message" },
        { type: "text", text: "Second message" },
      ],
    };
  },
});

Devolver una imagen

Usa imageContent para crear un objeto de contenido para una imagen:

import { imageContent } from "fastmcp";

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return imageContent({
      url: "https://example.com/image.png",
    });

    // or...
    // return imageContent({
    //   path: "/path/to/image.png",
    // });

    // or...
    // return imageContent({
    //   buffer: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64"),
    // });

    // or...
    // return {
    //   content: [
    //     await imageContent(...)
    //   ],
    // };
  },
});

La función imageContent acepta las siguientes opciones:

  • url: La URL de la imagen.
  • timeoutMs: Tiempo de espera opcional para la descarga de una URL en milisegundos (por defecto 30 segundos).
  • path: La ruta al archivo de imagen.
  • buffer: Los datos de la imagen como un buffer.

Solo se debe especificar uno de url, path o buffer.

El ejemplo anterior es equivalente a:

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return {
      content: [
        {
          type: "image",
          data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
          mimeType: "image/png",
        },
      ],
    };
  },
});

Comportamiento de ping configurable

FastMCP incluye un mecanismo de ping configurable para mantener la salud de la conexión. El comportamiento del ping se puede personalizar mediante las opciones del servidor:

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  ping: {
    // Explicitly enable or disable pings (defaults vary by transport)
    enabled: true,
    // Configure ping interval in milliseconds (default: 5000ms)
    intervalMs: 10000,
    // Set log level for ping-related messages (default: 'debug')
    logLevel: "debug",
  },
});

Por defecto, el comportamiento del ping está optimizado para cada tipo de transporte:

  • Habilitado para conexiones SSE y HTTP streaming (que se benefician del keep-alive)
  • Deshabilitado para conexiones stdio (donde los pings generalmente son innecesarios)

Este enfoque configurable ayuda a reducir la verbosidad de los registros y optimizar el rendimiento para diferentes escenarios de uso.

Mantener vivas las llamadas largas a herramientas (streamKeepalive)

Los pings viajan en el flujo independiente servidor-a-cliente, por lo que nunca mantienen abierta la conexión de una llamada individual a una herramienta — y en modo stateless ese flujo no existe en absoluto. Una herramienta que se ejecuta durante minutos sin producir salida deja su conexión de respuesta en silencio, y un tiempo de espera de conexión inactiva frente al servidor (el valor predeterminado de AWS ALB es 60 segundos) la cierra antes de que se escriba el resultado.

Esto aplica a ambos modos de sesión. Una implementación con estado mantiene su flujo independiente cálido con pings mientras la conexión que transporta la llamada a la herramienta queda inactiva y se cierra de todos modos; sin estado no tiene flujo independiente desde el principio.

streamKeepalive escribe periódicamente en el flujo de respuesta de la propia llamada a la herramienta en curso, que es la conexión que de otro modo se cerraría. Es opcional:

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  streamKeepalive: {
    // Opt in; disabled by default
    enabled: true,
    // Keep comfortably below the shortest idle timeout on the path
    intervalMs: 20000,
  },
});

Cada keepalive es un notifications/message (nivel configurable mediante logLevel, por defecto debug) etiquetado con el registrador fastmcp-keepalive, relacionado con la llamada a la herramienta que se está sirviendo. Los keepalives comienzan cuando una herramienta empieza a ejecutarse y se detienen cuando la solicitud deja de esperarla — al tener éxito, error, tiempo de espera o cancelación del cliente — por lo que las conexiones inactivas permanecen en silencio. Como los mensajes están relacionados con la solicitud, esto funciona tanto en modo con estado como en modo stateless, y no necesita soporte del cliente más allá de la notificación de registro estándar.

Límites que vale la pena conocer:

  • No tiene efecto con httpStream.enableJsonResponse, que almacena en búfer una única respuesta JSON en lugar de hacer streaming, por lo que no hay un flujo abierto en el que escribir.
  • Solo cubre llamadas a herramientas. Otras solicitudes de larga duración (resources/read, prompts/get) siguen sin escribir nada hasta que terminan.
  • En stdio no hay proxy que mantener vivo, por lo que habilitarlo allí solo añade tráfico de notificaciones.

Cancelar llamadas largas a herramientas (context.signal)

Cada execute recibe un AbortSignal que se dispara una vez que su resultado ya no puede llegar a nadie. Reenvíalo a quien haga el trabajo real para que el trabajo se detenga con la llamada en lugar de sobrevivirla:

server.addTool({
  name: "fetch_report",
  parameters: z.object({ url: z.string() }),
  timeoutMs: 30000,
  execute: async (args, { signal }) => {
    const response = await fetch(args.url, { signal });
    return await response.text();
  },
});

Se aborta en cualquiera de estos tres eventos:

  • El cliente canceló la llamada — envió notifications/cancelled, que es lo que emite el SDK de MCP cuando un llamador aborta su propia solicitud.
  • La sesión terminó — el transporte se cerró, o la sesión se cerró explícitamente. Aquí no interviene ninguna notificación de cancelación.
  • Transcurrió timeoutMs — signal.reason es el mismo UserError que el llamador recibe, por lo que una herramienta puede distinguir un tiempo de espera de una cancelación.

La señal nunca se aborta después de que una llamada se completa normalmente, por lo que adjuntar una limpieza a ella es seguro.

Límites que vale la pena conocer:

  • Nada se mata por ti. FastMCP deja de esperar a la herramienta, pero la promesa que devolvió execute sigue ejecutándose hasta que se resuelve. Una herramienta que ignora la señal aún se ejecuta hasta completarse — solo que lo hace sin tener dónde informar.
  • Un cliente HTTP que desaparece a mitad de la solicitud no se detecta. El SDK de MCP solo aborta la señal propia de una solicitud por un notifications/cancelled explícito, y StreamableHTTPServerTransport no trata un flujo de respuesta abandonado como un cierre de sesión. Un llamador que cuelga sin terminar su sesión (DELETE) deja la herramienta ejecutándose hasta que termina o agota el tiempo. Establece timeoutMs en cualquier cosa costosa en lugar de confiar en la detección de desconexión.
  • load no recibe uno. Los recursos, las plantillas de recursos y los avisos reciben un contexto más pequeño sin signal.

Endpoint de verificación de salud

Cuando ejecutas FastMCP con el transporte httpStream puedes exponer opcionalmente un endpoint HTTP simple que devuelve una respuesta de texto plano útil para verificaciones de vitalidad del balanceador de carga o la orquestación de contenedores.

Habilita (o personaliza) el endpoint mediante la clave health en las opciones del servidor:

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  health: {
    // Enable / disable (default: true)
    enabled: true,
    // Body returned by the endpoint (default: 'ok')
    message: "healthy",
    // Path that should respond (default: '/health')
    path: "/healthz",
    // HTTP status code to return (default: 200)
    status: 200,
  },
});

await server.start({
  transportType: "httpStream",
  httpStream: { port: 8080 },
});

Ahora una solicitud a http://localhost:8080/healthz devolverá:

HTTP/1.1 200 OK
content-type: text/plain

healthy

El endpoint se ignora cuando el servidor se inicia con el transporte stdio.

Gestión de raíces

FastMCP admite Roots — una característica que permite a los clientes proporcionar un conjunto de ubicaciones raíz similares a sistemas de archivos que se pueden enumerar y actualizar dinámicamente. La característica Roots se puede configurar o deshabilitar en las opciones del servidor:

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  roots: {
    // Set to false to explicitly disable roots support
    enabled: false,
    // By default, roots support is enabled (true)
  },
});

Esto proporciona los siguientes beneficios:

  • Mejor compatibilidad con diferentes clientes que pueden no admitir Roots
  • Menos registros de errores al conectarse a clientes que no implementan la capacidad de roots
  • Control más explícito sobre las capacidades del servidor MCP
  • Degradación gradual cuando la funcionalidad de roots no está disponible

Puedes escuchar los cambios de raíz en tu servidor:

server.on("connect", (event) => {
  const session = event.session;

  // Access the current roots
  console.log("Initial roots:", session.roots);

  // Listen for changes to the roots
  session.on("rootsChanged", (event) => {
    console.log("Roots changed:", event.roots);
  });
});

Cuando un cliente no admite roots o cuando la funcionalidad de roots está explícitamente deshabilitada, estas operaciones manejarán la situación de manera elegante sin lanzar errores.

Devolver un audio

Usa audioContent para crear un objeto de contenido para un audio:

import { audioContent } from "fastmcp";

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return audioContent({
      url: "https://example.com/audio.mp3",
    });

    // or...
    // return audioContent({
    //   path: "/path/to/audio.mp3",
    // });

    // or...
    // return audioContent({
    //   buffer: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64"),
    // });

    // or...
    // return {
    //   content: [
    //     await audioContent(...)
    //   ],
    // };
  },
});

La función audioContent acepta las siguientes opciones:

  • url: La URL del audio.
  • timeoutMs: Tiempo de espera opcional para la descarga de una URL en milisegundos (por defecto 30 segundos).
  • path: La ruta al archivo de audio.
  • buffer: Los datos del audio como un buffer.

Solo se debe especificar uno de url, path o buffer.

El ejemplo anterior es equivalente a:

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return {
      content: [
        {
          type: "audio",
          data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
          mimeType: "audio/mpeg",
        },
      ],
    };
  },
});

Devolver tipo combinado

Puedes combinar varios tipos de esta manera y enviarlos de vuelta a la IA

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return {
      content: [
        {
          type: "text",
          text: "Hello, world!",
        },
        {
          type: "image",
          data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
          mimeType: "image/png",
        },
        {
          type: "audio",
          data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
          mimeType: "audio/mpeg",
        },
      ],
    };
  },

  // or...
  // execute: async (args) => {
  //   const imgContent = await imageContent({
  //     url: "https://example.com/image.png",
  //   });
  //   const audContent = await audioContent({
  //     url: "https://example.com/audio.mp3",
  //   });
  //   return {
  //     content: [
  //       {
  //         type: "text",
  //         text: "Hello, world!",
  //       },
  //       imgContent,
  //       audContent,
  //     ],
  //   };
  // },
});

Registrador personalizado

FastMCP te permite proporcionar una implementación de registrador personalizada para controlar cómo el servidor registra mensajes. Esto es útil para integrarse con la infraestructura de registro existente o personalizar el formato de registro.

import { FastMCP, Logger } from "fastmcp";

class CustomLogger implements Logger {
  debug(...args: unknown[]): void {
    console.log("[DEBUG]", new Date().toISOString(), ...args);
  }

  error(...args: unknown[]): void {
    console.error("[ERROR]", new Date().toISOString(), ...args);
  }

  info(...args: unknown[]): void {
    console.info("[INFO]", new Date().toISOString(), ...args);
  }

  log(...args: unknown[]): void {
    console.log("[LOG]", new Date().toISOString(), ...args);
  }

  warn(...args: unknown[]): void {
    console.warn("[WARN]", new Date().toISOString(), ...args);
  }
}

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  logger: new CustomLogger(),
});

Consulta src/examples/custom-logger.ts para ver ejemplos con Winston, Pino y registro basado en archivos.

Registro

Las herramientas pueden registrar mensajes al cliente usando el objeto log en el objeto de contexto:

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args, { log }) => {
    log.info("Downloading file...", {
      url,
    });

    // ...

    log.info("Downloaded file");

    return "done";
  },
});

El objeto log tiene los siguientes métodos:

  • debug(message: string, data?: SerializableValue)
  • error(message: string, data?: SerializableValue)
  • info(message: string, data?: SerializableValue)
  • warn(message: string, data?: SerializableValue)

Un cliente puede elevar el nivel mínimo que desea con la solicitud logging/setLevel, y FastMCP entonces descarta cualquier cosa menos severa en lugar de enviarla — por lo que después de logging/setLevel con error, log.debug y log.info no llegan a nadie. Hasta que un cliente lo solicite, se envía cada nivel. El nivel vigente se puede leer como session.loggingLevel.

Errores

Los errores que están destinados a mostrarse al usuario deben lanzarse como instancias de UserError:

import { UserError } from "fastmcp";

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    if (args.url.startsWith("https://example.com")) {
      throw new UserError("This URL is not allowed");
    }

    return "done";
  },
});

Progreso

Las herramientas pueden informar el progreso llamando a reportProgress en el objeto de contexto:

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args, { reportProgress }) => {
    await reportProgress({
      progress: 0,
      total: 100,
    });

    // ...

    await reportProgress({
      progress: 100,
      total: 100,
    });

    return "done";
  },
});

reportProgress acepta un message legible por humanos opcional junto con los campos numéricos, que los clientes pueden mostrar junto al indicador de progreso:

await reportProgress({
  progress: 40,
  total: 100,
  message: "Downloading chunk 4 of 10…",
});

Las notificaciones de progreso solo se emiten cuando el cliente opta por participar proporcionando un progressToken en la llamada a la herramienta; de lo contrario, reportProgress no hace nada. Como notifications/progress es parte de la especificación MCP (el campo message desde la revisión 2025-03-26), esta es la forma portátil de enviar actualizaciones incrementales durante una llamada a una herramienta de larga duración — consulta Salida en streaming a continuación para ver la diferencia.

Salida en streaming

FastMCP puede transmitir resultados parciales de las herramientas mientras aún se están ejecutando, lo que permite interfaces de usuario receptivas y comentarios en tiempo real. Esto es particularmente útil para:

  • Operaciones de larga duración que generan contenido de forma incremental
  • Generación progresiva de texto, imágenes u otros medios
  • Operaciones donde los usuarios se benefician de ver resultados parciales inmediatos

[!IMPORTANT] streamContent es una extensión de FastMCP, no parte de la especificación MCP. Emite una notificación notifications/tool/streamContent, que la especificación MCP no define — a partir de la revisión 2025-11-25 no existe un mecanismo estándar para transmitir la salida de herramientas (SEP-2998 es la propuesta en curso para añadir uno).

Los clientes descartan las notificaciones para las que no tienen un manejador registrado, de forma silenciosa y sin errores. Un cliente solo ve contenido transmitido si registra un manejador para el método (o establece un fallbackNotificationHandler), y no se conoce ningún cliente que lo renderice como salida de herramienta — MCP Inspector, por ejemplo, lo registra en su panel de notificaciones mediante un manejador de respaldo, pero el resultado de la herramienta en sí solo muestra lo que execute devolvió. La transmisión es, por tanto, principalmente útil cuando también controlas el cliente — consulta Consumo de contenido transmitido a continuación. Si necesitas actualizaciones incrementales que funcionen en cualquier cliente, usa reportProgress con un message en su lugar.

Para transmitir desde una herramienta, usa el método streamContent:

server.addTool({
  name: "generateText",
  description: "Generate text incrementally",
  parameters: z.object({
    prompt: z.string(),
  }),
  annotations: {
    streamingHint: true, // Advisory only; see below
    readOnlyHint: true,
  },
  execute: async (args, { streamContent }) => {
    // Send initial content immediately
    await streamContent({ type: "text", text: "Starting generation...\n" });

    // Simulate incremental content generation
    const words = "The quick brown fox jumps over the lazy dog.".split(" ");
    for (const word of words) {
      await streamContent({ type: "text", text: word + " " });
      await new Promise((resolve) => setTimeout(resolve, 300)); // Simulate delay
    }

    // Always return a final result. Returning nothing sends an empty tool
    // result, so clients that ignore the streamed notifications see no output
    // at all.
    return "The quick brown fox jumps over the lazy dog.";
  },
});

[!WARNING] Devolver undefined desde execute produce un resultado de herramienta con content vacío. Si transmites todo y no devuelves nada, la llamada a la herramienta se resuelve con un resultado vacío sin indicación de que se haya perdido algo — incluso en clientes que registran la notificación. Devuelve también el resultado completo y trata el contenido transmitido puramente como una mejora de renderizado progresivo.

La anotación streamingHint es metadatos de asesoramiento. Se reenvía textualmente a los clientes en tools/list, pero no habilita ni limita streamContent, y FastMCP mismo nunca la lee. No se conoce ningún cliente que actúe sobre ella hoy, aunque SEP-2998 propone estandarizar el mismo nombre de anotación.

Consumo de contenido transmitido

Un cliente ve estas notificaciones solo si registra un manejador para el método (o establece un fallbackNotificationHandler):

import { z } from "zod";

const StreamContentNotificationSchema = z.object({
  method: z.literal("notifications/tool/streamContent"),
  params: z.object({
    content: z.array(z.any()),
    toolName: z.string(),
  }),
});

client.setNotificationHandler(
  StreamContentNotificationSchema,
  (notification) => {
    const { content, toolName } = notification.params;
    // Render the partial content however you like.
  },
);

Ten en cuenta que las notificaciones solo llevan toolName, no una solicitud o token de progreso, por lo que las llamadas concurrentes a la misma herramienta en una sesión no se pueden distinguir.

La transmisión funciona con todos los tipos de contenido (texto, imagen, audio) y se puede combinar con informes de progreso:

server.addTool({
  name: "processData",
  description: "Process data with streaming updates",
  parameters: z.object({
    datasetSize: z.number(),
  }),
  annotations: {
    streamingHint: true,
  },
  execute: async (args, { streamContent, reportProgress }) => {
    const total = args.datasetSize;

    for (let i = 0; i < total; i++) {
      // Standard progress notification: reaches every spec-compliant client
      await reportProgress({
        progress: i,
        total,
        message: `Processed ${i} of ${total} items`,
      });

      // Richer partial content: only reaches clients that opt in
      if (i % 10 === 0) {
        await streamContent({
          type: "text",
          text: `Processed ${i} of ${total} items...\n`,
        });
      }

      await new Promise((resolve) => setTimeout(resolve, 50));
    }

    return "Processing complete!";
  },
});

Elicitación

Las herramientas pueden solicitar información adicional al usuario durante la ejecución mediante elicitación, usando el método elicit en el objeto de contexto. El cliente debe anunciar el modo de capacidad elicitation correspondiente — elicitation: { form: {} } para solicitudes de formulario (el predeterminado) y/o elicitation: { url: {} } para solicitudes de URL.

server.addTool({
  name: "delete-file",
  description: "Delete a file",
  parameters: z.object({
    path: z.string(),
  }),
  execute: async (args, { elicit }) => {
    const response = await elicit({
      message: `Are you sure you want to delete ${args.path}?`,
      requestedSchema: {
        type: "object",
        properties: {
          confirmed: {
            type: "boolean",
          },
        },
        required: ["confirmed"],
      },
    });

    if (response.action !== "accept" || !response.content?.confirmed) {
      return "Deletion cancelled.";
    }

    // ...

    return `Deleted ${args.path}`;
  },
});

La respuesta action es "accept", "decline" o "cancel"; al aceptar, content contiene las respuestas del usuario que coinciden con requestedSchema. La elicitación también está disponible fuera de las herramientas mediante session.requestElicitation.

Anotaciones de herramientas

A partir de la Especificación MCP (2025-03-26), las herramientas pueden incluir anotaciones que proporcionan contexto y control más ricos al añadir metadatos sobre el comportamiento de una herramienta:

server.addTool({
  name: "fetch-content",
  description: "Fetch content from a URL",
  parameters: z.object({
    url: z.string(),
  }),
  annotations: {
    title: "Web Content Fetcher", // Human-readable title for UI display
    readOnlyHint: true, // Tool doesn't modify its environment
    openWorldHint: true, // Tool interacts with external entities
  },
  execute: async (args) => {
    return await fetchWebpageContent(args.url);
  },
});

Las anotaciones disponibles son:

AnotaciónTipoPredeterminadoDescripción
titlestring-Un título legible para la herramienta, útil para la visualización en la interfaz de usuario
readOnlyHintbooleanfalseSi es verdadero, indica que la herramienta no modifica su entorno
destructiveHintbooleantrueSi es verdadero, la herramienta puede realizar actualizaciones destructivas (solo tiene sentido cuando readOnlyHint es falso)
idempotentHintbooleanfalseSi es verdadero, llamar a la herramienta repetidamente con los mismos argumentos no tiene efecto adicional (solo tiene sentido cuando readOnlyHint es falso)
openWorldHintbooleantrueSi es verdadero, la herramienta puede interactuar con un "mundo abierto" de entidades externas

Estas anotaciones ayudan a los clientes y LLMs a comprender mejor cómo usar las herramientas y qué esperar al llamarlas.

Recursos

Recursos representan cualquier tipo de dato que un servidor MCP quiera poner a disposición de los clientes. Esto puede incluir:

  • Contenido de archivos
  • Capturas de pantalla e imágenes
  • Archivos de registro
  • Y más

Cada recurso se identifica mediante un URI único y puede contener datos de texto o binarios.

server.addResource({
  uri: "file:///logs/app.log",
  name: "Application Logs",
  mimeType: "text/plain",
  async load() {
    return {
      text: await readLogFile(),
    };
  },
});

[!NOTE]

load puede devolver múltiples recursos. Esto podría usarse, por ejemplo, para devolver una lista de archivos dentro de un directorio cuando se lee el directorio.

async load() {
  return [
    {
      text: "Contenido del primer archivo",
    },
    {
      text: "Contenido del segundo archivo",
    },
  ];
}

También puedes devolver contenido binario en load:

async load() {
  return {
    blob: 'base64-encoded-data'
  };
}

load también recibe auth (el valor devuelto por tu función authenticate, si lo hay) y un objeto context como segundo y tercer argumento. context refleja los campos client, log, session y sessionId disponibles para tool.execute (consulta Seguimiento de ID de sesión y ID de solicitud); reportProgress y streamContent no se incluyen ya que están vinculados al token de progreso de una llamada de herramienta:

server.addResource({
  uri: "file:///logs/app.log",
  name: "Application Logs",
  mimeType: "text/plain",
  async load(auth, context) {
    context.log.info("loading application logs", { requestedBy: auth?.userId });

    return {
      text: await readLogFile(),
    };
  },
});

Suscripción a actualizaciones de recursos

Los clientes pueden suscribirse a un recurso con el método MCP resources/subscribe para ser notificados cuando su contenido cambie. FastMCP anuncia la capacidad subscribe automáticamente para cualquier servidor que exponga recursos, rastrea las suscripciones de cada cliente y te permite emitir una actualización con sendResourceUpdated:

server.addResource({
  uri: "file:///logs/app.log",
  name: "Application Logs",
  mimeType: "text/plain",
  async load() {
    return { text: await readLogFile() };
  },
});

// Whenever the underlying data changes, notify subscribed clients:
await server.sendResourceUpdated("file:///logs/app.log");

sendResourceUpdated solo notifica a los clientes que se han suscrito al URI dado, por lo que es seguro llamarlo siempre que tus datos cambien. FastMCP también anuncia la capacidad listChanged para herramientas, recursos y prompts, y emite notifications/tools/list_changed / notifications/resources/list_changed / notifications/prompts/list_changed automáticamente cuando agregas o eliminas herramientas, recursos, plantillas de recursos o prompts en tiempo de ejecución.

Una sesión no puede mostrar un tipo de primitiva que nunca negoció. MCP establece las capacidades durante initialize y se mantienen durante toda la vida de la sesión, por lo que un cliente que se conectó mientras el servidor no tenía recursos no verá los recursos agregados posteriormente — FastMCP registra una advertencia nombrando la capacidad y deja esa sesión intacta. Registra uno de cada tipo que planees agregar antes de start(), o haz que el cliente se reconecte; las sesiones que se conecten después verán todo.

Plantillas de recursos

También puedes definir plantillas de recursos:

server.addResourceTemplate({
  uriTemplate: "file:///logs/{name}.log",
  name: "Application Logs",
  mimeType: "text/plain",
  arguments: [
    {
      name: "name",
      description: "Name of the log",
      required: true,
    },
  ],
  async load({ name }) {
    return {
      text: `Example log content for ${name}`,
    };
  },
});

Al igual que los recursos simples, load también recibe auth y context como segundo y tercer argumento (consulta Recursos).

Autocompletado de argumentos de plantillas de recursos

Proporciona funciones complete para los argumentos de plantillas de recursos para habilitar el autocompletado automático:

server.addResourceTemplate({
  uriTemplate: "file:///logs/{name}.log",
  name: "Application Logs",
  mimeType: "text/plain",
  arguments: [
    {
      name: "name",
      description: "Name of the log",
      required: true,
      complete: async (value) => {
        if (value === "Example") {
          return {
            values: ["Example Log"],
          };
        }

        return {
          values: [],
        };
      },
    },
  ],
  async load({ name }) {
    return {
      text: `Example log content for ${name}`,
    };
  },
});

Recursos integrados

FastMCP proporciona un método conveniente embedded() que simplifica la inclusión de recursos en las respuestas de herramientas. Esta característica reduce la duplicación de código y facilita la referencia a recursos desde dentro de las herramientas.

Uso básico

server.addTool({
  name: "get_user_data",
  description: "Retrieve user information",
  parameters: z.object({
    userId: z.string(),
  }),
  execute: async (args) => {
    return {
      content: [
        {
          type: "resource",
          resource: await server.embedded(`user://profile/${args.userId}`),
        },
      ],
    };
  },
});

Trabajo con plantillas de recursos

El método embedded() funciona perfectamente con plantillas de recursos:

// Define a resource template
server.addResourceTemplate({
  uriTemplate: "docs://project/{section}",
  name: "Project Documentation",
  mimeType: "text/markdown",
  arguments: [
    {
      name: "section",
      required: true,
    },
  ],
  async load(args) {
    const docs = {
      "getting-started": "# Getting Started\n\nWelcome to our project!",
      "api-reference": "# API Reference\n\nAuthentication is required.",
    };
    return {
      text: docs[args.section] || "Documentation not found",
    };
  },
});

// Use embedded resources in a tool
server.addTool({
  name: "get_documentation",
  description: "Retrieve project documentation",
  parameters: z.object({
    section: z.enum(["getting-started", "api-reference"]),
  }),
  execute: async (args) => {
    return {
      content: [
        {
          type: "resource",
          resource: await server.embedded(`docs://project/${args.section}`),
        },
      ],
    };
  },
});

Trabajo con recursos directos

También funciona con recursos definidos directamente:

// Define a direct resource
server.addResource({
  uri: "system://status",
  name: "System Status",
  mimeType: "text/plain",
  async load() {
    return {
      text: "System operational",
    };
  },
});

// Use in a tool
server.addTool({
  name: "get_system_status",
  description: "Get current system status",
  parameters: z.object({}),
  execute: async () => {
    return {
      content: [
        {
          type: "resource",
          resource: await server.embedded("system://status"),
        },
      ],
    };
  },
});

Prompts

Prompts permiten a los servidores definir plantillas de prompts reutilizables y flujos de trabajo que los clientes pueden mostrar fácilmente a usuarios y LLMs. Proporcionan una forma potente de estandarizar y compartir interacciones comunes con LLMs.

server.addPrompt({
  name: "git-commit",
  description: "Generate a Git commit message",
  arguments: [
    {
      name: "changes",
      description: "Git diff or description of changes",
      required: true,
    },
  ],
  load: async (args) => {
    return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
  },
});

Al igual que los recursos, load también recibe auth y context como segundo y tercer argumento (consulta Recursos):

server.addPrompt({
  name: "git-commit",
  description: "Generate a Git commit message",
  arguments: [
    {
      name: "changes",
      description: "Git diff or description of changes",
      required: true,
    },
  ],
  load: async (args, auth, context) => {
    context.log.debug("generating git commit prompt", { user: auth?.userId });

    return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
  },
});

Autocompletado de argumentos de prompts

Los prompts pueden proporcionar autocompletado para sus argumentos:

server.addPrompt({
  name: "countryPoem",
  description: "Writes a poem about a country",
  load: async ({ name }) => {
    return `Hello, ${name}!`;
  },
  arguments: [
    {
      name: "name",
      description: "Name of the country",
      required: true,
      complete: async (value) => {
        if (value === "Germ") {
          return {
            values: ["Germany"],
          };
        }

        return {
          values: [],
        };
      },
    },
  ],
});

Autocompletado de argumentos de prompts usando enum

Si proporcionas un array enum para un argumento, el servidor proporcionará automáticamente completados para el argumento.

server.addPrompt({
  name: "countryPoem",
  description: "Writes a poem about a country",
  load: async ({ name }) => {
    return `Hello, ${name}!`;
  },
  arguments: [
    {
      name: "name",
      description: "Name of the country",
      required: true,
      enum: ["Germany", "France", "Italy"],
    },
  ],
});

OpenAPI

fromOpenAPI() (de fastmcp/openapi) convierte un documento OpenAPI 3.x en un servidor FastMCP, una herramienta por operación — manejando $refs externos (especificaciones de múltiples archivos), resolución de servers[0].url relativos y colisiones de aplanamiento de parámetros en el proceso:

import { fromOpenAPI } from "fastmcp/openapi";

const server = await fromOpenAPI({
  spec: "https://petstore3.swagger.io/api/v3/openapi.json",
  include: (operation) => operation.tags.includes("pet"),
});

await server.start({ transportType: "stdio" });

Consulta OpenAPI a MCP para la referencia completa de opciones, autenticación y limitaciones conocidas.

Autenticación

FastMCP admite autenticación OAuth 2.1 con proveedores preconfigurados, lo que te permite asegurar tu servidor con una configuración mínima.

OAuth con proveedores preconfigurados

Usa la opción auth con un proveedor para habilitar la autenticación OAuth:

import { FastMCP, getAuthSession, GoogleProvider, requireAuth } from "fastmcp";

const server = new FastMCP({
  auth: new GoogleProvider({
    baseUrl: "https://your-server.com",
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  }),
  name: "My Server",
  version: "1.0.0",
});

server.addTool({
  canAccess: requireAuth,
  description: "Get user profile",
  execute: async (_args, { session }) => {
    const { accessToken } = getAuthSession(session);
    const response = await fetch(
      "https://www.googleapis.com/oauth2/v2/userinfo",
      {
        headers: { Authorization: `Bearer ${accessToken}` },
      },
    );
    return JSON.stringify(await response.json());
  },
  name: "get-profile",
});

Proveedores disponibles:

ProveedorImportaciónCaso de uso
GoogleProviderfastmcpGoogle OAuth
GitHubProviderfastmcpGitHub OAuth
AzureProviderfastmcpAzure/Entra ID
OAuthProviderfastmcpCualquier proveedor OAuth 2.0

Proveedor OAuth genérico (para SAP, Auth0, Okta, etc.):

import { FastMCP, OAuthProvider } from "fastmcp";

const server = new FastMCP({
  auth: new OAuthProvider({
    authorizationEndpoint: process.env.OAUTH_AUTH_ENDPOINT!,
    baseUrl: "https://your-server.com",
    clientId: process.env.OAUTH_CLIENT_ID!,
    clientSecret: process.env.OAUTH_CLIENT_SECRET!,
    scopes: ["openid", "profile"],
    tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT!,
  }),
  name: "My Server",
  version: "1.0.0",
});

Autorización de herramientas

Controla el acceso a las herramientas usando la propiedad canAccess con funciones auxiliares integradas:

import {
  requireAuth,
  requireScopes,
  requireRole,
  requireAll,
  requireAny,
  getAuthSession,
} from "fastmcp";

// Require any authenticated user
server.addTool({
  canAccess: requireAuth,
  name: "user-tool",
  // ...
});

// Require specific OAuth scopes
server.addTool({
  canAccess: requireScopes("read:user", "write:data"),
  name: "scoped-tool",
  // ...
});

// Require specific role
server.addTool({
  canAccess: requireRole("admin"),
  name: "admin-tool",
  // ...
});

// Combine with AND logic
server.addTool({
  canAccess: requireAll(requireAuth, requireRole("admin")),
  name: "admin-only",
  // ...
});

// Combine with OR logic
server.addTool({
  canAccess: requireAny(requireRole("admin"), requireRole("moderator")),
  name: "staff-tool",
  // ...
});

Autorización personalizada:

Para lógica personalizada, pasa una función directamente:

server.addTool({
  name: "custom-auth-tool",
  canAccess: (auth) =>
    auth?.role === "admin" && auth?.department === "engineering",
  execute: async () => "Access granted!",
});

Extracción de datos de sesión:

Usa getAuthSession para acceso seguro por tipos a la sesión OAuth en tus funciones de ejecución de herramientas:

import { getAuthSession, GoogleSession } from "fastmcp";

server.addTool({
  canAccess: requireAuth,
  name: "get-profile",
  execute: async (_args, { session }) => {
    // Type-safe destructuring (throws if not authenticated)
    const { accessToken } = getAuthSession(session);

    // Or with provider-specific typing:
    // const { accessToken } = getAuthSession<GoogleSession>(session);

    const response = await fetch("https://api.example.com/user", {
      headers: { Authorization: `Bearer ${accessToken}` },
    });
    return JSON.stringify(await response.json());
  },
});

Nota: También puedes acceder a session.accessToken directamente, pero debes manejar el caso en que session no esté definido. El auxiliar getAuthSession lanza un error claro si la sesión no está autenticada, lo que lo hace más seguro cuando se usa con canAccess: requireAuth.

Autenticación personalizada

Para escenarios que no son OAuth (claves API, tokens personalizados), usa la opción authenticate:

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  authenticate: (request) => {
    const apiKey = request.headers["x-api-key"];

    if (apiKey !== "123") {
      throw new Response(null, {
        status: 401,
        statusText: "Unauthorized",
      });
    }

    return { id: 1, role: "user" };
  },
});

server.addTool({
  name: "sayHello",
  execute: async (args, { session }) => {
    return `Hello, ${session.id}!`;
  },
});

Proxy OAuth

La opción auth usa el Proxy OAuth integrado de FastMCP que actúa como intermediario seguro entre clientes MCP y proveedores OAuth ascendentes. El proxy maneja el flujo completo de autorización OAuth 2.1, incluido el Registro Dinámico de Clientes (DCR), PKCE, gestión de consentimiento y gestión de tokens con cifrado y patrones de intercambio de tokens habilitados por defecto.

Características clave:

  • 🔐 Seguro por defecto: Cifrado automático (AES-256-GCM) y patrón de intercambio de tokens
  • 🚀 Configuración cero: Genera claves automáticamente y maneja flujos OAuth automáticamente
  • 🔌 Proveedores preconfigurados: Soporte integrado para Google, GitHub y Azure
  • 🎯 Cumplimiento RFC: Implementa DCR (RFC 7591), PKCE y OAuth 2.1
  • 🔑 JWKS opcional: Soporte para verificación de tokens RS256/ES256 (mediante dependencia opcional jose)

Inicio rápido:

import { FastMCP, getAuthSession, GoogleProvider, requireAuth } from "fastmcp";

const server = new FastMCP({
  auth: new GoogleProvider({
    baseUrl: "https://your-server.com",
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  }),
  name: "My Server",
  version: "1.0.0",
});

server.addTool({
  canAccess: requireAuth,
  name: "protected-tool",
  execute: async (_args, { session }) => {
    const { accessToken } = getAuthSession(session);
    // Use accessToken to call upstream APIs
    return "Authenticated!";
  },
});

Configuración avanzada:

Para más control sobre el comportamiento de OAuth, puedes usar la opción oauth directamente:

import { FastMCP } from "fastmcp";
import { GoogleProvider } from "fastmcp/auth";

const authProvider = new GoogleProvider({
  baseUrl: "https://your-server.com",
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  scopes: ["openid", "profile", "email"],
});

const server = new FastMCP({
  name: "My Server",
  oauth: {
    authorizationServer: authProvider
      .getProxy()
      .getAuthorizationServerMetadata(),
    enabled: true,
    proxy: authProvider.getProxy(),
  },
  version: "1.0.0",
});

Documentación:

Endpoints de descubrimiento OAuth

FastMCP también admite endpoints de descubrimiento OAuth para la integración directa con proveedores OAuth, compatible con Especificación MCP 2025-03-26 y Especificación MCP 2025-06-18. Esto proporciona endpoints de descubrimiento estándar que cumplen con RFC 8414 (Metadatos del servidor de autorización OAuth 2.0) y RFC 9470 (Metadatos de recursos protegidos OAuth 2.0):

import { FastMCP } from "fastmcp";
import buildGetJwks from "get-jwks";
import fastJwt, { type DecodedJwt } from "fast-jwt";

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  oauth: {
    enabled: true,
    authorizationServer: {
      issuer: "https://auth.example.com",
      authorizationEndpoint: "https://auth.example.com/oauth/authorize",
      tokenEndpoint: "https://auth.example.com/oauth/token",
      jwksUri: "https://auth.example.com/.well-known/jwks.json",
      responseTypesSupported: ["code"],
    },
    protectedResource: {
      resource: "mcp://my-server",
      authorizationServers: ["https://auth.example.com"],
    },
  },
  authenticate: async (request) => {
    const authHeader = request.headers.authorization;

    if (!authHeader?.startsWith("Bearer ")) {
      throw new Response(null, {
        status: 401,
        statusText: "Missing or invalid authorization header",
      });
    }

    const token = authHeader.slice(7); // Remove 'Bearer ' prefix

    // Validate OAuth JWT access token using OpenID Connect discovery
    try {
      // Create JWKS client for token verification
      const getJwks = buildGetJwks();

      // Create JWT verifier
      const verify = fastJwt.createVerifier({
        async key({ header }: DecodedJwt) {
          const publicKey = await getJwks.getPublicKey({
            kid: header.kid,
            alg: header.alg,
            domain: "https://auth.example.com",
          });
          return publicKey;
        },
        algorithms: ["RS256"],
      });

      // Verify the JWT token
      const payload = await verify(token);

      return {
        userId: payload.sub,
        scope: payload.scope,
        email: payload.email,
        // Include other claims as needed
      };
    } catch (error) {
      throw new Response(null, {
        status: 401,
        statusText: "Invalid OAuth token",
      });
    }
  },
});

Si tu servidor MCP se publica bajo una ruta de emisor, configura también la ruta base de transmisión HTTP:

server.start({
  transportType: "httpStream",
  httpStream: {
    basePath: "/issuer1",
    endpoint: "/mcp",
    port: 8080,
  },
});

Con esta configuración, FastMCP sirve los metadatos del servidor de autorización de la ruta del emisor en /.well-known/oauth-authorization-server/issuer1, mientras que los metadatos de recursos protegidos permanecen disponibles para el endpoint MCP en /.well-known/oauth-protected-resource/issuer1/mcp.

Esta configuración expone automáticamente los endpoints de descubrimiento OAuth:

  • /.well-known/oauth-authorization-server - Metadatos del servidor de autorización (RFC 8414)
  • /.well-known/oauth-authorization-server<basePath> - Metadatos del servidor de autorización cuando httpStream.basePath está configurado (RFC 8414 Sección 3)
  • /.well-known/oauth-protected-resource - Metadatos de recursos protegidos (RFC 9728)
  • /.well-known/oauth-protected-resource<endpoint> - Metadatos de recursos protegidos en sub-ruta (MCP 2025-11-25)

Mecanismo de descubrimiento (Especificación MCP 2025-11-25):

Los clientes descubren los metadatos de recursos protegidos utilizando el siguiente orden de búsqueda:

  1. Encabezado WWW-Authenticate - Método principal (manejado automáticamente por mcp-proxy)
  2. Sub-ruta well-known - /.well-known/oauth-protected-resource<endpoint> (por ejemplo, /.well-known/oauth-protected-resource/mcp)
  3. Raíz well-known - /.well-known/oauth-protected-resource (respaldo)

Tanto los endpoints de sub-ruta como los de raíz devuelven metadatos idénticos, garantizando compatibilidad con todas las implementaciones de clientes MCP.

Para la validación de tokens JWT, puedes usar bibliotecas como get-jwks y fast-jwt para tokens JWT OAuth.

Pasar encabezados a través del contexto

Si estás exponiendo tu servidor MCP a través de HTTP, es posible que desees permitir que los clientes proporcionen claves sensibles mediante encabezados, que luego se pueden pasar a las APIs con las que interactúan tus herramientas, permitiendo que cada cliente proporcione sus propias claves de API. Esto se puede hacer capturando los encabezados HTTP en la sección authenticate y almacenándolos en la sesión para que las herramientas los referencien más adelante.

import { FastMCP } from "fastmcp";
import { IncomingHttpHeaders } from "http";

// Define the session data type
interface SessionData {
  headers: IncomingHttpHeaders;
  [key: string]: unknown; // Add index signature to satisfy Record<string, unknown>
}

// Create a server instance
const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  authenticate: async (request: any): Promise<SessionData> => {
    // Authentication logic
    return {
      headers: request.headers,
    };
  },
});

// Tool to display HTTP headers
server.addTool({
  name: "headerTool",
  description: "Reads HTTP headers from the request",
  execute: async (args: any, context: any) => {
    const session = context.session as SessionData;
    const headers = session?.headers ?? {};

    const getHeaderString = (header: string | string[] | undefined) =>
      Array.isArray(header) ? header.join(", ") : (header ?? "N/A");

    const userAgent = getHeaderString(headers["user-agent"]);
    const authorization = getHeaderString(headers["authorization"]);
    return `User-Agent: ${userAgent}\nAuthorization: ${authorization}\nAll Headers: ${JSON.stringify(headers, null, 2)}`;
  },
});

// Start the server
server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8080,
  },
});

Un cliente que se conecte a esto podría verse así:

import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

const transport = new StreamableHTTPClientTransport(
  new URL(`http://localhost:8080/mcp`),
  {
    requestInit: {
      headers: {
        Authorization: "Test 123",
      },
    },
  },
);

const client = new Client({
  name: "example-client",
  version: "1.0.0",
});

(async () => {
  await client.connect(transport);

  // Call a tool
  const result = await client.callTool({
    name: "headerTool",
    arguments: {
      arg1: "value",
    },
  });

  console.log("Tool result:", result);
})().catch(console.error);

Lo que aparecería en la consola después de que el cliente se ejecute sería algo como esto:

Tool result: {
  content: [
    {
      type: 'text',
      text: 'User-Agent: node\n' +
        'Authorization: Test 123\n' +
        'All Headers: {\n' +
        '  "host": "localhost:8080",\n' +
        '  "connection": "keep-alive",\n' +
        '  "authorization": "Test 123",\n' +
        '  "content-type": "application/json",\n' +
        '  "accept": "application/json, text/event-stream",\n' +
        '  "accept-language": "*",\n' +
        '  "sec-fetch-mode": "cors",\n' +
        '  "user-agent": "node",\n' +
        '  "accept-encoding": "gzip, deflate",\n' +
        '  "content-length": "163"\n' +
        '}'
    }
  ]
}

Seguimiento de ID de sesión y ID de solicitud

FastMCP expone automáticamente los IDs de sesión y solicitud a los manejadores de herramientas a través del parámetro de contexto. Esto permite la gestión de estado por sesión y el seguimiento de solicitudes.

ID de sesión (context.sessionId):

  • Disponible solo para transportes basados en HTTP (HTTP Stream, SSE)
  • Extraído del encabezado Mcp-Session-Id
  • Permanece constante en múltiples solicitudes del mismo cliente
  • Útil para mantener estado por sesión, contadores o datos específicos del usuario

ID de solicitud (context.requestId):

  • Disponible para todos los transportes cuando el cliente lo proporciona
  • Único para cada solicitud individual
  • Útil para el rastreo y depuración de solicitudes
import { FastMCP } from "fastmcp";
import { z } from "zod";

const server = new FastMCP({
  name: "Session Counter Server",
  version: "1.0.0",
});

// Per-session counter storage
const sessionCounters = new Map<string, number>();

server.addTool({
  name: "increment_counter",
  description: "Increment a per-session counter",
  parameters: z.object({}),
  execute: async (args, context) => {
    if (!context.sessionId) {
      return "Session ID not available (requires HTTP transport)";
    }

    const counter = sessionCounters.get(context.sessionId) || 0;
    const newCounter = counter + 1;
    sessionCounters.set(context.sessionId, newCounter);

    return `Counter for session ${context.sessionId}: ${newCounter}`;
  },
});

server.addTool({
  name: "show_ids",
  description: "Display session and request IDs",
  parameters: z.object({}),
  execute: async (args, context) => {
    return `Session ID: ${context.sessionId || "N/A"}
Request ID: ${context.requestId || "N/A"}`;
  },
});

server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8080,
  },
});

Casos de uso:

  • Gestión de estado por sesión: Mantener contadores, cachés o datos temporales únicos para cada sesión de cliente
  • Autenticación y autorización de usuarios: Rastrear usuarios autenticados a través de solicitudes
  • Gestión de recursos específicos de sesión: Asignar y gestionar recursos por sesión
  • Implementaciones multi-tenant: Aislar datos y operaciones por sesión
  • Rastreo de solicitudes: Rastrear solicitudes individuales para depuración y monitoreo

Ejemplo:

Consulta src/examples/session-id-counter.ts para un ejemplo completo que demuestra la gestión de contadores basada en sesión.

Notas:

  • Los IDs de sesión se generan automáticamente por la capa de transporte MCP
  • En modo sin estado, los IDs de sesión no se persisten entre solicitudes
  • Para el transporte stdio, sessionId será undefined ya que no existe el concepto de sesión HTTP

Proporcionar instrucciones

Puedes proporcionar instrucciones al servidor usando la opción instructions:

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  instructions:
    'Instructions describing how to use the server and its features.\n\nThis can be used by clients to improve the LLM\'s understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt.',
});

Iconos y metadatos del servidor

Puedes anunciar iconos y metadatos relacionados para tu servidor. Los clientes que admiten iconos pueden mostrarlos en su interfaz:

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
  websiteUrl: "https://example.com",
  icons: [
    {
      src: "https://example.com/icon.png",
      mimeType: "image/png",
      sizes: ["48x48"],
    },
  ],
});

Un title opcional también se pasa a MCP initialize (serverInfo), para clientes que prefieren un nombre para mostrar en lugar del name del servidor.

Sesiones

El objeto session es una instancia de FastMCPSession y describe las sesiones de cliente activas.

server.sessions;

Asignamos una nueva instancia de servidor para cada conexión de cliente para permitir la comunicación 1:1 entre un cliente y el servidor.

Eventos de servidor tipados

Puedes escuchar eventos emitidos por el servidor usando el método on:

server.on("connect", (event) => {
  console.log("Client connected:", event.session);
});

server.on("disconnect", (event) => {
  console.log("Client disconnected:", event.session);
});

FastMCPSession

FastMCPSession representa una sesión de cliente y proporciona métodos para interactuar con el cliente.

Consulta Sesiones para ejemplos de cómo obtener una instancia de FastMCPSession.

requestElicitation

requestElicitation crea una solicitud de elicitación para recopilar información adicional del usuario a través del cliente y devuelve la respuesta. El cliente debe anunciar el modo de capacidad elicitation correspondiente: elicitation: { form: {} } para solicitudes de formulario (el predeterminado) y/o elicitation: { url: {} } para solicitudes de URL.

await session.requestElicitation({
  message: "What is your name?",
  requestedSchema: {
    type: "object",
    properties: {
      name: { type: "string" },
    },
    required: ["name"],
  },
});

Dentro de una herramienta, prefiere el método elicit del objeto de contexto.

requestSampling

requestSampling crea una solicitud de muestreo y devuelve la respuesta.

await session.requestSampling({
  messages: [
    {
      role: "user",
      content: {
        type: "text",
        text: "What files are in the current directory?",
      },
    },
  ],
  systemPrompt: "You are a helpful file system assistant.",
  includeContext: "thisServer",
  maxTokens: 100,
});

Opciones

requestSampling acepta un segundo parámetro opcional para opciones de solicitud:

await session.requestSampling(
  {
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: "What files are in the current directory?",
        },
      },
    ],
    systemPrompt: "You are a helpful file system assistant.",
    includeContext: "thisServer",
    maxTokens: 100,
  },
  {
    // Progress callback - called when progress notifications are received
    onprogress: (progress) => {
      console.log(`Progress: ${progress.progress}/${progress.total}`);
    },

    // Abort signal for cancelling the request
    signal: abortController.signal,

    // Request timeout in milliseconds (default: DEFAULT_REQUEST_TIMEOUT_MSEC)
    timeout: 30000,

    // Whether progress notifications reset the timeout (default: false)
    resetTimeoutOnProgress: true,

    // Maximum total timeout regardless of progress (no default)
    maxTotalTimeout: 60000,
  },
);

Opciones:

  • onprogress?: (progress: Progress) => void - Callback para notificaciones de progreso del extremo remoto
  • signal?: AbortSignal - Señal de aborto para cancelar la solicitud
  • timeout?: number - Tiempo de espera de solicitud en milisegundos
  • resetTimeoutOnProgress?: boolean - Si las notificaciones de progreso restablecen el tiempo de espera
  • maxTotalTimeout?: number - Tiempo de espera máximo total independientemente de las notificaciones de progreso

clientCapabilities

La propiedad clientCapabilities contiene las capacidades del cliente.

session.clientCapabilities;

loggingLevel

La propiedad loggingLevel describe el nivel de registro establecido por el cliente. Lee info hasta que el cliente envía logging/setLevel; los mensajes menos severos que el nivel que el cliente estableció se descartan en lugar de enviarse.

session.loggingLevel;

roots

La propiedad roots contiene las raíces establecidas por el cliente.

session.roots;

server

La propiedad server contiene una instancia del servidor MCP asociada con la sesión.

session.server;

Eventos de sesión tipados

Puedes escuchar eventos emitidos por la sesión usando el método on:

session.on("rootsChanged", (event) => {
  console.log("Roots changed:", event.roots);
});

session.on("error", (event) => {
  console.error("Error:", event.error);
});

Ejecutar tu servidor

Pruebas unitarias con transporte en memoria

server.connect(transport) adjunta el servidor a un transporte que construyes tú mismo, en lugar de permitir que start() cree uno. Combinado con el InMemoryTransport del SDK, esto te permite ejecutar un servidor en proceso: sin puerto que vincular, sin subproceso que generar, que es generalmente lo que deseas para probar un servidor stdio:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";

async function createTestClient(server: FastMCP) {
  const [clientTransport, serverTransport] =
    InMemoryTransport.createLinkedPair();

  const client = new Client({ name: "test-client", version: "0.0.0" });

  const [session] = await Promise.all([
    server.connect(serverTransport),
    client.connect(clientTransport),
  ]);

  return { client, session };
}

test("adds two numbers", async () => {
  const { client } = await createTestClient(server);

  expect(
    await client.callTool({ arguments: { a: 2, b: 3 }, name: "add" }),
  ).toEqual({
    content: [{ text: "5", type: "text" }],
  });

  await client.close();
});

La sesión se construye a partir de las herramientas, recursos y prompts registrados en la instancia, exactamente como start() la construye, por lo que tus pruebas ejercitan la misma conexión que usa el servidor real, incluido el filtrado de canAccess y los eventos connect/disconnect.

Pasa la autenticación de sesión como segundo argumento, equivalente a lo que tu función authenticate devolvería:

await server.connect(serverTransport, { id: 7, role: "admin" });

connect devuelve el FastMCPSession, por lo que puedes hacer afirmaciones sobre session.clientCapabilities, session.roots y el resto. El ciclo de vida del transporte te pertenece: stop() no cierra los transportes pasados a connect, así que cierra el cliente (y la sesión, si necesitas que disconnect se active) cuando la prueba termine.

Probar con mcp-cli

La forma más rápida de probar y depurar tu servidor es con fastmcp dev:

npx fastmcp dev server.js
npx fastmcp dev server.ts

Esto ejecutará tu servidor con mcp-cli para probar y depurar tu servidor MCP en la terminal.

Para llamar a una herramienta de forma no interactiva (por ejemplo, en scripts o pruebas automatizadas), pasa --tool y JSON opcional --args:

npx fastmcp dev server.ts --tool add --args '{"a":1,"b":2}'

Esto imprime el resultado de la herramienta como JSON y sale, en lugar de abrir el inspector interactivo. --watch no tiene efecto en este modo, ya que el servidor se inicia para una sola llamada.

Inspeccionar con MCP Inspector

Otra forma es usar el MCP Inspector oficial para inspeccionar tu servidor con una interfaz web:

npx fastmcp inspect server.ts

Preguntas frecuentes

¿Cómo usar con Claude Desktop?

Sigue la guía https://modelcontextprotocol.io/quickstart/user y agrega la siguiente configuración:

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "npx",
      "args": ["tsx", "/PATH/TO/YOUR_PROJECT/src/index.ts"],
      "env": {
        "YOUR_ENV_VAR": "value"
      }
    }
  }
}

¿Cómo ejecutar FastMCP detrás de un proxy?

Consulta este problema para un ejemplo de uso de FastMCP con express y http-proxy-middleware.

Vitrina

[!NOTA]

Si has desarrollado un servidor usando FastMCP, ¡por favor envía un PR para mostrarlo aquí!

[!NOTA]

Si estás buscando un repositorio de plantilla para construir tu propio servidor MCP, consulta fastmcp-boilerplate.

Agradecimientos

Este proyecto se prueba con BrowserStack.