1. なぜ統合と移行を語るのか
これまでは主に、こちらの都合のよい形で API やツールを設計してきました。現実の世界ではたいてい逆で、すでに次のようなものが存在します:
- モノリス、または複数のマイクロサービス;
- REST/GraphQL API;
- 本番で何年も回り続けているビジネスロジック.
そして突然、「Apps SDK と MCP を通じて私たちのプロダクトを ChatGPT に接続してください」という課題が現れます。
すべてを「理想的な MCP サーバー」向けに書き直す—これは選択肢ではありません。既存の世界の上に、バックエンドの言語を ChatGPT の言語に翻訳する薄いレイヤー(ツール、リソース、スキーマ)を丁寧に被せる必要があります。
2番目の問題: プロダクトは生き物です。スキーマや API は変わります。通常のフロントエンドなら、フィールドを変えた瞬間に TypeScript のエラーが出ます。LLM アプリの世界はもっと厄介で、モデルは自信満々に古いフォーマットを送り続け、tool は落ち、ビルドがきれいに失敗する代わりに次のような事態になります:
- MCP サーバーでのランタイムエラー;
- 「そのフィールドはだいたいこういう意味だろう」というハルシネーション;
- 品質インシデントの増加.
そこで本講義では、MCP+Apps 層を次のように捉えます:
- 既存バックエンドへのアダプター;
- 何年にもわたって維持すべき契約;
- 移行の対象(バージョン、アノテーション、scopes、SDK).
2. 統合アーキテクチャ: 既存バックエンドの上のアダプターとしての MCP
基本像
スタックをおさらいしますが、今度は本番運用の観点から:
flowchart LR U[ChatGPT のユーザー] --> G[ChatGPT モデル] G -->|App を呼び出す| W["ウィジェット (Apps SDK, Next.js)"] G -->|tools.call| MCP[MCP サーバー / Gateway] MCP --> S1["Gift Service (既存のサービス)"] MCP --> S2["Commerce Service (注文、ACP)"]
ChatGPT はあなたの世界と直接やり取りするのではなく、MCP プロトコル経由で通信します: tools/call の呼び出しやリソースの一覧、イベントのストリーミングなど。
この構成における MCP サーバーはまさに アダプター です。ChatGPT(JSON‑RPC、ツール)とあなたのサービス(REST/DB/キュー)を双方理解し、相互に変換します。
Gateway/Adapter としての MCP
典型的な状況: すでに REST エンドポイントを持つ Gift Service がある:
// 既存の REST API の例
GET /api/gifts/recommendations?budget=100&occasion=birthday
POST /api/orders
新しいビジネスロジックを書くのではなく、MCP 層でそれを Tool に包むだけです:
// mcp/tools/recommendGifts.ts
import { z } from "zod";
import { server } from "./mcpServer"; // SDK の仮のインスタンス
const recommendGiftsInput = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
});
server.registerTool({
name: "recommend_gifts",
description: "予算の範囲でギフトのアイデアを選定する",
inputSchema: recommendGiftsInput,
async execute(args) {
const { occasion, budgetUsd } = recommendGiftsInput.parse(args);
const res = await fetch(
`https://api.myapp.com/gifts/recommendations?budget=${budgetUsd}&occasion=${occasion}`,
);
return res.json(); // 重要: モデルとウィジェットの双方に扱いやすい JSON を返す
},
});
ギフト選定のロジックは既存サービスの中に残ります。MCP 層は ChatGPT の言語とあなたの API の言語を相互に翻訳する「薄い通訳」です。
場合によっては MCP 層が複数のバックエンドサービスへのルーティングも担います。その場合は完全な MCP Gateway となります—この役割は、プロダクションとネットワークのモジュールでさらに深掘りします。
Monolith-integrated MCP と Sidecar MCP
この MCP 層をどこに組み付けるか、基本的に 2 つの選択肢があります。
文章にすると次のとおりです:
| 方式 | 説明 | MCP コードの置き場所 |
|---|---|---|
| Monolith-integrated | すべてを 1 つの Next.js/Node サービスに集約 | Next.js の API ルートまたは Express 内 |
| Sidecar MCP | API と通信する独立コンテナ/サービス | 独立した Node/Go アプリケーション |
小規模プロジェクトでは前者で十分なことが多いです。Next.js アプリを Vercel にデプロイし、ルート /mcp または /api/mcp を用意して、MCP サーバーを他の API の隣に置きます。
例(大幅に簡略化):
// app/api/mcp/route.ts (Next.js 16)
import { NextRequest } from "next/server";
import { mcpHandler } from "@/mcp/server";
export async function POST(req: NextRequest) {
const body = await req.json();
const response = await mcpHandler.handle(body); // JSON-RPC リクエスト
return new Response(JSON.stringify(response), {
headers: { "content-type": "application/json" },
});
}
より本格的なアーキテクチャで、複数のドメインサービス(Gift、Commerce、Analytics)がある場合は、MCP 層を専用の Gateway サービスに切り出す方が便利です。ChatGPT からの MCP トラフィックを受け付け、ツール名に応じて各バックエンドにルーティングします。
重要: ChatGPT と Apps SDK の観点では、どちらにしても 1 つの MCP サーバーです。モノリス内で動かすか、独立したマイクロサービスとして動かすかは、あなたのアーキテクチャ上の判断です。
MCP 層の居場所は理解できました。次は、この層が 何を 受け取り、何を 返すのか—そこでスキーマと契約の話になります。
3. Single Source of Truth: スキーマ、型、契約テスト
内部 DTO、外部の REST 契約、さらに MCP のツール用スキーマが存在すると、つい「スキーマを目分量で描く」誘惑に駆られます。その結果は目に見えています:
- バックエンドのフィールドを変え、ツールのスキーマ更新を忘れる;
- モデルは古いフォーマットを送り続ける;
- 本番でのランタイム問題が増える.
健全な道は 1 つの真実の源を作り、どこでもそれを使うこと。TypeScript の世界では Zod などのライブラリを使い、MCP SDK が JSON Schema に変換できるようにするのが便利です。
GiftGenius 用の共通 Zod スキーマ
学習用の GiftGenius では、Gift サービスが入力検証に Zod を使っているとします:
// domain/gifts.ts
import { z } from "zod";
export const giftRecommendationInputSchema = z.object({
occasion: z.string().describe("用途: birthday、wedding など"),
budgetUsd: z.number().int().positive(),
recipientProfile: z.string().describe("相手の簡単なプロフィール"),
});
export type GiftRecommendationInput = z.infer<
typeof giftRecommendationInputSchema
>;
この同じスキーマを次に使います:
- REST エンドポイント(リクエストボディの検証);
- MCP ツール(inputSchema として);
- テスト(フィクスチャの基礎).
スキーマを MCP ツールに接続する
// mcp/tools/recommendGifts.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";
import { server } from "../mcpServer";
server.registerTool({
name: "recommend_gifts",
description: "プロフィールと予算に基づくギフトの選定",
inputSchema: giftRecommendationInputSchema,
async execute(args) {
const input = giftRecommendationInputSchema.parse(args);
const res = await fetch("https://api.myapp.com/gifts/recommendations", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(input),
});
return res.json();
},
});
SDK は Zod スキーマを自動で JSON Schema に変換し、ChatGPT は tools/list でそれを参照できます。これにより:
- ツールの引数型とコードが厳密に結び付く;
- スキーマ変更時に TypeScript のコンパイラがハンドラの更新を促す.
MCP ↔ backend の契約テスト
ここで言う契約テストは、難しいものではなく、いくつかの現実的なチェックです。
もっとも単純な unit/contract テストは次のようになります:
// tests/mcp/recommendGifts.contract.test.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";
test("サンプルのリクエストがツールのスキーマに適合する", () => {
const sample = {
occasion: "birthday",
budgetUsd: 150,
recipientProfile: "同僚、ガジェット好き",
};
expect(() => giftRecommendationInputSchema.parse(sample)).not.toThrow();
});
このテストは世界のすべてを保証するわけではありませんが、スキーマを変更したのにフィクスチャの更新を忘れた、といった MCP 層とバックエンドの齟齬を少なくとも検出できます。
この考え方は次のように簡単に拡張できます:
- 外部 API(Stripe、CMS)応答のモック;
- テスト環境で実際の MCP サーバーに対して MCP クライアントを走らせる.
4. ツールとリソースのバージョニング戦略
スキーマはいずれ変わります。重要なのは「単にフィールドを改名するだけ、何も起きないだろう」としないこと。LLM の世界では、ビルドだけでなくモデルの振る舞いも壊れます。古いプロンプト、保存済みの会話、ゴールデンケースが古い契約を期待し続けます。
アディティブ変更 vs 破壊的変更
大きく 2 つに分かれます。
アディティブ変更 — 何かを追加するが既存を壊さない:
- 応答に新しい任意フィールドを追加;
- 既定値付きの新しい任意引数を追加;
- UI やモデルが無害に扱える enum の追加値.
たとえば、ツールの応答に deliveryEstimateDays を追加しても、古いウィジェットがそれを無視するなら安全です。スキーマは拡張されますが、誰もそれを必須にはしません。
破壊的変更 — 既存の期待を壊す:
- 存在しなかったフィールドを必須にする;
- 型を変更する(文字列 → オブジェクト);
- 引数の意味を変える(USD の予算 → ローカル通貨の予算)にもかかわらずフィールド名はそのまま.
この場合の唯一の安全な道は、新しいツールのバージョンを作ることです。
パターン Tool_v2
典型パターン: recommend_gifts があり、スキーマを大きく変えたい。古いツールはいじらず、新しい recommend_gifts_v2 を作る。
// v1
const recommendGiftsInput_v1 = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
});
// v2: 通貨と配送フィルターのサポート
const recommendGiftsInput_v2 = z.object({
occasion: z.string(),
maxPrice: z.number().int().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
deliverByDate: z.string().optional(); // ISO-文字列
});
server.registerTool({
name: "recommend_gifts",
description: "DEPRECATED: recommend_gifts_v2 を使用してください",
inputSchema: recommendGiftsInput_v1,
async execute(args) { /* 旧ロジック */ },
});
server.registerTool({
name: "recommend_gifts_v2",
description:
"予算・通貨・配送期限を考慮したギフト選定",
inputSchema: recommendGiftsInput_v2,
async execute(args) { /* 新ロジック */ },
});
モデルや古いプロンプト/エージェントは、更新するまで recommend_gifts を使い続けます。新しいシナリオは recommend_gifts_v2 を前提に書きます。
移行期間の後:
- ゴールデンケースとエージェントが v2 に切り替わる;
- メトリクスが v1 の呼び出しがほぼないことを示す;
その時点で v1 の縮退を慎重に始められます(例: まず dev/staging のツール一覧から隠し、その後 prod でも隠す)。
リソースのバージョニング
バージョンが必要なのは Tools だけではありません。もし resources(例: ギフトの静的カタログ)があるなら、これもバージョニングした方がよいです。
よくある方法:
- リソース名にバージョンを埋め込む: gift_catalog.v1.json, gift_catalog.v2.json;
- URI/パラメータでバージョンを渡す: /api/catalog?version=1.
狙いは同じです。既存のシナリオの足元でデータをすり替えず、明示的に固定されたバージョンのカタログを提供します。
ダウンタイムなしの移行
ツール移行の一般的なサイクル:
- 古い版と並行して新バージョン(_v2)を追加する。
- App/エージェント/system‑prompt を更新して、新版を使うようにする。
- 両方のバージョンでゴールデンケースと LLM‑eval を走らせ、重要なシナリオで品質が落ちていないことを確認する。
- v1 と v2 の使用状況(およびエラー)をメトリクスで監視する。
- v1 のトラフィックが 0 に近づいたら、段階的に無効化を開始する。
このやり方は、スキーマ移行、SDK/プロトコルの更新、Auth の変更にも有効です。こうしてツールとリソースが v1/v2 と慎重なアディティブ変更で進化していくのと同様に、もう一つの大きな契約が認証と認可(OAuth、scopes、.well-known)です。これらも長寿命で、丁寧な移行が必要です。
5. 認証の進化: .well-known、scopes、既存 OAuth
既にあなたのプロダクトが OAuth 2.1/OpenID Connect の世界で動いているなら、MCP での ChatGPT 連携は「新しいログイン」ではなく、共通ルールに従ってあなたの Authorization Server と対話すべき新しいクライアントです。
MCP と .well-known/oauth-protected-resource
OAuth 2.1/OpenID Connect と Auth Server の設定の全体像は別モジュールで扱います(認証モジュールを参照)。ここでは実務的側面—MCP リソースが ChatGPT に OAuth 保護を知らせ、リンクフローをどう起動するか—にだけ集中します。
保護された MCP リソースの標準的なパターン:
- MCP サーバーが特別なエンドポイント /.well-known/oauth-protected-resource を公開する;
- その応答で、どのリソースがどの AS(Authorization Server)に保護されているかを伝える;
- MCP 呼び出しで 401 を返す際、ヘッダー WWW-Authenticate にこの .well-known へのリンクを入れ、ChatGPT が自動的に OAuth フロー(「Link account」)を開始できるようにする。
Express による最小例:
// mcp-auth/.well-known.ts
import express from "express";
const app = express();
app.get("/.well-known/oauth-protected-resource", (_req, res) => {
res.json({
resource: "https://mcp.myapp.com",
authorization_servers: [
"https://auth.myapp.com/.well-known/openid-configuration",
],
});
});
app.listen(3000);
そしてクライアントへのヒント付きの 401 ハンドラー:
res
.status(401)
.set(
"WWW-Authenticate",
'Bearer resource_metadata="https://mcp.myapp.com/.well-known/oauth-protected-resource"',
)
.end();
ChatGPT はこのヘッダーを見ると、どの AS に行き、どのように OAuth フローを開始するかを理解します。
スコープと認可の移行
Scopes も移行の火種になり得ます。Auth モジュールで詳しく扱いましたが、統合/移行の文脈で重要な点がいくつかあります。
GiftGenius が最初はカタログの読み取り(gifts.read)だけを持ち、その後、注文作成のために gifts.write を追加したとしましょう。やるべきことは:
- 新しい scope をクライアント(ChatGPT App)の設定に追加する;
- MCP サーバーを更新し、実際に何かを変更するツールに対してのみその scope を要求する;
- 必要に応じて .well-known に変更点を記述する.
UX の観点では、ユーザーが新機能を使おうとした際に、ChatGPT アプリの権限拡張の確認が出るかもしれません。進行中の対話の途中で予告なく表示されないよう、こうした変更は次のように扱うべきです:
- 告知する(リリースノート、ドキュメント);
- テスト用 AS を使って staging で検証する;
- ツールの説明(destructiveHint など)も更新し、モデルが意識的に「危険な」ツールを呼べるようにする。
6. メタデータとアノテーション: ヒント層で契約を補助する
Auth 層は、あなたの App を通じて 誰が何をできるか に答えます。しかしトークンや scope が正しくても、モデルが どのように ツールを呼び、ユーザーに説明するかは重要です。ここで役立つのが追加のヒント層、すなわち メタデータとアノテーション です。
契約(スキーマ)はツールが 何を 受け取り、何を 返すかを定義します。メタデータとアノテーションは、モデルが いつ、どのように ツールを呼ぶべきかを理解する助けになります。破壊的な操作を追加したり、UI を変えたり、外部統合を導入したりといった App の進化時に特に重要です。
_meta["openai/widgetDescription"] と widgetCSP
Apps SDK と MCP の記述には、OpenAI がプロトコルを拡張するための特別なフィールド _meta があります。例えば:
- _meta["openai/widgetDescription"] — ウィジェットが何を表示するかの簡潔な説明。モデルは UI を冗長に言い換えずに App を適切にアナウンスするのに使えます;
- _meta["openai/widgetCSP"] — ウィジェットに必要な CSP ドメイン(fetch/画像/スクリプト)を宣言します。
UI を変更するとき(例: 注文フローに新しいステップを追加)には、widgetDescription を更新して、モデルが何が起きているかを引き続き正しく説明できるようにするとよいでしょう。
ツールのアノテーション(readOnlyHint、destructiveHint、openWorldHint)
アノテーションはシンプルなブール値ですが、UX と安全性に大きな影響があります:
- readOnlyHint: true — 読み取り専用。モデルは余計な確認なしに呼びやすくなります。
- destructiveHint: true — 破壊的操作の可能性。ChatGPT は明示的な確認を求めます。
- openWorldHint: true — データを外部に公開したり、「非常に大量の情報」を返す可能性があり、要約が必要になることがあります。
アノテーション付きのツール記述の例:
server.registerTool({
name: "delete_saved_gift",
description: "ユーザーの保存済みギフトを削除する",
inputSchema: z.object({ giftId: z.string() }),
annotations: {
readOnlyHint: false,
destructiveHint: true,
openWorldHint: false,
},
async execute({ giftId }) {
// ...ギフトを削除する
},
});
移行時に新しい「危険な」ツールを追加する場合、アノテーションは頼れる味方です。ChatGPT が隠れて実行しないようにし、より慎重な振る舞いを促します。
アノテーションは「本物の」防御ではない点には注意してください。影響するのはクライアントとモデルの振る舞いだけです。実際の安全性はサーバー側(Auth、scopes、検証)が担保します。
7. SDK と MCP 仕様の移行
MCP と Apps SDK は活発に進化しています。capabilities の新しいフィールド、メッセージ型、_meta/annotations の追加など。「2025 年時点では」に代表される注意がドキュメントにありますが、これとうまく付き合う必要があります。
したがって、SDK/仕様のアップグレードは App の日常的な作業であり、「いつかやる」まれなイベントではありません。
一般的なアップグレード手順
健全なアップデートの流れは概ね次のとおりです:
- Apps SDK/MCP SDK の新バージョンの changelog を読み、破壊的変更の可能性を洗い出す。
- 依存を dev/staging 環境で更新し、prod は触らない。
- MCP Inspector / Jam などのクライアントを流す:
- ハンドシェイクを確認;
- tools/list / resources/list を確認;
- いくつかの試験的な tools/call を実行。
- 新機能に合わせてツールの説明や _meta を更新:
- 例: 新しい annotations や widgetDescription を追加。
- 前回の講義で触れたゴールデンケースと LLM‑eval を回し、App の品質面の振る舞いに劣化がないことを確認する。
- その後にのみ prod へ展開。可能であればカナリア/フィーチャーフラグでトラフィックの一部に限定する。
例: 新しい SDK で openWorldHint を追加する
新しい Apps SDK が openWorldHint をサポートし、外部レビューを探索してノイズの多い結果を返す可能性のある search_public_reviews にこのフラグを付けることにしたとします。
手順は次のとおりです:
- SDK と型定義を更新する;
- ツール記述に annotations.openWorldHint = true を追加する;
- system‑prompt を更新し、これから外部世界へ問い合わせを行う旨をエージェントが明示的に説明するようにする;
- 特にプライバシー/PII に関するセーフティのゴールデンケースを回し、モデルがむやみに饒舌になっていないことを確認する。
ここまでで SDK やアノテーション更新の一般プロセスを見ました。では、recommend_gifts ツールの進化という具体シナリオに落とし込みましょう。
8. ミニケース: GiftGenius における recommend_gifts の進化
具体シナリオでまとめてみます。
初期バージョン
基本のツールは次のような形でした:
const recommendGiftsInput_v1 = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
recipientProfile: z.string(),
});
server.registerTool({
name: "recommend_gifts",
description: "USD でギフトのアイデアを選定する",
inputSchema: recommendGiftsInput_v1,
async execute(args) {
const input = recommendGiftsInput_v1.parse(args);
return giftService.recommend(input); // 内部関数
},
});
米国ユーザーと 1 通貨だけなら問題ありません。
新しいビジネス要件: マルチ通貨と期限
プロダクトチームから新要件が来ました:
- EUR/GBP をサポートする;
- 配送期限を考慮する(誕生日まで 3 日しかないのに 1 か月後に届くギフトは出さない);
- 応答に配送目安も含めたい。
安直なやり方: フィールドを変更するだけ:
- budgetUsd を maxPrice に改名;
- currency を追加;
- 応答に deliveryEstimateDays を追加.
何がまずいのか?
古いプロンプト(ゴールデンケースや system‑prompt の記述を含む)や保存済みの会話は budgetUsd を送り続けます。モデルはそのフィールドがなくなったことを知りません。MCP 層は parse で落ち、ChatGPT App の動作が実ユーザーの環境で突然壊れます。
正しいやり方:
- 新しいスキーマと新ツール _v2 を追加する。
const recommendGiftsInput_v2 = z.object({
occasion: z.string(),
maxPrice: z.number().int().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
recipientProfile: z.string(),
deliverByDate: z.string().optional(),
});
server.registerTool({
name: "recommend_gifts_v2",
description:
"通貨と希望配送日を考慮したギフト選定",
inputSchema: recommendGiftsInput_v2,
async execute(args) {
const input = recommendGiftsInput_v2.parse(args);
return giftService.recommendV2(input); // 新ロジック
},
});
- recommend_gifts はそのまま残し、description に DEPRECATED の注記を追加する。
- system‑prompt と App の説明を更新し、モデルが recommend_gifts_v2 を優先して使うようにする(指示に明記してよい)。
- GiftGenius のウィジェットを更新し、新しい応答形式(deliveryEstimateDays など)を理解させる。
- 代表的シナリオ(特定日までのギフト選定)についてゴールデンケースを LLM‑eval で回す。
テストと可観測性
用意しておきたいテスト:
新しい入力の契約テスト:
test("v2 は EUR と期限を含むシナリオを受け入れる", () => {
const sample = {
occasion: "birthday",
maxPrice: 100,
currency: "EUR",
recipientProfile: "同僚",
deliverByDate: "2025-12-24",
};
expect(() => recommendGiftsInput_v2.parse(sample)).not.toThrow();
});
本番での観測:
- recommend_gifts_v2 と recommend_gifts の呼び出し比率;
- v1 のエラーレート(増えていないことを期待);
- 移行前後のゴールデンケースに対する LLM‑eval スコア(以前の講義で方法を扱いました)。
v2 が品質と利用メトリクスの両面で「勝った」ら、v1 の段階的な無効化を計画できます。
3 つの要点に絞れば: (1) MCP は薄いアダプターであって新しいモノリスではない; (2) スキーマ、Auth、アノテーションは ChatGPT とあなたのバックエンドの間の長寿命な契約であり、通常の API と同じくらい慎重にバージョニングとテストを行うべき; (3) SDK/仕様のあらゆる移行は、staging、ゴールデンケース、可観測性を伴う通常のエンジニアリングプロセスであって、「金曜の夜にパッケージを更新」する類のものではない。 このプリズムで ChatGPT App を見れば、既存プロダクトとの統合は混沌ではなくなります。
9. MCP/SDK の統合と移行でよくある誤り
誤り1: MCP を「新しいバックエンド」と見なし、薄いアダプターにしない。
MCP 層にビジネスロジックすべて(DB アクセス、ドメイン規則、計算)を持ち込みたくなることがあります。これは MCP サーバーを別モノリスに変えてしまい、他のバックエンドと同期が難しくなります。健全なのは MCP を既存サービスの上の Gateway/Adapter に留めること。ドメインロジックは ChatGPT 以前と同じ場所に置き、MCP は JSON を行き来させるだけにします。
誤り2: 同一オブジェクトに複数のスキーマ定義を持つ。
DB 用、REST API 用、MCP ツール用に「ギフト」の定義が 3 種類あり、それぞれ微妙に違う—よくあるアンチパターンです。最終的に静的型付けも契約もテストも崩れます。Zod/TypeBox などの単一スキーマを Single Source of Truth として用い、MCP 用の JSON Schema を生成することで、このリスクを大きく下げられます。
誤り3: スキーマ移行の誤り—「静かな」破壊的変更。
フィールド名の改名や意味の変更を、ツール名を変えずに行うのは隠れた退行への道です。モデルは古いフォーマットを送り続け、インシデントは一部のユーザーでのみ遅れて顕在化します。重大な変更では *_v2 を作り、古い版を並行稼働させ、非推奨の注記とモニタリングを活用しましょう。
誤り4: Auth 変更とスコープを軽視する。
副作用のある新ツールを追加したのに、scopes や .well-known を更新し忘れた—ユーザーはシナリオ途中で 401 に遭遇するか、逆に MCP が適切な認可なしに破壊的操作を実行し始めるかもしれません。Auth 層の移行もスキーマ移行と同等に、staging、テスト、段階的な権限拡張で計画してください。
誤り5: アノテーション(destructiveHint、readOnlyHint、openWorldHint)を使わない。
どのツールが安全で、どれが潜在的に危険かをモデルに示さないと、意外な挙動に繋がります。無害な get_catalog で確認を求め、逆にデータ削除を予告なく実行する、といったことです。適切なアノテーションは、ユーザーにとって予測可能な挙動をもたらし、品質と安全のインシデントを減らします。
誤り6: ゴールデンケースを回さずに SDK を「そのまま本番更新」。
新しい SDK/仕様はフィールドの追加、ハンドシェイクの挙動変更、メッセージ構造の変更を伴うことがあります。「依存を更新してデプロイ」だけだと、モデルが必要なツールを呼ばなくなった、エラーメッセージの文言が変わった、などの品質退行を招きます。まずは dev/staging で MCP Inspector、次にゴールデンケースと LLM‑eval、最後に本番です。
誤り7: ビジネスロジックを特定のツールバージョンに強く結び付ける。
内部の Gift Service のロジックが recommend_gifts 固有に依存していると、痛みなく recommend_gifts_v2 に移行するのが難しくなります。ベストプラクティスは、内部サービスは独自のルールで進化させ、*_v1、*_v2 のツールは薄いアダプターとして、古い/新しい外部契約を共通のドメイン構造にマッピングすることです。
誤り8: ツールのバージョン別の可観測性がない。
ログやメトリクスで、どのツールのどのバージョンが呼ばれたかを区別できないと、移行のデバッグが手探りになります。ツール名、スキーマ/SDK のバージョン、主要パラメータをログに記録し、どんな退行も特定の変更に結び付けやすくしましょう。
GO TO FULL VERSION