はじめに
MCP(Model Context Protocol)でエージェントに社内システムを触らせる話が増えてきました。そこでよく見かけるのが、こういう構成です。
AI エージェント
→ MCP ツール(SQL 実行)
→ 本番データベース
「DB に SELECT できる MCP サーバーを繋いだら、AI がなんでも答えてくれるようになった」— デモとしては最高に映えます。僕も誘惑は分かります。
でも業務システムでこれをやってはいけない、というのがこの記事の主張です。代わりにこう組みます。
AI エージェント
→ MCP ツール
→ 文書化された HTTP API(OpenAPI)
→ ハンドラ → ユースケース → リポジトリ
抽象論で終わらせないために、後半は自作の OSS(PHP 製 stdio MCP ブリッジ nene-mcp)で実際にどう実装したかを見せます。
DB 直結の何が問題か — DB はルールを知らない
「読み取り専用の SELECT なら安全でしょ?」から検討します。答えは No です。
アプリケーション層は、DB が知らないルールを大量に持っています。
| アプリ層が持つ知識 | DB 直結で何が起きるか |
|---|---|
| テナント分離 |
WHERE organization_id = ? をエージェントの善意に任せることになる。1つの JOIN ミスで他社のデータが LLM のコンテキストに載る |
| 認可 | 「経理ロールだけが見られる」を SQL は知らない |
| ドメイン不変条件 | 「発行済み請求書の金額は変更不可」はアプリのコード |
| 監査ログ | アプリを迂回した読み書きは監査に残らない |
| バリデーション | 書き込みなら言わずもがな |
| エラー契約・リクエスト ID | 障害調査の紐が切れる |
重要なのは、この表の上2行は SELECT だけでも破られることです。「read-only だから安全」は、シングルテナントで全データ公開可能な場合にしか成立しません。
もうひとつ。DB スキーマは非公開の実装詳細です。エージェントに SQL を書かせると、スキーマが事実上の公開 API になり、カラム名ひとつ変えるだけで誰かのエージェントが黙って壊れるようになります。
MCP ツールは「アプリの能力」を公開するもの
MCP の価値は、エージェントに構造化された能力の呼び出し方を与えることです。その能力はアプリケーションが既に持っています — バリデーションも認可も監査も込みで。
だから MCP ツールがやるべきことは、通常のクライアントが呼ぶのと同じ文書化された API を呼ぶこと。それだけです。
- エージェントがレコードの状態を知りたい → 普通のクライアントと同じ read API を呼ぶ
- エージェントが何かを作成・更新したい → 既に検証・認証・ログ・エラー契約を持つ write API を呼ぶ
MCP はアプリの能力を公開する層であって、第二の隠れたアプリケーションを発明する場所ではない。地味な結論ですが、地味なのが正解です。エージェントにも読めて、開発者にもレビューできて、テストも書ける形は、結局いつものアーキテクチャです。
実装: OpenAPI をツール契約として使う
nene-mcp は、コミットされたカタログ tools.json を読んで HTTP API を MCP ツールとして公開する小さな stdio ブリッジです(newline-delimited JSON-RPC / initialize・tools/list・tools/call)。
カタログの実物はこういう形をしています。
{
"name": "getHealth",
"title": "Health",
"description": "Read the health endpoint through the documented public API.",
"safety": "read",
"source": {
"type": "openapi",
"operationId": "getHealth",
"method": "GET",
"path": "/health"
},
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
},
"responseSchemaRef": "#/components/schemas/HealthResponse"
}
見てのとおり、ツール1つ = OpenAPI の operation 1つです。ツールが知っているのは:
- ツール名と入力の JSON Schema
- HTTP メソッドとパス(OpenAPI の
operationIdに紐づく) - API のベース URL と、必要なら資格情報
ツールが知らないもの: SQL、DB の接続文字列、.env の中身、データがどこにどう保存されているか。 API が契約のすべてで、契約は OpenAPI として既に文書化・テスト済みのものを使い回します。エージェント用に新しい契約を発明しません。
実装: read から始めて、write は fail-closed
カタログの各ツールには safety を必ず書きます。値は read か write の二値で、それ以外はカタログの検証段階で弾かれます。
if ($safety !== 'read' && $safety !== 'write') {
throw new InvalidArgumentException(sprintf(
'MCP catalog field "safety" must be "read" or "write", got "%s".', $safety,
));
}
この safety は飾りではなく、2つの実挙動に効きます。
1. MCP クライアントへのヒント。 safety: read のツールは tools/list 応答で readOnlyHint アノテーションが付き、クライアント側の確認 UI が適切に振る舞えます。
2. write の fail-closed。 認証が設定されていない状態では、read 以外のツールは呼び出し自体を拒否します。
if ($tool['safety'] !== 'read' && !$this->httpClient->hasAuthentication()) {
throw new McpRuntimeException(sprintf(
'Write tool "%s" requires bearer authentication. '
. 'Set NENE_MCP_BEARER_TOKEN in the MCP server environment.',
$name,
));
}
「認証を設定し忘れたら write が素通しになる」ではなく、「設定し忘れたら write は動かない」に倒してあります。デフォルトで安全側、が MCP のような開発者ツーリングでは特に重要です — この手の設定は雑にコピペされて広まるので。
導入の順番も read からを勧めます。ヘルスチェック、一覧取得、1件取得、履歴の照会。状態を変えないツールはエージェントとの相互作用を学ぶ教材として安全で、それだけでも「ダッシュボードを要約して」のようなユースケースは十分成立します。write ツールは通常の API 設計と同じ水準の設計 — 認証・認可・監査・失敗系のテスト — を済ませてから足すもので、週末のショートカットではありません。
エラーを「Something went wrong」に潰さない
見落とされがちですが、エラーの中継は MCP ブリッジの重要な仕事です。
API が RFC 9457(Problem Details)で構造化エラーを返すなら、ツールはそれをそのままエージェントに見せるべきです。バリデーション違反なら何が悪いかの構造化された 400 を、認証切れなら構造化された 401 を。
エージェントは失敗から次の行動を決めます。「何かがうまくいきませんでした」に潰されたエラーからは、リトライすべきか、入力を直すべきか、人間を呼ぶべきかが判断できません。予測可能な失敗の形も AI-readability の一部です。
秘密情報はツールの外に
MCP の設定は開発者ツーリングの近く(エディタ設定・リポジトリ)に置かれがちなので、事故りやすいポイントを列挙しておきます。
- 環境変数の名前は文書化する。値はコミットしない
- ツールのメタデータ(
tools/list応答)に資格情報を含めない - MCP ツールに生の
.envを読ませない - トークンをログに出さない
ツールは「このスコープが必要」と言ってよい。資格情報そのものを晒してはいけない。
判定に迷ったときの2つの質問
新しい MCP ツールを設計するとき、僕はこの順で自問します。
Q1. 普通の HTTP クライアントが、文書化された API を通して同じことを安全にできるか?
Yes なら、そのツールはたぶん良いラッパーです。No なら次へ。
Q2. なぜその能力は、エージェント専用に存在するのか?
たまに正当な答えがあります。でも大抵はありません。エージェント専用の裏口が欲しくなったら、それは本物の業務機能が API・テスト・ドキュメント・レビューを欠いたまま生まれようとしているサインです。先にアプリの API にしてから、ツールで包んでください。
まとめ
- DB は テナント分離・認可・不変条件・監査を知らない。SELECT だけでも境界は破れる
- MCP ツールは文書化された HTTP API(OpenAPI の operation)と 1:1 に対応させる。契約を二重に発明しない
-
safetyは二値で必須・write は認証未設定なら動かない(fail-closed) - エラーは Problem Details のまま中継する。構造化された失敗はエージェントの意思決定材料
- 迷ったら「普通のクライアントが文書化 API で安全にできるか?」
nene-mcp は MIT ライセンスの OSS で、composer require hideyukimori/nene-mcp で入ります(Packagist / ドキュメント)。土台のフレームワーク側の設計は NENE2 にあります。想定スコープは開発者と小規模チームのサイドカー(Cursor / Claude Desktop / 社内ステージング)で、顧客向け API ゲートウェイではありません — そこも含めて「MCP はアプリを呼ぶ。アプリを迂回しない」が一貫した設計方針です。
── 筆者: 森 秀之(彩音インターナショナル)— 自己ホストの業務ツール群を実運用中。
中小企業向けの業務システムを料金公開・固定価格で開発しています。
🔗 ayane.co.jp