この記事はAIが公開仕様を調べ、独立した小例を作成・実行した技術メモです。仕上げは AI との協働でやっています。著者本人の経験談や会社の実装を再現したものではありません。
resultが返ってきても、成功とは限らない
MCPのtools/callをアプリに取り込むとき、"result" in responseだけで成功フラグを立てると、実行エラーや追加入力待ちまで完了として扱ってしまいます。まず応答の種類を分け、その後に業務データの確認と再実行判断を置きます。
対象はMCP 2026-07-28版です。既存の2025年版の接続へこのまま追加するコードではありません。実装が使う仕様版とSDKの対応を先に確認してください。ここで実行したのは合成応答の分類試験だけで、SDK・サーバー間の相互運用性や実際の業務成功は未検証です。
四つの分岐と、分類できない応答
公式Tools仕様の正常結果、Input Required Tool Results、Error Handlingを根拠に、アプリ側の分類名を次のように置きます。分類名はこの記事の小例のもので、MCPの追加フィールドではありません。
| 応答の手掛かり | 小例の分類 | この時点で完了に進めるか |
|---|---|---|
JSON-RPCのerror
|
protocol_error |
進めない |
resultType: complete、isError: true
|
tool_error |
進めない |
resultType: input_required |
input_required |
入力の受け渡しが必要 |
resultType: complete、isError: falseまたは省略 |
normal_complete |
内容の確認へ進む |
| 分岐に使う型が違う、対応外の版など | unclassified |
成功へ昇格させず調査する |
completeは業務成功の証明ではありません。エラーもこの型で返るため、isErrorとの組合せを見ます。追加入力は同じ引数の再送とも違います。応答に対応する入力を渡す処理が必要ですが、小例はその処理を実行しません。
合成応答で誤分類を検出する
SDKなどで応答全体を検証した後に置く、アプリの分岐だけを試験します。この関数はMCPスキーマ検証器ではありません。JSON-RPCのID照合、各contentの型、inputRequestsの詳細、業務のoutputSchemaは別途検証する前提です。無効な応答をすべて検出できるとは主張しません。
以下をresponse_test.pyに保存します。INPUTの内容も実利用者の回答ではなく、この記事で作った値です。
import unittest
def classify(response):
# Application branch checks, not a full MCP schema validator.
if ("error" in response) == ("result" in response):
return "unclassified"
if "error" in response:
return "protocol_error"
result = response["result"]
if not isinstance(result, dict):
return "unclassified"
kind = result.get("resultType")
if kind == "input_required":
return "input_required"
if kind != "complete":
return "unclassified"
flag = result.get("isError", False)
if type(flag) is not bool:
return "unclassified"
return "tool_error" if flag else "normal_complete"
def envelope(result):
return {"jsonrpc": "2.0", "id": 1, "result": result}
def complete(**fields):
return envelope({"resultType": "complete", "content": [], **fields})
INPUT = envelope({
"resultType": "input_required",
"inputRequests": {"choose": {
"method": "elicitation/create",
"params": {"mode": "form", "message": "Choose a label",
"requestedSchema": {"type": "object", "properties": {
"label": {"type": "string"}}, "required": ["label"]}}
}},
"requestState": "synthetic-state",
})
PROTOCOL_ERROR = {"jsonrpc": "2.0", "id": 1,
"error": {"code": -32602, "message": "Unknown tool"}}
CASES = [
("normal_false", complete(isError=False), "normal_complete"),
("normal_omitted", complete(), "normal_complete"),
("tool_error", complete(isError=True), "tool_error"),
("additional_input", INPUT, "input_required"),
("protocol_error", PROTOCOL_ERROR, "protocol_error"),
("string_flag", complete(isError="false"), "unclassified"),
("integer_flag", complete(isError=0), "unclassified"),
("null_flag", complete(isError=None), "unclassified"),
("unknown_result_type", envelope({"resultType": "future"}), "unclassified"),
("old_shape", envelope({"content": []}), "unclassified"),
("null_result", envelope(None), "unclassified"),
("both_fields", {**complete(), "error": PROTOCOL_ERROR["error"]}, "unclassified"),
]
class ReceiverTest(unittest.TestCase):
receiver = staticmethod(classify)
def test_categories(self):
for name, response, expected in CASES:
with self.subTest(name=name):
self.assertEqual(self.receiver(response), expected)
if __name__ == "__main__":
unittest.main()
python3 -m unittest response_test -v
Python 3.14.7で実行し、1テストメソッド内の12ケースがすべて通りました。Ran 1 testはケース数が1という意味ではありません。subTestで一つずつ期待分類を固定しています。
正常なisError省略と、文字列"false"を分けた点も確認します。文字列の真偽値をPythonのbool()で変換すると、空でない"false"は真になります。一方、数値0を== Falseで許すと型の違いを見落とします。小例では分岐フィールドの型違いを成功として通さないため、type(flag) is boolで限定しています。
old_shapeは古い仕様の応答が間違っているという判定ではありません。この小例の対象版に分類を限定するためのケースです。複数版を支えるなら、対応版ごとに正規化してから分類し、その版のテストを別に用意します。未知の型を正常と推測して補完する扱いは避けます。
同じテストで、悪い実装を止められるか
正常系だけを通しても、成功条件が広すぎる実装は残ります。そこで同じ12ケースと期待値を変えず、分類関数だけを二通りに置き換えました。
一つ目はresultの存在だけでnormal_completeを返すものです。二つ目はcompleteならisErrorを無視してnormal_completeを返すものです。両方とも、ほかの分岐は元の関数へ戻すようにして差分を限定しました。
| 分類関数 | 12ケース中の失敗 | エラー | 結果 |
|---|---|---|---|
| 掲載した関数 | 0 | 0 | 通過 |
resultがあれば正常完了 |
9 | 0 | 失敗 |
completeならisErrorを無視 |
5 | 0 | 失敗 |
これは作った二つの誤分類を検出できたという結果です。現実の全障害を検出できる、という結果ではありません。期待値を関数の出力から生成すると、この比較は成立しなくなるので、ケース側の分類名は固定してあります。
手元でも一つ目を確認するには、関数の冒頭へ次の二行を足して同じコマンドを実行します。9ケースの失敗を確認したら、追加した二行を戻します。
if "result" in response:
return "normal_complete"
分類したあとに、成功条件を置く
この小例には再試行ループを入れていません。tool_errorという分類だけでは、同じ操作を繰り返してよいか分からないからです。入力を直す必要があるエラー、追加入力待ち、状態確認が必要な操作を、共通のリトライへ流さないようにします。
実装へ組み込むときは、たとえば読み取り専用の件数取得なら「期待するキーがあり、件数の型・範囲が正しい」をnormal_completeの後で検査します。書き込みなら、それに加えて対象IDや変更後の状態を別の観測で確認します。ここで挙げた条件は導入時の例であり、本稿で実サービスへ書き込んで測定した結果ではありません。
まず自分の成功判定を一つ選び、isError: trueとinput_requiredを与えて成功へ進まないことを確認してください。分類器の通過、返された内容の確認、業務完了の観測を分けておけば、何を確認済みなのかをテスト名でも説明できます。
参照資料
- MCP 2026-07-28 Tools仕様:応答種別・実行エラーの根拠。
- Python unittest公式資料:subTestによるケースごとの確認。
仕様は2026年10月9日に確認しました。小例は標準ライブラリだけで動かし、ネットワーク通信・認証・業務操作は行っていません。