Partilhar via


Criar um servidor MCP TypeScript usando Aplicativos de Contêiner do Azure

Este artigo explica como criar um servidor MCP (Model Context Protocol) usando Node.js e TypeScript. O servidor executa ferramentas e serviços em um ambiente sem servidor. Use essa estrutura como ponto de partida para criar servidores MCP personalizados.

Aceder ao código

Explore o exemplo de servidor MCP (Remote Model Context Protocol) do TypeScript . Ele demonstra como usar o Node.js e o TypeScript para criar um servidor MCP remoto e implantá-lo nos Aplicativos de Contêiner do Azure.

Vá para a seção passo a passo do código para entender como esse exemplo funciona.

Visão geral da arquitetura

O diagrama a seguir mostra a arquitetura simples do aplicativo de exemplo: Diagrama mostrando a arquitetura do Visual Studio Code hospedando o agente e o cliente MCP para o MCP Server.

O servidor MCP é executado como um aplicativo em contêiner nos Aplicativos de Contêiner do Azure (ACA). Ele utiliza um backend TypeScript Node.jspara fornecer ferramentas ao cliente MCP através do Protocolo de Contexto de Modelo. Todas as ferramentas funcionam com um banco de dados SQLite back-end.

Custo

Para manter os custos baixos, este exemplo usa níveis de preços básicos ou de consumo para a maioria dos recursos. Ajuste a camada conforme necessário e exclua recursos quando terminar para evitar cobranças.

Pré-requisitos

  1. Visual Studio Code - Versão mais recente para dar suporte ao desenvolvimento do MCP Server.
  2. Copiloto do GitHub Extensão de código do Visual Studio
  3. Bate-papo do Copilot do GitHub Extensão de código do Visual Studio
  4. CLI do desenvolvedor do Azure (azd)

Um contêiner de desenvolvimento inclui todas as dependências necessárias para este artigo. Você pode executá-lo no GitHub Codespaces (em um navegador) ou localmente usando o Visual Studio Code.

Para seguir este artigo, certifique-se de que cumpre estes pré-requisitos:

Ambiente de desenvolvimento aberto

Siga estas etapas para configurar um ambiente de desenvolvimento pré-configurado com todas as dependências necessárias.

O GitHub Codespaces executa um contêiner de desenvolvimento gerenciado pelo GitHub com o Visual Studio Code for the Web como interface. Use o GitHub Codespaces para a configuração mais simples, pois ele vem com as ferramentas e dependências necessárias pré-instaladas para este artigo.

Importante

Todas as contas do GitHub podem usar Codespaces por até 60 horas gratuitas por mês com duas instâncias principais. Para mais informações, consulte o armazenamento mensal incluído e as horas principais do GitHub Codespaces.

Use as etapas a seguir para criar um novo espaço de código do GitHub na main ramificação do Azure-Samples/mcp-container-ts repositório do GitHub.

  1. Clique com o botão direito do mouse no botão a seguir e selecione Abrir link na nova janela. Esta ação permite que você tenha o ambiente de desenvolvimento e a documentação abertos lado a lado.

    Abrir no Codespaces do GitHub

  2. Na página Criar espaço de código , revise e selecione Criar novo espaço de código.

  3. Aguarde até que o codespace inicie. Pode demorar alguns minutos.

  4. Entre no Azure com a CLI do Desenvolvedor do Azure no terminal na parte inferior da tela.

    azd auth login
    
  5. Copie o código do terminal e cole-o em um navegador. Siga as instruções para autenticar com sua conta do Azure.

Você executa o restante das tarefas neste contêiner de desenvolvimento.

Observação

Para executar o servidor MCP localmente:

  1. Configure seu ambiente conforme descrito na seção Configuração do ambiente local no repositório de exemplo.
  2. Configure seu servidor MCP para usar o ambiente local seguindo as instruções na seção Configurar o servidor MCP no Visual Studio Code no repositório de exemplo.
  3. Vá para a seção Usar ferramentas de servidor TODO MCP no modo de agente para continuar.

Implantar e executar

O repositório de exemplo contém todos os arquivos de código e configuração para a implantação do Azure do servidor MCP. As etapas a seguir o orientam pelo processo de implantação do Azure do servidor MCP de exemplo.

Publicar no Azure

Importante

Os recursos do Azure nesta seção começam a custar dinheiro imediatamente, mesmo que você pare o comando antes que ele seja concluído.

  1. Execute o seguinte comando da CLI do Desenvolvedor do Azure para provisionamento de recursos do Azure e implantação de código-fonte:

    azd up
    
  2. Use a tabela a seguir para responder aos prompts:

    Pronta Resposta
    Nome do ambiente Mantenha-o curto e minúsculo. Adicione o seu nome ou alias. Por exemplo, my-mcp-server. Ele é usado como parte do nome do grupo de recursos.
    Subscrição Selecione a assinatura na qual criar os recursos.
    Localização (para hospedagem) Selecione um local perto de você na lista.
    Local para o modelo OpenAI do Azure Selecione um local perto de você na lista. Se o mesmo local estiver disponível como seu primeiro local, selecione isso.
  3. Aguarde até que o aplicativo seja implantado. A implantação geralmente leva entre 5 e 10 minutos para ser concluída.

  4. Quando a implantação estiver concluída, você poderá acessar o servidor MCP usando a URL fornecida na saída. O URL tem esta aparência:

https://<env-name>.<container-id>.<region>.azurecontainerapps.io
  1. Copie o URL para a área de transferência. Você precisará dele na próxima seção.

Configurar o servidor MCP no Visual Studio Code

Configure o servidor MCP em seu ambiente local do VS Code adicionando a URL ao mcp.json arquivo na .vscode pasta.

  1. Abra o arquivo mcp.json na pasta .vscode.

  2. Localize a mcp-server-sse-remote seção no arquivo. Deve ter a seguinte aparência:

        "mcp-server-sse-remote": {
        "type": "sse",
        "url": "https://<container-id>.<location>.azurecontainerapps.io/sse"
    }
    
  3. Substitua o valor existente url pelo URL copiado na etapa anterior.

  4. Guarde o arquivo mcp.json na pasta .vscode.

Usar ferramentas de servidor TODO MCP no modo de agente

Depois de modificar o servidor MCP, pode-se usar as ferramentas que ele fornece no modo de agente. Para usar ferramentas MCP no modo de agente:

  1. Abra a vista de bate-papo (Ctrl+Alt+I) e selecione modo de agente na lista suspensa.

  2. Selecione o botão Ferramentas para visualizar a lista de ferramentas disponíveis. Opcionalmente, selecione ou desmarque as ferramentas que deseja usar. Você pode pesquisar ferramentas digitando na caixa de pesquisa.

  3. Digite um prompt como "Preciso enviar um e-mail para meu gerente na quarta-feira" na caixa de entrada do chat e observe como as ferramentas são invocadas automaticamente conforme necessário, como na captura de tela a seguir:

    Captura de tela mostrando a invocação das ferramentas do servidor MCP.

Observação

Por padrão, quando uma ferramenta é invocada, você precisa confirmar a ação antes que ela seja executada. Caso contrário, as ferramentas podem ser executadas localmente em sua máquina e podem executar ações que modificam arquivos ou dados.

Use as opções suspensas do botão Continuar para confirmar automaticamente a ferramenta específica para a sessão atual, espaço de trabalho ou todas as invocações futuras.

Explorando o código de exemplo

Esta seção fornece uma visão geral dos arquivos de chave e da estrutura de código no exemplo de servidor MCP. O código está organizado em vários componentes principais:

  • index.ts: O principal ponto de entrada para o servidor MCP, que configura o Express.js servidor HTTP e roteamento.
  • server.ts: A camada de transporte que faz a gestão das ligações de eventos Server-Sent (SSE) e da manipulação do protocolo MCP.
  • tools.ts: Contém lógica de negócios e funções de utilidade para o servidor MCP.
  • types.ts: Define os tipos e interfaces do TypeScript usados em todo o servidor MCP.

index.ts - Como o servidor inicia e aceita conexões HTTP

O index.ts arquivo é o principal ponto de entrada para o servidor MCP. Ele inicializa o servidor, configura o servidor HTTP Express.js e define o roteamento para os pontos finais de eventos de Server-Sent (SSE).

Criar a instância do servidor MCP

O trecho de código a seguir inicializa o servidor MCP usando a StreamableHTTPServer classe, que é um wrapper em torno da classe MCP Server principal. Essa classe manipula a camada de transporte para eventos Server-Sent (SSE) e gerencia conexões de cliente.

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

Conceitos:

  • Padrão de composição: SSEPServer envolve a classe de baixo nível Server
  • Declaração de capacidades: O servidor anuncia que suporta ferramentas (mas não recursos/prompts)
  • Convenção de nomenclatura: O nome do servidor torna-se parte da identificação MCP

Configurar rotas Express

O trecho de código a seguir configura o servidor Express.js para lidar com solicitações HTTP de entrada para conexões SSE e tratamento de mensagens:

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);
});

Conceitos:

  • Padrão de dois pontos finais: GET para estabelecer conexão SSE, POST para enviar mensagens
  • Padrão de delegação: Rotas expressas delegadas imediatamente para SSEPServer

Gestão do ciclo de vida do processo

O trecho de código a seguir lida com o ciclo de vida do servidor, incluindo iniciar o servidor e desligá-lo normalmente em sinais de terminação:

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

Conceitos:

  • Encerramento controlado: limpeza adequada ao pressionar Ctrl+C
  • Limpeza assíncrona: a operação de fechamento do servidor é assíncrona
  • Gerenciamento de recursos: importante para conexões SSE

Camada de transporte: server.ts

O server.ts arquivo implementa a camada de transporte para o servidor MCP, especificamente manipulando conexões de eventos de Server-Sent (SSE) e roteando mensagens do protocolo MCP.

Configurar uma conexão de cliente SSE e criar um transporte

A SSEPServer classe é a camada de transporte principal para manipular eventos de Server-Sent (SSE) no servidor MCP. Ele usa a SSEServerTransport classe para gerenciar conexões de cliente individuais. Gere múltiplos transportes e o seu ciclo de vida.

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

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

Conceitos:

  • Gestão do Estado: Rastreia os transportes atuais e todos os transportes
  • Mapeamento de sessão: transports o objeto mapeia IDs de sessão para instâncias de transporte
  • Delegação do construtor: configura imediatamente os manipuladores de solicitação

Estabelecimento de conexão SSE (handleGetRequest)

O handleGetRequest método é responsável por estabelecer uma nova conexão SSE quando um cliente faz uma solicitação GET para o /sse ponto de extremidade.

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...
  }
}

Conceitos:

  • Criação de transporte: Novo SSEServerTransport para cada solicitação GET
  • Gerenciamento de sessão: ID de sessão gerado automaticamente armazenado em cache
  • Manipuladores de eventos: Limpeza no encerramento da conexão
  • Conexão MCP: server.connect() estabelece conexão de protocolo
  • Fluxo assíncrono: a configuração da conexão é assíncrona com limites de erro

Processamento de mensagens (handlePostRequest)

O handlePostRequest método processa solicitações POST de entrada para lidar com mensagens MCP enviadas pelo cliente. Ele usa o ID de sessão dos parâmetros de consulta para encontrar a instância de transporte correta.

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 */);
  }
}

Conceitos:

  • Pesquisa de sessão: usa sessionId o parâmetro de consulta para localizar o transporte
  • Validação de sessão: Valida primeiro a conexão SSE.
  • Delegação de mensagens: o transporte é responsável pelo processamento efetivo de mensagens
  • Respostas de erro: códigos de erro HTTP adequados para sessões ausentes

Configuração do manipulador de protocolo MCP (setupServerRequestHandlers)

O setupServerRequestHandlers método registra os seguintes manipuladores para solicitações de protocolo MCP:

  • Um manipulador para ListToolsRequestSchema, que retorna a lista de ferramentas TODO disponíveis.
  • Um manipulador para CallToolRequestSchema que localiza e executa a ferramenta solicitada com os argumentos fornecidos.

Esse método usa esquemas Zod para definir os formatos de solicitação e resposta 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 }] };
  });
}

Conceitos:

  • Schema-Based Roteamento: Utiliza esquemas Zod para tratamento de solicitações de tipo seguro
  • Descoberta de ferramentas: ListToolsRequestSchema retorna a matriz estática TodoTools
  • Execução de ferramentas: CallToolRequestSchema localiza e executa ferramentas
  • Tratamento de erros: manipulação graciosa de ferramentas desconhecidas
  • Formato de resposta: estrutura de resposta compatível com MCP
  • Segurança do tipo: os tipos TypeScript garantem a passagem correta do argumento

Lógica de negócio: tools.ts

O tools.ts arquivo define a funcionalidade real disponível para clientes MCP:

  • Metadados da ferramenta (nome, descrição, esquemas)
  • Esquemas de validação de entrada
  • Lógica de execução da ferramenta
  • Integração com camada de banco de dados

Este Servidor MCP define quatro ferramentas de gerenciamento TODO:

  • add_todo: Cria um novo item TODO
  • complete_todo: Marca um item TODO como concluído
  • delete_todo: Exclui um item TODO
  • list_todos: Lista todos os itens TODO
  • update_todo_text: Atualiza o texto de um item TODO existente

Padrão de definição de ferramenta

As ferramentas são definidas como uma matriz de objetos, cada um representando uma operação TODO específica. No trecho de código a seguir, a addTodo ferramenta é definida:

{
  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 definição de ferramenta tem:

  • name: Identificador único da ferramenta
  • description: Breve descrição da finalidade da ferramenta
  • inputSchema: Esquema Zod definindo o formato de entrada esperado
  • outputSchema: Zod esquema definindo o formato de saída esperado
  • execute: Função que implementa a lógica da ferramenta

Essas definições de ferramentas são importadas em server.ts e expostas através do manipulador ListToolsRequestSchema.

Conceitos:

  • Design de ferramenta modular: Cada ferramenta é um objeto autônomo
  • Validação de esquema JSON: inputSchema define parâmetros esperados
  • Segurança de tipo: os tipos TypeScript correspondem às definições de esquema
  • Execução assíncrona: Todas as execuções de ferramentas são assíncronas
  • Integração de banco de dados: chama funções de banco de dados importadas
  • Human-Readable Respostas: retorna cadeias de caracteres formatadas, não dados brutos

Exportação de conjunto de ferramentas

As ferramentas são exportadas como uma matriz estática, tornando-as fáceis de importar e usar no servidor. Cada ferramenta é um objeto com seus metadados e lógica de execução. Essa estrutura permite que o servidor MCP descubra e execute dinamicamente ferramentas com base nas solicitações do cliente.

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

Conceitos:

  • Registro estático: Ferramentas definidas no tempo de carregamento do módulo
  • Estrutura da matriz: A matriz simples torna as ferramentas fáceis de iterar
  • Importar/Exportar: Separação limpa da lógica do servidor

Tratamento de erros de execução da ferramenta

A função de cada ferramenta execute lida eficientemente com erros e retorna mensagens claras em vez de gerar exceções. Essa abordagem garante que o servidor MCP forneça uma experiência de usuário perfeita.

As ferramentas lidam com vários cenários de erro:

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.`;
}

Conceitos:

  • Verificação de resposta do banco de dados: usa info.changes para detetar falhas
  • Degradação graciosa: retorna mensagens de erro descritivas vs lançamento
  • User-Friendly Erros: Mensagens adequadas para interpretação por IA

Camada de dados: db.ts

O db.ts arquivo gerencia a conexão de banco de dados SQLite e manipula operações CRUD para o aplicativo TODO. Ele usa a biblioteca para acesso síncrono ao better-sqlite3 banco de dados.

Inicialização do banco de dados

O banco de dados é inicializado conectando-se ao SQLite e criando tabelas se elas não existirem. O trecho de código a seguir mostra o processo de inicialização:

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 });
}

Conceitos:

  • In-Memory Banco de dados: :memory: significa dados perdidos na reinicialização (somente demonstração/teste)
  • Modo WAL: Write-Ahead registo de eventos para melhor desempenho
  • Definição de esquema: tabela simples TODO com ID auto-incrementado
  • Tratamento de erros: Tratamento normal de falhas de inicialização
  • Integração de registo: as operações na base de dados são registadas para fins de depuração

Padrões de operação CRUD

O db.ts arquivo fornece quatro operações CRUD principais para gerenciar itens TODO:

Criar operação:

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);
}

Leia a operação:

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),
  }));
}

Operação de atualização:

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);
}

Operação de eliminação:

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;
}

Conceitos:

  • Instruções preparadas: Proteção contra injeção de SQL
  • Type Casting: tipos TypeScript explícitos para resultados de consulta
  • Transformação de dados: convertendo inteiros SQLite em booleanos
  • Operações atômicas: Cada função é uma única transação de banco de dados
  • Consistência do valor de retorno: as funções retornam metadados da operação
  • Programação defensiva: padrão de verificação antes de excluir

Design de esquema

O esquema de banco de dados é definido no db.ts arquivo usando uma instrução SQL simples. A todos tabela tem três 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
);

Utilitários auxiliares: helpers/ diretório

O helpers/ diretório fornece funções de utilitário e classes para o servidor.

Log estruturado para debugging e monitorização: helpers/logs.ts

O helpers/logs.ts arquivo fornece um utilitário de log estruturado para o servidor MCP. Ele usa a biblioteca debug para registo de logs e chalk para saída com códigos de cores no console.

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); },
  };
};

Gestão de sessões para transportes SSE: helpers/cache.ts

O helpers/cache.ts arquivo usa um Map para armazenar transportes SSE por ID de sessão. Essa abordagem permite que o servidor encontre e gerencie rapidamente conexões ativas.

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

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

Observação

O TransportsCache é um cache simples na memória. Na produção, considere o uso de uma solução mais robusta como o Redis ou um banco de dados para gerenciamento de sessão.

Resumo do fluxo de execução

O diagrama a seguir ilustra a jornada completa da solicitação do cliente para o servidor MCP e vice-versa, incluindo a execução da ferramenta e as operações do banco de dados:

Diagrama mostrando a jornada completa da solicitação do cliente para o servidor MCP e vice-versa.

Limpar espaços de código do GitHub

Exclua o ambiente do GitHub Codespaces para maximizar suas horas gratuitas por núcleo.

Importante

Para obter mais informações sobre o armazenamento gratuito e as horas principais da sua conta do GitHub, consulte Armazenamento mensal incluído e horas principais do GitHub Codespaces.

  1. Faça login no painel do GitHub Codespaces.

  2. Encontre seus Codespaces ativos criados a partir do Azure-Samples//mcp-container-ts repositório GitHub.

  3. Abra o menu de contexto do espaço de código e selecione Excluir.

Obter ajuda

Registre seu problema nos problemas do repositório.