A solid stone wall separates two rooms; the Gopher slides an open toolbox into a window through the wall, and on the other side the Ghost, a stone android, reaches in and lifts out a wrench.

Run it

An agent filing issues through an MCP server

The crash-triage agent reaching its issue tracker only through the Model Context Protocol, with the server started as a separate process. Replay the run offline, serve the tracker to any MCP client, and pin the server’s tool definitions so a changed description stops the run.

Draws on chapter 10 of the bookUpdated

This page works on one stage of the loop

  1. 1Build the request
  2. 2Call the model
  3. 3Run the tools (this page)
  4. 4Decide

The tracker is the one part of the triage job that lives in another program, with its own data. In this example the agent reaches it only through MCP: the program starts the tracker’s MCP server as a child process, lists its tools, and offers them to the model under prefixed names such as tracker__issue_search. The model’s replies are recorded; the server, the protocol, and every tool run for real.

What it shows

  • A hand-written MCP client and server in Go, newline-delimited JSON-RPC over standard input and output. MCP in Go explains both sides.
  • Server tools adapted into agent tools, with names prefixed by the server’s name, descriptions labeled as the server’s, and untrusted hints ignored.
  • A server whose tool definitions can be pinned by hash, so a change stops the run before it starts.

Run it

From the code directory of a clone of go-agents-book-code, with no OPENROUTER_API_KEY set:

$ go run ./ch10

It replays a 13-turn run of the same task as the ch07 example, four crash reports with two already filed. Notice the tool names in the second turn:

go run ./ch10, turn 2
turn 2
  model says:  Four reports fall into three groups. I'll read each group's
               details first.
  model asks:  crash_read {"id": "cr_fd4982"}
  model asks:  crash_read {"id": "cr_1e2ea0"}
  model asks:  crash_read {"id": "cr_f9ee9e"}
  model asks:  tracker__issue_search {}
  tool result: cr_fd4982: panic in v1.4.0 (commit 9c41e... (16 lines)
  tool result: cr_1e2ea0: panic in v1.4.0 (commit 9c41e... (16 lines)
  tool result: cr_f9ee9e: panic in v1.4.0 (commit 9c41e... (17 lines)
  tool result: #101 Crash in auth.parseBearer: runtime ... (5 lines)

tracker__issue_search is the server’s issue_search, reached over the protocol. On this run the agent marked both already-filed crashes as duplicates of their existing issues, filed one new issue through the server, and scored 16 out of 16 on the catalog’s checks, for $0.0055:

The end of the run
stopped: the model answered
166511 tokens in (153368 from the cache), 4745 out, $0.0055
with the sub-agent's calls: $0.0055

diagnosis:
  fp_4828a6 cr_1e2ea0,cr_ee551c: fixed, root cause store/store.go (*Store).Get, issue #103
  fp_6b14b3 cr_f9ee9e: duplicate, root cause auth/auth.go parseBearer, duplicate of #101
  fp_2bcd17 cr_fd4982: duplicate, root cause notify/notify.go (*Dispatcher).Stop, duplicate of #102
filed #103: store.Get returns (nil, nil) for a deleted reminder, crashing api.owned

graded against the catalog: 16 of 16 checks passed

The prompt had changed since chapter 7, which is where the duplicates came from; the protocol didn’t change what the agent could do. In the chapter’s three runs each way on the default model, the agent worked with the server’s tools as readily as with its own hand-written ones, and every run passed every check.

Serve the tracker to anything

The server is an ordinary command, and any MCP client can start it: the triage agent, or an assistant app you already use.

ch10/cmd/tracker-mcp/main.go
func main() {
	data := flag.String("data", "issues.json",
		"file the issues are kept in")
	flag.Parse()
	log.SetOutput(os.Stderr) // standard output carries only messages
	err := trackermcp.Serve(context.Background(), *data, time.Now,
		os.Stdin, os.Stdout, os.Stderr)
	if err != nil {
		log.Fatal(err)
	}
}

Its log goes to standard error, since standard output carries only protocol messages: a single leftover fmt.Println would reach the client as a line it can’t parse as a message. To connect it to an assistant app, add a server entry to that app’s MCP configuration naming the command to run, go run ./ch10/cmd/tracker-mcp -data /path/to/issues.json, from the code directory; each app documents its own format.

Pin the server’s tools

A server’s tool descriptions go straight into the model’s context. One sentence in a description decided 79 of 80 first moves in the book’s measurement, so the client can pin each tool’s definition by hash after a person reads it:

$ go run ./ch10 -print-digests > pins.json
$ go run ./ch10 -tracker-pins pins.json

The first prints the four tools’ digests; the second runs the replay with them checked. Next, edit a single word in the description of issue_search in ch10/triage/issuetools.go, then repeat the second command. The run stops before it starts, with an error naming the tool, its new digest, and the pinned one.

Run the tests

$ go test ./ch10/mcp/ ./ch10/mcptools/

They cover the protocol (discovery, pagination, cancellation at a timeout, falling back to the older handshake for servers that need it), launching and shutting down a server process in the sequence the protocol requires, keeping the agent’s API key out of the server’s environment, and the trust rules: hidden characters removed, descriptions labeled, and pins that fail on any change to a description or a schema.

Key files

File What it holds
ch10/mcp/jsonrpc.go JSON-RPC messages, errors, and line framing
ch10/mcp/client.go The client: one reader, calls matched by ID, timeouts, starting and stopping a server
ch10/mcp/server.go The server: one goroutine per request, cancellation, pagination
ch10/mcptools/adapter.go Server tools as agent tools
ch10/mcptools/trust.go Labels, hidden characters, schema limits, digests, and pins
ch10/trackermcp/trackermcp.go The tracker’s four tools, served
ch10/sdk The same server on the official MCP Go SDK, in a module of its own