MCP / coding-agent integration
horologium-mcp is a thin, stateless MCP adapter: every MCP tool maps 1:1 onto
exactly one Product Truth read endpoint. It doesn't infer, cache, join, or
resolve anything itself — every fact an agent sees through it (canonical ID,
derivation, an Unresolved reason, an exact evidence citation) is precisely
what the web dashboard sees, because both read the same API. It is
read-only — there is no MCP tool for submitting proposals or writing
anything to Product Truth.
Tool map
Fourteen tools, one per read endpoint:
| MCP tool | API call |
|---|---|
get_workspace | GET /v1/workspace |
list_entities | GET /v1/entities |
get_entity | GET /v1/entities/{id} |
get_entity_claims | GET /v1/entities/{id}/claims |
get_entity_relationships | GET /v1/entities/{id}/relationships |
get_entity_evidence | GET /v1/entities/{id}/evidence |
list_findings | GET /v1/findings |
get_finding | GET /v1/findings/{id} |
get_evidence | GET /v1/evidence/{id} |
list_deployment_targets | GET /v1/deployment-targets |
get_deployment_target_state | GET /v1/deployment-targets/{key}/state |
get_capability_timeline | GET /v1/capabilities/{id}/timeline |
list_property_defs | GET /v1/property-defs |
get_version | GET /v1/version |
get_entity already bundles a capability's evidence, open findings, and
per-target deployment reality in one response — that's the tool to reach for
first when an agent needs "what do we know about this capability, and can we
trust it?" Every entity-scoped tool accepts an at (RFC3339) parameter for
asking what was true at a point in time, rather than needing a separate
history tool for each one — see Product Shape and temporal
history.
get_workspace (GET /v1/workspace, ADR-0016 D4) tells an agent what it's
actually connected to: the workspace/product identity and a data_class
field. data_class: "synthetic" means every fact returned by every other
tool this session describes fictional demo data — call it once at the start
of a session, especially the hosted-demo session below, to confirm you're
not looking at anything real before you quote it back to anyone.
Configuration
| Env var | Default | Purpose |
|---|---|---|
HOROLOGIUM_API_BASE_URL | http://127.0.0.1:8080 | Base URL of a running horologium-server |
The adapter runs over stdio — the transport Claude Code and other MCP-compatible coding agents use to launch a local MCP server.
Adding it to a coding agent
Claude Code — the copy/pasteable path
The one-line setup, from the repo root, against a horologium-server you
already have running at http://127.0.0.1:8080:
claude mcp add horologium -e HOROLOGIUM_API_BASE_URL=http://127.0.0.1:8080 \
-- uv run --project python/horologium-mcp horologium-mcp
claude mcp add <name> -e KEY=VALUE -- <command> [args...] registers a
stdio server — the transport this adapter speaks — and writes the entry into
your local Claude Code MCP config (-s local, the default; pass -s project to check the entry into .mcp.json for a team instead). Verify the
connection came up before asking anything:
claude mcp list
# horologium: uv run --project python/horologium-mcp horologium-mcp - ✓ Connected
Removing it later: claude mcp remove horologium.
The equivalent .mcp.json (for a team, or a non-Claude-Code client)
If you'd rather check the config into the repo (claude mcp add -s project
does this for you), or you're wiring up a different MCP-capable client, this
is the same server as a raw config block — add it to your project's
.mcp.json (create one at the repo root if you don't already have one —
don't overwrite an existing one):
{
"mcpServers": {
"horologium": {
"command": "uv",
"args": ["run", "--project", "python/horologium-mcp", "horologium-mcp"],
"env": {
"HOROLOGIUM_API_BASE_URL": "http://127.0.0.1:8080"
}
}
}
}
Against a self-hosted instance running under Docker Compose
{
"mcpServers": {
"horologium": {
"command": "docker",
"args": ["compose", "run", "--rm", "-T", "horologium-mcp"]
}
}
}
This connects to the same self-host instance over the internal Compose network — the same Product Truth the dashboard shows, no separate data path.
Against the hosted demo
Safe, synthetic-only configuration:
The hosted demo (workspace demo, product acme-store, data_class: "synthetic" — ADR-0016 D7) is a fictional company's Product Truth: no real
repository, customer, or credential is ever behind it. Point either setup
above at its origin instead of a local server, and nothing else changes —
the MCP adapter has no separate "demo mode", it's the same stateless HTTP
client pointed at a different HOROLOGIUM_API_BASE_URL:
claude mcp add horologium-demo \
-e HOROLOGIUM_API_BASE_URL=https://horologium-demo-551243130607.us-central1.run.app \
-- uv run --project python/horologium-mcp horologium-mcp
or the .mcp.json equivalent:
{
"mcpServers": {
"horologium-demo": {
"command": "uv",
"args": ["run", "--project", "python/horologium-mcp", "horologium-mcp"],
"env": {
"HOROLOGIUM_API_BASE_URL": "https://horologium-demo-551243130607.us-central1.run.app"
}
}
}
}
No credentials are required — the demo has none of its own (see
Security, privacy and data boundaries), and it is
read-only (HOROLOGIUM_READ_ONLY=true, ADR-0016 D7), so nothing an agent
does through this server can write anything back. Ask get_workspace first
— it should report "data_class": "synthetic" — to confirm the agent knows
it's looking at demo data before it quotes anything back to you. This is the
fastest way to try MCP integration before setting up a self-host instance —
see the hosted demo walkthrough for what data it
exposes.
Full public-hosted verification (a fresh machine, zero prior setup,
connecting to the public URL above with nothing self-hosted) depends on the
Alpha hosted deployment itself landing (HORO-524) and the distribution
policy it depends on (HORO-540, deliberately still open) — the steps above
are exactly what that flow will look like once both land. Every step in this
document is verified today against a local horologium-server instance
seeded with the same golden fixture the hosted demo serves; see "Verifying
it end to end" below.
Portability to other MCP-capable clients
horologium-mcp speaks standard MCP over stdio with no Claude-specific
behavior — the server, its tool schemas, and its transport are identical
regardless of which client launches it. Any MCP-capable client that can
launch a local stdio server and pass it environment variables (Codex CLI,
Cursor, and other MCP-compatible IDEs generally support this) should be able
to run it, using that client's own config format with the same command
(uv), args (run --project python/horologium-mcp horologium-mcp), and
env (HOROLOGIUM_API_BASE_URL) values shown above — consult that client's
own MCP documentation for its exact config file shape, since that varies by
client and this repository only verifies Claude Code directly (see
"Verifying it end to end" below). Clients that only speak HTTP/SSE
transports are not supported today — this adapter ships stdio only (see
python/horologium-mcp/src/horologium_mcp/server.py's main()); that is a
genuine constraint, not a documentation gap, and is unaffected by this
ticket.
What to ask
Once connected, these are the high-value questions this integration exists to answer — each maps to a small tool sequence, never a single opaque "summarize everything" call:
- "What's actually running in production right now?" →
list_deployment_targets, thenget_deployment_target_statefor the target withis_primary_production: true. Against the golden fixture this reports production on1.8.0while beta is already on2.0.0—main != productionis a fact this tool surfaces directly, not something the agent has to infer from a git log. - "What does Product Truth say about Refund?" →
list_entitieswithq="Refund"to resolve the name to a canonicalentity_id, thenget_entityfor its claims, evidence, open findings, and per-target deployment reality in one response. - "Which sources conflict?" →
list_findings(optionally filteredkind="documentation_conflict"orkind="claim_conflict"), thenget_findingfor one finding'sbasis_ids(the disagreeing claims — ontology §6). Match those claim IDs againstget_entity_claims's result to get each claim's ownevidence_ids, thenget_evidenceper ID to show the two disagreeing citations side by side. - "Show deployment/history/evidence for this feature." →
get_capability_timelinefor the transition events (not sampled snapshots),get_entity_evidencefor its citations, andget_deployment_target_stateper target for current reality — three separate tool calls an agent composes itself; the MCP layer does not join them into one response (ADR-0009).
A fuller example: "What's the current refund-window capability's status,
and is it live on production?" The agent calls list_entities to resolve
the capability, then get_entity for its evidence, open findings, and
per-target deployment reality — the same documentation_conflict and
released_not_live findings described in Integrity states and the
evidence model, each with a citation the
agent (or you) can follow to the exact source.
Verifying it end to end
Two layers of automated verification exist in the repository, both proving the same invariant — an agent sees exactly what Web/API see, never a second, MCP-specific interpretation:
python/horologium-mcp/tests/contract/test_rest_parity.py(HORO-466): every tool, called through its real MCP tool-dispatch path, is asserted byte-for-byte equal to the matching REST call, against a realhorologium-serverseeded with the golden scenario (crates/horologium-fixtures's hyphenatedseed-golden-fixture— seedocs/testing.mdfor why that binary and nothorologium-api's own underscoredseed_golden_fixture). Covers the exactbeta=2.0.0/production=1.8.0"released but not live" scenario above.python/horologium-mcp/tests/contract/test_high_value_queries.py(HORO-527): the four questions above, each encoded as its own test against the same live fixture server, asserting the specific facts an agent must see — production pinned below beta, the Refund capability's conflicting claims, a documentation-conflict finding's two citations, and a non-empty timeline plus evidence for the same capability — and aget_workspacetest asserting the reporteddata_classmatches the server's own configuration.
Run both with (see python/horologium-mcp/README.md's "Contract tests"
section for the full Postgres/binary setup):
uv run pytest python/horologium-mcp/tests/contract
If you're verifying your own setup by hand instead: connect a client with
claude mcp add as shown above, ask an agent a Product Truth question
through MCP, then confirm the same answer (same canonical IDs, same
citations) comes back from curl-ing the equivalent /v1/* endpoint
directly.
What this proves, and what it doesn't: every command above runs against
a horologium-server bound to 127.0.0.1 — a fresh Claude Code client
connecting locally to that instance, exactly as documented above, is
verified end to end. Connecting to the public hosted URL from a genuinely
fresh machine with nothing self-hosted is not yet verified this way, because
the public hosted deployment depends on HORO-524 (and the still-open
HORO-540 distribution decision) landing first — see the note under "Against
the hosted demo" above.