Dela via


Skapa en TypeScript MCP-server med Hjälp av Azure Container Apps

Den här artikeln beskriver hur du skapar en MCP-server (Model Context Protocol) med hjälp av Node.js och TypeScript. Servern kör verktyg och tjänster i en serverlös miljö. Använd den här strukturen som utgångspunkt för att skapa anpassade MCP-servrar.

Gå till koden

Utforska serverexemplet TypeScript remote Model Context Protocol (MCP). Den visar hur du använder Node.js och TypeScript för att skapa en fjärransluten MCP-server och distribuera den till Azure Container Apps.

Gå till avsnittet med kodgenomgång för att förstå hur det här exemplet fungerar.

Översikt över arkitektur

Följande diagram visar exempelappens enkla arkitektur: Diagram som visar arkitektur från Visual Studio Code som är värd för agenten och MCP-klienten till MCP Server.

MCP-servern körs som en containerbaserad app i Azure Container Apps (ACA). Den använder en Node.js/TypeScript-serverdel för att tillhandahålla verktyg till MCP-klienten via Model Context Protocol. Alla verktyg fungerar med en SQLite-databas för serverdelen.

Kostnad

För att hålla kostnaderna låga använder det här exemplet prisnivåer för bas- eller förbrukning för de flesta resurser. Justera nivån efter behov och ta bort resurser när du är klar för att undvika avgifter.

Förutsättningar

  1. Visual Studio Code – den senaste versionen för mcp-serverutveckling.
  2. GitHub Copilot Visual Studio Code-tillägg
  3. GitHub Copilot Chat Visual Studio Code-tillägg
  4. Azure Developer CLI (azd)

En utvecklingscontainer innehåller alla beroenden som du behöver för den här artikeln. Du kan köra den i GitHub Codespaces (i en webbläsare) eller lokalt med hjälp av Visual Studio Code.

Följ den här artikeln genom att se till att du uppfyller följande krav:

Öppna utvecklingsmiljön

Följ de här stegen för att konfigurera en förkonfigurerad utvecklingsmiljö med alla nödvändiga beroenden.

GitHub Codespaces kör en utvecklingscontainer som hanteras av GitHub med Visual Studio Code för webben som gränssnitt. Använd GitHub Codespaces för den enklaste installationen, eftersom den levereras med nödvändiga verktyg och beroenden förinstallerade för den här artikeln.

Viktigt!

Alla GitHub-konton kan använda Codespaces i upp till 60 timmar kostnadsfritt varje månad med två kärninstanser. För mer information, se GitHub Codespaces månadsvis inkluderade lagrings- och kärntimmar.

Använd följande steg för att skapa ett nytt GitHub Codespace på grenen main av Azure-Samples/mcp-container-ts GitHub-lagringsplatsen.

  1. Högerklicka på följande knapp och välj Öppna länk i nytt fönster. Med den här åtgärden kan du öppna utvecklingsmiljön och dokumentationen sida vid sida.

    Öppna i GitHub Codespaces

  2. På sidan Skapa kodområde granskar du och väljer sedan Skapa nytt kodområde.

  3. Vänta på att kodområdet startar. Det kan ta några minuter.

  4. Logga in på Azure med Azure Developer CLI i terminalen längst ned på skärmen.

    azd auth login
    
  5. Kopiera koden från terminalen och klistra sedan in den i en webbläsare. Följ anvisningarna för att autentisera med ditt Azure-konto.

Du utför resten av uppgifterna i den här utvecklingscontainern.

Anmärkning

Så här kör du MCP-servern lokalt:

  1. Konfigurera din miljö enligt beskrivningen i avsnittet Lokal miljökonfiguration i exempellagringsplatsen.
  2. Konfigurera MCP-servern så att den använder den lokala miljön genom att följa anvisningarna i avsnittet Konfigurera MCP-servern i Visual Studio Code på exempellagringsplatsen.
  3. Gå till avsnittet Använd TODO MCP-serververktyg i agentläge för att fortsätta.

Driftsätta och köra

Exempellagringsplatsen innehåller alla kod- och konfigurationsfiler för MCP-serverns Azure-distribution. Följande steg beskriver exempeldistributionsprocessen för MCP-servern i Azure.

Distribuera till Azure

Viktigt!

Azure-resurser i det här avsnittet börjar kosta pengar omedelbart, även om du stoppar kommandot innan det är klart.

  1. Kör följande Azure Developer CLI-kommando för Azure-resursetablering och källkodsdistribution:

    azd up
    
  2. Använd följande tabell för att besvara anvisningarna:

    Omedelbar Svar
    Miljönamn Håll det kort och med små bokstäver. Lägg till ditt namn eller alias. Till exempel my-mcp-server. Den används som en del av resursgruppens namn.
    Prenumeration Välj den prenumeration som resurserna ska skapas i.
    Plats (för värd) Välj en plats nära dig i listan.
    Plats för Azure OpenAI-modellen Välj en plats nära dig i listan. Om samma plats är tillgänglig som din första plats väljer du det.
  3. Vänta tills appen har distribuerats. Distributionen tar vanligtvis mellan 5 och 10 minuter att slutföra.

  4. När distributionen är klar kan du komma åt MCP-servern med hjälp av URL:en som anges i utdata. URL:en ser ut så här:

https://<env-name>.<container-id>.<region>.azurecontainerapps.io
  1. Kopiera URL till urklipp. Du behöver det i nästa avsnitt.

Konfigurera MCP-servern i Visual Studio Code

Konfigurera MCP-servern i din lokala VS Code-miljö genom att lägga till URL:en i mcp.json filen i .vscode mappen.

  1. Öppna filen mcp.json i mappen .vscode.

  2. Leta upp avsnittet mcp-server-sse-remote i filen. Det bör se ut så här:

        "mcp-server-sse-remote": {
        "type": "sse",
        "url": "https://<container-id>.<location>.azurecontainerapps.io/sse"
    }
    
  3. Ersätt det befintliga url värdet med den URL som du kopierade i föregående steg.

  4. mcp.json Spara filen i .vscode mappen.

Använda TODO MCP-serververktyg i agentläge

När du har modifierat MCP-servern kan du använda de verktyg som den tillhandahåller för agentläge. Så här använder du MCP-verktyg i agentläge:

  1. Öppna chattvyn (Ctrl+Alt+I) och välj Agentläge i listrutan.

  2. Välj knappen Verktyg för att visa listan över tillgängliga verktyg. Du kan också välja eller avmarkera de verktyg som du vill använda. Du kan söka efter verktyg genom att skriva i sökrutan.

  3. Ange en uppmaning som "Jag måste skicka ett e-postmeddelande till min chef på onsdag" i rutan för chattinmatning och lägg märke till hur verktyg anropas automatiskt efter behov, som i följande skärmbild:

    Skärmbild som visar anropet av MCP-serververktyg.

Anmärkning

När ett verktyg anropas måste du som standard bekräfta åtgärden innan verktyget körs. Annars kan verktyg köras lokalt på datorn och kan utföra åtgärder som ändrar filer eller data.

Använd listrutan Fortsätt för att automatiskt bekräfta det specifika verktyget för den aktuella sessionen, arbetsytan eller alla framtida anrop.

Utforska exempelkoden

Det här avsnittet innehåller en översikt över nyckelfilerna och kodstrukturen i MCP-serverexemplet. Koden är uppdelad i flera huvudkomponenter:

  • index.ts: Huvudinmatningspunkten för MCP-servern, som konfigurerar Express.js HTTP-server och routning.
  • server.ts: Transportskiktet som hanterar SSE-anslutningar (Server-Sent Events) och MCP-protokollhantering.
  • tools.ts: Innehåller affärslogik och verktygsfunktioner för MCP-servern.
  • types.ts: Definierar TypeScript-typer och gränssnitt som används i hela MCP-servern.

index.ts – Hur servern startar och accepterar HTTP-anslutningar

Filen index.ts är den viktigaste startpunkten för MCP-servern. Den initierar servern, konfigurerar Express.js HTTP-server och definierar routning för Server-Sent-händelser (SSE) slutpunkter.

Skapa MCP-serverinstansen

Följande kodfragment initierar MCP-servern med hjälp av StreamableHTTPServer klassen, som är en omslutning runt mcp-kärnklassen Server . Den här klassen hanterar transportlagret för Server-Sent Events (SSE) och hanterar klientanslutningar.

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

Begrepp:

  • Kompositionsmönster: SSEPServer omsluter klassen på låg nivå Server
  • Kapacitetsdeklaration: Servern meddelar att den stöder verktyg (men inte resurser/uppmaningar)
  • Namngivningskonvention: Servernamn blir en del av MCP-identifiering

Konfigurera Express-vägar

Följande kodfragment konfigurerar Express.js-servern för att hantera inkommande HTTP-begäranden för SSE-anslutningar och meddelandehantering:

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

Begrepp:

  • Mönster för två slutpunkter: GET för att upprätta SSE-anslutning, POST för att skicka meddelanden
  • Delegeringsmönster: Expressvägar delegerar omedelbart till SSEPServer

Processlivscykelhantering

Följande kodfragment hanterar serverns livscykel, inklusive att starta servern och stänga av den på ett korrekt sätt vid avslutningssignaler:

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

Begrepp:

  • Smidig avstängning: Rätt avslutning vid Ctrl+C
  • Asynkron rensning: Serverstängningsåtgärden är asynkron
  • Resurshantering: Viktigt för SSE-anslutningar

Transportskikt: server.ts

Filen server.ts implementerar transportskiktet för MCP-servern, särskilt hantering av SSE-anslutningar (Server-Sent Events) och routning av MCP-protokollmeddelanden.

Konfigurera en SSE-klientanslutning och skapa en transport

Klassen SSEPServer är det viktigaste transportskiktet för hantering av Server-Sent händelser (SSE) på MCP-servern. Den använder SSEServerTransport klassen för att hantera enskilda klientanslutningar. Den hanterar flera transporter och deras livscykel.

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

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

Begrepp:

  • Tillståndshantering: Spårar både aktuell transport och alla transporter
  • Sessionsmappning: transports objekt mappar sessions-ID:t till transportinstanser
  • Konstruktordelegering: Konfigurerar omedelbart begärandehanterare

SSE-anslutningsetablering (handleGetRequest)

Metoden handleGetRequest ansvarar för att upprätta en ny SSE-anslutning när en klient gör en GET-begäran till /sse slutpunkten.

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

Begrepp:

  • Skapande av transport: Ny SSEServerTransport för varje GET-begäran
  • Sessionshantering: Automatiskt genererat sessions-ID som lagras i cacheminnet
  • Händelsehanterare: Rensa vid anslutningsstängning
  • MCP-anslutning: server.connect() upprättar protokollanslutning
  • Async-flöde: Anslutningskonfigurationen är asynkron med felgränser

Meddelandebearbetning (handlePostRequest)

Metoden handlePostRequest bearbetar inkommande POST-begäranden för att hantera MCP-meddelanden som skickas av klienten. Den använder sessions-ID:t från frågeparametrarna för att hitta rätt transportinstans.

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

Begrepp:

  • Sessionssökning: Använder sessionId frågeparameter för att hitta transport
  • Sessionsverifiering: Verifierar SSE-anslutningen först.
  • Meddelandedelegering: Transport hanterar faktisk meddelandebearbetning
  • Felsvar: Rätt HTTP-felkoder för sessioner som saknas

Konfiguration av MCP-protokollhanterare (setupServerRequestHandlers)

Metoden setupServerRequestHandlers registrerar följande hanterare för MCP-protokollbegäranden:

  • En hanterare för ListToolsRequestSchema som returnerar listan över tillgängliga TODO-verktyg.
  • En hanterare för CallToolRequestSchema som letar upp och kör det begärda verktyget med de angivna argumenten.

Den här metoden använder Zod-scheman för att definiera de förväntade formaten för begäran och svar.

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

Begrepp:

  • Schema-Based Routning: Använder Zod-scheman för hantering av typsäkra begäranden
  • Verktygsidentifiering: returnerar statisk TodoTools-matris
  • Verktygskörning: CallToolRequestSchema hittar och kör verktyg
  • Felhantering: Korrekt hantering av okända verktyg
  • Svarsformat: MCP-kompatibel svarsstruktur
  • Typsäkerhet: TypeScript-typer säkerställer korrekt argumentöverföring

Affärslogik: tools.ts

Filen tools.ts definierar de faktiska funktioner som är tillgängliga för MCP-klienter:

  • Verktygsmetadata (namn, beskrivning, scheman)
  • Valideringsscheman för indata
  • Verktygets körningslogik
  • Integrering med databasskikt

Den här MCP-servern definierar fyra TODO-hanteringsverktyg:

  • add_todo: Skapar ett nytt TODO-objekt
  • complete_todo: Markerar ett TODO-objekt som slutfört
  • delete_todo: Tar bort ett TODO-objekt
  • list_todos: Visar en lista över alla TODO-objekt
  • update_todo_text: Uppdaterar texten i ett befintligt TODO-objekt

Mönster för verktygsdefinition

Verktygen definieras som en matris med objekt som var och en representerar en specifik TODO-åtgärd. I följande kodfragment addTodo definieras verktyget:

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

Varje verktygsdefinition har:

  • name: Unik identifierare för verktyget
  • description: Kort beskrivning av verktygets syfte
  • inputSchema: Zod-schema som definierar det förväntade indataformatet
  • outputSchema: Zod-schema som definierar det förväntade utdataformatet
  • execute: Funktion som implementerar verktygets logik

Dessa verktygsdefinitioner importeras i server.ts och exponeras via ListToolsRequestSchema hanteraren.

Begrepp:

  • Design av modulära verktyg: Varje verktyg är ett fristående objekt
  • JSON-schemaverifiering: inputSchema definierar förväntade parametrar
  • Typsäkerhet: TypeScript-typer matchar schemadefinitioner
  • Asynkron körning: Alla verktygskörningar är asynkrona
  • Databasintegration: Anropar importerade databasfunktioner
  • Human-Readable Svar: Returnerar formaterade strängar, inte rådata

Export av verktygsmatris

Verktygen exporteras som en statisk matris, vilket gör dem enkla att importera och använda på servern. Varje verktyg är ett objekt med dess metadata och körningslogik. Med den här strukturen kan MCP-servern dynamiskt identifiera och köra verktyg baserat på klientbegäranden.

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

Begrepp:

  • Statisk registrering: Verktyg som definierats vid modulens inläsningstid
  • Matrisstruktur: Enkel matris gör verktygen enkla att iterera
  • Import/Export: En ren separation från serverlogik

Felhantering vid verktygskörning

Varje verktygs execute funktion hanterar fel smidigt och returnerar tydliga meddelanden i stället för att utlösa undantag. Den här metoden säkerställer att MCP-servern ger en smidig användarupplevelse.

Verktyg hanterar olika felscenarier:

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

Begrepp:

  • Databassvarskontroll: Används info.changes för att identifiera fel
  • Elegant nedbrytning: Returnerar beskrivande felmeddelanden istället för att kasta
  • User-Friendly Fel: Meddelanden som är lämpliga för AI-tolkning

Datalag: db.ts

Filen db.ts hanterar SQLite-databasanslutningen och hanterar CRUD-åtgärder för TODO-appen. Det använder biblioteket better-sqlite3 för synkron databasåtkomst.

Databasinitialisering

Databasen initieras genom att ansluta till SQLite och skapa tabeller om de inte finns. Följande kodfragment visar initieringsprocessen:

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

Begrepp:

  • In-Memory Database: :memory: innebär att data går förlorade vid omstart (endast demo/testning)
  • WAL-läge: Write-Ahead loggning för bättre prestanda
  • Schemadefinition: Enkel TODO-tabell med autoincrement-ID
  • Felhantering: Korrekt hantering av initieringsfel
  • Loggningsintegrering: Databasåtgärder loggas för felsökning

CRUD-åtgärdsmönster

Filen db.ts innehåller fyra huvudsakliga CRUD-åtgärder för att hantera TODO-objekt:

Skapa åtgärd:

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

Läsoperation:

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

Uppdateringsåtgärd:

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

Ta bort åtgärd:

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

Begrepp:

  • Förberedda instruktioner: Skydd mot SQL-inmatning
  • Typgjutning: Tydligt definierade TypeScript-typer för frågeresultat
  • Datatransformering: Konvertera SQLite-heltal till booleska värden
  • Atomiska åtgärder: Varje funktion är en enskild databastransaktion
  • Returvärdeskonsekvens: Funktioner returnerar operationsmetadata
  • Defensiv programmering: Mönster för kontroll före borttagning

Schemadesign

Databasschemat definieras i db.ts filen med hjälp av en enkel SQL-instruktion. Tabellen todos har tre fält:

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

Hjälpverktyg: helpers/ katalog

Katalogen helpers/ innehåller verktygsfunktioner och klasser för servern.

Strukturerad loggning för felsökning och övervakning: helpers/logs.ts

Filen helpers/logs.ts innehåller ett strukturerat loggningsverktyg för MCP-servern. Det använder biblioteket debug för loggning och chalk för färgkodade utdata i konsolen.

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

Sessionshantering för SSE-överföringar: helpers/cache.ts

Filen helpers/cache.ts använder en Map för att lagra SSE-transporter med sessions-ID. Med den här metoden kan servern snabbt hitta och hantera aktiva anslutningar.

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

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

Anmärkning

TransportsCache är en enkel minnesintern cache. Överväg att använda en mer robust lösning som Redis eller en databas för sessionshantering i produktion.

Sammanfattning av exekveringsflöde

Följande diagram visar den fullständiga begäranderesan från klienten till MCP-servern och tillbaka, inklusive verktygskörning och databasåtgärder:

Diagram som visar hela begäranderesan från klienten till MCP-servern och tillbaka.

Städa upp GitHub Codespaces

Ta bort GitHub Codespaces-miljön för att maximera dina kostnadsfria kärntimmar.

Viktigt!

Mer information om ditt GitHub-kontos kostnadsfria lagrings- och kärntimmar finns i GitHub Codespaces månadsvis inkluderade lagrings- och kärntimmar.

  1. Logga in på GitHub Codespaces-kontrollpanelen.

  2. Hitta dina aktiva Codespaces som skapats från GitHub-lagret Azure-Samples//mcp-container-ts.

  3. Öppna snabbmenyn för kodområdet och välj Ta bort.

Få hjälp

Rapportera ditt ärende i lagringsplatsens ärenden.