Duración: 30 minutos (15 min demostración + 15 min práctica)
Tipo: Demostración en vivo seguida de ejercicio práctico hands-on
Objetivo: Crear un servidor MCP funcional desde cero y ejecutarlo
- [Demostración] Crear un proyecto de servidor MCP básico en C# / .NET 10.0
- [Demostración] Implementar el método
initializepara handshake - [Demostración] Exponer recursos estáticos (
resources/listyresources/read) - [Todos] Probar el servidor con solicitudes HTTP directas
- [Práctica] Extender el servidor con un segundo recurso (productos)
flowchart TB
A["Program.cs<br/>(Entry Point)"]
B["McpServerBase<br/>(Shared Library)"]
C["DemoServer<br/>(Implementation)"]
D["data/customers.json<br/>(Static Data)"]
A --> C
C -.inherits.-> B
C --> D
style A fill:#bbdefb
style B fill:#c8e6c9
style C fill:#fff9c4
style D fill:#ffccbc
src/McpWorkshop.Servers/
└── DemoServer/
├── Program.cs # ASP.NET Core minimal API
├── DemoServer.csproj # Proyecto .NET
└── Models/
└── Customer.cs # Modelo de datos
💬 Instructor: "Usamos
dotnet new webporque es la plantilla más ligera de ASP.NET Core. No necesitamos MVC, solo un endpoint HTTP simple."
1.1 Crear estructura
# Crear proyecto web API
cd src/McpWorkshop.Servers
dotnet new web -n Exercise1Server -f net10.0
# Agregar referencia a la librería compartida
cd Exercise1Server
dotnet add reference ../../McpWorkshop.Shared/McpWorkshop.Shared.csproj
# Agregar a solución y verificar compilación
cd ../../..
dotnet sln add src/McpWorkshop.Servers/Exercise1Server/Exercise1Server.csproj
dotnet build1.2 Crear carpetas
# Crear proyecto web API
cd src/McpWorkshop.Servers/Exercise1Server
mkdir Models✅ Checkpoint: Debe compilar sin errores.
💬 Instructor: "Este es un modelo simple de cliente. En un sistema real vendría de SQL Server o Cosmos DB. Hoy usamos JSON estático para simplificar."
Archivo: src/McpWorkshop.Servers/Exercise1Server/Models/Customer.cs
namespace Exercise1Server.Models;
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public DateTime Created { get; set; }
}Archivo: src/McpWorkshop.Servers/Exercise1Server/Models/Product.cs
namespace Exercise1Server.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public bool InStock { get; set; }
}✅ Checkpoint: Dos modelos creados.
Archivo: src/McpWorkshop.Servers/Exercise1Server/Program.cs
💬 Instructor - Parte 1: "Configuramos los servicios con DI. Inyectamos el logger estructurado y la configuración del servidor desde nuestra librería compartida."
using System.Text.Json;
using Exercise1Server.Models;
using McpWorkshop.Shared.Logging;
using McpWorkshop.Shared.Mcp;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
// Configurar servicios
builder.Services.AddSingleton<IStructuredLogger, StructuredLogger>();
builder.Services.Configure<McpWorkshop.Shared.Configuration.WorkshopSettings>(options =>
{
options.Server.Name = "Exercise1Server";
options.Server.Version = "1.0.0";
options.Server.ProtocolVersion = "2024-11-05";
options.Server.Port = 5001;
});
var app = builder.Build();
// Variables para almacenar los datos cargados durante initialize
List<Customer>? customers = null;
List<Product>? products = null;
// Health check endpoint
app.MapGet("/", (IOptions<McpWorkshop.Shared.Configuration.WorkshopSettings> settings) => Results.Ok(new
{
status = "healthy",
server = settings.Value.Server.Name,
version = settings.Value.Server.Version,
timestamp = DateTime.UtcNow
}));
// Endpoint principal MCP
app.MapPost("/mcp", async (
JsonRpcRequest request,
IStructuredLogger logger,
IOptions<McpWorkshop.Shared.Configuration.WorkshopSettings> settings) =>
{
var requestId = request.Id?.ToString() ?? "unknown";
IDictionary<string, object>? paramsDict = null;
if (request.Params != null)
{
paramsDict = JsonSerializer.Deserialize<IDictionary<string, object>>(JsonSerializer.Serialize(request.Params));
}
try
{
var response = request.Method switch
{
"initialize" => HandleInitialize(request.Id, settings, ref customers, ref products),
"resources/list" => HandleResourcesList(request.Id),
"resources/read" => HandleResourcesRead(request.Id, paramsDict, customers, products),
_ => CreateErrorResponse(-32601, "Method not found", null, request.Id)
};
logger.LogResponse(request.Method, requestId, 200, 0);
return Results.Ok(response);
}
catch (Exception ex)
{
logger.LogError(request.Method, requestId, ex);
return Results.Ok(CreateErrorResponse(-32603, "Internal error", ex.Message, request.Id));
}
});
await app.RunAsync("http://localhost:5001");
// Métodos Helper
static JsonRpcResponse HandleInitialize(
object? requestId,
IOptions<McpWorkshop.Shared.Configuration.WorkshopSettings> settings,
ref List<Customer>? customers,
ref List<Product>? products)
{
// Cargar datos de muestra durante la inicialización
customers = LoadData<Customer>("../../../data/customers.json");
products = LoadData<Product>("../../../data/products.json");
return new JsonRpcResponse
{
JsonRpc = "2.0",
Result = new
{
protocolVersion = "2024-11-05",
capabilities = new
{
resources = new { },
tools = new { }
},
serverInfo = new
{
name = settings.Value.Server.Name,
version = settings.Value.Server.Version
}
},
Id = requestId
};
}
static JsonRpcResponse HandleResourcesList(object? requestId)
{
return new JsonRpcResponse
{
JsonRpc = "2.0",
Result = new
{
resources = new[]
{
new
{
uri = "mcp://customers",
name = "Customers Database",
description = "Lista completa de clientes registrados",
mimeType = "application/json"
},
new
{
uri = "mcp://products",
name = "Products Catalog",
description = "Catálogo de productos disponibles",
mimeType = "application/json"
}
}
},
Id = requestId
};
}
static JsonRpcResponse HandleResourcesRead(
object? requestId,
IDictionary<string, object>? parameters,
List<Customer>? customers,
List<Product>? products)
{
// Validar que los datos estén inicializados
if (customers == null || products == null)
{
throw new InvalidOperationException("Los datos no han sido inicializados. Debe llamar a 'initialize' primero.");
}
// Parsear el URI del recurso
string? uri = null;
if (parameters != null && parameters.TryGetValue("uri", out var uriValue))
{
if (uriValue is JsonElement jsonElement)
{
uri = jsonElement.GetString();
}
else if (uriValue is string strValue)
{
uri = strValue;
}
}
var content = uri switch
{
"mcp://customers" => JsonSerializer.Serialize(customers, new JsonSerializerOptions { WriteIndented = true }),
"mcp://products" => JsonSerializer.Serialize(products, new JsonSerializerOptions { WriteIndented = true }),
_ => throw new ArgumentException($"Unknown resource URI: {uri}")
};
return new JsonRpcResponse
{
JsonRpc = "2.0",
Result = new
{
contents = new[]
{
new
{
uri,
mimeType = "application/json",
text = content
}
}
},
Id = requestId
};
}
static JsonRpcResponse CreateErrorResponse(int code, string message, object? data, object? id)
{
return new JsonRpcResponse
{
JsonRpc = "2.0",
Error = new JsonRpcError
{
Code = code,
Message = message,
Data = data
},
Id = id
};
}
static List<T> LoadData<T>(string path)
{
var json = File.ReadAllText(path);
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<List<T>>(json, options) ?? new List<T>();
}💬 Instructor - Resumen:
- "Un endpoint
/mcprecibe todas las solicitudes JSON-RPC"- "Usamos pattern matching para rutear a los handlers"
- "Initialize negocia capabilities, list muestra recursos, read devuelve contenido"
- "Los datos vienen de JSON estático - en producción serían consultas a BD"
✅ Checkpoint: El código compila sin errores.
💬 Instructor: "El servidor correrá en puerto 5001. Ahora todos van a probarlo con solicitudes HTTP."
cd src/McpWorkshop.Servers/Exercise1Server
dotnet runSalida esperada:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\code\workshop-mcp\src\McpWorkshop.Servers\Exercise1Server
Antes de probar MCP, verifica que el servidor responde:
Invoke-WebRequest -Uri "http://localhost:5001" -Method GETSalida esperada:
{
"status": "healthy",
"server": "Exercise1Server",
"version": "1.0.0",
"timestamp": "2024-11-22T10:30:00Z"
}💬 Instructor: "Abran una segunda terminal y ejecuten esto todos juntos"
$body = @{
jsonrpc = "2.0"
method = "initialize"
params = @{
protocolVersion = "2024-11-05"
capabilities = @{}
clientInfo = @{ name = "WorkshopClient"; version = "1.0.0" }
}
id = "init-001"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:5001/mcp" `
-Method POST `
-Body $body `
-ContentType "application/json"✅ Debe devolver: serverInfo con nombre "Exercise1Server" y capabilities.
💬 Instructor: "¡Perfecto! El servidor respondió con su información. Ahora sabemos que habla MCP 2024-11-05."
$body = @{
jsonrpc = "2.0"
method = "resources/list"
params = @{}
id = "list-001"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:5001/mcp" `
-Method POST `
-Body $body `
-ContentType "application/json"✅ Debe devolver: Array con 2 recursos (mcp://customers y mcp://products).
💬 Instructor: "Perfecto. El servidor lista ambos recursos. Ahora vamos a leer cada uno."
$body = @{
jsonrpc = "2.0"
method = "resources/read"
params = @{ uri = "mcp://customers" }
id = "read-001"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:5001/mcp" `
-Method POST `
-Body $body `
-ContentType "application/json"✅ Debe devolver: JSON con array de clientes.
$body = @{
jsonrpc = "2.0"
method = "resources/read"
params = @{ uri = "mcp://products" }
id = "read-002"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:5001/mcp" `
-Method POST `
-Body $body `
-ContentType "application/json"✅ Debe devolver: JSON con array de productos.
💬 Instructor: "¡Excelente! Este es el flujo completo MCP: initialize → list → read. Así funciona el protocolo."
sequenceDiagram
participant C as Cliente (Terminal)
participant S as Exercise1Server
participant D as Data Files
Note over C,S: 1. Initialize
C->>S: POST /mcp<br/>{method: "initialize"}
S-->>C: {serverInfo, capabilities}
Note over C,S: 2. Resources/List
C->>S: POST /mcp<br/>{method: "resources/list"}
S-->>C: {resources: [customers, products]}
Note over C,S: 3. Resources/Read (Customers)
C->>S: POST /mcp<br/>{method: "resources/read", uri: "mcp://customers"}
S->>D: Leer customers.json
D-->>S: Array de clientes
S-->>C: {contents: [...]}
Note over C,S: 4. Resources/Read (Products)
C->>S: POST /mcp<br/>{method: "resources/read", uri: "mcp://products"}
S->>D: Leer products.json
D-->>S: Array de productos
S-->>C: {contents: [...]}
Cada mensaje tiene:
- ✅
jsonrpc: "2.0"- Identificador de protocolo - ✅
method- Qué operación ejecutar - ✅
params- Parámetros de entrada - ✅
id- Para correlacionar request/response
Cliente envía: Servidor responde:
{ {
"method": "...", "result": {...},
"params": {...}, "id": "..."
"id": "..." }
}
El cliente y servidor acuerdan qué funcionalidades soportan:
- Cliente dice: "Puedo recibir notificaciones"
- Servidor dice: "Tengo recursos y herramientas"
mcp://customers
mcp://products
mcp://orders
Esquema de URI personalizado para identificar recursos de forma única.
Has completado el ejercicio exitosamente si:
- El servidor compila sin errores
- El servidor se ejecuta en
http://localhost:5001 -
initializedevuelve serverInfo correcto -
resources/listmuestra 2 recursos (customers y products) -
resources/readdevuelve datos de customers -
resources/readdevuelve datos de products
¡Si todos los checkboxes están marcados, lo lograste! 🎉
# Ver qué proceso usa el puerto
netstat -ano | findstr :5001
# Cambiar puerto en Program.cs a 5002
app.Run("http://localhost:5002");
# Y actualizar URLs de prueba# Verificar que el archivo existe (incluido en el repositorio)
Get-Item data/customers.json # Debe existir
# Si no existe, verifica que clonaste el repositorio correctamente
# Ajustar ruta en LoadData si es necesario
var customers = LoadData<Customer>("../../../../data/customers.json");# Usar -Depth 10 en ConvertTo-Json
$body | ConvertTo-Json -Depth 10# Verificar referencia
dotnet list reference # Debe mostrar McpWorkshop.Shared
# Si no está, agrégala
dotnet add reference ../../McpWorkshop.Shared/McpWorkshop.Shared.csproj- Configuración de servicios con DI (Dependency Injection)
- Registro de logger y settings
- ASP.NET Core Minimal API
- Pattern matching con
switchexpressions - Deserialización de parámetros dinámicos
- Generación de respuestas estructuradas
- URIs como identificadores (
mcp://resource-name) - Listado dinámico de recursos disponibles
- Lectura de contenido desde fuentes locales (JSON)
- Códigos de error estándar JSON-RPC (-32601, -32603)
- Try-catch para excepciones
- Logging estructurado
Si terminaste antes de los 30 minutos, prueba estas extensiones:
- Crea
Models/Order.cs - Carga los datos:
var orders = LoadData<Order>("../../../data/orders.json"); - Agrega el recurso en
HandleResourcesList - Agrega el caso en
HandleResourcesRead
Modifica HandleResourcesRead para aceptar parámetros opcionales:
var country = paramsDict?["country"]?.ToString();
if (uri == "mcp://customers" && !string.IsNullOrEmpty(country))
{
var filtered = customers.Where(c => c.Country == country).ToList();
content = JsonSerializer.Serialize(filtered, new JsonSerializerOptions { WriteIndented = true });
}Result = new
{
contents = new[] { ... },
metadata = new
{
timestamp = DateTime.UtcNow,
count = customers.Count
}
}- ✅ Servidor MCP funcional en ~150 líneas de C#
- ✅ Tres métodos MCP:
initialize,resources/list,resources/read - ✅ Dos recursos estáticos: clientes y productos
- ✅ Endpoint HTTP único (
/mcp) para todas las operaciones - ✅ Integración con logging estructurado
Bloque 4 (Ejercicio 2): Consultas Paramétricas con Herramientas (20 min)
Aprenderás a:
- Implementar herramientas invocables (
tools/call) - Validar parámetros de entrada con JSON Schema
- Ejecutar búsquedas y filtros dinámicos
- Combinar múltiples fuentes de datos
- Documentación MCP: https://modelcontextprotocol.io/specification/2025-06-18
Preparado por: Instructor del taller MCP
Versión: 2.0.0 (Fusión de bloques 3 y 4)
Última actualización: Febrero 2026