Condividi tramite


Integrare un'app di Servizio App come server MCP per GitHub Copilot Chat (Node.js)

In questa esercitazione si apprenderà come esporre le funzionalità di un'app Express.js tramite il protocollo MCP (Model Context Protocol), aggiungerlo come strumento a GitHub Copilot e interagire con l'app usando il linguaggio naturale in modalità agente di Chat Copilot.

Screenshot che mostra gitHub Copilot che chiama il server Todos MCP ospitato nel servizio app di Azure.

Se l'applicazione Web dispone già di funzionalità utili, ad esempio shopping, prenotazione hotel o gestione dei dati, è facile rendere disponibili queste funzionalità per:

Aggiungendo un server MCP all'app Web, si abilita un agente per comprendere e usare le funzionalità dell'app quando risponde alle richieste degli utenti. Ciò significa che qualsiasi operazione che l'app può fare, anche l'agente può fare.

  • Aggiungere un server MCP alla web app.
  • Testare il server MCP in locale in modalità agente Chat di GitHub Copilot.
  • Distribuire il server MCP nel servizio app di Azure e connetterlo in GitHub Copilot Chat.

Prerequisites

Questa esercitazione presuppone che si stia usando l'esempio usato in Esercitazione: Distribuire un'app Web Node.js + MongoDB in Azure.

Aprire almeno l'applicazione di esempio in GitHub Codespaces e distribuire l'app eseguendo azd up.

Aggiungere un server MCP all'app Web

  1. Nel terminale codespace aggiungere i pacchetti npm necessari al progetto:

    npm install @modelcontextprotocol/sdk@latest zod
    
    1. Aprire route/index.js. Per semplicità dello scenario, si aggiungerà qui tutto il codice del server MCP.
  2. All'inizio di routes/index.js, aggiungere le seguenti istruzioni richieste:

    const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
    const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
    const { z } = require('zod');
    
  3. Nella parte inferiore del file, sopra module.exports = router; aggiungere la seguente route per il server MCP.

    router.post('/api/mcp', async function(req, res, next) {
      try {
        // Stateless server instance for each request
        const server = new McpServer({
          name: "task-crud-server", 
          version: "1.0.0"
        });
    
        // Register tools
        server.registerTool(
          "create_task",
          {
            description: 'Create a new task',
            inputSchema: { taskName: z.string().describe('Name of the task to create') },
          },
          async ({ taskName }) => {
            const task = new Task({
              taskName: taskName,
              createDate: new Date(),
            });
            await task.save();
            return { content: [ { type: 'text', text: `Task created: ${JSON.stringify(task)}` } ] };
          }
        );
    
        server.registerTool(
          "get_tasks",
          {
            description: 'Get all tasks'
          },
          async () => {
            const tasks = await Task.find();
            return { content: [ { type: 'text', text: `All tasks: ${JSON.stringify(tasks, null, 2)}` } ] };
          }
        );
    
        server.registerTool(
          "get_task",
          {
            description: 'Get a task by ID',
            inputSchema: { id: z.string().describe('Task ID') },
          },
          async ({ id }) => {
            try {
              const task = await Task.findById(id);
              if (!task) {
                  throw new Error();
              }
              return { content: [ { type: 'text', text: `Task: ${JSON.stringify(task)}` } ] };
            } catch (error) {
                return { content: [ { type: 'text', text: `Task not found with ID: ${id}` } ], isError: true };
            }
          }
        );
    
        server.registerTool(
          "update_task",
          {
            description: 'Update a task',
            inputSchema: {
              id: z.string().describe('Task ID'),
              taskName: z.string().optional().describe('New task name'),
              completed: z.boolean().optional().describe('Task completion status')
            },
          },
          async ({ id, taskName, completed }) => {
            try {
              const updateData = {};
              if (taskName !== undefined) updateData.taskName = taskName;
              if (completed !== undefined) {
                updateData.completed = completed;
                if (completed === true) {
                  updateData.completedDate = new Date();
                }
              }
    
              const task = await Task.findByIdAndUpdate(id, updateData);
              if (!task) {
                throw new Error();
              }
              return { content: [ { type: 'text', text: `Task updated: ${JSON.stringify(task)}` } ] };
            } catch (error) {
              return { content: [ { type: 'text', text: `Task not found with ID: ${id}` } ], isError: true };
            }
          }
        );
    
        server.registerTool(
          "delete_task",
          {
            description: 'Delete a task',
            inputSchema: { id: z.string().describe('Task ID to delete') },
          },
          async ({ id }) => {
            try {
              const task = await Task.findByIdAndDelete(id);
              if (!task) {
                throw new Error();
              }
              return { content: [ { type: 'text', text: `Task deleted successfully: ${JSON.stringify(task)}` } ] };
            } catch (error) {
              return { content: [ { type: 'text', text: `Task not found with ID: ${id}` } ], isError: true };
            }
          }
        );
    
        // Create fresh transport for this request
        const transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: undefined,
        });
    
        // Clean up when request closes
        res.on('close', () => {
          transport.close();
          server.close();
        });
    
        await server.connect(transport);
        await transport.handleRequest(req, res, req.body);
    
      } catch (error) {
        console.error('Error handling MCP request:', error);
        if (!res.headersSent) {
          res.status(500).json({
            jsonrpc: '2.0',
            error: {
              code: -32603,
              message: 'Internal server error',
            },
            id: null,
          });
        }
      }
    });
    

    Questa route imposta l'endpoint server MCP su <url>/api/mcp e usa il modello di modalità senza stato in MCP TypeScript SDK.

    • server.registerTool() aggiunge uno strumento al server MCP con la relativa implementazione.
    • L'SDK usa zod per la convalida dell'input.
    • description nell'oggetto di configurazione e describe() in inputSchema forniscono descrizioni leggibili per gli strumenti e l'input. Consentono all'agente chiamante di comprendere come usare gli strumenti e i relativi parametri.

    Questo percorso duplica la funzionalità create-read-update-delete (CRUD) dei percorsi esistenti, che non è necessaria, ma verrà mantenuto per semplicità. Una procedura consigliata consiste nello spostare la logica dell'app in un modulo, quindi chiamare il modulo da tutte le route.

Testare il server MCP in locale

  1. Nel terminale codespace eseguire l'applicazione con npm start.

  2. Selezionare Apri nel browser e quindi aggiungere un'attività.

    Lasciare npm start in esecuzione. Attualmente, il server MCP è in esecuzione http://localhost:3000/api/mcp.

  3. Nel codespace aprire Copilot Chat e quindi selezionare Modalità agente nella casella di richiesta.

  4. Selezionare il pulsante Strumenti e quindi aggiungi altri strumenti nell'elenco a discesa.

    Screenshot che mostra come aggiungere un server MCP in modalità agente Di Chat di GitHub Copilot.

  5. Selezionare Aggiungi server MCP.

  6. Selezionare HTTP (HTTP o Eventi inviati dal server).

  7. In Immettere l'URL del server digitare http://localhost:3000/api/mcp.

  8. In Immettere l'ID server digitare todos-mcp o qualsiasi nome desiderato.

  9. Selezionare Impostazioni area di lavoro.

  10. Nella nuova finestra di chat di Copilot, digita qualcosa come "Mostrami i 'todos'."

  11. Per impostazione predefinita, GitHub Copilot mostra una conferma di sicurezza quando si richiama un server MCP. Seleziona Continua.

    Screenshot che mostra il messaggio di sicurezza predefinito da una chiamata MCP in GitHub Copilot Chat.

    Verrà visualizzata una risposta che indica che la chiamata allo strumento MCP ha esito positivo.

    Screenshot che mostra la risposta alla chiamata dello strumento MCP nella finestra della chat di GitHub Copilot.

Distribuire il server MCP su App Service

  1. Tornare al terminale codespace, distribuire le modifiche eseguendo il commit delle modifiche (metodo GitHub Actions) o eseguendo azd up (metodo dell'interfaccia della riga di comando per sviluppatori di Azure).

  2. Nell'output AZD trovare l'URL dell'app. L'URL è simile al seguente nell'output AZD:

     Deploying services (azd deploy)
    
       (✓) Done: Deploying service web
       - Endpoint: <app-url>
     
  3. Una volta completato azd up, aprire .vscode/mcp.json. Modificare l'URL in <app-url>/api/mcp.

  4. Sopra la configurazione del server MCP modificata, selezionare Avvia.

    Screenshot che mostra come avviare manualmente un server MCP dal file di mcp.json locale.

  5. Avviare una nuova finestra di Chat di GitHub Copilot. Dovrebbe essere possibile visualizzare, creare, aggiornare ed eliminare attività nell'agente Copilot.

Procedure consigliate per la sicurezza

Quando il server MCP viene chiamato da un agente basato su modelli linguistici di grandi dimensioni (LLM), prestare attenzione agli attacchi di prompt injection. Considerare le procedure consigliate per la sicurezza seguenti:

  • Autenticazione e autorizzazione: proteggere il server MCP con l'autenticazione Microsoft Entra per garantire che solo gli utenti o gli agenti autorizzati possano accedere agli strumenti. Per una guida dettagliata, vedere Proteggere le chiamate MCP (Model Context Protocol) al Servizio app di Azure da Visual Studio Code con l'autenticazione di Microsoft Entra.
  • Convalida e purificazione dell'input: il codice di esempio di questa esercitazione usa zod per la convalida dell'input, assicurandosi che i dati in ingresso corrispondano allo schema previsto. Per maggiore sicurezza, prendere in considerazione:
    • Convalida e purificazione di tutti gli input dell'utente prima dell'elaborazione, in particolare per i campi usati nelle query o nell'output del database.
    • Escape dell'output nelle risposte per impedire lo scripting tra siti (XSS) se l'API viene usata dai browser.
    • Applicazione di schemi rigorosi e valori predefiniti nei modelli per evitare dati imprevisti.
  • HTTPS: L'esempio si basa sul servizio app di Azure, che applica HTTPS per impostazione predefinita e fornisce certificati TLS/SSL gratuiti per crittografare i dati in transito.
  • Principio dei privilegi minimi: esporre solo gli strumenti e i dati necessari per il caso d'uso. Evitare di esporre operazioni sensibili, a meno che non sia necessario.
  • Limitazione della velocità e regolazione: usare Gestione API o middleware personalizzato per evitare attacchi denial-of-service e abusi.
  • Registrazione e monitoraggio: accesso e utilizzo dei log degli endpoint MCP per il controllo e il rilevamento delle anomalie. Monitorare l'attività sospetta.
  • Configurazione CORS: limitare le richieste tra le origini ai domini attendibili se si accede al server MCP dai browser. Per altre informazioni, vedere Abilitare CORS.
  • Aggiornamenti regolari: mantenere aggiornate le dipendenze per attenuare le vulnerabilità note.

Altre risorse

Integrare l'intelligenza artificiale nelle applicazioni del servizio app di Azure