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

無料レッスン

MCPサーバーをテストする

このレッスンのあとで: 台本のクライアントで、サーバーをテストできる。

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

台本のクライアントでテストする

台本のクライアントは、決まったメッセージのリストをサーバーに送り、すべての応答を確かめる短いプログラムです。疲れず、段を飛ばしません。

サーバーは、うっかり壊れやすいものです。一文字の編集で応答が変わっても、本物のクライアントが失敗するまで、誰も気づきません。

このレッスンのゴール

メモ

台本のクライアントで、サーバーをテストできる。

キーワード

ことばいみれい
台本送るメッセージと、期待する応答の組のリスト
期待テストが求める応答
実際サーバーが本当に送った応答
違い二つが最初に合わない場所
ワイルドカードどの値にも合う印<any>

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

台本は、メッセージと応答を組にする

台本は、段のリストです。各段には、送るメッセージと、期待する応答があります。通知の段は、応答がないことを期待します。

バージョン番号のように、変わる値もあります。テストはそこにワイルドカードを付け、そこが変わっても失敗にしません。ほかはすべて、正確に合わなければなりません。

例: 足し算ツールの台本

段送るもの期待する応答
1initialize求めた版、tools を知らせる、サーバーの版は何でもよい
2準備完了の通知応答なし
3tools/listadd という名前のツールが一つ
4add 2 と 3テキスト 5、isError は false
5add "2" と 3ツールのエラー、isError は true

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

最初の違いを、場所とともに報告する

「失敗」としか言わないテストは、時間を無駄にします。良いテストは場所を言います。パス result.content[0].text は、応答の中の一つの値を指します。

チェッカーは、期待した応答を決まった順にたどり、最初の違いで止まります。何を期待し、何を得たかを述べます。

よくある間違い

注意

正しく動くサーバーにだけテストを走らせる人は多いです。一度も失敗したことのないテストは、何かを捕まえられると示されたことがありません。サーバーをわざと壊し、テストが失敗するかを確かめます。そのあと、壊したのを元に戻します。

モデル用の補助を書く

コマンド
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' 'function send(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'const SCHEMA = { type: \"object\", properties: { a: { type: \"integer\" }, b: { type: \"integer\" } }, required: [\"a\", \"b\"] };' 'function words(text, bad) { return { content: [{ type: \"text\", text: text }], isError: bad }; }' > server6.js"

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

サーバーを書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function handle(msg) {' '  if (msg.id === undefined) return;' '  const p = msg.params || {};' '  const args = p.arguments || {};' '  if (msg.method === \"initialize\") return send({ id: msg.id, result: { protocolVersion: p.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: \"adder\", version: \"1.0\" } } });' >> server6.js"

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

サーバーを書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  if (msg.method === \"tools/list\") return send({ id: msg.id, result: { tools: [{ name: \"add\", description: \"整数を二つ足す\", inputSchema: SCHEMA }] } });' '  if (msg.method === \"tools/call\" && p.name === \"add\") {' '    if (Number.isInteger(args.a) && Number.isInteger(args.b)) return send({ id: msg.id, result: words(String(args.a + args.b), false) });' '    return send({ id: msg.id, result: words(\"a and b must be whole numbers\", true) });' '  }' '  if (msg.method === \"tools/call\") return send({ id: msg.id, error: { code: -32602, message: \"Unknown tool: \" + p.name } });' '  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)); });' >> server6.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\", [process.argv[2] || \"server6.js\"]);' 'const any = \"<any>\";' 'const one = function (id, method, params) { return { jsonrpc: \"2.0\", id: id, method: method, params: params }; };' 'const add = function (id, args) { return one(id, \"tools/call\", { name: \"add\", arguments: args }); };' 'const ok = function (id, result) { return { jsonrpc: \"2.0\", id: id, result: result }; };' 'const text = function (t, bad) { return { content: [{ type: \"text\", text: t }], isError: bad }; };' > check6.js"

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

チェッカーを書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const steps = [' '  [one(1, \"initialize\", { protocolVersion: \"2025-03-26\" }), ok(1, { protocolVersion: \"2025-03-26\", capabilities: { tools: {} }, serverInfo: { name: \"adder\", version: any } })],' '  [{ jsonrpc: \"2.0\", method: \"notifications/initialized\" }, null],' '  [one(2, \"tools/list\"), ok(2, { tools: [{ name: \"add\", description: any, inputSchema: any }] })],' '  [add(3, { a: 2, b: 3 }), ok(3, text(\"5\", false))],' '  [add(4, { a: \"2\", b: 3 }), ok(4, text(\"a and b must be whole numbers\", true))],' '  [one(5, \"tools/call\", { name: \"nope\" }), { jsonrpc: \"2.0\", id: 5, error: { code: -32602, message: \"Unknown tool: nope\" } }],' '];' >> check6.js"

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

チェッカーを書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function isObj(x) { return Boolean(x) && typeof x === \"object\"; }' 'function name(path, k, list) { return list ? path + \"[\" + k + \"]\" : (path ? path + \".\" + k : k); }' 'function diff(want, got, path) {' '  if (want === any) return null;' '  const both = isObj(want) && isObj(got) && Array.isArray(want) === Array.isArray(got);' '  const said = \" expected \" + JSON.stringify(want) + \", got \" + JSON.stringify(got);' '  if (both === false) return JSON.stringify(want) === JSON.stringify(got) ? null : (path || \"(root)\") + \":\" + said;' >> check6.js"

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

チェッカーを書く(4)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  const list = Array.isArray(want);' '  if (list && (want.length === got.length) === false) return (path || \"(root)\") + \": expected \" + want.length + \" items, got \" + got.length;' '  const keys = Object.keys(want).sort();' '  for (const k of keys) {' '    if ((k in got) === false) return name(path, k, list) + \": missing\";' '    const d = diff(want[k], got[k], name(path, k, list));' '    if (d) return d;' '  }' '  const extra = Object.keys(got).sort().filter(function (k) { return keys.indexOf(k) < 0; });' '  return extra.length > 0 ? name(path, extra[0], list) + \": unexpected\" : null;' '}' >> check6.js"

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

チェッカーを書く(5)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const lines = [];' 'const waiting = [];' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (l) {' '  if (waiting.length > 0) waiting.shift()(JSON.parse(l)); else lines.push(JSON.parse(l));' '});' 'function next() { return lines.length > 0 ? Promise.resolve(lines.shift()) : new Promise(function (r) { waiting.push(r); }); }' >> check6.js"

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

チェッカーを書く(6)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  let passed = 0, total = 0, first = null;' '  for (let i = 0; i < steps.length; i++) {' '    server.stdin.write(JSON.stringify(steps[i][0]) + \"\n\");' '    if (steps[i][1] === null) continue;' '    total += 1;' '    const d = diff(steps[i][1], await next(), \"\");' '    console.log((d ? \"FAIL step \" + (i + 1) + \" at \" + d : \"PASS step \" + (i + 1)));' '    if (d === null) passed += 1; else if (first === null) first = d;' '  }' '  server.stdin.end();' '  console.log(\"passed \" + passed + \" of \" + total);' '  if (first === null) return;' '  const r = await ask([{ role: \"user\", content: \"私のサーバーのテストが、次の違いで失敗しました。考えられる原因を一文で書いてください: \" + first }], \"サーバーが返した文字列が、テストの期待した文字列と違っています。\", 60);' '  console.log(\"モデル: \" + r.text);' '}' 'main();' >> check6.js"

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

良いサーバーにチェッカーを走らせる

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

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

悪い編集を入れた複製を作る

コマンド
docker exec -w /home/node/project agent sh -c "sed 's/args.a + /args.a - /' server6.js > bad6.js"

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

悪い複製にチェッカーを走らせる

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

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

二つの Run を並べる

Run確かめた段結果
良いサーバー1、3、4、5、6passed 5 of 5
悪い複製1、3、4、5、6passed 4 of 5
悪い複製、段 4add 2 と 3result.content[0].text: expected "5", got "-1"

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

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

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