Módulo 4: MCP Server en TypeScript
Implementar Resources con URIs y Templates
Implementar Resources con URIs y Templates
Descripción de la cápsula
En la cápsula anterior implementaste tools — acciones que el modelo ejecuta. Ahora pasas al complemento natural: resources — datos que el modelo puede leer. Si los tools son las manos del modelo, los resources son sus ojos.
En el módulo 3 viste resources de forma conceptual y con un ejemplo mínimo. Ahora vas a implementar resources completos en TypeScript: resources estáticos con URIs fijos, resources dinámicos con URI templates, resources que leen del filesystem, resources que agregan datos, y resources con suscripciones.
La diferencia clave entre un tool y un resource es la intención: un resource expone datos sin modificar nada. Es pull-based — el cliente pide y el server responde. No hay side effects. Esta restricción no es una limitación — es lo que hace a los resources predecibles y seguros de usar.
Repaso rápido: Resources en el SDK
API de server.resource()
// Resource estático (URI fijo)
server.resource(
name, // string — identificador interno
uri, // string — URI del resource (e.g., "config://app/settings")
metadata, // { description, mimeType } — metadata para el cliente
handler // async (uri) => ResourceResult
);
// Resource dinámico (URI template)
server.resource(
name, // string — identificador interno
template, // ResourceTemplate — URI con placeholders
metadata, // { description, mimeType }
handler // async (uri, params) => ResourceResult
);
Estructura del resultado
return {
contents: [
{
uri: uri.href, // el URI solicitado
mimeType: "...", // tipo de contenido
text: "...", // contenido de texto
},
],
};
Resources estáticos: URIs fijos
Un resource estático tiene un URI que no cambia. Siempre apunta al mismo dato, aunque el contenido puede variar (porque lee datos frescos cada vez).
Ejemplo 1: Estado del sistema
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as os from "os";
const server = new McpServer({
name: "system-monitor",
version: "1.0.0",
});
server.resource(
"system-status",
"system://status",
{
description: "Estado actual del sistema: CPU, memoria, uptime, plataforma",
mimeType: "application/json",
},
async (uri) => {
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
const status = {
platform: os.platform(),
arch: os.arch(),
hostname: os.hostname(),
uptime: {
seconds: os.uptime(),
formatted: formatUptime(os.uptime()),
},
memory: {
total: formatBytes(totalMem),
used: formatBytes(usedMem),
free: formatBytes(freeMem),
usagePercent: ((usedMem / totalMem) * 100).toFixed(1) + "%",
},
cpus: os.cpus().length,
loadAverage: os.loadavg(),
nodeVersion: process.version,
timestamp: new Date().toISOString(),
};
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(status, null, 2),
}],
};
}
);
function formatBytes(bytes: number): string {
const gb = bytes / (1024 * 1024 * 1024);
return `${gb.toFixed(2)} GB`;
}
function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${days}d ${hours}h ${minutes}m`;
}
Cuándo usar: Datos que siempre existen y tienen un URI natural. No necesitas parámetros para pedirlos.
Ejemplo 2: Configuración de la aplicación
import * as fs from "fs/promises";
import * as path from "path";
server.resource(
"app-config",
"config://app/current",
{
description: "Configuración actual de la aplicación desde package.json y archivos de config",
mimeType: "application/json",
},
async (uri) => {
const projectDir = process.cwd();
const config: Record<string, unknown> = {};
try {
const pkgJson = await fs.readFile(path.join(projectDir, "package.json"), "utf-8");
const pkg = JSON.parse(pkgJson);
config.package = {
name: pkg.name,
version: pkg.version,
description: pkg.description,
dependencies: Object.keys(pkg.dependencies || {}),
devDependencies: Object.keys(pkg.devDependencies || {}),
};
} catch {
config.package = { error: "package.json no encontrado" };
}
try {
const tsConfigJson = await fs.readFile(path.join(projectDir, "tsconfig.json"), "utf-8");
const tsConfig = JSON.parse(tsConfigJson);
config.typescript = {
target: tsConfig.compilerOptions?.target,
module: tsConfig.compilerOptions?.module,
strict: tsConfig.compilerOptions?.strict,
outDir: tsConfig.compilerOptions?.outDir,
};
} catch {
config.typescript = { error: "tsconfig.json no encontrado" };
}
const envVars: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("APP_") || key.startsWith("MCP_")) {
envVars[key] = value || "";
}
}
config.env = Object.keys(envVars).length > 0 ? envVars : { note: "No APP_* o MCP_* env vars found" };
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(config, null, 2),
}],
};
}
);
Ejemplo 3: Estructura del proyecto
server.resource(
"project-tree",
"project://structure",
{
description: "Árbol de archivos y carpetas del proyecto actual con tamaños",
mimeType: "application/json",
},
async (uri) => {
const projectDir = process.cwd();
interface FileNode {
name: string;
type: "file" | "directory";
size?: string;
children?: FileNode[];
}
async function buildTree(dir: string, depth: number = 0): Promise<FileNode[]> {
if (depth > 4) return [];
const entries = await fs.readdir(dir, { withFileTypes: true });
const nodes: FileNode[] = [];
const sorted = entries
.filter(e => !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "build")
.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (const entry of sorted) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const children = await buildTree(fullPath, depth + 1);
nodes.push({ name: entry.name, type: "directory", children });
} else {
const stats = await fs.stat(fullPath);
nodes.push({
name: entry.name,
type: "file",
size: `${(stats.size / 1024).toFixed(1)} KB`,
});
}
}
return nodes;
}
const tree = await buildTree(projectDir);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
root: projectDir,
tree,
generatedAt: new Date().toISOString(),
}, null, 2),
}],
};
}
);
Resources dinámicos: URI Templates
Los resource templates permiten que un mismo resource sirva datos diferentes según los parámetros del URI. Son la solución para datos parametrizables donde los parámetros son simples.
Cómo funciona ResourceTemplate
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
server.resource(
"internal-name",
new ResourceTemplate("scheme://type/{parameter}", {
list: async () => {
// Retorna la lista de URIs concretos que el cliente puede pedir
return [
{ uri: "scheme://type/value1", name: "Valor 1" },
{ uri: "scheme://type/value2", name: "Valor 2" },
];
},
}),
{ description: "...", mimeType: "..." },
async (uri, params) => {
// params.parameter contiene el valor del placeholder
// ...
}
);
El callback list es lo que le dice al cliente qué instancias concretas del template existen. Es opcional — si no lo provees, el cliente puede construir URIs según el template.
Ejemplo 4: Archivos por ruta
server.resource(
"file-content",
new ResourceTemplate("file:///{filePath}", {
list: async () => {
const projectDir = process.cwd();
const files: Array<{ uri: string; name: string; description: string }> = [];
async function collectFiles(dir: string): Promise<void> {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "build") continue;
const fullPath = path.join(dir, entry.name);
const relativePath = path.relative(projectDir, fullPath);
if (entry.isDirectory()) {
await collectFiles(fullPath);
} else {
files.push({
uri: `file:///${relativePath}`,
name: entry.name,
description: `Contenido de ${relativePath}`,
});
}
}
}
await collectFiles(projectDir);
return files;
},
}),
{
description: "Contenido de un archivo del proyecto por su ruta relativa",
mimeType: "text/plain",
},
async (uri, params) => {
const filePath = params.filePath as string;
const projectDir = process.cwd();
const absolutePath = path.resolve(projectDir, filePath);
if (!absolutePath.startsWith(projectDir)) {
throw new Error("Acceso denegado: la ruta está fuera del proyecto");
}
try {
const content = await fs.readFile(absolutePath, "utf-8");
const ext = path.extname(filePath);
const mimeTypes: Record<string, string> = {
".ts": "text/typescript",
".js": "text/javascript",
".json": "application/json",
".md": "text/markdown",
".html": "text/html",
".css": "text/css",
};
return {
contents: [{
uri: uri.href,
mimeType: mimeTypes[ext] || "text/plain",
text: content,
}],
};
} catch {
throw new Error(`Archivo no encontrado: ${filePath}`);
}
}
);
Ejemplo 5: Logs por fecha
server.resource(
"logs-by-date",
new ResourceTemplate("logs://app/{date}", {
list: async () => {
const logsDir = path.join(process.cwd(), "logs");
try {
const files = await fs.readdir(logsDir);
return files
.filter(f => f.endsWith(".log"))
.map(f => {
const date = f.replace(".log", "");
return {
uri: `logs://app/${date}`,
name: `Logs del ${date}`,
description: `Archivo de logs del día ${date}`,
};
});
} catch {
return [];
}
},
}),
{
description: "Logs de la aplicación por fecha (formato: YYYY-MM-DD)",
mimeType: "text/plain",
},
async (uri, params) => {
const date = params.date as string;
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(date)) {
throw new Error(`Formato de fecha inválido: ${date}. Usa YYYY-MM-DD`);
}
const logPath = path.join(process.cwd(), "logs", `${date}.log`);
try {
const content = await fs.readFile(logPath, "utf-8");
const lines = content.split("\n");
return {
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: `=== Logs del ${date} ===\nTotal líneas: ${lines.length}\n\n${content}`,
}],
};
} catch {
throw new Error(`No hay logs para la fecha ${date}`);
}
}
);
Ejemplo 6: Resource template con múltiples parámetros
server.resource(
"git-diff",
new ResourceTemplate("git://diff/{branch1}/{branch2}", {
list: undefined,
}),
{
description: "Diff entre dos ramas de Git",
mimeType: "text/plain",
},
async (uri, params) => {
const branch1 = params.branch1 as string;
const branch2 = params.branch2 as string;
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
try {
const { stdout } = await execAsync(`git diff ${branch1}...${branch2}`, {
cwd: process.cwd(),
maxBuffer: 1024 * 1024 * 5,
});
return {
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: stdout || `No hay diferencias entre ${branch1} y ${branch2}`,
}],
};
} catch (error) {
throw new Error(`Error al obtener diff: ${error instanceof Error ? error.message : "desconocido"}`);
}
}
);
Cómo el cliente descubre resources
El protocolo MCP define dos métodos para descubrimiento:
resources/list
El cliente envía resources/list y recibe la lista de resources disponibles:
{
"resources": [
{
"uri": "system://status",
"name": "system-status",
"description": "Estado actual del sistema",
"mimeType": "application/json"
},
{
"uri": "config://app/current",
"name": "app-config",
"description": "Configuración actual"
}
]
}
Para resource templates, el callback list es lo que genera esta lista. Sin list, el template no aparece en la lista estática pero el cliente aún puede construir URIs válidos.
resources/read
El cliente envía resources/read con un URI específico:
{
"uri": "system://status"
}
Y recibe el contenido:
{
"contents": [
{
"uri": "system://status",
"mimeType": "application/json",
"text": "{\"platform\": \"darwin\", ...}"
}
]
}
Descubrimiento en la práctica
Cuando conectas tu server a Claude Code, esto es lo que sucede:
1. Claude Code (host) envía resources/list
2. Tu server responde con la lista de resources disponibles
3. Claude Code presenta los resources al modelo como contexto disponible
4. Cuando el modelo necesita datos, pide resources/read con un URI específico
5. Tu server ejecuta el handler y retorna los datos
En MCP Inspector, puedes ver este flujo en el tab Resources: la lista de resources disponibles y la capacidad de leer cada uno.
Resource vs Tool: cuándo usar cuál
Esta decisión la vas a tomar constantemente. Aquí hay una guía ampliada:
Usa Resource cuando:
✅ Los datos son de solo lectura
✅ Los parámetros caben en un URI (string, número, fecha)
✅ Quieres que el dato sea descubrible (aparece en resources/list)
✅ El dato es contextual — el modelo lo necesita como background
✅ No hay side effects de ningún tipo
Usa Tool cuando:
✅ La operación modifica estado
✅ Los parámetros son complejos (objetos anidados, arrays, booleanos combinados)
✅ Necesitas validación detallada de inputs con Zod
✅ La operación puede fallar y necesitas error handling granular
✅ Quieres que el modelo decida cuándo invocar (no solo exponer datos)
Casos grises
Leer un archivo por path → Resource template (file:///{path})
Buscar archivos con filtros complejos → Tool (search_files)
Configuración del sistema → Resource estático (config://app)
Cambiar configuración → Tool (update_config)
Últimos 10 logs → Resource estático (logs://recent)
Buscar en logs con regex → Tool (search_logs)
Regla práctica: Si los parámetros caben en un URI y no hay side effects, usa Resource. Si necesitas validación Zod o hay side effects, usa Tool.
Patrones avanzados
Patrón 1: Resource con datos agregados
server.resource(
"project-stats",
"stats://project/summary",
{
description: "Estadísticas del proyecto: archivos por tipo, tamaño total, líneas de código",
mimeType: "application/json",
},
async (uri) => {
const projectDir = process.cwd();
const stats: Record<string, { count: number; totalSize: number; totalLines: number }> = {};
let totalFiles = 0;
async function scanDir(dir: string): Promise<void> {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "build") continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await scanDir(fullPath);
} else {
totalFiles++;
const ext = path.extname(entry.name) || "(sin extensión)";
if (!stats[ext]) stats[ext] = { count: 0, totalSize: 0, totalLines: 0 };
const fileStat = await fs.stat(fullPath);
stats[ext].count++;
stats[ext].totalSize += fileStat.size;
try {
const content = await fs.readFile(fullPath, "utf-8");
stats[ext].totalLines += content.split("\n").length;
} catch {
// binary file
}
}
}
}
await scanDir(projectDir);
const formatted = Object.entries(stats)
.sort((a, b) => b[1].count - a[1].count)
.map(([ext, data]) => ({
extension: ext,
files: data.count,
totalSize: `${(data.totalSize / 1024).toFixed(1)} KB`,
totalLines: data.totalLines,
}));
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
projectDir,
totalFiles,
byExtension: formatted,
generatedAt: new Date().toISOString(),
}, null, 2),
}],
};
}
);
Patrón 2: Resource con contenido markdown
No todos los resources retornan JSON. Markdown es ideal para documentación y reportes:
server.resource(
"api-docs",
"docs://api/endpoints",
{
description: "Documentación de los endpoints disponibles de la API",
mimeType: "text/markdown",
},
async (uri) => {
const docs = `# API Endpoints
## Autenticación
### POST /auth/login
Autentica un usuario y retorna un token JWT.
**Body:**
\`\`\`json
{
"email": "user@example.com",
"password": "secreto"
}
\`\`\`
**Response 200:**
\`\`\`json
{
"token": "eyJ...",
"expiresIn": 3600
}
\`\`\`
## Usuarios
### GET /users
Retorna la lista de usuarios paginada.
**Query params:** \`page\`, \`limit\`, \`sort\`
### GET /users/:id
Retorna un usuario por su ID.
### POST /users
Crea un nuevo usuario. Requiere autenticación.
---
*Documentación generada automáticamente*
`;
return {
contents: [{
uri: uri.href,
mimeType: "text/markdown",
text: docs,
}],
};
}
);
Patrón 3: Resource que combina múltiples fuentes
server.resource(
"project-health",
"health://project/overview",
{
description: "Vista completa de la salud del proyecto: dependencias, seguridad, code quality",
mimeType: "application/json",
},
async (uri) => {
const projectDir = process.cwd();
const health: Record<string, unknown> = {};
try {
const lockfile = await fs.readFile(path.join(projectDir, "package-lock.json"), "utf-8");
const lock = JSON.parse(lockfile);
const depCount = Object.keys(lock.packages || {}).length;
health.dependencies = {
total: depCount,
lockfileExists: true,
};
} catch {
health.dependencies = { lockfileExists: false };
}
try {
const gitignore = await fs.readFile(path.join(projectDir, ".gitignore"), "utf-8");
health.git = {
gitignoreExists: true,
ignoresNodeModules: gitignore.includes("node_modules"),
ignoresBuild: gitignore.includes("build") || gitignore.includes("dist"),
ignoresEnv: gitignore.includes(".env"),
};
} catch {
health.git = { gitignoreExists: false };
}
try {
const tsconfig = await fs.readFile(path.join(projectDir, "tsconfig.json"), "utf-8");
const config = JSON.parse(tsconfig);
health.typescript = {
strictMode: config.compilerOptions?.strict === true,
target: config.compilerOptions?.target,
};
} catch {
health.typescript = { configured: false };
}
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(health, null, 2),
}],
};
}
);
Troubleshooting
"Resource no aparece en MCP Inspector"
Causa: El resource se registró después de server.connect().
Solución: Registra todos los resources antes de conectar el transport:
server.resource("...", "...", {}, async () => { ... }); // ← primero
await server.connect(transport); // ← después
"Resource template no resuelve parámetros"
Causa: El placeholder en el URI template no coincide con el nombre usado en params.
Solución:
// El placeholder {date} se mapea a params.date
new ResourceTemplate("logs://app/{date}", { list: undefined })
// En el handler:
async (uri, params) => {
const date = params.date as string; // ← mismo nombre que el placeholder
}
"Error: Cannot read property 'href' of undefined"
Causa: El handler recibe uri como URL, no como string.
Solución:
async (uri) => {
return {
contents: [{
uri: uri.href, // ← usar .href para convertir a string
text: "...",
}],
};
}
"Resource retorna pero los datos son viejos"
Causa: Los datos se calculan en el registro, no en la lectura.
Solución:
// ❌ Datos calculados una sola vez
const data = await getExpensiveData();
server.resource("...", "...", {}, async (uri) => {
return { contents: [{ uri: uri.href, text: JSON.stringify(data) }] };
});
// ✅ Datos calculados en cada lectura
server.resource("...", "...", {}, async (uri) => {
const data = await getExpensiveData(); // ← fresco cada vez
return { contents: [{ uri: uri.href, text: JSON.stringify(data) }] };
});
"MIME type causa que el contenido no se muestre bien"
Causa: El mimeType declarado no coincide con el formato real del contenido.
Solución:
// Si retornas JSON → "application/json"
// Si retornas texto plano → "text/plain"
// Si retornas markdown → "text/markdown"
// En caso de duda → "text/plain" siempre funciona
Ejercicios
Ejercicio 1: Resource estático de environment (Fácil)
Implementa un resource estático con URI env://app/info que retorne información del entorno: versión de Node.js, plataforma, directorio de trabajo, y variables de entorno que empiecen con APP_.
Ver solución
server.resource(
"env-info",
"env://app/info",
{
description: "Información del entorno de ejecución",
mimeType: "application/json",
},
async (uri) => {
const appEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("APP_") && value) {
appEnv[key] = value;
}
}
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
cwd: process.cwd(),
pid: process.pid,
appEnvVars: appEnv,
timestamp: new Date().toISOString(),
}, null, 2),
}],
};
}
);
Ejercicio 2: Resource template para dependencias (Medio)
Implementa un resource template con URI deps://package/{name} que retorne información de un paquete npm instalado (versión, descripción) leyendo su package.json desde node_modules.
Ver solución
server.resource(
"dependency-info",
new ResourceTemplate("deps://package/{name}", {
list: async () => {
try {
const pkgJson = await fs.readFile(
path.join(process.cwd(), "package.json"), "utf-8"
);
const pkg = JSON.parse(pkgJson);
const allDeps = {
...pkg.dependencies,
...pkg.devDependencies,
};
return Object.keys(allDeps).map(name => ({
uri: `deps://package/${name}`,
name: `${name} (${allDeps[name]})`,
description: `Información del paquete ${name}`,
}));
} catch {
return [];
}
},
}),
{
description: "Información detallada de un paquete npm instalado",
mimeType: "application/json",
},
async (uri, params) => {
const name = params.name as string;
const pkgPath = path.join(process.cwd(), "node_modules", name, "package.json");
try {
const content = await fs.readFile(pkgPath, "utf-8");
const pkg = JSON.parse(content);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
name: pkg.name,
version: pkg.version,
description: pkg.description,
license: pkg.license,
homepage: pkg.homepage,
repository: pkg.repository,
main: pkg.main,
dependencies: Object.keys(pkg.dependencies || {}).length,
}, null, 2),
}],
};
} catch {
throw new Error(`Paquete '${name}' no encontrado en node_modules`);
}
}
);
Ejercicio 3: Resource con múltiples contenidos (Medio)
Implementa un resource que retorne el contenido de los 3 archivos más recientes de un directorio. Usa el array contents para retornar múltiples archivos en un solo resource.
Ver solución
server.resource(
"recent-files",
"files://recent/top3",
{
description: "Los 3 archivos más recientemente modificados del proyecto",
mimeType: "text/plain",
},
async (uri) => {
const projectDir = process.cwd();
const allFiles: Array<{ path: string; mtime: Date }> = [];
async function collectFiles(dir: string): Promise<void> {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "build") continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await collectFiles(fullPath);
} else {
const stats = await fs.stat(fullPath);
allFiles.push({ path: fullPath, mtime: stats.mtime });
}
}
}
await collectFiles(projectDir);
const top3 = allFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()).slice(0, 3);
const contents = await Promise.all(
top3.map(async (file) => {
const content = await fs.readFile(file.path, "utf-8").catch(() => "(binary file)");
const relativePath = path.relative(projectDir, file.path);
return {
uri: `file:///${relativePath}`,
mimeType: "text/plain",
text: `// === ${relativePath} (modified: ${file.mtime.toISOString()}) ===\n${content}`,
};
})
);
return { contents };
}
);
Ejercicio 4: Resource template con validación (Difícil)
Implementa un resource template metrics://cpu/{period} donde period puede ser "1min", "5min", o "15min". Retorna el load average correspondiente del sistema.
Ver solución
server.resource(
"cpu-metrics",
new ResourceTemplate("metrics://cpu/{period}", {
list: async () => [
{ uri: "metrics://cpu/1min", name: "CPU Load (1 min)", description: "Load average del último minuto" },
{ uri: "metrics://cpu/5min", name: "CPU Load (5 min)", description: "Load average de 5 minutos" },
{ uri: "metrics://cpu/15min", name: "CPU Load (15 min)", description: "Load average de 15 minutos" },
],
}),
{
description: "Load average de CPU por período: 1min, 5min, o 15min",
mimeType: "application/json",
},
async (uri, params) => {
const period = params.period as string;
const loadAvg = os.loadavg();
const periodMap: Record<string, { index: number; label: string }> = {
"1min": { index: 0, label: "1 minuto" },
"5min": { index: 1, label: "5 minutos" },
"15min": { index: 2, label: "15 minutos" },
};
const config = periodMap[period];
if (!config) {
throw new Error(`Período inválido: ${period}. Usa: 1min, 5min, o 15min`);
}
const cpuCount = os.cpus().length;
const load = loadAvg[config.index];
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
period: config.label,
loadAverage: load.toFixed(2),
cpuCores: cpuCount,
utilizationPercent: ((load / cpuCount) * 100).toFixed(1) + "%",
status: load / cpuCount > 0.8 ? "HIGH" : load / cpuCount > 0.5 ? "MODERATE" : "NORMAL",
timestamp: new Date().toISOString(),
}, null, 2),
}],
};
}
);
Resumen
En esta cápsula aprendiste:
- Resources estáticos tienen URIs fijos y son ideales para datos que siempre existen (estado del sistema, configuración)
- Resource templates (
ResourceTemplate) permiten URIs con parámetros dinámicos ({id},{date},{path}) - El callback
listen resource templates permite que el cliente descubra qué instancias concretas existen resources/listyresources/readson los dos métodos del protocolo para descubrimiento y lectura- Resources retornan datos con mimeType declarado —
application/json,text/plain,text/markdown - La regla de decisión: parámetros simples + solo lectura → Resource; parámetros complejos o side effects → Tool
- Resources se registran antes de conectar el transport
Recursos adicionales
- MCP Specification — Resources - Especificación oficial
- URI Template RFC 6570 - Estándar de URI templates
- MCP TypeScript SDK — ResourceTemplate - API de ResourceTemplate
- MIME Types Reference - Lista de MIME types
- Node.js os module - API del módulo os usado en los ejemplos
- MCP Inspector - Para probar tus resources visualmente
Siguiente cápsula: Transports — cómo tu server se comunica con el host. stdio para desarrollo local, HTTP/SSE para servidores remotos, y Streamable HTTP como el futuro del protocolo.