15
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【簡単】Honoで作る!MCPサーバー on AWS Lambda(認証なし / APIキー認証 / OAuth認証)

Posted at

こちらの投稿を多くの方に見ていただきました。ありがとうございます。

今回はMCPの公式SDKとHonoを使ってMCPサーバーを作ってみる手順を紹介します。

こちらの記事を参考にさせていただきました。というか、ほぼそのままです(笑)

HonoでMCPサーバーを動かす
https://zenn.dev/georgia1/articles/dd4fb566e470fe

単純なMCPサーバーを構築するだけだととても簡単にできたので、オプションとして認証を付けた3パターンを紹介します。

  • 認証なし
  • APIキーによる認証
  • OAuth認証

ソースコードはすべてGitHubに公開しているので実際に構築される方はこちらも参照してください。

余談ですが、今回初めてHonoを使いました。
めっちゃお手軽かつ、Lambdaと相性がいいなと感じまして、今後も使っていきたいなと思います。

認証なし版

こちらのSAMプロジェクトです。

ライブラリーを追加します。

npm add @modelcontextprotocol/sdk hono fetch-to-node

ポイントをまとめるとこのようになります。

  • AWS Lambda上で動作させるため、Expressの代わりにHonoを使う
  • Streamable HTTP仕様をステートレスで使う
  • MCPのTypeScript SDKとHonoの互換性のためfetch-to-nodeを使う

このことだけ気をつければ、ほぼ公式SDKのサンプルのままです。

diffのマークが付いているところがExpressの場合との差分です

app.ts
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
+ import { toFetchResponse, toReqRes } from 'fetch-to-node';
+ import { Hono } from 'hono';
+ import { handle } from 'hono/aws-lambda';
  import { z } from 'zod';
  
+ const app = new Hono();
  
  const server = new McpServer({
      name: 'example-server',
      version: '1.0.0',
  });
  
  server.registerTool(
      'calculate-bmi',
      {
          description: 'Calculate Body Mass Index',
          inputSchema: {
              weightKg: z.number(),
              heightM: z.number(),
          },
      },
      async ({ weightKg, heightM }) => ({
          content: [
              {
                  type: 'text',
                  text: String(weightKg / (heightM * heightM)),
              },
          ],
      }),
  );
  
  app.post('/mcp', async (c) => {
      console.log('Received POST MCP request');
  
      const body = await c.req.json();
  
      try {
          const transport = new StreamableHTTPServerTransport({
+             sessionIdGenerator: undefined,
+             enableJsonResponse: true,
          });
  
+         const { req, res } = toReqRes(c.req.raw);
          res.on('close', () => {
              transport.close();
              server.close();
          });
  
          await server.connect(transport);
          await transport.handleRequest(req, res, body);
+         return await toFetchResponse(res);
      } catch (error) {
          console.error('Error handling MCP request:', error);
          return c.json(
              {
                  jsonrpc: '2.0',
                  error: {
                      code: -32603,
                      message: 'Internal server error',
                  },
                  id: null,
              },
              500,
          );
      }
  });
  
+ export const lambdaHandler = handle(app);

ビルドしてデプロイします。

sam build && sam deploy

API GatewayのURLの末尾に/mcpを付与したURLがエンドポイントとなります。

https://**********.execute-api.us-east-1.amazonaws.com/mcp

VSCodeをMCPクライアントとして使用する場合は、mcp.jsonにURLを指定します。

.vscode/mcp.json
{
    "servers": {
        "simple-mcp": {
            "url": "https://**********.execute-api.us-east-1.amazonaws.com/mcp"
        }
    }
}

APIキー認証版

こちらのSAMプロジェクトです。

認証なしとの違いは、HonoのBearer Auth Middlewareを使用し、HTTPヘッダーで認証を行います。

bearerAuthをインポートし、ミドルウェアを作成します。作成したミドルウェアをAPIの定義時に指定します。

以下の場合は、環境変数にセットしたAPI_KEYが唯一正しいAPIキーとなります。複数のキーを指定する方法や関数でチェックすることもできるようです。(あくまでサンプルとして環境変数を使用してますので、機密情報の取り扱いはご注意ください)

import { bearerAuth } from 'hono/bearer-auth';
const authMiddleware = bearerAuth({
    token: process.env.API_KEY || '',
    invalidTokenMessage: 'Invalid API key',
    noAuthenticationHeaderMessage: 'Authorization header is required',
});
app.post('/mcp', authMiddleware, async (c) => {

VSCodeをMCPクライアントとして使用する場合は、mcp.jsonにURLとヘッダーを指定します。

JSONに認証情報を記載するのはあまりおすすめできないため、inputsとして設定するほうが良いと思います。設定方法は公式ドキュメントを参照ください。こちら

.vscode/mcp.json
{
    "servers": {
        "apikey-authorization": {
            "url": "https://**********.execute-api.us-east-1.amazonaws.com/mcp",
            "headers": {
                "Authorization": "Bearer your-secret-api-key-here"
            }
        },
    }
}

OAuth認証

こちらのSAMプロジェクトです。

さて、OAuth認証対応版ですが、ここまでと異なり、かなり複雑です。

というのも、MCPの認証の仕様として、Dynamic Client Registrationというプロトコルに対応する必要があります。(こちら

Amazon Cognitoが残念ながらDynamic Client Registrationに対応しておらず、不足している部分を自前の実装で対応しました。正規の認証フローと異なる部分もあるので、セキュリティ的にあまりよろしくない可能性も高いのですが、「一応動くもの」として紹介させてもらいます。

実装の詳細はGitHubを参照してもらうとして、ここではDynamic Client Registrationのフローを眺めてみようと思います。

Phase 1: Initial MCP Request (Unauthenticated)

まずは、認証情報なしに、/mcpエンドポイントにPOSTリクエストを送信します。認証が必要なので、401で返答するのですが、この際にHTTPヘッダーへWWW-Authenticateを付与します。WWW-Authenticateには次のステップで使用する/.well-known/oauth-protected-resourceエンドポイントのURLが含まれます。

API Gatewayのハマりポイントとして、REST APIの場合のみ、 レスポンスのHTTPヘッダーに含めたWWW-AuthenticateX-Amzn-Remapped-WWW-Authenticateに置き換わります。回避方法が見つけられませんでした。

そのため、HTTP API(新しい方)を使うとこの制約はないので、こちらを採用しております。

数日ハマりましたね、これ。

https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html

Phase 2: Metadata Discovery

MCPクライアントは、認証に必要なメタデータを取得します。

具体的には/.well-known/oauth-protected-resourceエンドポイントと/.well-known/oauth-authorization-serverにリクエストを送信します。

メタデータとしてこのあと呼び出す/registerauthorizetokenなどのエンドポイント情報が取得します。

Phase 3: Dynamic Client Registration

ここでクライアントを認証サーバーに登録します。登録の結果として、認証処理に必要なClient IDを取得します。

今回実装したものでは、Client IDをCognitoのClinent IDと一致させていますが、Dynamic Client Registrationの仕様としては推奨されない形だと思います。
Cognitoを使用するための制限として割り切ってますが、採用の是非がご検討ください。

Phase 4: Authorization Flow Start

必要な情報が揃ったので、認証処理を開始しますが、ここもひとひねり入ってます。

Cognitoには事前にコールバックURLを登録しておく必要がありますが、MCPクライアントが動的なため、Cognitoには登録されていないコールバックURL(VSCodeが待ち構えるURL)へリダイレクトする必要があります。事前登録済みではないコールバックURLを指定すると、そもそも認証前にエラーとなってしまいます。

その対策として、/callbackとしてエンドポイントを用意し、そこで一度リダイレクトを受けた後、再度MCPクライアントのコールバックURLに再リダイレクトする仕組みとしました。

これもやっていいかはわかりません。。

Phase 5: Cognito Authentication

ここはCognitoのHosted UIでの認証を行うフローです。通常のCognitoのログインと変わりません。
ログインが成功したら、コールバックエンドポイントにリダイレクトします。

Phase 6: OAuth Callback Processing

コールバックエンドポイントは、MCPクライアントがトークン発行に使用するcodeなどの情報を詰め直してMCPクライアントに再リダイレクトを行います。

Phase 7: Token Exchange

いよいよトークンを発行するのですが、ここもプロキシを挟む必要がありました。トークンを発行する際に必要なパラメーターの中にコールバックURLの情報があるため、どうしてもこうなってしまいました。

Phase 8: Authenticated MCP Request

無事、トークンが発行されたので、MCPクライアントは、認証情報を付与して/MCPへリクエストできるようになりました。

まとめ

Honoがいい!

MCPの公式SDKを使いつつ、Lambda上でMCPサーバーが構築できるので色々使い道がありそうです。

OAuth認証がこれでいいかは若干怪しいですが、少なくとも、個人や自社向けのMCPサーバーをサーバーレスで構築できるので、可能性は無限大!

APIキー認証をもうちょっと高度化すれば実用的になりそうに思います。

15
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?