クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

入力を確かめるMCPツール

このレッスンのあとで: 動く前に入力を確かめるツールを、提供できる。

75分のレッスンコマンド10件AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

ツールは入力をどう確かめるか

MCPのツールには、名前、説明、入力スキーマがあります。スキーマは、引数がどんな形でなければならないかを述べます。サーバーは、何かを動かす前に、引数をそれに照らして確かめます。

引数を埋めるのはモデルで、モデルは時々間違えます。良い拒否は、何を直せばよいかを教えます。

このレッスンのゴール

メモ

動く前に入力を確かめるツールを、提供できる。

キーワード

ことばいみれい
スキーマ入力がどんな形かを述べたもの
引数ツール呼び出しの、名前の付いた値
検証使う前に入力を確かめること
enum許される値の決まったリストudon, onigiri, salad
ツールのエラー何がまずいかを述べる、isError が true の result

学生が入力するセルは、このプレビューでは空白です。

スキーマは引数ごとの規則を並べる

引数には、それぞれ型があります。enum、数の範囲、いちばん長い長さを持つものもあります。スキーマは、どの引数が必須かを示し、ほかの名前を許さないと述べることもできます。

サーバーは、決まった順に規則を確かめ、最初の一つだけでなく、すべての問題を集めます。

例: order_lunch ツール

引数規則通る例通らない例
item必須。udon、onigiri、salad のどれかonigiriramen
quantity必須。1 から 5 の整数29、または文字の "2"
note任意。20 文字までの文字列no onions21 文字のメモ
ほかの名前許されないなし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 2Ordered 2 onigiri、isError は false
ramen、quantity 9、colour red三つの問題を一文に、isError は true
引数なしmissing: item; missing: quantity
quantity が文字の "2"quantity: expected integer
delete_menuプロトコルのエラー -32602

学生が入力するセルは、このプレビューでは空白です。

このラボとソースについて: AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。