60-minute workshop · bring your laptop · you drive an agent from exercise one
RED GREEN REFACTOR — now with agents
validate_spec checks its structure, refine_requirement critiques its wording — and the agent iterates until both are clean| 0–5 | Welcome, setup check (mvn -q package should be green) |
| 5–13 | TDD refresher, agents, and the SDD → BDD → TDD stack |
| 13–20 | The plumbing: MCP in seven minutes — the pre-built server and client, and how tools reach any agent |
| 20–32 | EX 1 Draft the spec with your agent, iterate with validate_spec |
| 32–52 | EX 2 Spec → Gherkin → Red/Green/Refactor, agentically |
| 52–55 | Why this works reliably |
| 55–60 | Takeaways, where to go next, Q&A |
The agent turns the crank. You steer.
| Pins | Canonical artifact | In this repo | |
|---|---|---|---|
| SDD spec-driven | the feature | versioned spec + acceptance criteria | requirements/requirements.json |
| BDD behavior-driven | one behavior | Gherkin scenario (Given/When/Then) | features/string_calculator.feature via Cucumber |
| TDD test-driven | one unit | failing JUnit test | StringCalculatorTest.java |
run_tests tool runs Cucumber + JUnit together: one bar, one colorTDD pins the unit. BDD pins the behavior. SDD pins the feature.
For an agent to run your tests, read your requirements, and respect your workflow, it needs a standard way to discover and call your tools.
That standard is the Model Context Protocol.
Think "USB-C for AI tooling": one connector, any host (Cursor, Claude, your own client), any tool.
+-----------------------------+ +---------------------------+
| HOST (Cursor, Claude, ...) | | MCP SERVER (yours!) |
| | stdio / | |
| +---------------------+ | HTTP | tools: |
| | MCP CLIENT |<--+----------+> list_requirements |
| | (one per server) | | JSON-RPC | get_requirement |
| +---------------------+ | 2.0 | validate_spec |
| ^ | | refine_requirement |
| | | | run_tests |
| LLM agent | | get_tdd_state |
+-----------------------------+ | start_refactor |
+---------------------------+
Three message types: requests (have an id), responses, notifications (no id).
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": { "protocolVersion": "2025-11-25",
"capabilities": { },
"clientInfo": { "name": "tdd-workshop-agent", "version": "1.0.0" } } }
{ "jsonrpc": "2.0", "id": 1,
"result": { "protocolVersion": "2025-11-25",
"capabilities": { "tools": { "listChanged": true }, "logging": { } },
"serverInfo": { "name": "tdd-workflow-server", "version": "1.0.0" },
"instructions": "Drives a spec-driven TDD/BDD workflow..." } }
initialize — version + capability negotiationnotifications/initialized — client says "ready"tools/list — discovery: what am I allowed to do?tools/call — invocation: do itDiscovery is the superpower: the agent adapts to whatever tools the server offers — no hardcoding.
mcp-server/ is complete, spec-driven, and tested to 100% coverage. Two minutes on its shape:
// TddMcpServer.main — the whole composition root
WorkflowToolHandlers handlers = new WorkflowToolHandlers(
() -> RequirementsRepository.load(requirementsFile), // re-read fresh per call
new SpecValidator(requirementsFile, root), // structure: is it usable?
new RequirementRefiner(), // wording: is it any good?
new MavenTestRunner(root),
new TddStateMachine());
McpSyncServer server = McpServerFactory.create(
new StdioServerTransportProvider(McpJsonDefaults.getMapper()), handlers);
One transport, seven tools. A stdio server must never print to stdout — that corrupts the JSON-RPC stream. Diagnostics go to stderr.
new SyncToolSpecification(
Tool.builder("get_requirement", Map.of(
"type", "object",
"properties", Map.of("id", Map.of(
"type", "string",
"description", "The requirement id, e.g. REQ-003")),
"required", List.of("id")))
.description("Get the user story and acceptance criteria for one requirement. "
+ "Turn each acceptance criterion into a failing JUnit test ...")
.build(),
(exchange, request) -> handlers.getRequirement(request.arguments()))
isErrorrun_tests
// MavenTestRunner: launch `mvn -q -B test -pl kata`, then parse Surefire XML
TestRunSummary summary = testRunner.runKataTests();
var phase = tdd.recordTestRun(summary); // failures -> RED, all pass -> GREEN
body.put("phase", phase.name()); // the agent literally sees the bar color
body.put("failureDetails", summary.failureDetails());
body.put("nextStep", tdd.suggestion()); // "Write the simplest code that passes..."
The TddStateMachine enforces the discipline in code:
start_refactor throws unless the bar is GREEN. The server is itself spec-driven — its own
requirements file, Cucumber scenarios, and 100% coverage: mvn verify -pl mcp-server.
ServerParameters params = ServerParameters.builder("java")
.args("-Dworkshop.root=" + root, "-jar", serverJar.toString())
.build();
var transport = new StdioClientTransport(params, McpJsonDefaults.getMapper());
McpSyncClient client = McpClient.sync(transport)
.requestTimeout(Duration.ofMinutes(6)) // run_tests invokes Maven
.clientInfo(new McpSchema.Implementation("tdd-workshop-agent", "1.0.0"))
.build();
client.initialize(); // the handshake
var tools = client.listTools(); // discovery
var result = client.callTool( // invocation
CallToolRequest.builder("run_tests").arguments(Map.of()).build());
Run it: mvn -q package && java -jar mcp-client/target/tdd-agent.jar
The spec comes first — and the agent helps write it, then refine it. Prompt your IDE agent:
"Add a new requirement torequirements/requirements.json: newlines may separate numbers in addition to commas. Follow the existing format — unique id, title, user story, acceptance criteria phrased Given/When/Then, status pending. Then callvalidate_specand fix every issue until the spec is valid. Then callrefine_requirementon the new requirement and reword it from the findings until there are none. Do not write scenarios or code yet — we are only agreeing on the spec."
validate_spec owns structure, refine_requirement owns wording quality| you | describe the feature in a sentence of intent |
| agent | drafts the requirement into requirements.json |
| structure | validate_spec → "criterion must be phrased Given/When/Then" → agent fixes → "valid": true (a well-formed draft is often valid on the first call — force an issue to demo the loop) |
| wording | refine_requirement → "'quickly' is ambiguous", "only happy paths — add an edge case" → agent rewords → validates → refines → "clean": true |
| you | read the story and criteria aloud — is this what we meant? Approve. |
The server re-reads the file on every call, so the spec the agent just wrote is the spec that gets critiqued. A valid, clean, approved spec is the entry ticket — no scenario or code before it.
Wire the server into your IDE agent (Cursor shown; Claude Desktop is identical JSON):
// .cursor/mcp.json (already in the repo)
{
"mcpServers": {
"tdd-workflow": {
"command": "java",
"args": ["-Dworkshop.root=${workspaceFolder}",
"-jar", "${workspaceFolder}/mcp-server/target/tdd-mcp-server.jar"]
}
}
}
Then prompt the agent:
"Using the tdd-workflow tools: validate the spec first, then find the next pending requirement, add a Gherkin scenario for its acceptance criteria to the feature file (tag it with the requirement id), reuse or add step definitions, add a matching JUnit unit test, run the tests to show RED, then implement the simplest code to reach GREEN, then refactor, then mark the requirement implemented in requirements/requirements.json. Ask me before each phase change."
| agent | validate_spec → the spec (including your new requirement) is valid |
| agent | get_requirement("REQ-003") → acceptance criteria + feature file location |
| agent | writes the @REQ-003 Gherkin scenario (BDD) and any @Test methods (TDD) |
| you | review the scenario — is this the behavior I want? (the spec review) |
| RED | run_tests → Cucumber + JUnit failures returned to the agent |
| agent | implements the simplest passing code in StringCalculator |
| GREEN | run_tests → all pass |
| REFACTOR | start_refactor → clean up → run_tests stays green |
| you | approve; agent marks REQ-003 implemented in the spec; repeat with REQ-004..006 |
validate_spec gates the pipeline — no scenario or code from a broken specrun_tests tells itvalidate_spec for structure, refine_requirement for wording — until valid and cleanwrite_test tool that scaffolds test files · add resources exposing the requirements doc · add prompts for each TDD phasenpx @modelcontextprotocol/inspectorQuestions — and let's see your green bars.
Option A — MCP Inspector (browser UI + message log):
npx @modelcontextprotocol/inspector \
java -Dworkshop.root=$PWD -jar mcp-server/target/tdd-mcp-server.jar
Option B — be the client yourself; paste one line at a time:
java -Dworkshop.root=$PWD -jar mcp-server/target/tdd-mcp-server.jar
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"me","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_tdd_state","arguments":{}}}
isError: true lets agents read failures and self-correct