ツールは入力をどう確かめるか
MCPのツールには、名前、説明、入力スキーマがあります。スキーマは、引数がどんな形でなければならないかを述べます。サーバーは、何かを動かす前に、引数をそれに照らして確かめます。
引数を埋めるのはモデルで、モデルは時々間違えます。良い拒否は、何を直せばよいかを教えます。
このレッスンのゴール
メモ
動く前に入力を確かめるツールを、提供できる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| スキーマ | 入力がどんな形かを述べたもの | |
| 引数 | ツール呼び出しの、名前の付いた値 | |
| 検証 | 使う前に入力を確かめること | |
| enum | 許される値の決まったリスト | udon, onigiri, salad |
| ツールのエラー | 何がまずいかを述べる、isError が true の result |
学生が入力するセルは、このプレビューでは空白です。
スキーマは引数ごとの規則を並べる
引数には、それぞれ型があります。enum、数の範囲、いちばん長い長さを持つものもあります。スキーマは、どの引数が必須かを示し、ほかの名前を許さないと述べることもできます。
サーバーは、決まった順に規則を確かめ、最初の一つだけでなく、すべての問題を集めます。
例: order_lunch ツール
| 引数 | 規則 | 通る例 | 通らない例 |
|---|---|---|---|
| item | 必須。udon、onigiri、salad のどれか | onigiri | ramen |
| quantity | 必須。1 から 5 の整数 | 2 | 9、または文字の "2" |
| note | 任意。20 文字までの文字列 | no onions | 21 文字のメモ |
| ほかの名前 | 許されない | なし | colour |
学生が入力するセルは、このプレビューでは空白です。
拒否は、直すものをモデルに伝える
悪い引数の呼び出しは、壊れた要求ではありません。要求としては正しいのです。だからサーバーは、問題を並べた文を付けて、isError が true の普通の result を返します。
存在しないツールの呼び出しは別です。それはプロトコルのエラーで、コードは -32602 です。
よくある間違い
注意
モデルは必ずスキーマに従うから、サーバーは確かめなくてよいと考える人がいます。モデルは、名前を抜かし、数の代わりに文字を送り、作り上げた名前を足すことがあります。クライアントも確かめないかもしれません。サーバーが最後の砦なので、必ず確かめます。
モデル用の補助を書く
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", { method: \"POST\", headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" }, body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 80 }), signal: AbortSignal.timeout(30000) });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' return { text: data.choices[0].message.content, recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, recorded: true };' ' }' '}' 'module.exports = { ask };' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
サーバーを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const SCHEMA = { type: \"object\", properties: { item: { type: \"string\", enum: [\"udon\", \"onigiri\", \"salad\"] }, quantity: { type: \"integer\", minimum: 1, maximum: 5 }, note: { type: \"string\", maxLength: 20 } }, required: [\"item\", \"quantity\"], additionalProperties: false };' 'function send(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function typeOk(r, v) { return r.type === \"string\" ? typeof v === \"string\" : Number.isInteger(v); }' > server3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
サーバーを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function problems(args) {' ' const out = [];' ' SCHEMA.required.forEach(function (n) { if ((n in args) === false) out.push(\"missing: \" + n); });' ' Object.keys(SCHEMA.properties).forEach(function (n) {' ' if ((n in args) === false) return;' ' const r = SCHEMA.properties[n], v = args[n];' ' if (typeOk(r, v) === false) return out.push(n + \": expected \" + r.type);' >> server3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
サーバーを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' if (r.enum && r.enum.indexOf(v) < 0) out.push(n + \": must be one of \" + r.enum.join(\", \"));' ' if (typeof r.minimum === \"number\" && (v < r.minimum || v > r.maximum)) out.push(n + \": must be between \" + r.minimum + \" and \" + r.maximum);' ' if (typeof r.maxLength === \"number\" && v.length > r.maxLength) out.push(n + \": at most \" + r.maxLength + \" characters\");' ' });' ' Object.keys(args).sort().forEach(function (n) { if ((n in SCHEMA.properties) === false) out.push(\"extra: \" + n); });' ' return out;' '}' >> server3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
サーバーを書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function words(text, bad) { return { content: [{ type: \"text\", text: text }], isError: bad }; }' 'function handle(msg) {' ' if (msg.id === undefined) return;' ' const p = msg.params || {};' ' if (msg.method === \"initialize\") return send({ id: msg.id, result: { protocolVersion: p.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: \"lunch\", version: \"0.3\" } } });' ' if (msg.method === \"tools/list\") return send({ id: msg.id, result: { tools: [{ name: \"order_lunch\", description: \"社員食堂のランチを注文する\", inputSchema: SCHEMA }] } });' >> server3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
サーバーを書く(5)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' if (p.name === \"order_lunch\") {' ' const args = p.arguments || {};' ' const found = problems(args);' ' if (found.length > 0) return send({ id: msg.id, result: words(\"Could not run order_lunch: \" + found.join(\"; \"), true) });' ' return send({ id: msg.id, result: words(\"Ordered \" + args.quantity + \" \" + args.item, false) });' ' }' ' send({ id: msg.id, error: { code: -32602, message: \"Unknown tool: \" + p.name } });' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (line) { handle(JSON.parse(line)); });' >> server3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
台本のクライアントを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const server = require(\"child_process\").spawn(\"node\", [\"server3.js\"]);' 'const order = function (id, args) { return { id: id, method: \"tools/call\", params: { name: \"order_lunch\", arguments: args } }; };' 'const script = [' ' { id: 1, method: \"initialize\", params: { protocolVersion: \"2025-03-26\" } },' ' { method: \"notifications/initialized\" },' ' order(2, { item: \"onigiri\", quantity: 2 }),' ' order(3, { item: \"ramen\", quantity: 9, colour: \"red\" }),' ' order(4, {}),' ' order(5, { item: \"udon\", quantity: \"2\" }),' ' { id: 6, method: \"tools/call\", params: { name: \"delete_menu\", arguments: {} } },' '];' > client3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
台本のクライアントを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const seen = [];' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (line) {' ' const m = JSON.parse(line);' ' console.log(\"<- \" + (m.result && m.result.content ? m.result.content[0].text + \" (isError \" + m.result.isError + \")\" : line));' ' seen.push(m);' ' if (seen.length === 6) server.stdin.end();' '});' 'script.forEach(function (m) { server.stdin.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); });' >> client3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
台本のクライアントを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'server.on(\"close\", async function () {' ' const shown = seen[2].result.content[0].text;' ' const r = await ask([{ role: \"user\", content: \"ツールが呼び出しを断りました。呼び出した側は何を直すべきか、一文で書いてください。 \" + shown }], \"item は udon、onigiri、salad のどれかにし、quantity は 1 から 5 にして、colour は付けないでください。\", 60);' ' console.log(\"モデル: \" + r.text);' '});' >> client3.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
クライアントをサーバーに向けて動かす
docker exec -w /home/node/project agent node client3.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
台本が呼ぶもの
台本はセッションを始め、order_lunch を五回、存在しないツールを一回呼びます。一つは良い呼び出しです。残りは、規則を一つ破る、いくつも破る、必須の名前をすべて抜かす、のどれかです。
最後に、モデルが拒否の一つを読み、何を直すかを一文で書きます。モデルがないと「記録済みサンプル」になります。
呼び出しごとの答え
| 呼び出し | 答え |
|---|---|
| onigiri、quantity 2 | Ordered 2 onigiri、isError は false |
| ramen、quantity 9、colour red | 三つの問題を一文に、isError は true |
| 引数なし | missing: item; missing: quantity |
| quantity が文字の "2" | quantity: expected integer |
| delete_menu | プロトコルのエラー -32602 |
学生が入力するセルは、このプレビューでは空白です。