IronMCP Framework Starter
README.md
# 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`
## Inicio rapido
```bash
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:
```bash
npm run dev:http
```
En otra terminal:
```bash
bash scripts/smoke-http.sh
```
Health check HTTP normal:
```bash
curl http://127.0.0.1:3000/healthz
```
Para probar STDIO con MCP Inspector:
```bash
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:
```env
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`:
```env
API_AUTH_TYPE=bearer-env
API_TOKEN=demo-token
API_BASE_URL=https://httpbin.org
```
Luego ejecuta:
```bash
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](https://open-meteo.com/en/docs)
para uso **no comercial**, sin API key. El servicio gratuito tiene límites de uso;
consulta sus [condiciones](https://open-meteo.com/en/pricing). 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:
```env
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](examples/http/call-get-weather.json). 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](examples/postman/iron-mcp.postman_collection.json)
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:
```json
{
"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
```bash
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
```txt
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/skills/create-mcp-tool/SKILL.md). [AGENTS.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`](#clima-actual-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](.agents/skills/create-mcp-tool/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:
```ts
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:
```bash
GET /api/v2.0/mobile/upselling/subscribers/{subscriberId}/balances/data?_format=json&load_roaming=false&onlinetetheringquota=false
```
La tool se llamará:
```txt
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`:
```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:
```txt
src/tools/get-subscriber-data-balance.tool.ts
```
Agregar este código:
```ts
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:
```txt
src/tools/index.ts
```
Debe quedar similar a esto:
```ts
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:
```txt
examples/http/call-subscriber-data-balance.json
```
Con este contenido:
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_subscriber_data_balance",
"arguments": {
"subscriberId": "50230448966"
}
}
}
```
---
## 5. Validar el código
Ejecutar:
```bash
npm run validate
```
Si todo está correcto, debe pasar:
```txt
mcp:validate
typecheck
lint
format:check
test
secrets:check
```
---
## 6. Levantar el servidor HTTP
```bash
npm run dev:http
```
---
## 7. Probar la tool
En otra terminal:
```bash
bash scripts/smoke-http.sh examples/http/call-subscriber-data-balance.json
```
La respuesta debe incluir información como:
```json
{
"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:
```bash
npm run validate
```
## Conventional Commits
IronMCP utiliza Conventional Commits.
Ejemplos:
```bash
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
[](https://www.paypal.com/paypalme/nworldt)
Or send a donation directly to:
```txt
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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues