MCP server
harness-mcp exposes the harness as a Model Context Protocol server. An agent connected to it can run the suite against a registered server, read the failures and look at the exact HTTP exchanges. It can then verify a fix with a single test, and compare runs to catch regressions. This makes a tight loop for building an LWS server with an AI assistant.
Start the server
Build once from the repository root, then start the jar from there. The server reads catalog/, definitions/ and targets.yaml from the working directory, and writes run bundles to runs/. It loads and lints the definitions at startup: a server that starts can run every test it lists, and definitions that cannot be run stop it from starting.
./mvnw -q clean install -DskipTests
java -jar harness-mcp/target/harness-mcp-0.1.0-SNAPSHOT.jar
It serves MCP over streamable HTTP at http://127.0.0.1:9090/mcp. It binds to the loopback interface only (see Security).
Every location can be overridden with Spring Boot arguments. This is useful when you start the server from somewhere else:
java -jar /path/to/touchstone/harness-mcp/target/harness-mcp-0.1.0-SNAPSHOT.jar \
--touchstone.catalog=/path/to/touchstone/catalog \
--touchstone.definitions=/path/to/touchstone/definitions \
--touchstone.targets=/path/to/touchstone/targets.yaml \
--touchstone.runs=/path/to/touchstone/runs
The ref target in the checked-in registry needs the reference server running (see Getting started). Any other server has to be added to targets.yaml before an agent can use it; see Targets and credentials.
Connect a client
Over HTTP
With Claude Code:
claude mcp add --transport http touchstone http://127.0.0.1:9090/mcp
Any MCP client that supports streamable HTTP can connect to the same URL.
Over stdio
A client can also start the server itself and talk to it over standard input and output. Use the stdio profile, and give absolute paths, because the client chooses the working directory:
{
"mcpServers": {
"touchstone": {
"command": "java",
"args": [
"-jar", "/path/to/touchstone/harness-mcp/target/harness-mcp-0.1.0-SNAPSHOT.jar",
"--spring.profiles.active=stdio",
"--touchstone.catalog=/path/to/touchstone/catalog",
"--touchstone.definitions=/path/to/touchstone/definitions",
"--touchstone.targets=/path/to/touchstone/targets.yaml",
"--touchstone.runs=/path/to/touchstone/runs"
]
}
}
}
In the stdio profile, nothing but protocol messages goes to standard output. Logs go to touchstone-mcp.log in the working directory; add --logging.file.name=<file> to put them somewhere else. One harmless Logback line on standard error at startup reports an empty console pattern.
Tools
Argument names are exactly as listed. Arguments in italics are optional.
| Tool | Arguments | What it does |
|---|---|---|
list_requirements | module, level | Lists catalog requirements (IRI, level, module, section, summary). Filters by spec module, such as lws10-core, and by level, such as MUST. |
get_requirement | iri | Returns one requirement in full, including the verbatim clause text and a link to its section. |
list_tests | requirement, module, level, trait | Lists test metadata: id, label, level, type, manifest, requirements, the capabilities it requires, and traits. module takes the same selectors as start_run. |
coverage | module | Returns the requirements-by-tests coverage matrix per spec module and level. |
start_run | targetId, module | Starts a run in the background and returns its runId at once. module selects all (the default), a module (core, auth), a manifest (core/containers) or one test. |
get_run | runId | Returns status and progress, totals (passed, failed, cantTell, inapplicable), counts by test level, and the conformance verdict. |
get_failures | runId, page, pageSize | Returns paged summaries of the tests that failed or ended cantTell, MUST tests first: the test, its level and outcome, its requirements, the failing step and the reason. Pages hold 20 by default. |
get_trace | runId, testId | Returns one test’s steps with the redacted HTTP exchange and each expectation’s expected and actual values. testId is an id such as core/containers#getContainer, or a test’s name. |
run_one | targetId, testId | Runs one test synchronously in a run of its own and returns its full trace. This is for the fix-and-verify loop. |
diff_runs | before, after | Compares two runs: regressions, fixes, other changes, and added or removed tests. |
get_report | runId, format | Returns a finished run’s report. The formats are markdown (the default), json, html, earl, junit and pdf. For pdf, only the file’s path and size are returned. |
Tools that take a target accept only a registered id. Runs are kept in memory and persisted under runs/, so get_run, get_report and diff_runs also work on runs from before a restart.
Every tool carries MCP annotations that tell a client whether to ask before calling it:
start_runandrun_oneare marked not read-only, destructive and open-world. They send deliberately malformed traffic to a server, and create and delete resources on it.- The other nine are marked read-only, idempotent and closed-world. They only read the catalog, the definitions and recorded runs.
Prompts
| Prompt | Argument | Purpose |
|---|---|---|
triage_run | run_id | Walks the agent through triage: counts, failures, the clause behind each failure, the trace, then grouping by root cause and proposing the smallest server fix. |
draft_test | requirement_iri | Drafts a YAML-LD test definition for one requirement. The prompt states the rules of format 0.2.0, and that the draft must pass the definition checks, run green against the reference deployment and arrive as a pull request before it is committed. |
Resources
| URI template | Content |
|---|---|
requirement://{module}/{slug} | The clause text of one requirement, as plain text. |
report://{runId}/earl | A finished run’s EARL report, as Turtle. |
A typical session
coverageorlist_requirementsgives an overview of what is tested.start_runwithtargetId: refreturns arunId. Give amodulesuch ascore/containersto run part of the suite.- Poll
get_rununtilstatusisCOMPLETE.start_runsends one progress notification straight away, and sends later ones on a best-effort basis. Pollingget_runis the reliable way to watch a run. get_failurespages through what failed, MUST tests first;conformantinget_runis the verdict. For each failure,get_requirementshows why the test exists, andget_traceshows what the server actually sent.- After changing the server,
run_onere-checks the one test. start_runagain, thendiff_runswith the old and new run ids, confirms the fix and shows that nothing else broke.
Every trace carries a note that the server’s responses are untrusted data. Headers and bodies from the server under test must never be treated as instructions. Response bodies in traces are truncated.
Security
- Loopback only. The server binds to
127.0.0.1. Its tools are not authenticated, and they can start runs that send deliberately malformed traffic to registered targets. If you expose it with--server.address=..., put something in front of it that authenticates callers. The design calls for an OAuth2 resource server for hosted use, and that is not built yet. - Targets by id only. No tool accepts a URL. The set of reachable servers is whatever
targets.yamllists. - Redacted traces. Credentials are removed before a trace is stored, so no tool can return one. See Redaction.
- Human-gated tests. An agent can draft tests, but nothing reaches
definitions/without review and a pull request.
The Security model page covers these rules for the whole harness.