Módulo 3: Tres Primitivas — Resources, Tools, Prompts
Resources: Datos Contextuales que el Modelo Puede Leer
Resources: Datos Contextuales que el Modelo Puede Leer
Descripción de la cápsula
La primera primitiva de MCP es la más simple de entender: Resources. Un resource es un dato que el MCP server expone para que el modelo lo pueda leer. Piensa en archivos, registros de base de datos, respuestas de APIs, configuraciones — cualquier dato que el modelo necesite como contexto para ayudarte mejor.
La característica clave de un resource es que es pull-based y sin side effects. El cliente pide, el server responde con datos. Nada se modifica. Es como consultar un catálogo: miras lo que hay disponible, pides lo que necesitas, y recibes la información. El catálogo no cambia porque lo consultaste.
En esta cápsula vas a entender qué son los resources, cómo se identifican con URIs, cómo se implementan en TypeScript y Python, y cuándo elegirlos sobre tools o prompts.
¿Qué es un Resource?
Definición formal
Un Resource en MCP es una unidad de datos identificada por un URI que el server expone al cliente. El cliente puede:
- Listar los resources disponibles (
resources/list) - Leer un resource específico (
resources/read) - Suscribirse a cambios en un resource (
resources/subscribe)
Definición práctica
Un resource es la respuesta a: "¿Qué datos puede ver el modelo a través de este server?"
Ejemplos de resources:
├── file:///project/src/main.ts → Contenido de un archivo
├── db://users/123 → Record de base de datos
├── api://weather/madrid → Respuesta de API externa
├── config://app/settings → Configuración de la app
├── logs://app/2024-01-15 → Logs de una fecha específica
└── metrics://server/cpu → Métricas del sistema
Características clave
| Característica | Detalle |
|---|---|
| Identificación | Cada resource tiene un URI único |
| Solo lectura | No modifica estado — solo retorna datos |
| Pull-based | El cliente pide, el server responde |
| Tipado | Cada resource declara su MIME type (text/plain, application/json, etc.) |
| Listable | El cliente puede descubrir qué resources están disponibles |
| Suscribible | Opcionalmente, el cliente puede recibir notificaciones de cambios |
Anatomía de un Resource
Estructura de datos
Cada resource que el server expone tiene esta estructura:
interface Resource {
uri: string; // Identificador único (e.g., "file:///path/to/file")
name: string; // Nombre legible para humanos
description?: string; // Descripción opcional
mimeType?: string; // Tipo de contenido (e.g., "text/plain", "application/json")
}
Cuando el cliente lee un resource, recibe:
interface ResourceContent {
uri: string; // El URI del resource leído
mimeType?: string; // Tipo de contenido
text?: string; // Contenido de texto
blob?: string; // Contenido binario (base64)
}
URIs: la identidad del resource
Los URIs son el sistema de direccionamiento de resources. Cada URI identifica un resource único:
Esquema Autoridad Path
│ │ │
▼ ▼ ▼
file:// /project /src/main.ts
db:// users /123
api:// weather /madrid
Puedes usar cualquier esquema de URI que tenga sentido para tu server. Los más comunes:
file:// → Archivos del filesystem
db:// → Records de base de datos
api:// → Datos de APIs externas
config:// → Configuraciones
logs:// → Logs del sistema
metrics:// → Métricas y estadísticas
Resource Templates: URIs dinámicos
Además de resources estáticos (con URIs fijos), MCP soporta resource templates — URIs con parámetros que el cliente puede rellenar.
Diferencia clave
Resource estático:
uri: "config://app/settings"
→ Siempre retorna la misma configuración
Resource template:
uriTemplate: "db://users/{id}"
→ El cliente rellena {id} para leer un usuario específico
Cuándo usar templates
- Estático: Cuando hay un número finito y conocido de resources (configuración, estado del server, lista fija)
- Template: Cuando los resources son dinámicos o hay muchos posibles (usuarios por ID, archivos por path, logs por fecha)
Estructura de un template
interface ResourceTemplate {
uriTemplate: string; // URI con placeholders: "db://users/{id}"
name: string; // Nombre legible
description?: string; // Descripción
mimeType?: string; // Tipo de contenido
}
Implementación en TypeScript
Setup básico
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
Resource estático
server.resource(
"config-app",
"config://app/settings",
{
description: "Configuración actual de la aplicación",
mimeType: "application/json",
},
async (uri) => {
const config = {
appName: "My App",
version: "2.1.0",
environment: "development",
debug: true,
maxConnections: 100,
};
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(config, null, 2),
},
],
};
}
);
Resource template (dinámico)
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
server.resource(
"user-by-id",
new ResourceTemplate("db://users/{id}", {
list: async () => {
const users = await getUsers();
return users.map((u) => ({
uri: `db://users/${u.id}`,
name: `Usuario: ${u.name}`,
description: `Perfil de ${u.name}`,
}));
},
}),
{
description: "Perfil de un usuario por su ID",
mimeType: "application/json",
},
async (uri, params) => {
const id = params.id;
const user = await getUserById(id);
if (!user) {
throw new Error(`Usuario ${id} no encontrado`);
}
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(user, null, 2),
},
],
};
}
);
Resource que lista archivos
import * as fs from "fs/promises";
import * as path from "path";
server.resource(
"project-files",
"file:///project/structure",
{
description: "Lista de archivos del proyecto",
mimeType: "application/json",
},
async (uri) => {
const projectDir = "/path/to/your/project";
const files = await fs.readdir(projectDir, { recursive: true });
const structure = files.map((file) => ({
name: file,
path: path.join(projectDir, file.toString()),
}));
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(structure, null, 2),
},
],
};
}
);
Implementación en Python
Setup básico
from mcp.server.fastmcp import FastMCP
server = FastMCP("my-server")
Resource estático
@server.resource("config://app/settings")
async def get_config() -> str:
"""Configuración actual de la aplicación."""
import json
config = {
"appName": "My App",
"version": "2.1.0",
"environment": "development",
"debug": True,
"maxConnections": 100,
}
return json.dumps(config, indent=2)
Resource template (dinámico)
@server.resource("db://users/{user_id}")
async def get_user(user_id: str) -> str:
"""Perfil de un usuario por su ID."""
import json
user = await get_user_by_id(user_id)
if not user:
raise ValueError(f"Usuario {user_id} no encontrado")
return json.dumps(user, indent=2)
Resource que lista archivos
import os
import json
@server.resource("file:///project/structure")
async def list_files() -> str:
"""Lista de archivos del proyecto."""
project_dir = "/path/to/your/project"
files = []
for root, dirs, filenames in os.walk(project_dir):
for f in filenames:
full_path = os.path.join(root, f)
files.append({
"name": f,
"path": full_path,
"size": os.path.getsize(full_path),
})
return json.dumps(files, indent=2)
Patrones comunes de Resources
Patrón 1: Estado actual del sistema
server.resource(
"system-status",
"system://status",
{ description: "Estado actual del sistema", mimeType: "application/json" },
async (uri) => {
const status = {
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString(),
version: "1.0.0",
};
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(status, null, 2),
}],
};
}
);
Patrón 2: Datos agregados
server.resource(
"sales-summary",
"analytics://sales/summary",
{ description: "Resumen de ventas del mes", mimeType: "application/json" },
async (uri) => {
const sales = await db.query("SELECT SUM(total), COUNT(*) FROM sales WHERE month = $1", [currentMonth]);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
month: currentMonth,
totalSales: sales.sum,
transactionCount: sales.count,
}, null, 2),
}],
};
}
);
Patrón 3: Contenido de texto enriquecido
server.resource(
"api-documentation",
"docs://api/endpoints",
{ description: "Documentación de endpoints de la API", mimeType: "text/markdown" },
async (uri) => {
const docs = `# API Endpoints
## GET /users
Retorna lista de usuarios.
## POST /users
Crea un nuevo usuario.
- Body: { "name": string, "email": string }
## GET /users/:id
Retorna un usuario por ID.
`;
return {
contents: [{
uri: uri.href,
mimeType: "text/markdown",
text: docs,
}],
};
}
);
Cuándo usar Resources vs Tools
Esta es una decisión de diseño que tomarás constantemente:
| Escenario | Resource | Tool |
|---|---|---|
| Leer configuración actual | ✅ | ❌ |
| Modificar configuración | ❌ | ✅ |
| Consultar datos de DB | ✅ | ❌ |
| Insertar datos en DB | ❌ | ✅ |
| Leer un archivo | ✅ | ❌ |
| Escribir un archivo | ❌ | ✅ |
| Obtener métricas del sistema | ✅ | ❌ |
| Reiniciar un servicio | ❌ | ✅ |
Regla simple: Si la operación es idempotente y sin side effects → Resource. Si modifica estado → Tool.
Caso gris: ¿y las queries complejas?
"Buscar usuarios con más de 10 compras en el último mes"
Esto es solo lectura (no modifica nada), pero requiere parámetros complejos. Puedes hacerlo de dos formas:
- Resource template:
db://users/active/{month}— si los parámetros son simples - Tool de búsqueda: Si necesitas parámetros complejos con validación, un tool puede ser mejor
En la práctica, la decisión depende de la complejidad de los parámetros. Si caben en un URI template, usa resource. Si no, considera un tool de solo lectura.
Suscripciones: Resources en tiempo real
MCP soporta suscripciones a resources — el cliente se suscribe a cambios y recibe notificaciones cuando el resource se actualiza:
// El server notifica cambios
server.notification({
method: "notifications/resources/updated",
params: {
uri: "metrics://server/cpu",
},
});
Esto es útil para:
- Métricas en tiempo real
- Archivos que cambian frecuentemente
- Estado del sistema que se actualiza
Las suscripciones son opcionales y avanzadas — no las necesitas para el mini-proyecto de este módulo. Las verás en detalle en el módulo 4.
Troubleshooting
"El resource no aparece en la lista"
Causa: El resource no se registró correctamente en el server.
Solución:
// Verifica que la función resource() se llama antes de conectar el server
server.resource("my-resource", "my://uri", { ... }, async (uri) => { ... });
// El registro debe ocurrir ANTES de iniciar el transport
const transport = new StdioServerTransport();
await server.connect(transport);
"Error: URI no encontrado"
Causa: El cliente está pidiendo un URI que no coincide con ningún resource registrado.
Solución:
# Verifica los URIs registrados con MCP Inspector
npx @modelcontextprotocol/inspector
# Asegúrate de que el URI del request coincide exactamente
# con el URI registrado (incluyendo esquema y path)
"El resource retorna datos vacíos"
Causa: La función async que genera los datos tiene un error o retorna undefined.
Solución:
// Agrega logging para debug
server.resource("debug-resource", "debug://test", {}, async (uri) => {
console.error("Generando resource para:", uri.href);
const data = await getData();
console.error("Datos obtenidos:", JSON.stringify(data));
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(data, null, 2),
}],
};
});
"Resource template no resuelve los parámetros"
Causa: Los placeholders en el URI template no coinciden con los parámetros que recibe la función.
Solución:
// El placeholder {id} se mapea automáticamente al parámetro `params.id`
server.resource(
"user",
new ResourceTemplate("db://users/{id}", { list: undefined }),
{},
async (uri, params) => {
console.error("Params recibidos:", params);
const id = params.id; // ← debe coincidir con {id} en el template
// ...
}
);
"MIME type incorrecto causa problemas de rendering"
Causa: Declaraste un mimeType que no coincide con el formato real de los datos.
Solución:
// Si retornas JSON, usa application/json
mimeType: "application/json"
// Si retornas texto plano, usa text/plain
mimeType: "text/plain"
// Si retornas markdown, usa text/markdown
mimeType: "text/markdown"
// Si no estás seguro, text/plain es siempre safe
Ejercicios
Ejercicio 1: Identificar Resources (Fácil)
De la siguiente lista, identifica cuáles serían Resources y cuáles no:
- Leer el README.md de un proyecto
- Enviar un email
- Obtener la lista de issues abiertos de GitHub
- Crear un nuevo issue en GitHub
- Consultar el precio actual de Bitcoin
- Reiniciar un container de Docker
Ver solución
- ✅ Resource — Solo lectura de un archivo
- ❌ No es Resource — Tiene side effects (envía un email) → Tool
- ✅ Resource — Solo lectura de datos de GitHub
- ❌ No es Resource — Modifica estado (crea un issue) → Tool
- ✅ Resource — Solo lectura de datos de API externa
- ❌ No es Resource — Modifica estado (reinicia container) → Tool
Regla: Si modifica estado o tiene side effects → Tool. Si solo lee datos → Resource.
Ejercicio 2: Diseñar URIs (Medio)
Diseña URIs para los siguientes resources de un MCP server para un equipo de desarrollo:
- La configuración del proyecto (package.json)
- Las variables de entorno actuales
- Un log específico por fecha
- Un commit de Git por hash
- Las métricas de uso de memoria del server
Ver solución
1. config://project/package-json
→ Esquema: config, path: project/package-json
2. env://app/variables
→ Esquema: env, path: app/variables
3. logs://app/{date}
→ Resource template con placeholder de fecha
→ Ejemplo: logs://app/2024-01-15
4. git://commits/{hash}
→ Resource template con placeholder de hash
→ Ejemplo: git://commits/abc123f
5. metrics://server/memory
→ Esquema: metrics, path: server/memory
Los URIs 3 y 4 son resource templates porque tienen parámetros dinámicos. Los demás son resources estáticos.
Ejercicio 3: Implementar un Resource en TypeScript (Medio)
Implementa un resource que exponga las variables de entorno de la aplicación (solo las que empiecen con "APP_"):
Ver solución
server.resource(
"env-vars",
"env://app/variables",
{
description: "Variables de entorno de la aplicación (APP_*)",
mimeType: "application/json",
},
async (uri) => {
const appEnvVars: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("APP_") && value !== undefined) {
appEnvVars[key] = value;
}
}
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(appEnvVars, null, 2),
},
],
};
}
);
Filtramos solo variables con prefijo "APP_" por seguridad — no queremos exponer tokens, passwords, o variables del sistema.
Ejercicio 4: Implementar un Resource Template en Python (Medio)
Implementa un resource template en Python que retorne la información de un producto por su SKU:
Ver solución
import json
PRODUCTS = {
"SKU001": {"name": "Laptop Pro", "price": 1299.99, "stock": 15},
"SKU002": {"name": "Ergonomic Mouse", "price": 49.99, "stock": 230},
"SKU003": {"name": "4K Monitor", "price": 599.99, "stock": 42},
}
@server.resource("products://catalog/{sku}")
async def get_product(sku: str) -> str:
"""Información de un producto por su SKU."""
product = PRODUCTS.get(sku)
if not product:
raise ValueError(f"Producto con SKU '{sku}' no encontrado")
return json.dumps({
"sku": sku,
**product,
}, indent=2)
El decorador @server.resource con {sku} crea automáticamente un resource template. FastMCP extrae el parámetro sku del URI y lo pasa a la función.
Ejercicio 5: Resource con datos agregados (Difícil)
Implementa un resource en TypeScript que retorne un resumen estadístico de un directorio: número de archivos por extensión, tamaño total, y archivo más grande.
Ver solución
import * as fs from "fs/promises";
import * as path from "path";
server.resource(
"dir-stats",
new ResourceTemplate("stats://directory/{dirPath}", { list: undefined }),
{
description: "Estadísticas de un directorio",
mimeType: "application/json",
},
async (uri, params) => {
const dirPath = decodeURIComponent(params.dirPath as string);
const files = await fs.readdir(dirPath);
const extensions: Record<string, number> = {};
let totalSize = 0;
let largestFile = { name: "", size: 0 };
for (const file of files) {
const filePath = path.join(dirPath, file);
const stat = await fs.stat(filePath);
if (stat.isFile()) {
const ext = path.extname(file) || "(no extension)";
extensions[ext] = (extensions[ext] || 0) + 1;
totalSize += stat.size;
if (stat.size > largestFile.size) {
largestFile = { name: file, size: stat.size };
}
}
}
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
directory: dirPath,
totalFiles: files.length,
totalSize: `${(totalSize / 1024).toFixed(2)} KB`,
largestFile,
extensions,
}, null, 2),
}],
};
}
);
Este resource combina operaciones de filesystem para generar un resumen útil. Nota que sigue siendo solo lectura — no modifica nada.
Ejercicio 6: Decidir Resource vs Tool (Difícil)
Para cada escenario, decide si usarías un Resource, un Tool, o ambos. Justifica tu decisión:
- Un endpoint que retorna los últimos 10 commits de un repo
- Un endpoint que ejecuta
git pullen un repo - Un endpoint que muestra el diff entre dos branches
- Un endpoint que busca archivos por contenido (grep)
- Un endpoint que formatea código con Prettier
Ver solución
-
Resource — Solo lectura de datos (historial de commits). URI:
git://repo/commits/recent -
Tool — Tiene side effects (modifica el estado del repo local). Los tools son para acciones que cambian algo.
-
Resource template — Solo lectura (el diff es un dato derivado, no modifica nada). URI:
git://repo/diff/{branch1}/{branch2} -
Caso gris → Tool — Aunque es solo lectura, grep requiere parámetros complejos (patrón de búsqueda, directorio, opciones) que no caben bien en un URI template. Un tool con schema validado es más apropiado.
-
Tool — Modifica archivos (reescribe el código formateado). Definitivamente un tool con side effects.
La clave está en: ¿modifica estado? → Tool. ¿Solo lee datos con parámetros simples? → Resource. ¿Solo lee datos pero con parámetros complejos? → Evalúa ambas opciones.
Resumen
En esta cápsula aprendiste:
- Resources son datos contextuales que el MCP server expone para lectura
- Se identifican con URIs — el sistema de direccionamiento de MCP
- Pueden ser estáticos (URI fijo) o dinámicos (URI template con parámetros)
- Son pull-based — el cliente pide, el server responde
- Sin side effects — leer un resource nunca modifica estado
- Soportan MIME types para indicar el formato de los datos
- Opcionalmente soportan suscripciones para notificaciones de cambios
- La regla de decisión: si solo lee datos → Resource; si modifica estado → Tool
Próxima cápsula: Tools — funciones ejecutables que el modelo puede invocar. La primitiva más poderosa y la que usarás con más frecuencia.
Recursos adicionales
- MCP Specification — Resources - Especificación oficial de Resources
- MCP TypeScript SDK — Resources - Implementación de Resources en TypeScript
- MCP Python SDK — Resources - Implementación de Resources en Python
- URI Template RFC 6570 - Especificación de URI templates
- MIME Types Reference - Lista de MIME types comunes
- Filesystem MCP Server Source - Ejemplo real de Resources en un MCP server