Поделиться через


Руководство: Развертывание сервера Node.js MCP в Azure Container Apps

В этом руководстве вы создадите сервер протокола MCP, предоставляющий инструменты управления задачами с помощью Express и TypeScript-SDK для MCP. Вы развертываете сервер в приложениях контейнеров Azure и подключаетесь к нему из чата GitHub Copilot в VS Code.

Изучив это руководство, вы:

  • Создание приложения Express, которое предоставляет средства MCP
  • Тестирование сервера MCP локально с помощью GitHub Copilot
  • Контейнеризация и развертывание приложения в приложениях контейнеров Azure
  • Подключите GitHub Copilot к развернутому серверу MCP

Предпосылки

Создайте каркас приложения

В этом разделе описано, как создать проект Node.js с помощью Express и пакета SDK ДЛЯ MCP TypeScript.

  1. Создайте каталог проекта и инициализируйте его:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    npm init -y
    
  2. Установите зависимости:

    npm install @modelcontextprotocol/sdk express zod
    npm install -D typescript @types/node @types/express tsx
    
  3. Создание tsconfig.json:

    {
        "compilerOptions": {
            "target": "ES2022",
            "module": "Node16",
            "moduleResolution": "Node16",
            "outDir": "./dist",
            "rootDir": "./src",
            "strict": true,
            "esModuleInterop": true,
            "declaration": true
        },
        "include": ["src/**/*"]
    }
    

    Эта конфигурация предназначена для ES2022 с модулем разрешения Node.js, выводом скомпилированных файлов в dist/, и включением строгой проверки типов.

  4. Обновите package.json , чтобы включить модули ES и добавить скрипты сборки и запуска. Добавьте или замените поля type и scripts.

    {
        "type": "module",
        "scripts": {
            "build": "tsc",
            "start": "node dist/index.js",
            "dev": "tsx watch src/index.ts"
        }
    }
    

    Это важно

    Задайте "type": "module". Код сервера MCP использует верхний уровень await, который поддерживается только в модулях ES.

  5. Создайте src/taskStore.ts для хранилища данных в памяти:

    export interface TaskItem {
        id: number;
        title: string;
        description: string;
        isComplete: boolean;
        createdAt: string;
    }
    
    class TaskStore {
        private tasks: TaskItem[] = [
            {
                id: 1,
                title: "Buy groceries",
                description: "Milk, eggs, bread",
                isComplete: false,
                createdAt: new Date().toISOString(),
            },
            {
                id: 2,
                title: "Write docs",
                description: "Draft the MCP tutorial",
                isComplete: true,
                createdAt: new Date(Date.now() - 86400000).toISOString(),
            },
        ];
        private nextId = 3;
    
        getAll(): TaskItem[] {
            return [...this.tasks];
        }
    
        getById(id: number): TaskItem | undefined {
            return this.tasks.find((t) => t.id === id);
        }
    
        create(title: string, description: string): TaskItem {
            const task: TaskItem = {
                id: this.nextId++,
                title,
                description,
                isComplete: false,
                createdAt: new Date().toISOString(),
            };
            this.tasks.push(task);
            return task;
        }
    
        toggleComplete(id: number): TaskItem | undefined {
            const task = this.tasks.find((t) => t.id === id);
            if (!task) return undefined;
            task.isComplete = !task.isComplete;
            return task;
        }
    
        delete(id: number): boolean {
            const index = this.tasks.findIndex((t) => t.id === id);
            if (index < 0) return false;
            this.tasks.splice(index, 1);
            return true;
        }
    }
    
    export const store = new TaskStore();
    

    Интерфейс TaskItem определяет структуру данных задачи. Класс TaskStore управляет массивом в памяти, предварительно заполненным примерами данных и предоставляет методы для перечисления, поиска, создания, переключения и удаления задач. Экспортируется объект-одиночка на уровне модуля для использования инструментами MCP.

Определение средств MCP

Затем вы определите сервер MCP с регистрацией инструментов, которые предоставляют хранилище задач клиентам ИИ.

  1. Создание src/index.ts:

    import express, { Request, Response } from "express";
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
    import { z } from "zod";
    import { store } from "./taskStore.js";
    
    const app = express();
    app.use(express.json());
    
    // Health endpoint for Container Apps probes
    app.get("/health", (_req: Request, res: Response) => {
        res.json({ status: "healthy" });
    });
    
    // Create the MCP server
    const mcpServer = new McpServer({
        name: "TasksMCP",
        version: "1.0.0",
    });
    
    // Register tools
    mcpServer.tool("list_tasks", "List all tasks with their ID, title, description, and completion status.", {}, async () => {
        return {
            content: [{ type: "text", text: JSON.stringify(store.getAll(), null, 2) }],
        };
    });
    
    mcpServer.tool(
        "get_task",
        "Get a single task by its numeric ID.",
        { task_id: z.number().describe("The numeric ID of the task to retrieve") },
        async ({ task_id }) => {
            const task = store.getById(task_id);
            return {
                content: [
                    {
                        type: "text",
                        text: task ? JSON.stringify(task, null, 2) : `Task with ID ${task_id} not found.`,
                    },
                ],
            };
        }
    );
    
    mcpServer.tool(
        "create_task",
        "Create a new task with the given title and description. Returns the created task.",
        {
            title: z.string().describe("A short title for the task"),
            description: z.string().describe("A detailed description of what the task involves"),
        },
        async ({ title, description }) => {
            const task = store.create(title, description);
            return {
                content: [{ type: "text", text: JSON.stringify(task, null, 2) }],
            };
        }
    );
    
    mcpServer.tool(
        "toggle_task_complete",
        "Toggle a task's completion status between complete and incomplete.",
        { task_id: z.number().describe("The numeric ID of the task to toggle") },
        async ({ task_id }) => {
            const task = store.toggleComplete(task_id);
            const msg = task
                ? `Task ${task.id} is now ${task.isComplete ? "complete" : "incomplete"}.`
                : `Task with ID ${task_id} not found.`;
            return { content: [{ type: "text", text: msg }] };
        }
    );
    
    mcpServer.tool(
        "delete_task",
        "Delete a task by its numeric ID.",
        { task_id: z.number().describe("The numeric ID of the task to delete") },
        async ({ task_id }) => {
            const deleted = store.delete(task_id);
            const msg = deleted ? `Task ${task_id} deleted.` : `Task with ID ${task_id} not found.`;
            return { content: [{ type: "text", text: msg }] };
        }
    );
    
    // Mount the MCP streamable HTTP transport
    const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
    
    app.post("/mcp", async (req: Request, res: Response) => {
        await transport.handleRequest(req, res, req.body);
    });
    
    app.get("/mcp", async (req: Request, res: Response) => {
        await transport.handleRequest(req, res);
    });
    
    app.delete("/mcp", async (req: Request, res: Response) => {
        await transport.handleRequest(req, res);
    });
    
    // Connect the transport to the MCP server
    await mcpServer.connect(transport);
    
    // Start the Express server
    const PORT = parseInt(process.env.PORT || "3000", 10);
    app.listen(PORT, () => {
        console.log(`MCP server running on http://localhost:${PORT}/mcp`);
    });
    

    Основные моменты:

    • McpServer в пакете SDK TypeScript определяет сервер MCP с регистрацией инструментов.
    • StreamableHTTPServerTransport обрабатывает протокол HTTP, доступный для потоковой передачи MCP. Параметр sessionIdGenerator: undefined запускает сервер в режиме без отслеживания состояния.
    • Средства используют схемы Zod для определения входных параметров с описанием.
    • Для проверок работоспособности контейнерных приложений требуется отдельная /health конечная точка.

Тестирование сервера MCP локально

Перед развертыванием в Azure убедитесь, что сервер MCP работает, запустив его локально и подключившись к нему с помощью GitHub Copilot.

  1. Запустите сервер разработки:

    npx tsx src/index.ts
    
  2. Откройте VS Code, откройте чат Copilot и выберите режим агента .

  3. Нажмите кнопку "Сервис" , а затем нажмите кнопку "Добавить дополнительные инструменты" ...>Добавьте СЕРВЕР MCP.

  4. Выберите HTTP (HTTP или Server-Sent события).

  5. Введите URL-адрес сервера: http://localhost:3000/mcp

    Замечание

    Локальный сервер разработки по умолчанию использует порт 3000. При контейнеризации Dockerfile задает переменной среды PORT значение 8080, чтобы сопоставить с целевым портом приложений контейнеров.

  6. Введите идентификатор сервера: tasks-mcp

  7. Выберите параметры рабочей области.

  8. Проверка с помощью запроса : "Показать мне все задачи"

  9. Выберите Продолжить, когда Copilot запрашивает подтверждение вызова средства.

Вы увидите, что Copilot возвращает список задач из хранилища в памяти.

Подсказка

Попробуйте использовать другие запросы, такие как "Создать задачу для проверки pr", "Пометить задачу 1 как завершенную" или "Удалить задачу 2".

Помещение приложения в контейнер

Упаковайте приложение как контейнер Docker, чтобы протестировать его локально перед развертыванием в Azure.

  1. DockerfileСоздайте:

    FROM node:20-slim AS build
    WORKDIR /app
    COPY package*.json .
    RUN npm ci
    COPY tsconfig.json .
    COPY src/ src/
    RUN npm run build
    
    FROM node:20-slim
    WORKDIR /app
    COPY package*.json .
    RUN npm ci --omit=dev
    COPY --from=build /app/dist ./dist
    ENV PORT=8080
    EXPOSE 8080
    CMD ["node", "dist/index.js"]
    

    Многоэтапная сборка компилирует TypeScript на первом этапе, а затем создает рабочий образ только с зависимостями среды выполнения и скомпилированных выходных данных JavaScript. Переменная PORT среды имеет значение 8080, чтобы соответствовать целевому порту контейнерных приложений.

  2. Проверьте локально:

    docker build -t tasks-mcp-server .
    docker run -p 8080:8080 tasks-mcp-server
    

    Подтвердить: curl http://localhost:8080/health

Развертывание в приложениях контейнеров Azure

После контейнеризации приложения разверните его в azure Container Apps с помощью Azure CLI. Команда az containerapp up создает образ контейнера в облаке, поэтому для этого шага не требуется Docker.

  1. Задайте переменные среды:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-node"
    
  2. Создайте группу ресурсов:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
  3. Создайте среду приложений контейнеров:

    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  4. Разверните приложение контейнера:

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  5. Настройте CORS, чтобы разрешить запросы GitHub Copilot.

    az containerapp ingress cors enable \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --allowed-origins "*" \
        --allowed-methods "GET,POST,DELETE,OPTIONS" \
        --allowed-headers "*"
    

    Замечание

    Для производственной среды замените подстановочные знаки * определенными доверенными источниками. См. руководство по серверам Secure MCP в контейнерных приложениях.

  6. Проверьте развертывание:

    APP_URL=$(az containerapp show \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --query "properties.configuration.ingress.fqdn" -o tsv)
    
    curl https://$APP_URL/health
    

Подключите GitHub Copilot к развернутому серверу

Теперь, когда сервер MCP запущен в Azure, настройте VS Code для подключения GitHub Copilot к развернутой конечной точке.

  1. В проекте создайте или обновите .vscode/mcp.json:

    {
        "servers": {
            "tasks-mcp-server": {
                "type": "http",
                "url": "https://<your-app-fqdn>/mcp"
            }
        }
    }
    

    Замените <your-app-fqdn> полным доменным именем из выходных данных развертывания.

  2. В VS Code откройте чат Copilot в режиме агента.

  3. Если сервер не появляется автоматически, выберите кнопку «Сервис» и проверьте, отображается ли tasks-mcp-server. При необходимости нажмите кнопку "Пуск ".

  4. Проверьте, как сервер MCP отвечает на запрос "Перечислить все мои задачи", чтобы убедиться в его правильной работе после развертывания.

Настройка масштабирования для интерактивного использования

По умолчанию приложения контейнеров Azure могут масштабироваться до нуля реплик. Для серверов MCP, которые обслуживают интерактивных клиентов, таких как Copilot, холодные запуски вызывают заметные задержки. Задайте минимальное число реплик для поддержания по крайней мере одного экземпляра:

az containerapp update \
    --name $APP_NAME \
    --resource-group $RESOURCE_GROUP \
    --min-replicas 1

Вопросы безопасности

В этом руководстве для простоты используется неавтоентизованный сервер MCP. Перед запуском сервера MCP в рабочей среде ознакомьтесь со следующими рекомендациями. Когда агент, работающий на больших языковых моделях (LLM), вызывает сервер MCP, учитывайте атаки инъекции команд.

  • Проверка подлинности и авторизация. Защита сервера MCP с помощью идентификатора Microsoft Entra. См. раздел "Безопасные серверы MCP" в приложениях контейнеров.
  • Проверка входных данных: схемы Zod обеспечивают безопасность типов, но добавляют проверку бизнес-правила для параметров средства. Рассмотрите такие библиотеки, как zod-express-middleware для проверки на уровне запроса.
  • HTTPS: приложения контейнеров Azure по умолчанию применяют ПРОТОКОЛ HTTPS с автоматическими сертификатами TLS.
  • Минимальные привилегии: предоставляйте только те инструменты, которые требуются для вашего случая. Избегайте средств, выполняющих разрушительные операции без подтверждения.
  • CORS: ограничение разрешенных источников доверенным доменам в рабочей среде.
  • Ведение журнала и мониторинг: Запись вызовов инструментов MCP для аудита. Используйте Azure Monitor и Log Analytics.

Очистите ресурсы

Если вы не планируете продолжать использовать это приложение, удалите группу ресурсов, чтобы удалить все ресурсы, созданные в этом руководстве:

az group delete --resource-group $RESOURCE_GROUP --yes --no-wait

Следующий шаг