Build your own Claude Code
MCP server·20 min

Client ↔ server round-trip

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage round-trip --watch

You built both halves. In Chapter 9 your client learned to frame a tools/call, send it, and parse { content, isError } back out. In this chapter your server learned to receive that call, run the tool, and answer. This step closes the loop: your client talks to your server, no real subprocess in between, and a result comes back the way it would over a live stdio pipe.

Nothing new gets implemented here — that's the point. If both sides speak the protocol correctly, wiring them together is just a function call: the client builds a tools/call request, the server's handle returns a response, and the client's parser pulls the text out the far end. A green here is proof your two chapters agree on the same wire format down to the byte.

client → server → client, in one process
client.buildCall("echo", { a: 1 })   →  { jsonrpc:"2.0", id, method:"tools/call", params:{ name, arguments } }
                                          │
                                  server.handle(req)
                                          │
                                          ▼
                          { result:{ content:[{type:"text", text:"echo({\"a\":1})"}], isError:false } }
                                          │
                              client.parseResult(resp)   →  "echo({\"a\":1})"

What to build

Your task

Wire the Ch9 client to your Ch10 server

Implement roundTrip(tools, tool, args) in src/mcp.ts. Stand up your MCPServer from the provided tools, then drive it through your Chapter 9 client code — don't shortcut by calling handle with a hand-built object.

  1. Construct the MCPServer with tools.
  2. Use the client to build a tools/call request for tool with arguments: args — the same request shape your Ch9 client sent over stdio.
  3. Pass that request to server.handle(...) (this stands in for the stdio transport).
  4. Use the client's result parser to extract the text from { content, isError }, and return it as result.

For the fixture (echo, { a: 1 }) that text is echo({"a":1}). Graded by program.ts mcp-roundtrip.

src/mcp.ts
import { MCPClient } from './client.ts' // your Chapter 9 client
import type { ToolDef } from './types.ts'

export function roundTrip(
  tools: ToolDef[],
  tool: string,
  args: Record<string, unknown>,
): { result: string } {
  const server = new MCPServer(tools)
  const client = new MCPClient()
  // 1. req = client.buildCall(tool, args)   // Ch9: builds the tools/call frame
  // 2. resp = server.handle(req)            // Ch10: runs it, returns { content, isError }
  // 3. text = client.parseResult(resp)      // Ch9: pulls content[0].text out
  // return { result: text }
}
The handle call IS the transport

Over a live connection, your client's request would be JSON-encoded, line-framed, written to the server's stdin, read back off its stdout, and parsed. Here you collapse all of that into one server.handle(req) call. The framing and subprocess plumbing are already proven in Chapter 9 — this step isolates the semantic round-trip: does the request the client builds match the request the server expects, and does the response the server emits match what the client knows how to read?

Drive it through the client — don't hand-roll the request

It's tempting to skip the client and call server.handle({ method: 'tools/call', params: { name: tool, arguments: args } }) directly. Don't. The whole value of this step is that the same client code that drove a real server in Chapter 9 now drives yours unchanged. If your client's request shape and your server's expectations have quietly diverged, hand-rolling the request papers over exactly the bug this step exists to surface.

How it's graded

Process mode. program.ts mcp-roundtrip reads case.json — serverTools (one echo tool), tool: 'echo', args: { a: 1 } — calls your roundTrip, and prints what comes back. The output is compared to fixtures/expected.json exactly: {"result":"echo({\"a\":1})"}. A correct round-trip threads the client-built request through handle and parses the text back out; any divergence between the two halves shows up as a mismatched (or empty) result.