facts-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@facts-mcpMeasure http://localhost:3000/api/orders for project checkout and report HTTP status"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
facts-mcp
An MCP server that only holds facts. It measures and it remembers; it never judges.
Point it at a service — a local app, a device on your network, a public API or website — and it returns the raw measurement with the time it was taken. Write down what you concluded, with the evidence, and it keeps that claim — for good. Nothing in it is a verdict, so an agent reading it does not have to verify the answer.
It knows nothing about any particular application. The project is a parameter of every call, and what the server may reach is set by whoever runs it, not by the code.
The three rules
Without these, this is just another agent you would have to check.
If something can be measured now, it is not answered from memory.
recall.*never measures. When it returns a claim backed by a measurement it also returnsremeasure_with— the exact tool and arguments to take that measurement again yourself.Every response carries the time it was observed, so staleness is visible. Each item in a collection carries its own age, not the age of the query.
No output field is a judgement. No
health,score,severity,passed,recommendation.okmeans only "the observation could be taken".
And the distinction that holds it together: an error of observation is not the observation
of an error. A cleanly received HTTP 500 is a success of the tool — ok: true,
http_status: 500. Only what prevented measuring is an error.
Related MCP server: Witness
Install and run
Requires Node 22.11 or newer. The package is not on npm yet; build it from a clone:
git clone https://github.com/Yesid8/mcp-measure-and-recall.git
cd mcp-measure-and-recall
npm ci
npm run buildIt speaks MCP over stdio. Point any MCP client at the built entry point, with a data directory of its own:
{
"mcpServers": {
"facts": {
"command": "node",
"args": ["/absolute/path/to/mcp-measure-and-recall/dist/main.js"],
"env": { "PLINTH_DATA_DIR": "/absolute/path/to/a/data/dir" }
}
}
}On Windows, write the paths with forward slashes or doubled backslashes: a single
backslash in JSON is an escape. If the client does not connect, read its stderr — every
refusal to start begins with facts-mcp: and says why. The guide for consumers is
docs/USO.md.
The six tools
Every call takes project, a free string that isolates one consumer's data from another's.
It is created on first use and returned normalized, so a typo is visible immediately.
Tool | What it returns |
| Status, headers, body, timings — and which IP it actually connected to |
| The messages that arrived in N seconds, each with absolute time and offset |
| The claim as written, with its evidence embedded |
| The claim marked retracted, with a reason. Never deleted |
| Claims with their age, their |
| The raw events of one claim, in order |
A worked example
Measure an endpoint:
// facts.measure.http
{ "project": "checkout", "url": "http://localhost:3000/api/orders" }{
"observation_id": "o_01K5Z9K2QX7T4BVWRM3P8QJHYD",
"observed_at": "2026-09-22T10:14:03.482+02:00",
"age_seconds": 0.14,
"age_basis": "monotonic",
"ok": true,
"error": null,
"data": {
"kind": "http",
"http_status": 200,
"body": { "orders": [] },
"peer": { "address": "127.0.0.1", "port": 3000, "family": 4 }
}
}Write down what you concluded, citing that measurement:
// facts.recall.assert
{
"project": "checkout",
"statement": "GET /api/orders responds 200 with an empty array",
"evidence": [{ "type": "observation", "observation_id": "o_01K5Z9K2QX7T4BVWRM3P8QJHYD" }],
"observed_by": "claude-opus-5 (session 3f9a1c7e)",
"tags": ["api", "orders"]
}Three days later, someone reads the registry:
// facts.recall.query → { "project": "checkout", "text": "orders" }
{
"claim_id": "c_01K5Z9M5P0RA2CF7XN4T6WQJBE",
"statement": "GET /api/orders responds 200 with an empty array",
"age_seconds": 259200.0,
"stale": true,
"remeasure_with": {
"tool": "facts.measure.http",
"arguments": { "project": "checkout", "url": "http://localhost:3000/api/orders" },
"omitted": []
}
}The server does not decide whether to re-measure. It gives you the age and the how; the
decision is yours. If the answer has changed, retract with a reason — the original claim
stays in the log, byte for byte, and facts.recall.history shows both events.
Where the data lives
$PLINTH_DATA_DIR/ # defaults to ~/.plinth
└── projects/<project>/logs/
├── claims.jsonl # append-only. Readable with tail, copyable with rsync
└── observations.jsonl # measurements, failed ones included, so they can be citedclaims.jsonl is append-only in the strict sense: a retraction is a new line, never an edit.
The current state is a projection over the events. There is no update and no delete — not
discouraged, absent from the interface.
Network safety
Measurements come from the server's network position, and the destination is validated by
resolved IP, not by name. A name is not a destination: evil.localhost can resolve to a
public address, and can resolve to loopback once and to something else a moment later.
Every resolved address is classified; if any falls outside the profile, the call is refused without opening a socket.
The connection is made to the validated address, with the original
Hostand SNI, which closes the window between checking and connecting.Every redirect hop repeats the whole check.
169.254.169.254and all link-local addresses are denied in every profile, allowlist included. That is the first stop of any exfiltration in a container.Environment proxies are ignored: a proxy would bypass the IP validation entirely.
The operator picks one profile when starting the server; no tool argument can widen it:
Profile | Reaches |
| Nothing |
| Services on the same machine |
| The same machine and private networks (RFC 1918, |
| Exactly the |
An allowlist entry is an exact host:port. There are no domain wildcards, because a
wildcard turns an MCP server into an outbound proxy with an extra step; a malformed entry
stops the server at startup rather than being ignored. The active profile is stated in
server/discover, so the model knows where it can measure from.
Secrets (authorization, cookie, set-cookie, API keys, JWTs, PEM blocks, URL userinfo,
query parameters such as ?token=) are sent intact to the destination and redacted
before anything is written to disk, returned, or logged — claims and retraction reasons
included. There is no window in which a credential sits in a file. remeasure_with leaves
out whatever was redacted rather than replaying a placeholder as a broken request, and names
it in omitted so you know what to supply.
Configuration
Read once at startup. A value that does not parse stops the server with a message naming the variable.
Variable | Default | Effect |
|
| Where the registry lives. Give each client its own: one server per directory |
|
|
|
| — | Comma-separated |
|
| Hops a call may follow when it asks to ( |
|
|
|
To measure a public site, for example:
"env": {
"PLINTH_DATA_DIR": "/absolute/path/to/a/data/dir",
"PLINTH_FACTS_NET_PROFILE": "allowlist",
"PLINTH_FACTS_ALLOW_HOSTS": "api.example.com:443,www.example.com:443"
}No variable introduces knowledge of any particular project: no base URLs, no routes, no credentials. That is the point — the same server serves every app you point it at.
What it will never do
measure.assert_status · measure.wait_until · recall.summarize · recall.rank ·
recall.delete · project.health
Each one breaks rule 1 or rule 3. The moment this server starts giving verdicts it stops being a source and becomes another agent you have to verify — which is exactly the work it exists to save.
Protocol
Implements MCP 2026-07-28 — the stateless revision: no initialize handshake, no sessions,
server/discover required, resultType on every result.
The wire format is implemented directly against the normative schema, which is vendored in
vendor/ and checked against the implementation at compile time. The official SDK for this
revision exists as @modelcontextprotocol/server 2.0; whether to adopt it is a measured
decision still to be taken, and every protocol detail lives behind src/protocol/ so that
swapping it touches nothing else. See ADR-0001
and ADR-0002.
Development
npm install
npm run typecheck # strict, with noUncheckedIndexedAccess and exactOptionalPropertyTypes
npm test # unit, contract and conformance tests
npm run test:coverage # the same, with a coverage floor that fails the run
npm run lint
npm run format:check
npm run buildCI runs all of the above on Linux and Windows, on Node 22 and 24. Windows is in the matrix on purpose: file truncation and stdin chunking behave differently there, and two of the bugs the tests now guard against only ever showed up on it.
The TLS tests generate a throwaway certificate with openssl and skip when it is not
installed; no key material is committed.
The tests are the contract, not examples. They fail if the server starts judging: a 500 must
stay ok: true, silence on a socket must stay a valid observation, a retraction must leave
the previous bytes untouched, and a clock jumping backwards must never produce a negative age.
The mechanical checks of the specification run as ordinary tests too: every schema field
described, no judgement key in any output, no secret in any file on disk, no socket opened
under deny-all, no application name anywhere in src/.
Specification in docs/. Prose is in Spanish; code, field names and error codes are in English. Contributions: CONTRIBUTING.md. Security reports: SECURITY.md.
License
0BSD: use, copy, modify and distribute it for any purpose, with or without fee, and with no attribution required.
This server cannot be deployed
Maintenance
Related MCP Connectors
Cryptographically anchored evidence for agents: verified run receipts, proof-gated settlement.
Machine-native utility network: verified evidence services for autonomous agents.
Attestation infrastructure for the agentic economy: signed, independently verifiable verdicts.
Market evidence with receipts: every claim resolves to a real stored record you can fetch back.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables submitting claims and receiving Ed25519-signed, hash-chained verdicts resolved against real external ground truth, supporting resolvers like GitHub PRs, on-chain transactions, URL JSON, HTTP status, and Kalshi markets.5352 npmMIT
- AlicenseNot gradedqualityAmaintenanceProvides evidence-oriented MCP service for cryptographically identified agents, bounded public contracts, privacy-preserving records, and append-only audit.Apache 2.0
- AlicenseBqualityBmaintenanceEnables software agents to maintain a signed, evidence-aware local ledger of claims, with policy-gated proposals, conflict queries, and constitution-pinned access through an MCP stdio server.73Apache 2.0
- AlicenseNot gradedqualityAmaintenanceEnables agents and applications to record, query, verify, and receive hash-chained event provenance over stdio, supporting self-metering and independent audit of autonomous system activity.6MIT