businessmcp について
businessmcpのmcp-gatewayは、サードパーティーのMCPサーバーに接続するゲートウェイを構築する開発者向けのNode.jsユーティリティ集です。URLの安全確認、上流エラーの分類、ツール名の衝突防止、トランスポートのフォールバックを提供しますが、ゲートウェイ本体やポリシー層は含みません。
主な機能
- 01公開HTTP URLとIPの安全確認
- 02上流エラーの分類
- 03衝突を避けるツール名の生成
- 04Streamable HTTPからSSEへの切り替え
- 05ユーザー向けエラー説明の生成
活用シーン
- 01ユーザー指定のMCPサーバーURLの検証
- 02上流接続エラーの説明
- 03ツール集約時の名前衝突の防止
- 04異なるトランスポートのサーバーへの接続
MCP 対応
上流のMCPサーバーに接続するゲートウェイの構築を支援するユーティリティです。トランスポート用ヘルパーは @modelcontextprotocol/client を使用しますが、ゲートウェイ集約やポリシー管理機能は含みません。
設定
リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。
https://businessmcp.com/api/mcpclaude mcp add --transport http com-businessmcp-businessmcp https://businessmcp.com/api/mcp{"mcpServers":{"com-businessmcp-businessmcp":{"type":"http","url":"https://businessmcp.com/api/mcp"}}}Cursor で開く{"servers":{"com-businessmcp-businessmcp":{"type":"http","url":"https://businessmcp.com/api/mcp"}}}VS Code で開く{"mcpServers":{"com-businessmcp-businessmcp":{"type":"http","url":"https://businessmcp.com/api/mcp"}}}{"mcpServers":{"com-businessmcp-businessmcp":{"serverUrl":"https://businessmcp.com/api/mcp"}}}コマンドはターミナルで実行し、JSON は各クライアントの MCP 設定に貼り付けて再起動します。認可が必要なサーバーは初回接続時にログインを求めます。
ツール
詳細
mcp-gateway
他者の MCP サーバーを前段で受け付けるサーバー、MCP ゲートウェイ向けのセキュリティ強化ユーティリティです。
これらは BusinessMCP の本番ゲートウェイから抽出したものです。このゲートウェイでは、ワークスペースがサードパーティの MCP サーバーに接続し、それらを単一のガバナンスされたエンドポイント経由で再公開できます。他者のサーバーを前段で受け付けるには、ユーザーが指定した URL を自社のインフラ上で取得する必要があり、問題が起きる原因は限られています。これが、私たちが運用しているものです。
MIT ライセンスです。2 つのコアモジュールには依存関係がなく、Node の組み込み機能を 2 つ使うだけです。
npm install @modelcontextprotocol/client # only if you use the transport helper
1. SSRF ガードと、テストスイートを通過しても残った回避策
ゲートウェイはユーザーから URL を受け取り、そこにアクセスします。ガードがなければ、http://169.254.169.254/ に直接アクセスされ、クラウドの認証情報を盗まれる可能性があります。
わかりやすい実装では、ホストを予約済み範囲のリストと照合します。私たちの実装もそうしており、IPv4 マップド IPv6 も含めていました。
const mapped = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/) // ← the bug
この正規表現は本番環境では一度も機能しませんでした。WHATWG URL パーサーである new URL() は、実際にすべての呼び出し元が使うものですが、IPv6 ホストを正規形に正規化します。そして、IPv4 マップドアドレスの正規形は16 進数です。
new URL('http://[::ffff:169.254.169.254]/').hostname
// '::ffff:a9fe:a9fe'
正規表現が照合していたドット区切りの表記は、呼び出し元が生成できない形式でした。ユニットテストが通ったのは、ドット区切りの文字列を直接ガードに渡していたためです。テストは成功していましたが、意味はありませんでした。実際のコードパスでは生成できない入力を使ってしか、ガードをテストしていなかったのです。
以下はすべて取得処理に到達しました。
| URL | URL.hostname | 実際のアクセス先 |
|---|---|---|
http://[::ffff:169.254.169.254]/ | ::ffff:a9fe:a9fe | クラウドメタデータ |
http://[::ffff:127.0.0.1]/ | ::ffff:7f00:1 | ループバック |
http://[::127.0.0.1]/ | ::7f00:1 | ループバック(IPv4 互換) |
http://[64:ff9b::169.254.169.254]/ | 64:ff9b::a9fe:a9fe | クラウドメタデータ(NAT64) |
http://[2002:a9fe:a9fe::]/ | 2002:a9fe:a9fe:: | クラウドメタデータ(6to4) |
リストにプレフィックスを追加しても解決にはなりません。 IPv6 には IPv4 アドレスを埋め込む仕組みがいくつかあり、テキストを照合する方法では、制御できない正規化処理に対していたちごっこをすることになります。このガードはアドレスを 8 個の 16 ビットグループにデコードし、把握している埋め込み方式(::ffff: マップド、:: 互換、64:ff9b:: NAT64、2002:: 6to4)をすべて展開して、内側の IPv4 アドレスを再帰的にチェックします。デコードできない入力は安全でないものとして扱います。解析できないアドレスの安全性を保証することはできません。
import { isPublicHttpUrl, isBlockedIp } from '@businessmcp/mcp-gateway'
await isPublicHttpUrl('http://[::ffff:169.254.169.254]/') // false
await isPublicHttpUrl('https://api.example.com/mcp') // true, after resolving every A/AAAA
isBlockedIp('::ffff:a9fe:a9fe') // true
isPublicHttpUrl はホスト名を名前解決し、返されたすべてのアドレスがパブリックであることを要求します。これは DNS リバインディングに対するベストエフォートの防御です。ただし、完全な対策ではありません。このチェックとソケット接続の間にリバインディングが起きる可能性があり、これは現実的な TOCTOU の隙間です。これを塞ぐには、解決した IP を接続に固定する必要があります。私たちはこの残存リスクを許容していますが、後から知るのではなく、あらかじめ把握しておくべきだと考えています。
この対策だけでは防げない、もう一つの落とし穴があります。ホストを一度検証してから fetch(url, { redirect: 'follow' }) を呼び出すのは回避策になります。サーバーが http://127.0.0.1/ に 302 リダイレクトすると、リダイレクト先は検証されません。リダイレクトは手動で追跡し、ホップごとに再検証してください。
2. 上流の障害を分類する — 決して握りつぶさない
このコードの最初のリリースでは、上流への呼び出しをすべて catch {} で囲み、サーバーをツール一覧から除外していました。認証失敗、未対応のトランスポート、タイムアウト、ブロックされたホスト、そして「サーバーは正常だが、何も返さなかった」は区別できず、つまり見えなくなっていました。ユーザーには、理由の説明が製品内のどこにもないまま、ツールがひっそり消えて見えていました。
import { classifyUpstreamError, describeFailure } from '@businessmcp/mcp-gateway'
const failure = classifyUpstreamError(err)
// { reason: 'auth_required' | 'transport_unsupported' | 'timeout'
// | 'blocked_host' | 'unreachable', detail: string }
describeFailure(failure) // a sentence you can show a user
この仕組みがあるからこそ、再試行の判断を適切に行えます。別のトランスポートで再試行する価値があるのは、ハンドシェイクに関する失敗だけです。認証拒否は、どちらの方法でも同じように失敗します。
3. 名前空間を分け、ツール名の衝突を防ぐ
N 個のサーバーを集約するには、ツール名にプレフィックスを付ける必要があります。MCP の名前は最大 64 文字のため、プレフィックスの追加と切り詰めによって、異なる 2 つの上流ツールが同じ文字列に変換されることがあります。その場合、名前で検索すると、誤ったサーバーに呼び出しが振り分けられます。
import { namespacedToolName } from '@businessmcp/mcp-gateway'
namespacedToolName('stripe', 'create_payment_intent')
// 'ext_stripe_create_payment_intent'
切り詰めが必要な場合は、完全な名前の短いハッシュを末尾に追加し、長い名前が重複しないようにします。
4. トランスポートのフォールバック
MCP の 2026-07-28 リビジョンでは、サーバー向けのレガシーな HTTP+SSE が廃止されましたが、接続先のサーバーには、まだこの方式のみをサポートするものが数多くあります。withUpstream はストリーム可能な HTTP を試し、SSE にフォールバックして、どちらが成功したかを通知します。これにより、成功した方式を保存し、次回の確認を省略できます。
import { withUpstream } from '@businessmcp/mcp-gateway'
const res = await withUpstream(
{ url, headers, transport: knownTransport },
(client) => client.listTools(),
{ onTransportResolved: (t) => save(t) },
)
if (!res.ok) log(describeFailure(res.failure))
ピア依存関係として @modelcontextprotocol/client が必要です。他のモジュールには必要ありません。
制約について
- フレームワークではなく、ユーティリティです。 ここにはゲートウェイはありません。集約、認証、ポリシーレイヤーもありません。これらは接続の保存方法と密接に関係するため、その上に見せかけの抽象化を設けるより、何も設けないほうが適切です。
- 外部監査は受けていません。 これは本番環境で実行しているものです。真剣に扱うべき理由にはなりますが、レビューの代わりにはなりません。
- 集約レイヤーとインスペクションレイヤーは意図的に含めていません。 データベースと密接に結び付いていることに加え、テストカバレッジがないためです。ここで公開しているものはすべて、テスト済みの範囲から提供しています。
- DNS リバインディングに関する TOCTOU の問題は未解決です。 詳細は上記のとおりです。
開発
このパッケージは、これらのモジュールを本番環境で積極的に使用している BusinessMCP モノレポから生成されています。修正はモノレポ側で行い、再生成してください。ここだけに適用したパッチは上書きされます。また、セキュリティガードのコピーを 2 つ手動で管理することこそ、セクション 1 の回避策を生んだ原因そのものです。
それでも、ここでの問題報告や報告の提出は歓迎します。
npm install
npx vitest run
npx tsc --noEmit
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。