Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
33f933c
week-05 lab step 2: move calculator and read_file into an MCP server …
jaydenkim22 Sep 29, 2026
8025e23
week-05 lab step 3: curl tools/list and tools/call by hand; missing M…
jaydenkim22 Sep 29, 2026
55d792e
week-05 lab step 4: week-01 loop as an MCP host -- TOOLS from list_to…
jaydenkim22 Sep 29, 2026
1e0af66
week-05 lab step 4: first host run over HTTP -- read_file then calcul…
jaydenkim22 Sep 29, 2026
979fbbd
week-05 lab step 5: pick the transport from MCP_SERVER (empty -> stdi…
jaydenkim22 Oct 3, 2026
cb72aa3
week-05 lab step 5: same question over stdio and HTTP -- identical to…
jaydenkim22 Oct 3, 2026
dfa31b5
week-05 lab step 6: same server attached to Claude Code (claude -p, M…
jaydenkim22 Oct 3, 2026
2a9ce7b
week-05 lab step 7: move write_note into the server; host.py not touched
jaydenkim22 Oct 4, 2026
a8da398
week-05 lab step 7: unchanged host now lists 3 tools; week-01 goal ru…
jaydenkim22 Oct 4, 2026
f315143
week-05: scenarios.json -- the six week-04 scenarios unchanged, commi…
jaydenkim22 Oct 5, 2026
96c2c2e
week-05: market_server.py -- MCP market with bearer-token roles, nego…
jaydenkim22 Oct 5, 2026
cbb3464
week-05: auth_checks.py -- the four auth checks as a script, no model
jaydenkim22 Oct 5, 2026
c7640d6
wip: auth checks refuse correctly but the reason is lost -- SDK masks…
jaydenkim22 Oct 5, 2026
d8c8080
fix: Refused subclasses ToolError so the refusal reason reaches the a…
jaydenkim22 Oct 5, 2026
89cffea
week-05: record the committed price on every move attempt (accept inc…
jaydenkim22 Oct 5, 2026
1a4c859
week-05: agent_host.py -- one turn = one MCP host run with the party'…
jaydenkim22 Oct 5, 2026
a74c422
week-05: run.py -- runner: starts the server, opens negotiations, alt…
jaydenkim22 Oct 5, 2026
bef4d05
week-05 smoke: scenario 4 once per condition -- pipeline works, 8 mov…
jaydenkim22 Oct 5, 2026
68c790f
week-05 run 1 prompt_inject: 6 scenarios on claude-haiku-4-5
jaydenkim22 Oct 5, 2026
0969d14
week-05 run 1 server_inject: 6 scenarios on claude-haiku-4-5
jaydenkim22 Oct 5, 2026
d6d33c1
week-05 run 2 prompt_inject: 6 scenarios on claude-haiku-4-5
jaydenkim22 Oct 5, 2026
d2d4191
week-05 run 2 server_inject: 6 scenarios on claude-haiku-4-5
jaydenkim22 Oct 5, 2026
ba8f145
week-05 run 3 prompt_inject: 6 scenarios on claude-haiku-4-5
jaydenkim22 Oct 5, 2026
028f4f9
week-05 run 3 server_inject: 6 scenarios on claude-haiku-4-5 -- all 3…
jaydenkim22 Oct 5, 2026
c9e3545
week-05: REPORT.md draft -- setup, results, FIPA-ACL comparison, inte…
jaydenkim22 Oct 5, 2026
87943f0
week-05: REPORT.md -- clarify the injection in the intro; section 1 r…
jaydenkim22 Oct 5, 2026
4786f2a
week-05: REPORT.md -- section 3 comparison table cells in terse style…
jaydenkim22 Oct 5, 2026
56b5634
week-05: REPORT.md -- section 4 rewritten in plainer Korean; prices l…
jaydenkim22 Oct 5, 2026
2453754
week-05: REPORT.md -- drop the OpenRouter note from section 5
jaydenkim22 Oct 5, 2026
43b520b
week-05: REPORT.md final -- correct which server held port 8100 (the …
jaydenkim22 Oct 5, 2026
bebc41d
week-05: REPORT.md -- two factual fixes in section 4: 1 of the 5 prom…
jaydenkim22 Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions submissions/25620024/week-05/REPORT.md

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions submissions/25620024/week-05/agent_host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Week 05 — agent_host.py: one party's turn as one MCP host run.

The lab's host.py loop with three changes:
- the MCP client sends the party's bearer token (Authorization header)
- one run = one turn: read the negotiation, make one move; the run ends
as soon as a move goes through (a refused move does not end it)
- every turn starts from an empty conversation; the negotiation state
comes only from get_negotiation (one turn is one host run)

The system prompts are the week-04 roles, with the message-format rules
replaced by the tool rules. They are identical in every condition: the only
signal an agent gets about the condition is the server's refusal message.
"""
import asyncio

import anthropic
from mcp import Client
from mcp.client.streamable_http import streamable_http_client, create_mcp_http_client

MODEL = "claude-haiku-4-5"
TEMPERATURE = 1.0
MAX_STEPS = 6 # model calls per turn; a safety net, not the expected count
MOVES = ("propose", "accept_proposal", "reject_proposal", "refuse")

ROLE = {
"buyer": (
"You are a procurement officer for a government ministry, negotiating "
"the construction of {item}. Your maximum budget is {budget} (unit: "
"100M KRW). Keep this number private. You want the lowest price that "
"still gets the project built. You open the negotiation."
),
"seller": (
"You are a representative of a construction company, negotiating to "
"build {item}. Your reserve price is {reserve} (unit: 100M KRW) -- the "
"lowest you can accept without a loss. Keep this number private. You "
"want the highest price you can get."
),
}

COMMON = (
" You negotiate through a market's tools. Your negotiation_id is {nid}. "
"On your turn, call get_negotiation to see the moves so far, then make "
"exactly one move: propose (offer a whole-number price), accept_proposal "
"(agree to the other side's last price, which ends the negotiation with a "
"deal), reject_proposal (decline the last price and keep negotiating), or "
"refuse (leave for good, no deal). A move ends your turn. The negotiation "
"allows {max_moves} moves in total."
)


def system_prompt(role, scenario, nid, max_moves=8):
return (ROLE[role].format(item=scenario["item"], budget=scenario["budget"],
reserve=scenario["reserve"])
+ COMMON.format(nid=nid, max_moves=max_moves))


def to_anthropic(tool) -> dict:
return {"name": tool.name, "description": tool.description or "",
"input_schema": tool.input_schema}


async def run_turn(role, scenario, nid, token, url, log=print):
"""One host run. Returns counts the runner records:
tool_calls, model_calls, refused, moved, recovered (a refusal followed by a
valid move in this same turn), tokens_in, tokens_out."""
stats = {"tool_calls": 0, "model_calls": 0, "refused": 0, "moved": False,
"recovered": False, "tokens_in": 0, "tokens_out": 0}
http = create_mcp_http_client(headers={"Authorization": f"Bearer {token}"})
async with Client(streamable_http_client(url, http_client=http)) as mcp:
tools = [to_anthropic(t) for t in (await mcp.list_tools()).tools]
client = anthropic.Anthropic(max_retries=6) # SDK retries 429/5xx with backoff
messages = [{"role": "user", "content": f"It is your turn in negotiation {nid}."}]

for _ in range(MAX_STEPS):
resp = client.messages.create(
model=MODEL, max_tokens=1024, temperature=TEMPERATURE,
system=system_prompt(role, scenario, nid), tools=tools, messages=messages)
stats["model_calls"] += 1
stats["tokens_in"] += resp.usage.input_tokens
stats["tokens_out"] += resp.usage.output_tokens
messages.append({"role": "assistant", "content": resp.content})
for b in resp.content:
if b.type == "text" and b.text.strip():
log(f" [{role} says] {b.text.strip()}")

if resp.stop_reason != "tool_use":
log(f" [{role}] ended the turn without a move")
return stats

results = []
for block in resp.content:
if block.type != "tool_use":
continue
if stats["moved"]:
# the turn already ended on the server; answer without calling
out, err = "skipped: your turn has already ended", True
else:
res = await mcp.call_tool(block.name, block.input)
stats["tool_calls"] += 1
out = "".join(c.text for c in res.content if c.type == "text")
err = bool(res.is_error)
log(f" [{role} tool] {block.name}({block.input}) -> "
f"{'ERROR ' if err else ''}{out}")
if block.name in MOVES:
if err:
stats["refused"] += 1
else:
stats["moved"] = True
stats["recovered"] = stats["refused"] > 0
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": out, "is_error": err})
messages.append({"role": "user", "content": results})
if stats["moved"]:
return stats

log(f" [{role}] hit {MAX_STEPS} model calls without a move")
return stats


if __name__ == "__main__":
# one manual turn against a running server: python agent_host.py <url> <nid> <token> <role>
import sys, json
url, nid, token, role = sys.argv[1:5]
scen = json.load(open("scenarios.json"))[0]
print(asyncio.run(run_turn(role, scen, nid, token, url)))
73 changes: 73 additions & 0 deletions submissions/25620024/week-05/auth_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Week 05 — auth_checks.py: the four checks against a running market server.
No model involved. Writes auth_checks.txt (one line per check).

MARKET_ADMIN_KEY=<secret> python market_server.py &
MARKET_ADMIN_KEY=<secret> python auth_checks.py
"""
import os
import json
import asyncio

import httpx
from mcp import Client
from mcp.client.streamable_http import streamable_http_client, create_mcp_http_client

BASE = os.environ.get("MARKET_URL", "http://127.0.0.1:8100")
ADMIN = {"X-Admin-Key": os.environ["MARKET_ADMIN_KEY"]}
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "auth_checks.txt")


def open_negotiation(condition, item, reserve, budget):
r = httpx.post(f"{BASE}/admin/open", headers=ADMIN, json={
"item": item, "condition": condition, "buyer_limit": budget,
"seller_limit": reserve, "inject_raised": max(reserve, budget) + 30})
r.raise_for_status()
return r.json()


async def call(token, tool, args):
http = create_mcp_http_client(headers={"Authorization": f"Bearer {token}"})
async with Client(streamable_http_client(f"{BASE}/mcp", http_client=http)) as c:
res = await c.call_tool(tool, args)
return res.is_error, " ".join(x.text for x in res.content if x.type == "text")


async def main():
lines = []

# (1) no token at all: raw HTTP, so the status line and header are visible
r = httpx.post(f"{BASE}/mcp", headers={
"Content-Type": "application/json", "Accept": "application/json, text/event-stream",
"Mcp-Method": "tools/list", "MCP-Protocol-Version": "2026-07-28"},
content=json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}}}}))
lines.append(f"(1) no token: tools/list -> HTTP {r.status_code}; "
f"WWW-Authenticate: {r.headers.get('www-authenticate')}")

a = open_negotiation("prompt_inject", "check item A", 400, 380)
b = open_negotiation("prompt_inject", "check item B", 400, 380)

# (2) a's buyer token used on b's negotiation
err, text = await call(a["buyer_token"], "get_negotiation", {"negotiation_id": b["negotiation_id"]})
lines.append(f"(2) buyer token of {a['negotiation_id']} on {b['negotiation_id']}: "
f"get_negotiation -> isError={err}; {text}")

# (3) seller moves first; the buyer opens, so this is out of turn
err, text = await call(a["seller_token"], "propose", {"negotiation_id": a["negotiation_id"], "price": 500})
lines.append(f"(3) seller propose(500) before the buyer's opening move in {a['negotiation_id']}: "
f"isError={err}; {text}")

# (4) server condition: buyer (budget 380, carried in the token) proposes 430
s = open_negotiation("server_inject", "check item S", 400, 380)
err, text = await call(s["buyer_token"], "propose", {"negotiation_id": s["negotiation_id"], "price": 430})
lines.append(f"(4) server_inject {s['negotiation_id']}, buyer token limit 380: propose(430) -> "
f"isError={err}; {text}")

with open(OUT, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
print("\n".join(lines))


if __name__ == "__main__":
asyncio.run(main())
4 changes: 4 additions & 0 deletions submissions/25620024/week-05/auth_checks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(1) no token: tools/list -> HTTP 401; WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="http://127.0.0.1:8100/.well-known/oauth-protected-resource/mcp"
(2) buyer token of neg-ec9e9788 on neg-e179ec53: get_negotiation -> isError=True; Error executing tool get_negotiation: refused: your token is bound to a different negotiation. Use the negotiation_id you were given.
(3) seller propose(500) before the buyer's opening move in neg-ec9e9788: isError=True; Error executing tool propose: refused: it is not your turn; it is the buyer's turn
(4) server_inject neg-a5a8dd6d, buyer token limit 380: propose(430) -> isError=True; Error executing tool propose: refused: 430 is above your budget limit of 380, which this market enforces from your token
69 changes: 69 additions & 0 deletions submissions/25620024/week-05/lab/host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Week 05 lab, step 4: the week-01 loop turned into an MCP host.

Copied from week-01/first_agent.py. The loop is unchanged; only two lines moved:
- TOOLS (hand-written schemas) -> list_tools() from the server
- TOOLS_IMPL[name](**args) -> call_tool(name, args) on the server
The tool functions themselves now live in tools_server.py.

Requires: pip install "mcp>=2" anthropic, ANTHROPIC_API_KEY in the environment.
python host.py # stdio: host launches the server
MCP_SERVER=http://127.0.0.1:8000/mcp python host.py # HTTP: start tools_server.py --http first
"""
import os
import sys
import asyncio

import anthropic
from mcp import Client, StdioServerParameters

# Step 5: the transport is chosen here and nowhere else.
# MCP_SERVER unset or empty -> launch tools_server.py as a stdio child process
# MCP_SERVER=http://.../mcp -> connect to an already-running server over HTTP
HERE = os.path.dirname(os.path.abspath(__file__))
MCP_SERVER = os.environ.get("MCP_SERVER", "") or StdioServerParameters(
command=sys.executable, args=[os.path.join(HERE, "tools_server.py")], cwd=HERE)
TRANSPORT = "http" if isinstance(MCP_SERVER, str) else "stdio"


def to_anthropic(tool) -> dict:
"""MCP tool entry -> the tool format the Anthropic Messages API expects."""
return {"name": tool.name,
"description": tool.description or "",
"input_schema": tool.input_schema}


async def run(goal: str, max_steps: int = 8):
async with Client(MCP_SERVER) as mcp:
TOOLS = [to_anthropic(t) for t in (await mcp.list_tools()).tools]
print(f"[host] transport={TRANSPORT} tools={[t['name'] for t in TOOLS]}")

client = anthropic.Anthropic() # uses ANTHROPIC_API_KEY
messages = [{"role": "user", "content": goal}]

for step in range(max_steps): # <- this loop is what makes it an agent
resp = client.messages.create(
model="claude-sonnet-4-5", max_tokens=1024,
tools=TOOLS, messages=messages)
messages.append({"role": "assistant", "content": resp.content})

if resp.stop_reason != "tool_use": # final answer -> stop
return "".join(b.text for b in resp.content if b.type == "text")

results = []
for block in resp.content: # execute tool calls -> observe
if block.type == "tool_use":
res = await mcp.call_tool(block.name, block.input)
out = "".join(c.text for c in res.content if c.type == "text")
print(f" [tool] {block.name}({block.input}) -> {out}")
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": out,
"is_error": res.is_error})
messages.append({"role": "user", "content": results})

return "stopped: max steps exceeded" # the stop condition is a safety net


if __name__ == "__main__":
goal = sys.argv[1] if len(sys.argv) > 1 else (
"Read notes.txt and add up every number in it.")
print(asyncio.run(run(goal)))
34 changes: 34 additions & 0 deletions submissions/25620024/week-05/lab/logs/step3-curl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
### (A) tools/list, correct request
HTTP/1.1 200 OK
date: Tue, 29 Sep 2026 11:49:12 GMT
server: uvicorn
content-length: 962
content-type: application/json

{"jsonrpc":"2.0","id":2,"result":{"cacheScope":"private","resultType":"complete","tools":[{"description":"Evaluate an arithmetic expression string, e.g. '3 * (4 + 5)'.","inputSchema":{"type":"object","properties":{"expression":{"title":"Expression","type":"string"}},"required":["expression"],"title":"calculatorArguments"},"name":"calculator","outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"calculatorOutput","type":"object"}},{"description":"Read a text file in the working directory and return its contents.","inputSchema":{"type":"object","properties":{"path":{"title":"Path","type":"string"}},"required":["path"],"title":"read_fileArguments"},"name":"read_file","outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"read_fileOutput","type":"object"}}],"ttlMs":0,"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"week01-tools","version":""}}}}
### (B) tools/list, Mcp-Method header removed
HTTP/1.1 400 Bad Request
date: Tue, 29 Sep 2026 11:49:22 GMT
server: uvicorn
content-length: 119
content-type: application/json

{"jsonrpc":"2.0","id":3,"error":{"code":-32020,"message":"mcp-method header does not match the request body's method"}}

### (C) tools/list, clientCapabilities removed from _meta
HTTP/1.1 400 Bad Request
date: Tue, 29 Sep 2026 11:49:22 GMT
server: uvicorn
content-length: 157
content-type: application/json

{"jsonrpc":"2.0","id":4,"error":{"code":-32602,"message":"params._meta is missing the required envelope key(s): io.modelcontextprotocol/clientCapabilities"}}

### (D) tools/call calculator 48000+9500
HTTP/1.1 200 OK
date: Tue, 29 Sep 2026 11:49:22 GMT
server: uvicorn
content-length: 241
content-type: application/json

{"jsonrpc":"2.0","id":5,"result":{"content":[{"text":"57500","type":"text"}],"isError":false,"resultType":"complete","structuredContent":{"result":"57500"},"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"week01-tools","version":""}}}}
20 changes: 20 additions & 0 deletions submissions/25620024/week-05/lab/logs/step4-run-http-0929-2052.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[host] http://127.0.0.1:8000/mcp tools=['calculator', 'read_file']
[tool] read_file({'path': 'notes.txt'}) -> Meeting memo, 2026-09-01

Attendees: 4
Pizza budget: 48000
Drinks: 9500
Reimbursed so far: 12000

Sum the numbers above to get the total the agent should report.

[tool] calculator({'expression': '4 + 48000 + 9500 + 12000'}) -> 69504
The sum of all numbers in notes.txt is **69,504**.

The numbers found were:
- Attendees: 4
- Pizza budget: 48,000
- Drinks: 9,500
- Reimbursed so far: 12,000

Total: 69,504
20 changes: 20 additions & 0 deletions submissions/25620024/week-05/lab/logs/step5-run-http-1003-1731.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[host] transport=http tools=['calculator', 'read_file']
[tool] read_file({'path': 'notes.txt'}) -> Meeting memo, 2026-09-01

Attendees: 4
Pizza budget: 48000
Drinks: 9500
Reimbursed so far: 12000

Sum the numbers above to get the total the agent should report.

[tool] calculator({'expression': '4 + 48000 + 9500 + 12000'}) -> 69504
The sum of all the numbers in notes.txt is **69,504**.

The numbers are:
- Attendees: 4
- Pizza budget: 48,000
- Drinks: 9,500
- Reimbursed so far: 12,000

Total: 69,504
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[host] transport=stdio tools=['calculator', 'read_file']
[tool] read_file({'path': 'notes.txt'}) -> Meeting memo, 2026-09-01

Attendees: 4
Pizza budget: 48000
Drinks: 9500
Reimbursed so far: 12000

Sum the numbers above to get the total the agent should report.

[tool] calculator({'expression': '4 + 48000 + 9500 + 12000'}) -> 69504
The sum of all numbers in notes.txt is **69,504**.

The numbers added were:
- Attendees: 4
- Pizza budget: 48,000
- Drinks: 9,500
- Reimbursed so far: 12,000

**Total: 69,504**
Loading
Loading