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

無料レッスン

スコープでMCPツールを限る

このレッスンのあとで: 誰が頼んでいるかに応じて、サーバーのツールを限ることができる。

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

誰が何を呼べるかをどう限るか

注文を出すツールと、すべての注文を取り消すツールを、同じ呼び出し側に開いてはいけません。サーバーはスコープで限ります。スコープは、呼び出し側にできることごとの短いラベルです。

標準入出力で動くサーバーには、サインインのページがありません。サーバーを起動する人が、持っているスコープを伝え、サーバーは呼び出しのたびに確かめます。

このレッスンのゴール

メモ

誰が頼んでいるかに応じて、サーバーのツールを限ることができる。

キーワード

ことばいみれい
スコープ呼び出し側にできる一つのことのラベルorders:write
付与呼び出し側が持つスコープ
拒否スコープが足りないので、呼び出しを断ること
ワイルドカード同じ書き出しのスコープすべてをまかなう付与orders:*
監査ログすべての呼び出しと判断の記録

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

ツールは、要るスコープを名指しする

サーバーは表を持ちます。ツールごとに、要るスコープが一つあります。呼び出し側は、付与のリストを持ちます。付与がスコープをまかなっていれば、呼び出しは許されます。

orders: のようなワイルドカードは、orders: で始まるスコープすべてをまかない、ほかはまかないません。付与 はすべてをまかないます。

例: 呼び出し側が二つの付与を持つ

ツール要るもの持つのは menu:read、orders:write判断
read_menumenu:readまかなわれるallow
place_orderorders:writeまかなわれるallow
cancel_allorders:adminまかなわれないdeny
burn_kitchenそのツールはない該当しないunknown

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

隠すことは、断ることではない

呼び出し側が使えないツールを、サーバーは tools/list から外してもかまいません。モデルが試さずにすみます。ただし、見せられなかったツールの呼び出しも、送ることはできます。

だから確認は、tools/call のたびに、必ず行います。スコープが足りなければ、そのスコープを名指ししたツールのエラーを返します。知らないツールは、これまでどおりプロトコルのエラーです。

よくある間違い

注意

危ないツールを一覧から隠して、それを安全だと言う人は多いです。隠したツールは、まだそこにあります。名前を知る人は誰でも呼べます。呼び出しのたびにスコープを確かめ、動いた呼び出しと同じく、断った呼び出しも記録します。

モデル用の補助を書く

コマンド
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 NEEDS = { read_menu: \"menu:read\", place_order: \"orders:write\", cancel_all: \"orders:admin\" };' 'const HAVE = (process.env.SCOPES || \"\").split(\",\");' 'let count = 0;' 'function send(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function words(text, bad) { return { content: [{ type: \"text\", text: text }], isError: bad }; }' 'function note(tool, decision) {' '  count += 1;' '  process.stderr.write(JSON.stringify({ n: count, tool: tool, decision: decision, needs: NEEDS[tool] || null }) + \"\n\");' '}' > server5.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

サーバーを書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function call(msg, p) {' '  if (Object.keys(NEEDS).indexOf(p.name) < 0) {' '    note(p.name, \"unknown\");' '    return send({ id: msg.id, error: { code: -32602, message: \"Unknown tool: \" + p.name } });' '  }' '  if (HAVE.indexOf(NEEDS[p.name]) < 0) {' '    note(p.name, \"deny\");' '    return send({ id: msg.id, result: words(\"Not allowed: \" + p.name + \" needs \" + NEEDS[p.name], true) });' '  }' '  note(p.name, \"allow\");' '  send({ id: msg.id, result: words(\"done: \" + p.name, false) });' '}' >> server5.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

サーバーを書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function listed() {' '  return Object.keys(NEEDS).filter(function (n) { return HAVE.indexOf(NEEDS[n]) >= 0; }).map(function (n) {' '    return { name: n, description: \"食堂のツール \" + n, inputSchema: { type: \"object\" } };' '  });' '}' 'function handle(msg) {' '  if (msg.id === undefined) return;' >> server5.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

サーバーを書く(4)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  const p = msg.params || {};' '  if (msg.method === \"initialize\") return send({ id: msg.id, result: { protocolVersion: p.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: \"lunch\", version: \"0.5\" } } });' '  if (msg.method === \"tools/list\") return send({ id: msg.id, result: { tools: listed() } });' '  if (msg.method === \"tools/call\") return call(msg, p);' '  send({ id: msg.id, error: { code: -32601, message: \"Method not found: \" + msg.method } });' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (line) { handle(JSON.parse(line)); });' >> server5.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

台本のクライアントを書く(1)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const env = Object.assign({}, process.env, { SCOPES: \"menu:read,orders:write\" });' 'const server = require(\"child_process\").spawn(\"node\", [\"server5.js\"], { env: env });' 'const call = function (id, name) { return { id: id, method: \"tools/call\", params: { name: name, arguments: {} } }; };' 'const script = [' '  { id: 1, method: \"initialize\", params: { protocolVersion: \"2025-03-26\" } },' '  { id: 2, method: \"tools/list\" },' '  call(3, \"read_menu\"), call(4, \"place_order\"), call(5, \"cancel_all\"), call(6, \"burn_kitchen\"),' '];' > client5.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

台本のクライアントを書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const seen = [];' 'const log = [];' 'require(\"readline\").createInterface({ input: server.stderr }).on(\"line\", function (line) { log.push(line); });' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (line) {' '  const m = JSON.parse(line);' '  const names = m.result && m.result.tools ? \"tools: \" + m.result.tools.map(function (t) { return t.name; }).join(\", \") : null;' '  console.log(\"<- \" + (names || (m.result && m.result.content ? m.result.content[0].text : JSON.stringify(m.error || m.result))));' '  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\"); });' >> client5.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

台本のクライアントを書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'server.on(\"close\", async function () {' '  log.forEach(function (l) { console.log(\"log \" + l); });' '  const refusal = seen[4].result.content[0].text;' '  const r = await ask([{ role: \"user\", content: \"この拒否を学生に一文で説明してください: \" + refusal }], \"あなたのアカウントには、すべての注文を取り消す権限がありません。\", 60);' '  console.log(\"モデル: \" + r.text);' '});' >> client5.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

クライアントをサーバーに向けて動かす

コマンド
docker exec -w /home/node/project agent node client5.js

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

台本がすること

クライアントは、環境に menu:read と orders:write のスコープを入れて、サーバーを起動します。ツールを一覧し、あるツールを三つと、ないツールを一つ呼びます。

最後にサーバーのログを表示します。モデルが、拒否の一つを一文で説明します。モデルがないと「記録済みサンプル」になります。

呼び出しごとの答えと記録

呼び出し答えログの判断
tools/listread_menu と place_order だけツール呼び出しでないので、記録なし
read_menudone: read_menuallow
place_orderdone: place_orderallow
cancel_allNot allowed: cancel_all needs orders:admindeny
burn_kitchenプロトコルのエラー -32602unknown

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

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

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