Compartir vía


Compilación de un servidor MCP de TypeScript mediante Azure Container Apps

En este artículo se explica cómo crear un servidor de Protocolo de contexto de modelo (MCP) mediante Node.js y TypeScript. El servidor ejecuta herramientas y servicios en un entorno sin servidor. Use esta estructura como punto de partida para crear servidores MCP personalizados.

Obtención del código

Explore el ejemplo del servidor de Protocolo de Contexto de Modelo Remoto (MCP) de TypeScript. Muestra cómo usar Node.js y TypeScript para crear un servidor MCP remoto e implementarlo en Azure Container Apps.

Vaya a la sección de tutorial de código para comprender cómo funciona este ejemplo.

Introducción a la arquitectura

En el diagrama siguiente se muestra la arquitectura sencilla de la aplicación de ejemplo: diagrama que muestra la arquitectura de Visual Studio Code que hospeda el agente y el cliente MCP en el servidor MCP.

El servidor MCP se ejecuta como una aplicación en contenedores en Azure Container Apps (ACA). Usa un back-end de Node.js/TypeScript para proporcionar herramientas al cliente MCP a través del protocolo de contexto de modelo. Todas las herramientas funcionan con una base de datos SQLite de back-end.

Costos

Para mantener bajos los costos, en este ejemplo se usan planes de tarifa básicos o de consumo para la mayoría de los recursos. Ajuste el nivel según sea necesario y elimine los recursos cuando haya terminado para evitar cargos.

Prerrequisitos

  1. Visual Studio Code : versión más reciente para admitir el desarrollo del servidor MCP.
  2. GitHub Copilot Extensión de Visual Studio Code
  3. Chat de GitHub Copilot Extensión de Visual Studio Code
  4. CLI para desarrolladores de Azure (azd)

Un contenedor de desarrollo incluye todas las dependencias que necesita para este artículo. Puede ejecutarlo en GitHub Codespaces (en un explorador) o localmente mediante Visual Studio Code.

Para seguir este artículo, asegúrese de cumplir estos requisitos previos:

Entorno de desarrollo abierto

Siga estos pasos para configurar un entorno de desarrollo preconfigurado con todas las dependencias necesarias.

GitHub Codespaces ejecuta un contenedor de desarrollo administrado por GitHub con Visual Studio Code para web como interfaz. Use GitHub Codespaces para la configuración más sencilla, ya que incluye las herramientas y dependencias necesarias preinstaladas para este artículo.

Importante

Todas las cuentas de GitHub pueden usar Codespaces durante un máximo de 60 horas gratis cada mes con dos instancias principales. Para obtener más información, consulte Almacenamiento y horas de núcleo incluidas mensualmente en GitHub Codespaces.

Siga estos pasos para crear un nuevo codespace de GitHub en la rama main del repositorio de GitHub Azure-Samples/mcp-container-ts.

  1. Haga clic con el botón derecho en el botón siguiente y seleccione Abrir vínculo en la nueva ventana. Esta acción le permite tener el entorno de desarrollo y la documentación abierta en paralelo.

    Abrir en GitHub Codespaces

  2. En la página Crear espacio de códigos , revise y seleccione Crear nuevo espacio de código.

  3. Espere a que se inicie Codespace. Esto puede llevar unos minutos.

  4. En el terminal de la parte inferior de la pantalla, inicie sesión en Azure con Azure Developer CLI.

    azd auth login
    
  5. Copie el código del terminal y péguelo en un navegador. Siga las instrucciones para autenticarse con su cuenta Azure.

Realiza el resto de las tareas de este contenedor de desarrollo.

Nota:

Para ejecutar el servidor MCP localmente:

  1. Configure el entorno como se describe en la sección Configuración del entorno local del repositorio de ejemplo.
  2. Configure el servidor MCP para que use el entorno local siguiendo las instrucciones de la sección Configurar el servidor MCP en Visual Studio Code del repositorio de ejemplo.
  3. Saltar a la sección Usar herramientas de servidor MCP TODO en modo de agente para continuar.

Implementación y ejecución

El repositorio de ejemplo contiene todos los archivos de código y configuración para la implementación de Azure del servidor MCP. Los pasos siguientes le guiarán por el proceso de implementación de Azure del servidor MCP de ejemplo.

Implementación en Azure

Importante

Los recursos de Azure de esta sección generan costes inmediatamente, incluso si interrumpe el comando antes de completarse.

  1. Ejecute el siguiente comando de Azure Developer CLI para el aprovisionamiento de recursos de Azure y la implementación de código fuente:

    azd up
    
  2. Use la siguiente tabla para responder a las solicitudes:

    Pronto Respuesta
    Nombre del entorno Manténgalo corto y en minúsculas. Agregue su nombre o alias. Por ejemplo: my-mcp-server. Se usa como parte del nombre del grupo de recursos.
    Suscripción Seleccione la suscripción en la que crear los recursos.
    Ubicación (para el hospedaje) Seleccione una ubicación cercana a usted en la lista.
    Ubicación del modelo de Azure OpenAI Seleccione una ubicación cercana a usted en la lista. Si está disponible la misma ubicación que la primera, selecciónela.
  3. Espere hasta que se implemente la aplicación. La implementación suele tardar entre 5 y 10 minutos en completarse.

  4. Una vez completada la implementación, puede acceder al servidor MCP mediante la dirección URL proporcionada en la salida. La dirección URL tiene este aspecto:

https://<env-name>.<container-id>.<region>.azurecontainerapps.io
  1. Copie la dirección URL en el Portapapeles. Lo necesitará en la sección siguiente.

Configuración del servidor MCP en Visual Studio Code

Configure el servidor MCP en el entorno de VS Code local agregando la dirección URL al mcp.json archivo de la .vscode carpeta .

  1. Abra el archivo mcp.json en la carpeta .vscode.

  2. Busque la mcp-server-sse-remote sección en el archivo. Debería tener este aspecto:

        "mcp-server-sse-remote": {
        "type": "sse",
        "url": "https://<container-id>.<location>.azurecontainerapps.io/sse"
    }
    
  3. Reemplace el valor existente url por la dirección URL que copió en el paso anterior.

  4. Guarde el mcp.json archivo en la .vscode carpeta .

Uso de las herramientas de servidor MCP de TODO en modo de agente

Después de modificar el servidor MCP, puede usar las herramientas, que proporciona en modo de agente. Para usar herramientas de MCP en modo de agente:

  1. Abra la vista Chat (Ctrl+Alt+I) y seleccione Modo de agente en la lista desplegable.

  2. Seleccione el botón Herramientas para ver la lista de herramientas disponibles. Opcionalmente, puede seleccionar o anular la selección de las herramientas que desea usar. Puede buscar herramientas escribiendo en el cuadro de búsqueda.

  3. Escriba un mensaje como "Necesito enviar un correo electrónico a mi administrador el miércoles" en el cuadro de entrada del chat y observe cómo las herramientas se invocan automáticamente según sea necesario, como en la captura de pantalla siguiente:

    Captura de pantalla que muestra la invocación de las herramientas del servidor MCP.

Nota:

De forma predeterminada, cuando se invoca una herramienta, debe confirmar la acción antes de que se ejecute la herramienta. De lo contrario, las herramientas se pueden ejecutar localmente en el equipo y pueden realizar acciones que modifiquen archivos o datos.

Use las opciones desplegables del botón Continuar para confirmar automáticamente la herramienta específica de la sesión actual, el área de trabajo o todas las invocaciones futuras.

Exploración del código de ejemplo

En esta sección se proporciona información general sobre los archivos clave y la estructura de código en el ejemplo de servidor MCP. El código se organiza en varios componentes principales:

  • index.ts: el punto de entrada principal del servidor MCP, que configura el Express.js servidor HTTP y el enrutamiento.
  • server.ts: la capa de transporte que administra las conexiones de eventos de Server-Sent (SSE) y el control del protocolo MCP.
  • tools.ts: contiene funciones de utilidad y lógica de negocios para el servidor MCP.
  • types.ts: define los tipos e interfaces de TypeScript que se usan en todo el servidor MCP.

index.ts - Cómo se inicia el servidor y acepta conexiones HTTP

El index.ts archivo es el punto de entrada principal para el servidor MCP. Inicializa el servidor, configura el Express.js servidor HTTP y define el enrutamiento para los puntos de conexión de eventos de Server-Sent (SSE).

Creación de la instancia del servidor MCP

El siguiente fragmento de código inicializa el servidor MCP mediante la StreamableHTTPServer clase , que es un contenedor alrededor de la clase MCP Server principal. Esta clase controla la capa de transporte para Server-Sent Eventos (SSE) y administra las conexiones de cliente.

const server = new StreamableHTTPServer(
  new Server(
    {
      name: 'todo-http-server',
      version: '1.0.0',
    },
    {
      capabilities: {
        tools: {},
      },
    }
  )
);

Conceptos:

  • Patrón de composición: SSEPServer envuelve la clase de bajo nivel Server
  • Declaración de funcionalidades: el servidor anuncia que admite herramientas (pero no recursos o avisos)
  • Convención de nomenclatura: el nombre del servidor se convierte en parte de la identificación de MCP

Configuración de rutas rápidas

El siguiente fragmento de código configura el servidor de Express.js para controlar las solicitudes HTTP entrantes para las conexiones SSE y el control de mensajes:

router.post('/messages', async (req: Request, res: Response) => {
  await server.handlePostRequest(req, res);
});

router.get('/sse', async (req: Request, res: Response) => {
  await server.handleGetRequest(req, res);
});

Conceptos:

  • Patrón de dos puntos de conexión: GET para establecer la conexión SSE, POST para enviar mensajes
  • Patrón de delegación: Las rutas rápidas se delegan inmediatamente a SSEPServer

Administración del ciclo de vida de los procesos

El siguiente fragmento de código controla el ciclo de vida del servidor, incluido iniciar el servidor y apagarlo correctamente en las señales de terminación:

process.on('SIGINT', async () => {
  log.error('Shutting down server...');
  await server.close();
  process.exit(0);
});

Conceptos:

  • Apagado correcto: limpieza adecuada con Ctrl+C
  • Limpieza asincrónica: La operación de cierre del servidor se realiza de manera asincrónica
  • Administración de recursos: importante para las conexiones SSE

Capa de transporte: server.ts

El server.ts archivo implementa la capa de transporte para el servidor MCP, que controla específicamente las conexiones de eventos de Server-Sent (SSE) y enruta los mensajes del protocolo MCP.

Configura una conexión de cliente SSE y crea un transporte

La SSEPServer clase es la capa de transporte principal para controlar eventos Server-Sent (SSE) en el servidor MCP. Usa la SSEServerTransport clase para administrar conexiones de cliente individuales. Administra varios transportes y su ciclo de vida.

export class SSEPServer {
  server: Server;
  transport: SSEServerTransport | null = null;
  transports: Record<string, SSEServerTransport> = {};

  constructor(server: Server) {
    this.server = server;
    this.setupServerRequestHandlers();
  }
}

Conceptos:

  • Administración del estado: realiza un seguimiento del transporte actual y de todos los transportes
  • Mapeo de sesión: transports el objeto asigna identificadores de sesión a instancias de transporte
  • Delegación de constructores: configura inmediatamente controladores de solicitudes

Establecimiento de conexión SSE (handleGetRequest)

El handleGetRequest método es responsable de establecer una nueva conexión SSE cuando un cliente realiza una solicitud GET al /sse punto de conexión.

async handleGetRequest(req: Request, res: Response) {
  log.info(`GET ${req.originalUrl} (${req.ip})`);
  try {
    log.info("Connecting transport to server...");
    this.transport = new SSEServerTransport("/messages", res);
    TransportsCache.set(this.transport.sessionId, this.transport);

    res.on("close", () => {
      if (this.transport) {
        TransportsCache.delete(this.transport.sessionId);
      }
    });

    await this.server.connect(this.transport);
    log.success("Transport connected. Handling request...");
  } catch (error) {
    // Error handling...
  }
}

Conceptos:

  • Creación de transporte: nueva instancia SSEServerTransport para cada solicitud GET
  • Administración de sesiones: identificador de sesión generado automáticamente almacenado en caché
  • Controladores de eventos: Limpieza al cerrar la conexión
  • Conexión MCP: server.connect() establece la conexión de protocolo
  • Flujo asincrónico: la configuración de la conexión es asincrónica con límites de error

Procesamiento de mensajes (handlePostRequest)

El handlePostRequest método procesa las solicitudes POST entrantes para controlar los mensajes MCP enviados por el cliente. Usa el identificador de sesión de los parámetros de consulta para buscar la instancia de transporte correcta.

async handlePostRequest(req: Request, res: Response) {
  log.info(`POST ${req.originalUrl} (${req.ip}) - payload:`, req.body);

  const sessionId = req.query.sessionId as string;
  const transport = TransportsCache.get(sessionId);
  if (transport) {
    await transport.handlePostMessage(req, res, req.body);
  } else {
    log.error("Transport not initialized. Cannot handle POST request.");
    res.status(400).json(/* error response */);
  }
}

Conceptos:

  • Búsqueda de sesión: utiliza el parámetro de consulta sessionId para encontrar el transporte.
  • Validación de sesión: valida primero la conexión SSE.
  • Delegación de mensajes: el transporte maneja el procesamiento real de mensajes.
  • Respuestas de error: códigos de error HTTP adecuados para las sesiones que faltan

Configuración del controlador de protocolo MCP (setupServerRequestHandlers)

El setupServerRequestHandlers método registra los siguientes controladores para las solicitudes de protocolo MCP:

  • Un controlador para ListToolsRequestSchema que devuelve la lista de herramientas de tareas pendientes disponibles.
  • Controlador para CallToolRequestSchema que busque y ejecute la herramienta solicitada con los argumentos proporcionados.

Este método usa esquemas Zod para definir los formatos de solicitud y respuesta esperados.

private setupServerRequestHandlers() {
  this.server.setRequestHandler(ListToolsRequestSchema, async (_request) => {
    return {
      tools: TodoTools,
    };
  });
  
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    
    const tool = TodoTools.find((tool) => tool.name === name);
    if (!tool) {
      return this.createJSONErrorResponse(`Tool "${name}" not found.`);
    }
    
    const response = await tool.execute(args as any);
    return { content: [{ type: "text", text: response }] };
  });
}

Conceptos:

  • Enrutamiento basado en esquema: usa esquemas Zod para la gestión de solicitudes con seguridad de tipos
  • Detección de herramientas: ListToolsRequestSchema devuelve la matriz todoTools estática.
  • Ejecución de herramientas: CallToolRequestSchema busca y ejecuta herramientas
  • Control de errores: control con gracia de herramientas desconocidas
  • Formato de respuesta: estructura de respuesta compatible con MCP
  • Seguridad de tipos: los tipos de TypeScript garantizan el paso correcto de argumentos.

Lógica de negocios: tools.ts

El tools.ts archivo define la funcionalidad real disponible para los clientes MCP:

  • Metadatos de la herramienta (nombre, descripción, esquemas)
  • Esquemas de validación de entrada
  • Lógica de ejecución de herramientas
  • Integración con la capa de base de datos

Este servidor MCP define cuatro herramientas de administración de tareas pendientes:

  • add_todo: crea un elemento TODO nuevo
  • complete_todo: marca un elemento TODO como completado.
  • delete_todo: elimina un elemento TODO.
  • list_todos: enumera todos los elementos TODO.
  • update_todo_text: actualiza el texto de un elemento TODO existente.

Patrón de definición de herramientas

Las herramientas se definen como una matriz de objetos, cada una de las cuales representa una operación TODO específica. En el fragmento de código siguiente, se define la addTodo herramienta:

{
  name: "addTodo",
  description: "Add a new TODO item to the list...",
  inputSchema: {
    type: "object",
    properties: {
      text: { type: "string" },
    },
    required: ["text"],
  },
  outputSchema: { type: "string" },
  async execute({ text }: { text: string }) {
    const info = await addTodo(text);
    return `Added TODO: ${text} (id: ${info.lastInsertRowid})`;
  },
}

Cada definición de herramienta tiene:

  • name: identificador único de la herramienta
  • description: breve descripción del propósito de la herramienta
  • inputSchema: esquema zod que define el formato de entrada esperado
  • outputSchema: esquema zod que define el formato de salida esperado
  • execute: función que implementa la lógica de la herramienta.

Estas definiciones de herramientas se importan en server.ts y se exponen a través del ListToolsRequestSchema manejador.

Conceptos:

  • Diseño de herramientas modulares: cada herramienta es un objeto independiente
  • Validación del esquema JSON: inputSchema define los parámetros esperados.
  • Seguridad de tipos: los tipos de TypeScript coinciden con las definiciones de esquema
  • Ejecución asincrónica: todas las ejecuciones de herramientas son asincrónicas
  • Integración de bases de datos: llama a funciones de base de datos importadas
  • Human-Readable Respuestas: devuelve cadenas con formato, no datos sin procesar

Exportación de matriz de herramientas

Las herramientas se exportan como una matriz estática, lo que facilita la importación y el uso en el servidor. Cada herramienta es un objeto con sus metadatos y lógica de ejecución. Esta estructura permite al servidor MCP detectar y ejecutar herramientas dinámicamente en función de las solicitudes de cliente.

export const TodoTools = [
  { /* addTodo */ },
  { /* listTodos */ },
  { /* completeTodo */ },
  { /* deleteTodo */ },
  { /* updateTodoText */ },
];

Conceptos:

  • Registro estático: herramientas definidas en tiempo de carga del módulo
  • Estructura de matriz: La matriz simple facilita la iteración de funciones
  • Importación y exportación: separación limpia de la lógica del servidor

Control de errores de ejecución de herramientas

La función execute de cada herramienta controla los errores sin problemas y devuelve mensajes claros en lugar de iniciar excepciones. Este enfoque garantiza que el servidor MCP proporcione una experiencia de usuario sin problemas.

Las herramientas controlan varios escenarios de error:

async execute({ id }: { id: number }) {
  const info = await completeTodo(id);
  if (info.changes === 0) {
    return `TODO with id ${id} not found.`;
  }
  return `Marked TODO ${id} as completed.`;
}

Conceptos:

  • Comprobación de respuestas de la base de datos: usa info.changes para detectar errores
  • Degradación elegante: devuelve mensajes de error descriptivos en lugar de generar excepciones
  • User-Friendly Errores: mensajes adecuados para su interpretación por IA

Capa de datos: db.ts

El db.ts archivo administra la conexión de base de datos de SQLite y controla las operaciones CRUD de la aplicación TODO. Usa la better-sqlite3 biblioteca para el acceso sincrónico a la base de datos.

Inicialización de la base de datos

La base de datos se inicializa mediante la conexión a SQLite y la creación de tablas si no existen. El siguiente fragmento de código muestra el proceso de inicialización:

const db = new Database(":memory:", {
  verbose: log.info,
});

try {
  db.pragma("journal_mode = WAL");
  db.prepare(
    `CREATE TABLE IF NOT EXISTS ${DB_NAME} (
     id INTEGER PRIMARY KEY AUTOINCREMENT,
     text TEXT NOT NULL,
     completed INTEGER NOT NULL DEFAULT 0
   )`
  ).run();
  log.success(`Database "${DB_NAME}" initialized.`);
} catch (error) {
  log.error(`Error initializing database "${DB_NAME}":`, { error });
}

Conceptos:

  • In-Memory Base de datos: :memory: significa que los datos se pierden al reiniciar (solo demostración o pruebas)
  • Modo WAL: registro de escritura previa para un mejor rendimiento
  • Definición de esquema: tabla TODO simple con identificador de autoincremento
  • Control de errores: control con gracia de errores de inicialización
  • Integración del registro: las operaciones de base de datos se registran para la depuración

Patrones de operación CRUD

El db.ts archivo proporciona cuatro operaciones CRUD fundamentales para administrar elementos TODO:

Operación de creación:

export async function addTodo(text: string) {
  log.info(`Adding TODO: ${text}`);
  const stmt = db.prepare(`INSERT INTO todos (text, completed) VALUES (?, 0)`);
  return stmt.run(text);
}

Operación de lectura:

export async function listTodos() {
  log.info("Listing all TODOs...");
  const todos = db.prepare(`SELECT id, text, completed FROM todos`).all() as Array<{
    id: number;
    text: string;
    completed: number;
  }>;
  return todos.map(todo => ({
    ...todo,
    completed: Boolean(todo.completed),
  }));
}

Operación de actualización:

export async function completeTodo(id: number) {
  log.info(`Completing TODO with ID: ${id}`);
  const stmt = db.prepare(`UPDATE todos SET completed = 1 WHERE id = ?`);
  return stmt.run(id);
}

Operación de eliminación:

export async function deleteTodo(id: number) {
  log.info(`Deleting TODO with ID: ${id}`);
  const row = db.prepare(`SELECT text FROM todos WHERE id = ?`).get(id) as
    | { text: string }
    | undefined;
  if (!row) {
    log.error(`TODO with ID ${id} not found`);
    return null;
  }
  db.prepare(`DELETE FROM todos WHERE id = ?`).run(id);
  log.success(`TODO with ID ${id} deleted`);
  return row;
}

Conceptos:

  • Instrucciones preparadas: Protección contra la inyección SQL
  • Conversión de tipos: tipos TypeScript explícitos para los resultados de la consulta
  • Transformación de datos: Conversión de enteros de SQLite en valores booleanos
  • Operaciones atómicas: cada función es una única transacción de base de datos
  • Coherencia del valor devuelto: metadatos de la operación de devolución de Functions
  • Programación defensiva: patrón check-before-delete

Diseño de esquema

El esquema de la base de datos se define en el db.ts archivo mediante una instrucción SQL simple. La todos tabla tiene tres campos:

CREATE TABLE todos (
  id INTEGER PRIMARY KEY AUTOINCREMENT,  -- Unique identifier
  text TEXT NOT NULL,                    -- TODO description  
  completed INTEGER NOT NULL DEFAULT 0   -- Boolean as integer
);

Utilidades auxiliares: helpers/ directorio

El helpers/ directorio proporciona funciones y clases de utilidad para el servidor.

Registro estructurado para depuración y supervisión: helpers/logs.ts

El helpers/logs.ts archivo proporciona una utilidad de registro estructurado para el servidor MCP. Usa la biblioteca debug para registrar y chalk para la salida codificada en color en la consola.

export const logger = (namespace: string) => {
  const dbg = debug('mcp:' + namespace);
  const log = (colorize: ChalkInstance, ...args: any[]) => {
    const timestamp = new Date().toISOString();
    const formattedArgs = [timestamp, ...args].map((arg) => {
      if (typeof arg === 'object') {
        return JSON.stringify(arg, null, 2);
      }
      return arg;
    });
    dbg(colorize(formattedArgs.join(' ')));
  };

  return {
    info(...args: any[]) { log(chalk.cyan, ...args); },
    success(...args: any[]) { log(chalk.green, ...args); },
    warn(...args: any[]) { log(chalk.yellow, ...args); },
    error(...args: any[]) { log(chalk.red, ...args); },
  };
};

Administración de sesiones para transportes SSE: helpers/cache.ts

El helpers/cache.ts archivo utiliza un Map para almacenar los transportes SSE por identificador de sesión. Este enfoque permite al servidor buscar y administrar rápidamente las conexiones activas.

import type { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse";

export const TransportsCache = new Map<string, SSEServerTransport>();

Nota:

TransportsCache es una caché en memoria sencilla. En producción, considere la posibilidad de usar una solución más sólida, como Redis o una base de datos para la administración de sesiones.

Resumen del flujo de ejecución

En el diagrama siguiente se muestra el recorrido de solicitud completo desde el cliente al servidor MCP y de vuelta, incluida la ejecución de herramientas y las operaciones de base de datos:

Diagrama que muestra el recorrido de solicitud completo del cliente al servidor MCP y de vuelta.

Limpieza de GitHub Codespaces

Elimine el entorno de GitHub Codespaces para maximizar las horas gratuitas por núcleo.

Importante

Para más información sobre el almacenamiento gratuito y las horas centrales de su cuenta de GitHub, consulte el almacenamiento mensual incluido y las horas centrales en GitHub Codespaces.

  1. Inicie sesión en el panel de GitHub Codespaces.

  2. Busque los espacios de código activos creados a partir del Azure-Samples//mcp-container-ts repositorio de GitHub.

  3. Abra el menú contextual del espacio de código y seleccione Eliminar.

Obtención de ayuda

Registre el problema en los problemas del repositorio.