9. An agent under the gate
The previous chapters used a model inside FoBiS. An agent (Claude Code, Codex, any client of the Model Context Protocol) works the other way round: the agent is the model, and FoBiS is a tool it calls. fobis mcp serves FoBiS's facts and checks over MCP:
claude mcp add fobis -- fobis mcp # Claude Code; other clients take the same commandA minimal client, in Python, speaks the protocol the way an agent does, one JSON-RPC request per line:
tools/mcp_client.py
"""A minimal MCP client: what an agent does with `fobis mcp`, one request per line on the server's stdin.
python3 tools/mcp_client.py list the tools, read-only ones marked
python3 tools/mcp_client.py call TOOL '{"k": "v"}' call one tool, print its text result
"""
import json
import subprocess
import sys
server = subprocess.Popen(["fobis", "mcp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
def request(ident, method, params=None):
message = {"jsonrpc": "2.0", "id": ident, "method": method, "params": params or {}}
server.stdin.write(json.dumps(message) + "\n")
server.stdin.flush()
return json.loads(server.stdout.readline())
hello = request(1, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "demo"}})
server.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
if sys.argv[1] == "list":
info = hello["result"]["serverInfo"]
print(f"{info['name']}: protocol {hello['result']['protocolVersion']}")
for tool in request(2, "tools/list")["result"]["tools"]:
mark = "read-only" if tool["annotations"].get("readOnlyHint") else "writes"
print(f" {tool['name']:<20} {mark:<10} {tool['title']}")
else:
reply = request(2, "tools/call", {"name": sys.argv[2], "arguments": json.loads(sys.argv[3])})["result"]
print("error" if reply.get("isError") else "result", "of", sys.argv[2])
print(reply["content"][0]["text"])
server.stdin.close()
server.wait()$ python3 tools/mcp_client.py list
fobis: protocol 2025-06-18
lint read-only Lint Fortran sources
review read-only Review a change
list_rules read-only List lint rules
explain_rule read-only Explain a lint rule
kb_search read-only Search the knowledge packs
kb_show read-only Show a knowledge-pack section
ask_context read-only Retrieve grounding for a question
query read-only Query the code graph
agents_md read-only Project briefing for agents
bindings read-only Plan C and Python bindings
introspect read-only Project build facts
check read-only Check the dependency graph
tree read-only Dependency tree
merge_conflicts read-only Analyse merge conflicts
commit_facts read-only Facts of the staged change
testgen_skeleton read-only Test skeleton for a module
ecosystem_status read-only Ecosystem status
ecosystem_impact read-only Impact of an API change
case_list read-only List the declared cases
provenance read-only Provenance of a result
build writes Build the project
test writes Run the tests
case_run writes Run cases
verify writes Verify results against references
matrix writes Build and test every mode x shadow
assess writes Assess numerical error and uncertainty
diff_run writes Compare results with a revision
bench writes Measure run time
profile writes Profile a case
modernize writes Verify mechanical modernisation
gate writes One verdict for a change
clean writes Clean build products
fetch writes Fetch dependenciesThe read-only tools give facts: where simpson is called, from the code graph rather than a text search:
$ python3 tools/mcp_client.py call query '{"kind": "callers", "argument": "simpson"}'
result of query
{
"kind": "callers",
"argument": "simpson",
"items": [
{
"caller": "area",
"path": "src/area.F90",
"line": 18,
"name": "simpson",
"kind": "function",
"resolved": [
"quad_rules::simpson"
],
"how": "unique",
"text": "print '(A, ES24.17)', 'simpson = ', simpson(sine, 0._R8P, pi, n)"
},
{
"caller": "integrate",
"path": "src/integrate.F90",
"line": 14,
"name": "simpson",
"kind": "function",
"resolved": [
"quad_rules::simpson"
],
"how": "unique",
"text": "print '(I6, 2ES18.4)', n, abs(trapezoid(sine, 0._R8P, pi, n) - 2._R8P), abs(simpson(sine, 0._R8P,..."
},
{
"caller": "integrate",
"path": "src/integrate.F90",
"line": 17,
"name": "simpson",
"kind": "function",
"resolved": [
"quad_rules::simpson"
],
"how": "unique",
"text": "print '(A)', 'Simpson, 64 intervals: '//trim(str(simpson(sine, 0._R8P, pi, 64), no_sign=.true., c..."
},
{
"caller": "integrate_mpi",
"path": "src/integrate_mpi.F90",
"line": 20,
"name": "simpson",
"kind": "function",
"resolved": [
"quad_rules::simpson"
],
"how": "unique",
"text": "piece = simpson(sine, a, b, n)"
},
{
"caller": "sweep",
"path": "src/sweep.F90",
"line": 18,
"name": "simpson",
"kind": "function",
"resolved": [
"quad_rules::simpson"
],
"how": "unique",
"text": "error(2, k) = abs(simpson(sine, 0._R8P, pi, n) - 2._R8P)"
},
{
"caller": "test_order",
"path": "test/test_order.F90",
"line": 12,
"name": "simpson",
"kind": "function",
"resolved": [
"quad_rules::simpson"
],
"how": "unique",
"text": "p_simpson = log(abs(simpson(sine, 0._R8P, pi, 16) - 2._R8P) / abs(simpson(sine, 0._R8P, pi, 32) -..."
},
{
"caller": "test_order",
"path": "test/test_order.F90",
"line": 12,
"name": "simpson",
"kind": "function",
"resolved": [
"quad_rules::simpson"
],
"how": "unique",
"text": "p_simpson = log(abs(simpson(sine, 0._R8P, pi, 16) - 2._R8P) / abs(simpson(sine, 0._R8P, pi, 32) -..."
}
],
"notes": [],
"files": 13
}The agent's edit
The agent "simplifies" Simpson's even-node loop, and gets the bound wrong:
$ git diff
diff --git a/src/quad_rules.F90 b/src/quad_rules.F90
index 563c154..4cb1281 100644
--- a/src/quad_rules.F90
+++ b/src/quad_rules.F90
@@ -84,7 +84,7 @@ contains
odd = odd + f(a + i * h)
enddo
even = 0._R8P
- do i = 2, n - 2, 2
+ do i = 2, n, 2
even = even + f(a + i * h)
enddo
s = (f(a) + f(b) + 4._R8P * odd + 2._R8P * even) * h / 3._R8PBefore it says it is done, it calls the gate:
$ python3 tools/mcp_client.py call gate '{"stages": ["build", "test", "verify"]}'
result of gate
{
"status": "fail",
"base": "",
"elapsed": 1.89,
"stages": [
{
"stage": "build",
"status": "pass",
"advisory": false,
"fact": "",
"details": [],
"commands": [
[
"build",
"--json"
]
],
"elapsed": 0.23
},
{
"stage": "test",
"status": "fail",
"advisory": false,
"fact": "test_exactness: failed: the program stopped itself: error stop at test/test_exactness.F90:10",
"details": [
"test_symmetry: failed: the program stopped itself: error stop at test/test_symmetry.F90:12"
],
"commands": [
[
"test",
"--json"
]
],
"elapsed": 0.93
},
{
"stage": "verify",
"status": "pass",
"advisory": false,
"fact": "",
"details": [],
"commands": [
[
"verify",
"--json"
]
],
"elapsed": 0.73
}
]
}The build passes and the test fails, with the location: test_exactness.F90:10, Simpson's rule on a cubic. The reference of area passes: the bug adds 2 f(b) to the sum, and for sin(x) on [0, pi] f(b) = sin(pi) is zero to rounding. One check rarely sees everything; the gate runs all of them and the agent gets the first failing fact.
fobis agents-md (track 4) writes the project's facts into AGENTS.md, so the agent knows the modes, the cases and the commands before it starts.
What you learned
fobis mcp, the read-only and the writing tools, an agent calling gate. Reference: MCP server, the gate.
This is the end of the tutorials. The guides and the reference cover every command and option.