🐫

既存サービスにMCPサーバーを組み込む際の設計ポイント

に公開

はじめに

FinatextのInsurtech事業でインターンをしている保坂です!
昨年6月からインターンをしており、生成AI活用機能やバックエンド開発に携わっています。

今回、保険ビジネスプラットフォーム『Inspire』にMCPを統合する機能開発を担当しました。これといったスタンダードのない新しい領域であるため、今回の実装を紹介し、MCP実装の設計をする上で考えるべき点をまとめていきます。

MCPの概要

そもそもMCPとは、ChatGPTなどのLLMがシステムの機能を呼び出せるようにする、専用窓口のようなものです。よくUSB-Cと例えられることもあります。


https://norahsakal.com/blog/mcp-vs-api-model-context-protocol-explained/?ref=apidog.com より

MCPは、ChatGPT DesktopやClaude Codeなどの呼び出す側であるMCPクライアントと、MCP呼び出しを受け取る側であるMCPサーバーから成り立ちます。
クライアントとサーバーの間では Streamable HTTP を使って通信し、bodyの中では JSON-RPC を用いて指示やデータの送信が行われます。
特に、tools/list でツール一覧を取得し、tools/call でツールを実行する、という流れでMCPの処理が進んでいきます。

この tools/listtools/call の2つがある、という点が重要です。後述する設計において、どのツールを見せるか、どう認可するか、という問題に直結します。

goでMCPを実装する上で

MCP実装と調べると、よくPythonやTypeScriptでの実装例が出てきますが、今回は既存システムがGoで実装されていたため、GoでMCPサーバーを実装しています。

MCPのSDKとして有名なものは、MCP公式のgo-sdk、third-partyのmcp-goがあります。GitHubのStarだけで見れば、mcp-goの方が倍近く多いです。

一方で公式のgo-sdkは、MCP公式SDK一覧でTier 1 SDKとして扱われ、「完全なプロトコル実装」「非実験的機能の実装」「conformance要件」「リリース・ドキュメント・依存関係ポリシー」が求められています。
https://modelcontextprotocol.io/docs/sdk
また、MCPバージョンとの互換表が用意されており、仕様変更が未だ激しいMCPに対してspec追従を担保するためには公式を用いるのが良いかと思います。
https://github.com/modelcontextprotocol/go-sdk

実際、Github Mcp Serverがgo-sdkに移行していたりと採用例も多いです。

設計する上で考えるべき点

ツールの量や、ツール処理の複雑さによって、MCPの設計は多岐にわたります。量や複雑さが似ているものを探して参考にしてください。

1. MCPサーバーをどこに置くか

MCPサーバーを実装する際には、MCP専用の独立サーバーとして切り出す方法と、既存APIサーバーにMCPエンドポイントを追加する方法があります。

分離する場合

メリット

  • アーキテクチャが複雑になりすぎず、MCPの機能を作り込んでもコードを綺麗に保ちやすい
  • スケーリングが独立して行える
  • 技術選定が独立して行えるため、モダンな技術を取り込める

デメリット

  • サーバーが増えるため、運用コストが増える
  • MCPサーバーから通常サーバーにHTTPを飛ばすため、認証認可の設計が複雑になったり、ログを追うのが面倒になったりする

分離せず、同じサーバーにMCPサーバーを載せる場合

メリット

  • 既存のDBやビジネスロジック、認証認可を使いまわせる
  • プロセス内で処理を呼び出すため、ネットワークレイテンシがない

デメリット

  • Streamable HTTPでコネクションが長時間つながる場合、タイムアウト設定に引っ掛かる可能性がある
  • コードベースが肥大化しやすい
  • 単体のMCPサーバーとして公開されている実装が多く、既存APIに載せる参考事例は少なめ

今回は、既存のGoバックエンドに /mcp エンドポイントを追加する構成にしました。既存のUseCase、DBアクセス、認証認可をそのまま利用でき、MCP用に別サーバーから既存APIを呼び直す必要がなかったためです。
そのため、今回のGo採用は「MCPだからGoを選んだ」というより、「既存のGoバックエンドにMCPの入口を追加する設計にした結果、Goで実装するのが自然だった」という位置づけです。

2. 参考にする実装について

何もないところから実装を考えるのは難しいので、既存を参照すると良いです。
特にMCPはまだ実装パターンが固まりきっていないため、最初から自分たちの都合だけで設計すると、MCPらしくない実装になってしまう可能性があります。そのため、公開されているMCPサーバーをいくつか見て、今回の規模や要件に近い部分を参考にしました。
ただし、既存実装をそのまま真似できるわけではありません。公開されているMCPサーバーの多くは独立サーバーとして実装されており、今回のように既存APIサーバーへ組み込む形とは前提が少し違います。
そのため、参考にしたのは「サーバー構成そのもの」よりも、「ツール定義の仕方」「ツールの出し分け」「認可情報との接続」といった考え方の部分です。

GitHub Mcp Server

非常にスタンダードなMCPサーバーです。PATに含まれる認可情報を用いて、tools/list で返ってくるツール一覧を絞り込むことで、ツールに対する認可を実装しています。
ツールの定義方法なども非常に参考になります。

https://github.com/github/github-mcp-server/blob/main/pkg/github/context_tools.go#L44

今回特に参考になったのは、tools/list の時点でユーザーに見せるツールを絞り込んでいる点です。
MCPでは、LLMが tools/list で返されたツール一覧を見て、使うべきツールを判断します。そのため、権限のないツールを一覧に出してしまうと、LLMはそのツールを使えるものとして扱ってしまいます。

https://github.com/github/github-mcp-server

AWS MCP server

利用できるツールの数が非常に多い場合に参考になります。
ツールをそのまま大量に利用可能にするのではなく、手順書を参照できるようにすることで、クライアントに不必要な情報を渡さずに、ツールが多い状況を打破しています。中規模なシステムならそこまでやる必要はないかもしれません。

今回の実装では、ツール数はそこまで多くなかったため、AWS MCP serverのような構成は採用しませんでした。ただし、今後ツール数が増えていく場合には、tools/list に何をどこまで載せるかは再検討が必要だと感じました。

https://github.com/awslabs/mcp

freee-mcp

最近公開されたfreeeのMCPサーバーです。ツールが多くなく、スタンダードなMCPサーバーとして非常に参考になります。既存のSaaS APIをMCP toolとしてどう見せるかという点で参考になりました。
特に、既存のSaaS APIをMCPのツールとしてどうラップするかや、アクセストークンの取り回しなどの実践的な部分で参考になる実装です。

https://github.com/freee/freee-mcp

3. ツールの可視性と認可

MCPサーバー上にある全てのツールを誰でも使える状態というのはセキュリティ上好ましくありません。ここで設計の肝になるのが、従来のWeb API開発とは少し異なるツールの可視性という概念です。

従来のWeb APIであれば、最終的な防御線として実行時に認可を行い、権限がなければ 403 Forbidden を返す設計が一般的です。
しかしMCPの場合、LLMは tools/list で返ってきた一覧を見て、これは自分が使える道具だと認識して推論を始めます。もし権限のないツールを一覧に見せてしまうと、LLMが使えないツールを何度も呼び出してはエラーになる、というループに陥り、UXが大きく崩壊するという欠点があります。

そのため、通常の認可ロジックとは分けて、権限があって見えるツールのみを tools/list で返す、というツールごとの可視性制御を実装する必要があります。

現状のMCPでは、 tools/list の標準的なツール情報だけから、そのユーザーがそのツールを本当に実行可能かを常に厳密に判断できるとは限りません。そのため、サーバー側で tools/list の結果をユーザーごとに絞り込むか、tools/call 側で認可エラーを返す必要があります。tools/list で返ってくるツールの情報にオプションで securitySchemes を載せるべき、のような議論が公式GitHub issuesでも起こっており、MCPの認証・認可は現在進行形で議論されている重要なテーマということがわかります。
https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1488

MCPでこの可視性を制御・設計する上では、以下のようなアプローチが考えられます。

アプローチ1: ミドルウェアでの動的フィルタリング

GitHub MCP Server には、Classic Personal Access Token ではトークンの scope を見て、必要な scope を満たさないツールを tools/list から隠す挙動があります。
リクエストの入り口にツール用のミドルウェアのような機構を挟み、トークンの権限を検証して、返すツールの配列から権限のないものを除外するやり方です。一番スタンダードで実装しやすい方法だと思います。

アプローチ2: セッションごとの動的ツールマウント

最初から固定のツールリストを持つのではなく、MCPクライアントとの接続に応じて、そのユーザーの属性や契約状況をDBから引き、使えるツールだけを動的に生成・マウントする手法です。SaaSの権限管理などの複雑な要件がある場合は、このくらい動的な設計が求められることもあります。

アプローチ3: 特に何もしない

toolsの可視性自体に何らかの制御を加えるのは、MCPサーバーの基礎からは外れた事項です。なのでツール数が多くなかったり、ツールが見えることの不便がなければ、特段可視性制御を入れなくても良いと思います。
実際freee-mcpなど、一部のMCPサーバーでは、ツール単位の厳密な可視性制御をせず、利用可能なツールを広めに公開する設計もあります。その場合でも、ツール数が増えるとLLMの選択精度やコンテキスト量に影響するため、toolsets や skills のような単位で利用範囲を絞る設計が必要になってきます。

前述の通り今回の実装では、既存の認証ミドルウェアで取得したシステム権限・ユーザー権限を使って、tools/list の段階で見せるツールを絞り込む構成にしています。
ただし、tools/list で隠しているからといって、tools/call 側の認可を省略してよいわけではありません。

そのため、今回の実装では、アプローチ1をベースに、リクエストごとに見せてよいツールだけを組み立てる形を取りました。

  1. tools/list では、そのユーザーに見えてよいツールだけを返す
  2. tools/call では、指定されたツールを本当に実行してよいかを再度確認する

4. クライアントの検証

実際の運用を考えると、ユーザー権限に加えて、どのMCPクライアントからアクセスされているかを検証することもセキュリティ上大切になってきます。
ここで使えるのがCIMD、Client ID Metadata Documentsという仕組みです。CIMDは、MCPサーバー側でリクエスト元のクライアントIDやメタデータを取得し、接続元のアプリを識別して検証するための仕様で、主にクライアントが不特定多数の場合に便利な仕様です。
2025年11月のMCP仕様アップデートで、このCIMDが単なるオプションからSHOULD要件に変更されました。本番環境を見据えてMCPサーバーを運用していくなら、クライアントの検証はこれから標準的に考えていくべき設計項目になりつつあるようです。

https://zenn.dev/mrmtsntr/articles/d87c425f695901

今回実装したアーキテクチャ

今回はクリーンアーキテクチャで開発しているプロジェクトにMCPサーバーを埋め込む形だったので、既存のhandlerからmoduleのUseCaseを呼び出す構成をそのまま活かしながら、MCP用の入口だけを追加する設計にしました。今回の実装をファイルで見ると、構成は次のようになります。

handler/
├── base.go
├── mcp.go
├── module/middleware/auth.go
└── mcp
    ├── server.go
    ├── request_meta.go
    ├── toolmanager
    │   ├── manager.go
    │   └── types.go
    └── tools
        ├── tools.go
        └── tool_1.go
  • handler/base.go
    既存のHandler群にMCP handlerを組み込む入口です。

  • handler/mcp.go
    /mcp のGET/POSTを受けて、MCP transportに処理を渡します。

  • handler/module/middleware/auth.go
    既存の認証認可を担当します。JWTを検証し、システム権限・ユーザー権限、roleなどをcontextに積みます。GitHubのtokenにつける権限を想像していただけると良いと思います。

  • handler/mcp/server.go
    Streamable HTTP handlerを組み立てる場所です。リクエストごとに、見せてよいツールだけを登録したMCP serverを返します。

  • handler/mcp/request_meta.go
    JSON-RPCのbodyを読んで、initializetools/listtools/call のどれかを判定します。

  • handler/mcp/toolmanager/types.go
    各ツールが必要とするシステム権限・ユーザー権限を定義します。

  • handler/mcp/toolmanager/manager.go
    contextに入っている認可情報を使って、visibleなツールだけを選別します。権限もここで判定し、使えないツールは一覧に出さないようにします。

  • handler/mcp/tools/tools.go
    公開するツール一覧をまとめる場所です。

  • handler/mcp/tools/tool_1.go
    ツールの本体です。既存のUseCaseを呼び出し、MCP向けのレスポンスに変換します。

GitHub Mcp Serverのツール可視性処理を参考にしつつ、ユーザーの権限に対して使えるツール一覧のみを tools/list で返すような仕様になっています。

流れとしては次のようになります。

ツール定義側では、各ツールに必要な権限を定義に含めています。これによって、ツールの仕様と認可条件を分散させずに管理できるようにしました。

func Tool_1(mod module.Usecase, auth auth_module.Usecase) toolmanager.Definition {
    tool := &sdkmcp.Tool{
        Name:        "tool_1",
        Title:       "Tool_1",
        Description: "description",
        Annotations: &sdkmcp.ToolAnnotations{
            IdempotentHint: true,
            ReadOnlyHint:   true,
        },
    }

    return toolmanager.Definition{
        Tool: tool,
        RequiredScopes: []string{
            "scope",
        },
        RequiredPermissions: []string{
            "permission",
        },
        Register: func(server *sdkmcp.Server) {
            sdkmcp.AddTool(server, tool, func(
                ctx context.Context,
                _ *sdkmcp.CallToolRequest,
                input tool_1_Input,
            ) (*sdkmcp.CallToolResult, any, error) {
                return tool_1(ctx, mod, auth, input)
            })
        },
    }
}

func tool_1(
    ctx context.Context,
    mod module.Usecase,
    auth auth_module.Usecase,
    input tool_1_Input,
) (*sdkmcp.CallToolResult, any, error) {

    ...

    result, err := mod.Get(ctx, input.ID)

    ...

    return &sdkmcp.CallToolResult{
        Content: []sdkmcp.Content{
            &sdkmcp.TextContent{Text: string(responseJSON)},
        },
        StructuredContent: response,
    }, nil, nil
}

おわりに

今回のMCPサーバー設計・開発は、既存の実装をリサーチしながら自分なりの正解を探っていく作業で、サーバー開発のとても良い勉強になりました!
特に苦戦したのが認証・認可周りでした。既存の認可基盤にMCPの仕組みを落とし込むのが難しかったです。
どの権限がどのツールに対応しているのか、確実かつ分かりやすく登録する仕組みを作るのに悩みましたが、結果的にかなりすっきりと実装できたと思っています。

MCPはまだデファクトスタンダードが確立されていない領域なので、これからも積極的に技術をキャッチアップしていきます!

Finatext Tech Blog

Discussion