Skip to main content
Glama
julioalberto64

IronMCP Framework Starter

IronMCP Framework Starter

Starter empresarial para crear servidores MCP en Node.js + TypeScript con:

  • Transporte stdio para clientes locales.

  • Transporte http con Streamable HTTP stateless.

  • Configuracion por YAML + variables de entorno.

  • Logs estructurados con Pino.

  • REST client centralizado con autenticacion.

  • Validadores de codigo y seguridad.

  • Contrato obligatorio para tools: schema, auth, timeout, auditoria e idempotencia.

  • Skill para crear tools con ayuda de una IA siguiendo las convenciones del framework.

  • Tool get_weather para consultar el clima por ciudad y resolver ubicaciones ambiguas.

Requisitos

  • Node.js 22.22.1+ (rama 22), 24.x o 26+

  • npm >=10

Related MCP server: Kroki MCP

Inicio rapido

npm install
cp .env.example .env
# Configura MCP_AUTH_TOKEN en .env con un token aleatorio antes de arrancar HTTP.
npm run validate

Para probar HTTP:

npm run dev:http

En otra terminal:

bash scripts/smoke-http.sh

Health check HTTP normal:

curl http://127.0.0.1:3000/healthz

Para probar STDIO con MCP Inspector:

npm run build
npm run mcp:inspect:stdio

Probar con un túnel de VS Code

Con el servidor activo en el puerto 3000, reenvía ese puerto desde el panel Ports de VS Code. Los túneles son privados de forma predeterminada: Postman o un cliente externo puede recibir una redirección al login de GitHub. Si necesitas acceso sin esa sesión, cambia Port Visibility > Public durante la prueba. Un puerto público permite que cualquiera con la URL acceda al servicio; conserva la autenticación MCP para las tools protegidas.

Autoriza el hostname exacto del túnel en .env, sin protocolo ni ruta, y reinicia el servidor:

HTTP_HOST=127.0.0.1
HTTP_ALLOWED_HOSTS=your-tunnel-3000.use2.devtunnels.ms

El servidor mantiene la validación de Host y permite también localhost; no es necesario escuchar en 0.0.0.0. Si cambia la dirección del túnel, actualiza la lista. En Postman configura mcp_url como https://your-tunnel-3000.use2.devtunnels.ms/mcp. Para comprobar únicamente el estado, usa GET https://your-tunnel-3000.use2.devtunnels.ms/healthz. Deja VS Code y el servidor ejecutándose mientras pruebas.

Probar la tool REST autenticada

El proyecto trae una tool llamada call_secure_api. Por defecto llama a https://httpbin.org/bearer, que espera un header Authorization: Bearer <token>.

Edita .env:

API_AUTH_TYPE=bearer-env
API_TOKEN=demo-token
API_BASE_URL=https://httpbin.org

Luego ejecuta:

npm run dev:http
bash scripts/smoke-http.sh examples/http/call-secure-api.json

Clima actual: get_weather

Consulta condiciones actuales modeladas por Open-Meteo para uso no comercial, sin API key. El servicio gratuito tiene límites de uso; consulta sus condiciones. Los datos requieren atribución CC BY 4.0, incluida en la salida de la tool. Las condiciones actuales proceden de modelos meteorológicos y pueden diferir de una observación local.

Parámetros de entrada:

Parámetro

Obligatorio

Descripción

city

Sí

Nombre de la ciudad, de 2 a 120 caracteres, sin país ni región.

countryCode

No

Código ISO del país de dos letras, por ejemplo GT o ES.

locationId

No

Entero positivo elegido entre los candidatos de una respuesta anterior.

La búsqueda exige coincidencia del nombre sin distinguir mayúsculas ni acentos. No acepta URLs ni rutas.

La tool requiere un principal autenticado con weather:read. Para habilitarla, añade tú ese scope a MCP_AUTH_SCOPES, conservando los permisos que necesites para las otras tools. Por ejemplo, para habilitar las tools incluidas:

MCP_AUTH_SCOPES=external-api:read,billing:read,weather:read

Reinicia el servidor con npm run dev:http después de cambiar la configuración. Si ya está ejecutándose, detenlo antes con Ctrl+C. La integración no modifica .env, tokens ni los permisos concedidos a los clientes existentes.

Usa context.restClient con dos destinos públicos fijos y sin headers de autenticación externos: https://geocoding-api.open-meteo.com/v1/search y https://api.open-meteo.com/v1/forecast. Estos destinos solo permiten GET a esas rutas; no reutilizan API_TOKEN, API_KEY ni cambian API_BASE_URL. Conservan cancelación, timeout, bloqueo de redirecciones y límite de respuesta del cliente.

Ejemplo: tools/call. Envía el JSON con POST /mcp, Bearer MCP y headers Content-Type: application/json y Accept: application/json, text/event-stream. El transporte es stateless, no requiere sesión y puede devolver eventos SSE. GET /healthz solo consulta el estado HTTP. La colección Postman incluye la petición Clima actual — requiere weather:read, usando {{mcp_url}} y {{mcp_token}}. Vuelve a importar la colección actualizada si todavía no aparece.

Body JSON de ejemplo:

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "city": "Madrid",
      "countryCode": "ES"
    }
  }
}

Una consulta resuelta devuelve result.status: "ok" junto con city, country, temperatureC, feelsLikeC y description en español, además de attribution al nivel superior. Ante homónimos devuelve result.status: "needs_clarification", un mensaje y candidates con ciudad, país, región si existe y locationId. Repite la llamada con city y el locationId elegido; añadir solo el país puede seguir siendo ambiguo. No consulta el clima hasta resolver la ubicación. Si la búsqueda alcanza el límite de 100 resultados, marca truncated: true y solicita precisión. Las ciudades ausentes, respuestas inválidas y fallos del proveedor se propagan como errores MCP, sin fabricar datos meteorológicos.

Seguridad y permisos

HTTP requiere MCP_AUTH_TOKEN cuando MCP_AUTH_ENABLED=true; debe tener entre 32 y 256 caracteres ASCII sin espacios. Genera un token aleatorio con openssl rand -hex 32. Si falta o es inválido, el servidor falla al iniciar. El cliente debe enviar Authorization: Bearer <token>. Para el script de prueba, exporta MCP_AUTH_TOKEN en la terminal antes de ejecutar bash scripts/smoke-http.sh.

MCP_AUTH_SCOPES define los permisos del token, separados por comas (por ejemplo, external-api:read,billing:read). Cada tool comprueba sus permisos antes de ejecutar el handler. Si se desactiva la autenticación HTTP, solo se pueden ejecutar tools públicas. En STDIO, el proceso local es un cliente de confianza y recibe únicamente los scopes configurados.

La autenticación permite 10 intentos fallidos por dirección remota en una ventana de 60 segundos. Después devuelve 429 con Retry-After, incluso si el siguiente token es correcto, hasta finalizar la ventana. El registro tiene un máximo de 1024 direcciones y no confía en X-Forwarded-For. El límite vive en cada proceso; detrás de un proxy, las peticiones comparten la dirección de ese proxy.

HTTP opera sin sesiones: cada petición crea y cierra su servidor MCP; no se devuelve mcp-session-id. Se admiten hasta 100 peticiones simultáneas con un plazo máximo de 30 segundos.

Al vencer el timeout o cancelar una petición MCP, el framework aborta context.signal y el context.restClient asociado. Las tools deben consumir ese cliente del contexto. Para otras operaciones asíncronas, pasa context.signal a las APIs que admitan cancelación y comprueba context.signal?.throwIfAborted() antes de efectos posteriores. JavaScript no puede interrumpir de forma forzada código que ignora la señal; cancelar una petición tampoco revierte operaciones ya ejecutadas por la API externa.

RestClient usa por defecto rutas relativas del origen configurado en API_BASE_URL, bloquea URLs absolutas y redirecciones, y limita cada respuesta a 2 MiB. La integración de clima utiliza servicios públicos con destinos y rutas fijos, sin reenviar las credenciales de esa API. Los headers y parámetros de consulta no se escriben en los logs REST. La tool de demostración call_secure_api solo permite /bearer; para otros endpoints crea una tool con rutas y permisos específicos.

Scripts principales

npm run dev:stdio        # arranca MCP por STDIO
npm run dev:http         # arranca MCP por HTTP en /mcp
npm run build            # compila TypeScript
npm run typecheck        # valida tipos
npm run lint             # ESLint
npm run format:check     # Prettier check
npm run test             # Vitest
npm run secrets:check    # Secretlint
npm run knip             # dependencias/codigo muerto
npm run mcp:validate     # validador propio del estandar MCP
npm run validate         # quality gate completo

Estructura

src/
  framework/
    auth/                 # proveedores de autenticacion
    rest/                 # REST client estandar
    transports/           # STDIO y HTTP
    config.ts             # unico lugar permitido para process.env
    create-mcp-server.ts  # registro estandarizado de tools
    errors.ts             # errores normalizados
    logger.ts             # Pino; stderr cuando transport=stdio
    types.ts              # contrato de tools
  tools/
    health.tool.ts
    call-secure-api.tool.ts
    get-subscriber-balance.tool.ts
    get-weather.tool.ts

Reglas del estandar

El validador propio tools/mcp-validate.mjs falla el build si detecta:

  • console.* en src/.

  • process.env fuera de src/framework/config.ts.

  • fetch( fuera de src/framework/rest/rest-client.ts.

  • import directo de axios.

  • tools sin inputSchema, timeoutMs, auth, audit, idempotent o handler.

  • posibles secretos hardcodeados en config/default.yaml o .env.example.

Crear tools con ayuda de una IA

La skill del repositorio está en .agents/skills/create-mcp-tool/SKILL.md. AGENTS.md indica a los agentes que la lean cuando se les solicite crear o ampliar una tool. Incluye el flujo de implementación, registro, permisos, cancelación, pruebas y ejemplos HTTP/Postman.

Puedes pedir: «Crea una tool para consultar un cliente usando GET /customers/{customerId}, con scope customers:read; implementa y valida el cambio». Incluye el contrato real de entrada y respuesta del endpoint. En clientes que permitan invocar skills por nombre, también puedes usar $create-mcp-tool.

La tool get_weather se agregó usando este flujo: la IA implementó la consulta por ciudad, su registro, las pruebas y los ejemplos a partir de la skill. Para otras tools, proporciona su propósito y el contrato de la integración que necesitas.

Si tu asistente no descubre skills ni lee AGENTS.md automáticamente, indícale que abra el SKILL.md anterior. El ejemplo completo de código y pruebas está en references/example.md; no se registra ninguna tool de demostración en el servidor por agregar esta guía.

Crear una nueva tool

Copia src/tools/health.tool.ts y cambia:

export const myTool = defineTool({
  name: "my_tool",
  title: "My Tool",
  description: "Descripcion clara para el modelo.",
  inputSchema: {
    id: z.string(),
  },
  timeoutMs: 5000,
  idempotent: true,
  auth: {
    required: true,
    scopes: ["my-domain:read"],
  },
  audit: {
    category: "read",
    pii: false,
  },
  async handler(input, context) {
    return context.restClient.get(`/items/${input.id}`);
  },
});

Luego registra la tool en src/tools/index.ts.

Notas importantes sobre STDIO

Cuando transport=stdio, el servidor MCP usa stdin/stdout para JSON-RPC. Por eso este starter manda logs a stderr y prohibe console.log en src/.

Siguiente paso sugerido

Cuando este starter ya funcione para tu equipo, el siguiente paso es convertir src/framework en paquete interno, por ejemplo @company/mcp-framework, y crear un CLI create-mcp que genere nuevos MCPs con esta misma estructura.

Crear una nueva tool: consultar balance de datos móviles

Esta guía muestra cómo crear una tool MCP que consume este endpoint REST:

GET /api/v2.0/mobile/upselling/subscribers/{subscriberId}/balances/data?_format=json&load_roaming=false&onlinetetheringquota=false

La tool se llamará:

get_subscriber_data_balance

Importante: nunca colocar el Bearer token directamente en el código. El token debe ir en .env.


1. Configurar variables de entorno

Editar el archivo .env:

API_BASE_URL=https://www.juliomorales.dev/apirest
API_AUTH_TYPE=bearer-env
API_TOKEN=TU_TOKEN_AQUI

2. Crear el archivo de la tool

Crear el archivo:

src/tools/get-subscriber-data-balance.tool.ts

Agregar este código:

import * as z from "zod/v4";

import { defineTool } from "../framework/types.js";

const getSubscriberDataBalanceInputSchema = z.object({
  subscriberId: z.string().min(8).describe("Número de suscriptor. Ejemplo: 50230448966"),
});

export function createGetSubscriberDataBalanceTool() {
  return defineTool({
    name: "get_subscriber_data_balance",
    title: "Get Subscriber Data Balance",
    description:
      "Consulta el balance de datos móviles disponibles, usados y vencimiento de un suscriptor Guatemala.",
    inputSchema: {
      subscriberId: z.string().min(8).describe("Número de suscriptor. Ejemplo: 50230448966"),
    },
    outputSchema: {
      description: z.string().optional(),
      remainingValue: z.number().optional(),
      remainingFormatted: z.string().optional(),
      usedValue: z.number().optional(),
      usedFormatted: z.string().optional(),
      reservedValue: z.number().optional(),
      reservedFormatted: z.string().optional(),
      expiresAt: z.string().optional(),
      expiresIn: z.string().optional(),
      includedApps: z.array(z.string()).optional(),
      onlineTetheringFormatted: z.string().optional(),
      raw: z.unknown(),
    },
    timeoutMs: 10000,
    idempotent: true,
    auth: {
      required: true,
      scopes: ["upselling:read", "balances:read"],
    },
    audit: {
      category: "external-api",
      pii: true,
    },
    async handler(input, context) {
      const parsedInput = getSubscriberDataBalanceInputSchema.parse(input);

      const response = await context.restClient.get<{
        data?: {
          local?: {
            description?: {
              value?: string;
              formattedValue?: string;
            };
            summaryRemainingValue?: {
              value?: number;
              formattedValue?: string;
            };
            summaryReservedAmount?: {
              value?: number;
              formattedValue?: string;
            };
            summaryUsedValue?: {
              value?: number;
              formattedValue?: string;
            };
            summaryDateValue?: {
              value?: string;
              formattedValue?: string;
            };
            includedApps?: {
              tags?: {
                value?: string[];
              };
            };
            onlineTethering?: {
              label?: string;
              value?: number;
            };
          };
        };
      }>(
        `/api/v2.0/mobile/upselling/subscribers/${encodeURIComponent(
          parsedInput.subscriberId,
        )}/balances/data?_format=json&load_roaming=false&onlinetetheringquota=false`,
        {
          headers: {
            Referer: "https://www.juliomorales.dev/",
            Accept: "application/json, text/plain, */*",
          },
        },
      );

      const local = response.data?.local;

      return {
        description: local?.description?.formattedValue ?? local?.description?.value,
        remainingValue: local?.summaryRemainingValue?.value,
        remainingFormatted: local?.summaryRemainingValue?.formattedValue,
        usedValue: local?.summaryUsedValue?.value,
        usedFormatted: local?.summaryUsedValue?.formattedValue,
        reservedValue: local?.summaryReservedAmount?.value,
        reservedFormatted: local?.summaryReservedAmount?.formattedValue,
        expiresAt: local?.summaryDateValue?.value,
        expiresIn: local?.summaryDateValue?.formattedValue,
        includedApps: local?.includedApps?.tags?.value,
        onlineTetheringFormatted: local?.onlineTethering?.label,
        raw: response,
      };
    },
  });
}

3. Registrar la tool

Editar:

src/tools/index.ts

Debe quedar similar a esto:

import { callSecureApiTool } from "./call-secure-api.tool.js";
import { createGetSubscriberBalanceTool } from "./get-subscriber-balance.tool.js";
import { createGetSubscriberDataBalanceTool } from "./get-subscriber-data-balance.tool.js";
import { healthTool } from "./health.tool.js";

export function createTools() {
  return [
    healthTool,
    callSecureApiTool,
    createGetSubscriberBalanceTool(),
    createGetSubscriberDataBalanceTool(),
  ];
}

4. Crear ejemplo HTTP para probar

Crear el archivo:

examples/http/call-subscriber-data-balance.json

Con este contenido:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_subscriber_data_balance",
    "arguments": {
      "subscriberId": "50230448966"
    }
  }
}

5. Validar el código

Ejecutar:

npm run validate

Si todo está correcto, debe pasar:

mcp:validate
typecheck
lint
format:check
test
secrets:check

6. Levantar el servidor HTTP

npm run dev:http

7. Probar la tool

En otra terminal:

bash scripts/smoke-http.sh examples/http/call-subscriber-data-balance.json

La respuesta debe incluir información como:

{
  "description": "30GB + REDES SOCIALES + MUSICA + SPORTS",
  "remainingFormatted": "15.48 GB",
  "usedFormatted": "14.52 GB",
  "reservedFormatted": "30 GB",
  "expiresIn": "11 días, 10 horas",
  "includedApps": ["WHATSAPP Incluido", "Redes Sociales", "Entretenimiento"]
}

Reglas importantes para developers

  1. No usar fetch directo.

  2. No usar axios directo.

  3. No colocar tokens en código.

  4. Siempre usar RestClient.

  5. Siempre validar input con Zod dentro del handler.

  6. Toda tool debe tener:

    • name

    • description

    • inputSchema

    • outputSchema

    • timeoutMs

    • auth

    • audit

    • idempotent

  7. Siempre ejecutar:

npm run validate

Conventional Commits

IronMCP utiliza Conventional Commits.

Ejemplos:

git commit -m "feat: add MCP authentication middleware"
git commit -m "fix: resolve HTTP transport issue"
git commit -m "docs: update README"
git commit -m "refactor: simplify tool registration"

❤️ Support IronMCP

If IronMCP helps you build better MCP servers, consider supporting the project.

Your contribution helps maintain the framework, add new features, improve documentation, and keep the project open source.

Donate via PayPal

Donate with PayPal

Or send a donation directly to:

info@nworldt.net

Every contribution, no matter the size, helps keep IronMCP growing.

Thank you for your support! 🚀

Knip conserva los exports del framework usados dentro de su propio archivo. El preset de Secretlint se declara en ignoreDependencies porque se carga dinámicamente desde .secretlintrc.json; secrets:check valida su ejecución.

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    D
    maintenance
    An MCP server framework featuring dynamic tool loading and a modular one-tool-per-file architecture for rapid development. It supports both Stdio and HTTP transport modes, offering automated test generation and centralized configuration management.
    11
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    A production-ready MCP server scaffold that features built-in authentication, Docker support, and a comprehensive CI/CD release pipeline. It provides a standardized template for deploying servers with multi-transport support and configurable read-only modes.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A modular framework for building and running multiple MCP servers with file operations, system information, and extensible tool/prompt/resource support.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A flexible, extensible framework for building MCP servers with API key authentication, user management, and dynamic tool sharing.
    7 npm
    11
    MIT