Deno + Lambda Web Adapterを使ってリモートMCPサーバーをLambdaで動かす

Deno + Lambda Web Adapterを使ってリモートMCPサーバーをLambdaで動かす

Clock Icon2025.06.25

はじめに

Model Context Protocol(MCP)は、Claude Code や Claude Desktop などの MCP クライアントが外部システムと連携するためのプロトコルです。最近では、Claude Code がリモート MCP サーバーに対応したため、Deno + Lambda Web Adapter を利用した Lambda 上で動くリモート MCP サーバーを実装してみました。

Claude CodeのリモートMCPサーバーの対応については、以下のブログがわかりやすいです。

https://dev.classmethod.jp/articles/shuntaka-support-claude-code-remote-mcp/

技術スタック

今回の実装で使用した技術は以下の通りです:

参考実装

https://github.com/briete/remote-mcp-server-sample

リモートMCPサーバーをLambdaで動かす

MCP サーバーの実装方法は主に以下の 3 つがあります。

  • stdio (標準入出力による通信)
  • SSE (Server-Sent Events: Streamable HTTP により現在は非推奨な扱い)
  • Streamable HTTP (2025-03-26 のプロトコルバージョンから追加)

stdio についてはローカル MCP サーバー(ローカルの PC で起動しておく方式)用なので、リモート MCP サーバーについては SSE (Server-Sent Events) または Streamable HTTP を利用することになります。最新の MCP の仕様では SSE は非推奨な扱いになっているので、ここでは Streamable HTTP を利用する方法で実装します。実装自体は Express.js による Web API のアプリケーションを実装する方法になります。

今回は簡易的な Basic 認証を利用した方式で実装しましたが、2025-03-26 バージョンの仕様から OAuth 2.1 に基づく認証仕様の追加がありました。自分でも試してみましたがうまく動かなかったため、こちらについては引き続き調査をしたいと思っています。

Streamable HTTP を利用する方式では、以下の 3 つの /mcp エンドポイントのうち必要なものを実装する必要があります。

  • POST /mcp : 必須
  • GET /mcp : SSE エンドポイントとの互換性のために実装(SSE がない場合は不要)
  • DELETE /mcp : ステートフル MCP サーバーを実装する場合は必須

今回実装するのはステートレスな MCP サーバですので、POST /mcp のみ実装することになります。

MCPサーバーの実装

コイントスをする簡易的な MCP サーバーを実装します。

server.ts

import { McpServer } from "npm:@modelcontextprotocol/sdk/server/mcp.js";

export function createMCPServer() {
  const server = new McpServer({
    name: "sample-mcp-server",
    version: "1.0.0",
  });

  // コイントスツール
  server.registerTool(
    "coin_flip",
    {
      title: "コイントス",
      description: "コインを投げて表(heads)か裏(tails)を返します",
      inputSchema: {},
    },
    () => {
      const result = Math.random() < 0.5 ? "表(heads)" : "裏(tails)";
      return {
        content: [{
          type: "text" as const,
          text: `コインの結果: ${result}`,
        }],
      };
    },
  );

  return server;
}

Expressのアプリケーションの実装

MCP TypeScript SDK の StreamableHTTPServerTransport を利用します。この辺の実装は以下の MCP TypeScript SDK の Streamable HTTPのWith Session Management (stateless) を参考にしています。ステートレス MCP サーバーの実装なのでセッション管理は行わないシンプルな構成です。

https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#streamable-http

index.ts

import express, { type Request, type Response } from "npm:express";
import cors from "npm:cors";
import basicAuth from "npm:basic-auth";
import { StreamableHTTPServerTransport } from "npm:@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createMCPServer } from "./server.ts";

const PORT = parseInt(Deno.env.get("PORT") || "8080");
const USERNAME = Deno.env.get("USERNAME");
const PASSWORD = Deno.env.get("PASSWORD");

if (!USERNAME || !PASSWORD) {
  Deno.exit(1);
}

const app = express();

app.use(cors({
  origin: true,
  credentials: true,
}));

app.use(express.json());

// Basic認証
const authenticate = (req: Request, res: Response, next: () => void) => {
  const credentials = basicAuth(req);

  if (!credentials || credentials.name !== USERNAME || credentials.pass !== PASSWORD) {
    res.status(401).set("WWW-Authenticate", 'Basic realm="Sample MCP Server"').json({
      error: "Authentication required",
    });
    return;
  }

  next();
};

// Streamable HTTPのエンドポイント
app.post("/mcp", authenticate, async (req: Request, res: Response) => {
  try {
    const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
    });
    const server = createMCPServer();

    res.on("close", () => {
      console.log("Request closed");
      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,
      });
    }
  }
});

app.listen(PORT, () => {
  console.log(`MCP Server running on port ${PORT}`);
  console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
});

// Lambda関数URL用のエクスポート
export default app;

今回はステートレスな MCP サーバーですが、ステートフルな MCP サーバーを実装する場合は以下の仕様に沿った MCP サーバーを実装する必要があります。

https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management

MCP TypeScript SDK にはセッション管理を含めたステートフル MCP サーバーの実装方法も記載されているので参考にしてみてください。

https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#with-session-management

Lambda Web Adapter用のDockerfile

Lambda Web Adapter を使うことで、Express.js のようなWebフレームワークをそのまま Lambda の上で動かすことができます。Lambda コンテナを使ってデプロイするため、Lambda Web Adapter を利用する Dockerfile を作成します。

FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS aws-lambda-adapter
FROM denoland/deno:bin-2.3.6 AS deno_bin
FROM debian:bookworm-20230703-slim AS deno_runtime

COPY --from=aws-lambda-adapter /lambda-adapter /opt/extensions/lambda-adapter
COPY --from=deno_bin /deno /usr/local/bin/deno

EXPOSE 8080
RUN mkdir /var/deno_dir
ENV DENO_DIR=/var/deno_dir

WORKDIR "/var/task"
COPY . /var/task

# Warmup caches
RUN timeout 20s deno task start || [ $? -eq 124 ] || exit 1

CMD ["deno", "task", "start"]

ECRにDockerイメージをデプロイしておく

ECR レジストリを別で作っておきます。以下のシェルスクリプトを使って Dockerfile のビルドとイメージの Push を行います。

#!/bin/bash

set -e

REGION="ap-northeast-1"
ECR_REGISTRY="123456789012.dkr.ecr.ap-northeast-1.amazonaws.com"
IMAGE_NAME="sample-mcp-server"

aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ECR_REGISTRY

docker build --platform=linux/amd64 -t $IMAGE_NAME .

docker tag $IMAGE_NAME:latest $ECR_REGISTRY/$IMAGE_NAME:latest

docker push $ECR_REGISTRY/$IMAGE_NAME:latest

echo "Deployed to $ECR_REGISTRY/$IMAGE_NAME:latest"

AWS CDKによるLambdaの構築

AWS CDK を使って、Lambda コンテナーを利用した Lambda 関数を作成します。今回は API Gateway は使わず AWS Lambda の関数 URL(Function URLs) の機能を利用し、Lambda 単体で HTTP エンドポイントを公開する構成にしました。Lambda 関数のみデプロイするだけで良くなり、MCP サーバーのようなシンプルな構成のアプリケーションにはオススメです。

app.ts:

#!/usr/bin/env -S deno run --allow-all

import { App } from "npm:aws-cdk-lib";
import { SampleMcpServerStack } from "./stack.ts";

const app = new App();

const env = {
    account: Deno.env.get("CDK_DEFAULT_ACCOUNT"),
    region: Deno.env.get("CDK_DEFAULT_REGION"),
};

const appContext: AppContext = app.node.tryGetContext("config");
if (!appContext) {
    console.error("Error: 'env' context not found in cdk.json");
    Deno.exit(1);
}

new SampleMcpServerStack(app, 'SampleMcpServerStack', {
    appContext,
    env,
});

export interface AppContext {
    ecrRepoName: string;
}

stack.ts:

import { Duration, Stack, StackProps } from "npm:aws-cdk-lib";
import * as lambda from "npm:aws-cdk-lib/aws-lambda";
import * as ecr from "npm:aws-cdk-lib/aws-ecr";
import * as iam from "npm:aws-cdk-lib/aws-iam";
import { Construct } from "npm:constructs";
import { AppContext } from "./app.ts";

export interface SampleMcpServerStackProps extends StackProps {
    appContext: AppContext;
}

export class SampleMcpServerStack extends Stack {
    public readonly mcpServerFunction: lambda.Function;

    constructor(scope: Construct, id: string, props: SampleMcpServerStackProps) {
        super(scope, id, props);

        const lambdaRole = new iam.Role(this, 'McpServerLambdaRole', {
            assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
            managedPolicies: [
                iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
            ]
        });

        this.mcpServerFunction = new lambda.Function(this, 'McpServerFunction', {
            functionName: 'remote-mcp-server-sample',
            code: lambda.Code.fromEcrImage(
                ecr.Repository.fromRepositoryName(
                    this,
                    `McpServerEcrRepo`,
                    props.appContext.ecrRepoName
                )
            ),
            environment: {
                USERNAME: "hoge",
                PASSWORD: "hoge",
            },
            handler: lambda.Handler.FROM_IMAGE,
            runtime: lambda.Runtime.FROM_IMAGE,
            role: lambdaRole,
            timeout: Duration.seconds(30),
            memorySize: 256,
        });

        this.mcpServerFunction.addFunctionUrl({
            authType: lambda.FunctionUrlAuthType.NONE,
            cors: {
                allowCredentials: true,
                allowedHeaders: ["*"],
                allowedMethods: [lambda.HttpMethod.ALL],
                allowedOrigins: ["*"],
                maxAge: Duration.days(1),
            },
        });
    }

Deno 経由で AWS CDK でデプロイします。

deno run --allow-all npm:aws-cdk --app 'deno run --allow-all cdk/app.ts' deploy

MCPクライアントから利用する

今回は Claude Code から利用してみたいと思います。AWS コンソールからデプロイした Lambda 関数のページに移動すると関数 URL が取得できると思いますので、こちらをコピーします。

remote-mcp-server-sample___関数___Lambda

以下のコマンドで先ほどコピーしたURLを設定し Claude Code に Streamable HTTP のリモート MCP サーバーを登録します。

claude mcp add mcp-server-sample --transport http --header "Authorization: Basic $(echo -n 'username:password' | base64)" -- https://xxx.lambda-url.ap-northeast-1.on.aws/mcp

「コイントスして」と Claude Code のプロンプトに入力すると、Lambda の MCP サーバーにアクセスされコイントスの結果が表示されました。

╭─────────────────────────────────────────────────────────────────────╮
│ ✻ Welcome to Claude Code!                                           │
│                                                                     │
│   /help for help, /status for your current setup                    │
│                                                                     │
│   cwd: /Users/sato.naoya/WebstormProjects/remote-mcp-server-sample  │
╰─────────────────────────────────────────────────────────────────────╯

 ※ Tip: Run /install-github-app to tag @claude right from your Github issues and PRs

╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ > コイントスして                                                                                                                                                            │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
> コイントスして
  ⎿  コインの結果: 裏(tails)

⏺ 裏(tails)が出ました。

まとめ

Deno と Lambda Web Adapter を使用して AWS Lambda 上で動作するリモート MCP サーバーを実装する方法を紹介しました。Lambda Web Adapter と Lambda Functions URLs を利用することで Lambda のみで完結するリモートMCPサーバーを作成することができます。

参考リンク

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.