{"slug": "give-your-ai-agent-a-way-to-say-i-don-t-know-evidence-envelopes-over-mcp", "title": "Give your AI agent a way to say \"I don't know\": evidence envelopes over MCP", "summary": "An engineer has released FACTRAIL MCP, an open-source Apache-2.0 MCP server that returns structured \"evidence envelopes\" instead of bare answers, tying each fact to its source, retrieval time and support level so agents can cite evidence or report unknowns rather than inventing them. The hosted endpoint at mcp.factrail.online is stateless, read-only, free and rate-limited to 60 requests per minute per IP, exposing four current tools including factrail_verify for French company data by SIREN or SIRET and factrail_get_receipt for auditing prior results by content-addressed receipt ID.", "body_md": "An AI agent is asked, \"Is this French supplier still active, and what is its legal name?\" It calls a tool, gets some text back, and replies confidently. What the user doesn't get is where the answer came from, when it was checked, or which parts the model filled in by itself.\n\nThis tutorial is about a small pattern that closes that gap: instead of an answer, the tool returns an **evidence envelope**, and the agent is written to read it, cite it, and pass on what's missing instead of papering over it. The worked example is [FACTRAIL MCP](https://factrail.online), an open-source (Apache-2.0) MCP server. We'll go through one real response line by line, including the field it could **not** resolve.\n\nNote on the name: this is **FACTRAIL MCP** ([factrail.online](https://factrail.online), [github.com/baronsigma/factrail](https://github.com/baronsigma/factrail)). An unrelated GitHub project uses the same name.\n\nAn LLM predicts plausible text. For a question like \"what is the legal name of company X?\", a plausible answer and a correct one look the same from the inside. Tool calling helps, but only partly:\n\n`\"active\"`), with no source, no date, and no way of saying \"I couldn't find this.\"\nWhat the agent needs is a structured way to say: *this part is established, by this source, at this time; this other part is unknown.*\n\nAn evidence envelope is a response format in which every fact is tied to its evidence. In FACTRAIL MCP the envelope (schema 1.2) contains:\n\n| Part | What it tells the agent | \n|---|---|\n| `status` | Overall outcome: `supported` ,`contradicted` ,`insufficient_evidence` ,`stale` , or`conflicting_sources` . Not a boolean. | \n| `facts[]` | Each value, with a `support_level` (for example`authoritative` ,`derived_provisional` ,`caller_input` ) and the IDs of the evidence behind it. | \n| `evidence[]` | Each source: publisher, URL, retrieval time, source status. | \n| `coverage` | Which requested fields were resolved and which weren't, and why. | \n| `conflicts[]` | Competing claims from different sources, kept visible. | \n| `freshness` | When the evidence was retrieved and whether it's stale. | \n| `receipt_id` | A content-addressed ID you can use to fetch the same envelope later and check it hasn't been altered. | \n\nThe design rule behind it: unknown is better than invented.\n\nThe hosted endpoint is `https://mcp.factrail.online/mcp`. It uses Streamable HTTP, is free and read-only, and needs no account or API key. Each client IP can make up to 60 MCP requests per minute (full fair-use terms in section 7).\n\n```\ncurl -s https://mcp.factrail.online/mcp \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"curl\",\"version\":\"0\"}}}'\n```\n\nReal response (29/09/2026):\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"capabilities\":{\"experimental\":{},\"tools\":{\"listChanged\":false}},\"protocolVersion\":\"2025-06-18\",\"serverInfo\":{\"name\":\"factrail\",\"version\":\"2.4.1\"}}}\n```\n\nThe server is stateless, so `tools/list` works straight away with the same headers and `{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\"}`. It returns 7 tools. Four are the current ones:\n\n| Tool | Title | Description starts with | \n|---|---|---|\n| `factrail_capabilities` | FACTRAIL Capabilities | \"Use when you need to discover what FACTRAIL can verify or assess before calling another tool.\" | \n| `factrail_verify` | Verify French Company | \"Use when you need verified, source-backed facts about a French company from its SIREN or SIRET.\" | \n| `factrail_assess` | Assess EU Import (Beta) | \"Use when you need a source-backed assessment of importing a product into the EU…\" | \n| `factrail_get_receipt` | Get Evidence Receipt | \"Use when you need to re-fetch or audit a previously returned FACTRAIL result by its receipt ID.\" | \n\nThe other three (`verify_french_company`, `assess_import`, `analyze_company`) are older compatibility tools. Their titles start with `[Deprecated]` and their descriptions point to the replacement. Everything is annotated `readOnlyHint: true`.\n\nThis matters for agents: the \"Use when…\" first sentence is what a model reads when it decides which tool to call, and the `[Deprecated]` marker steers it away from the legacy tools. The same metadata is published as a server card at `https://mcp.factrail.online/.well-known/mcp/server-card.json`, so directories and clients can read it without opening an MCP session.\n\nTested with `mcp` 2.2.0 on Python 3.13:\n\n```\npython3 -m venv .venv && . .venv/bin/activate && pip install mcp\npython\nimport asyncio, json\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamable_http_client\n\nURL = \"https://mcp.factrail.online/mcp\"\n\nasync def main():\n    async with streamable_http_client(URL) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n            caps = await session.call_tool(\"factrail_capabilities\", {})\n            ver = await session.call_tool(\"factrail_verify\", {\n                \"subject_type\": \"company_fr\",\n                \"identifier\": \"552081317\",\n                \"fields\": [\"status\", \"legal_name\", \"legal_form\",\n                           \"head_office\", \"naf_code\", \"creation_date\"],\n            })\n            envelope = json.loads(ver.content[0].text)  # also in ver.structuredContent\n            rec = await session.call_tool(\"factrail_get_receipt\",\n                                          {\"receipt_id\": envelope[\"receipt_id\"]})\n            print(json.dumps(envelope, indent=2))\n\nasyncio.run(main())\n```\n\n(Older 1.x releases of the SDK call the helper `streamablehttp_client`, and it yields three values. Check your installed version.)\n\n`mcp-remote` bridge (bridge tested)\n\n```\n{\n  \"mcpServers\": {\n    \"factrail\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"mcp-remote\", \"https://mcp.factrail.online/mcp\"]\n    }\n  }\n}\n```\n\nWe tested the bridge itself (`mcp-remote` 0.14.3, driven over stdio): `initialize` and `tools/list` came back with all 7 tools. Startup took about 16 seconds, because the bridge first probes for OAuth, which this server doesn't use.\n\nClaude Code:\n\n```\nclaude mcp add --transport http factrail https://mcp.factrail.online/mcp\n```\n\nCursor, in `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):\n\n```\n{ \"mcpServers\": { \"factrail\": { \"url\": \"https://mcp.factrail.online/mcp\" } } }\n```\n\nVS Code, in `.vscode/mcp.json`:\n\n```\n{ \"servers\": { \"factrail\": { \"type\": \"http\", \"url\": \"https://mcp.factrail.online/mcp\" } } }\n```\n\nBrowser-based MCP clients work too: the endpoint accepts requests from any `Origin` and answers CORS preflight requests. We checked this with a request carrying an `Origin` header and a preflight `OPTIONS` request, and both came back `200` with `access-control-allow-origin: *`.\n\n**Step 1: ask what's covered.** `factrail_capabilities` returns the scope in machine-readable form. In our run it listed two capabilities:\n\n`company_fr` (operation `verify`): sources \"INSEE Sirene\" and \"BODACC\", limitation \"French companies only\".`import` (operation `assess`): `official_taric.status` is `\"not_installed\"`. The tool itself is titled \"Assess EU Import (Beta)\".\nAn agent that reads this first knows not to use the tool for a German company, and not to treat any import result as official TARIC data.\n\n**Step 2: verify.** We asked for six fields about SIREN `552081317`, Électricité de France (EDF), a large public company.\n\n**Step 3: pass on the gaps, don't fill them.** This is the part that belongs in *your* agent code or prompt. Here's a small helper that turns an envelope into text the model can quote, with an explicit \"do not guess\" line for every unresolved field:\n\n``` php\ndef summarize_for_agent(envelope: dict) -> str:\n    \"\"\"Turn an EvidenceEnvelope into text an agent can pass on without filling gaps.\"\"\"\n    sources = {e[\"id\"]: e for e in envelope[\"evidence\"]}\n    lines = [f\"Overall: {envelope['status']} (coverage: {envelope['coverage']['level']})\"]\n    for fact in envelope[\"facts\"]:\n        src = \", \".join(\n            f\"{sources[i]['publisher']} @ {sources[i]['retrieved_at']}\" for i in fact[\"evidence_ids\"]\n        )\n        lines.append(f\"- {fact['field']}: {fact['value']} [{fact['support_level']}; {src}]\")\n    reasons = envelope[\"coverage\"].get(\"metadata\", {}).get(\"unresolved_field_reasons\", {})\n    for field in envelope[\"coverage\"][\"fields_unresolved\"]:\n        lines.append(f\"- {field}: UNKNOWN ({reasons.get(field, 'no reason given')}). Do not guess.\")\n    if envelope[\"conflicts\"]:\n        lines.append(f\"- {len(envelope['conflicts'])} source conflict(s): report them, don't pick one silently.\")\n    lines.append(f\"Receipt: {envelope['receipt_id']}\")\n    return \"\\n\".join(lines)\n```\n\nRun against the envelope below, it prints:\n\n```\nOverall: insufficient_evidence (coverage: partial)\n- status: active [authoritative; INSEE Sirene 3.11 @ 2026-09-29T05:18:42.087910Z]\n- legal_name: ELECTRICITE DE FRANCE [authoritative; INSEE Sirene 3.11 @ 2026-09-29T05:18:42.087910Z]\n- head_office: {'line_1': '22 AVENUE DE WAGRAM 22-30', 'line_2': None, 'locality': 'PARIS', 'postal_code': '75008', 'country': 'FR'} [authoritative; INSEE Sirene 3.11 @ 2026-09-29T05:18:42.249656Z]\n- naf_code: 35.11Z [authoritative; INSEE Sirene 3.11 @ 2026-09-29T05:18:42.087910Z]\n- creation_date: 1955-01-01 [authoritative; INSEE Sirene 3.11 @ 2026-09-29T05:18:42.087910Z]\n- legal_form: UNKNOWN (unsupported_fact). Do not guess.\nReceipt: fr_952a43c43fba95f2ceeef5040132dd3206c4fe6a7dd0c7b5e1a8140a32012cef\n```\n\n**Step 4: keep the receipt.** Store `receipt_id` next to whatever the agent decided. Any agent can re-fetch the same observation later with `factrail_get_receipt`.\n\nThis envelope was captured on 29/09/2026 (05:18 UTC) from the hosted server, then at version 2.4.0. We re-ran the same call on 2.4.1 later that morning and got the same format and values; only `generated_at` changed. It's trimmed only where marked `…`:\n\n```\n{\n  \"schema_version\": \"1.2\",\n  \"status\": \"insufficient_evidence\",\n  \"subject\": {\n    \"type\": \"company_fr\",\n    \"name\": \"ELECTRICITE DE FRANCE\",\n    \"identifiers\": { \"siren\": \"552081317\", \"siret\": \"55208131766522\" }\n  },\n  \"facts\": [\n    { \"field\": \"status\", \"value\": \"active\",\n      \"evidence_ids\": [\"insee-siren\"], \"provenance_type\": \"sourced\", \"support_level\": \"authoritative\", … },\n    { \"field\": \"legal_name\", \"value\": \"ELECTRICITE DE FRANCE\",\n      \"evidence_ids\": [\"insee-siren\"], \"support_level\": \"authoritative\", … },\n    { \"field\": \"head_office\",\n      \"value\": { \"line_1\": \"22 AVENUE DE WAGRAM 22-30\", \"line_2\": null,\n                 \"locality\": \"PARIS\", \"postal_code\": \"75008\", \"country\": \"FR\" },\n      \"evidence_ids\": [\"insee-siret\"], \"support_level\": \"authoritative\", … },\n    { \"field\": \"naf_code\", \"value\": \"35.11Z\",\n      \"evidence_ids\": [\"insee-siren\"], \"support_level\": \"authoritative\", … },\n    { \"field\": \"creation_date\", \"value\": \"1955-01-01\",\n      \"evidence_ids\": [\"insee-siren\"], \"support_level\": \"authoritative\", … }\n  ],\n  \"evidence\": [\n    { \"id\": \"insee-siren\", \"source_type\": \"government_registry\",\n      \"authority_class\": \"primary_official_registry\", \"publisher\": \"INSEE Sirene 3.11\",\n      \"url\": \"https://api.insee.fr/api-sirene/3.11/siren/552081317\",\n      \"retrieved_at\": \"2026-09-29T05:18:42.087910Z\", \"source_status\": \"available\", … },\n    { \"id\": \"insee-siret\", \"publisher\": \"INSEE Sirene 3.11\",\n      \"url\": \"https://api.insee.fr/api-sirene/3.11/siret/55208131766522\",\n      \"retrieved_at\": \"2026-09-29T05:18:42.249656Z\", \"source_status\": \"available\", … },\n    { \"id\": \"bodacc\", \"source_type\": \"government_bulletin\",\n      \"authority_class\": \"official_publication\", \"publisher\": \"DILA BODACC\",\n      \"retrieved_at\": \"2026-09-29T05:18:42.318612Z\", \"source_status\": \"available\",\n      \"metadata\": { \"source_error\": null, \"truncated\": true }, … }\n  ],\n  \"conflicts\": [],\n  \"coverage\": {\n    \"level\": \"partial\",\n    \"fields_requested\": [\"status\", \"legal_name\", \"legal_form\", \"head_office\", \"naf_code\", \"creation_date\"],\n    \"fields_resolved\":  [\"status\", \"legal_name\", \"head_office\", \"naf_code\", \"creation_date\"],\n    \"fields_unresolved\": [\"legal_form\"],\n    \"metadata\": {\n      \"unresolved_field_reasons\": { \"legal_form\": \"unsupported_fact\" },\n      \"authority_policies\": [\n        { \"policy_id\": \"company_fr.status.current_registry_over_publication\",\n          \"precedence\": [\"primary_official_registry\", \"official_publication\"],\n          \"reason\": \"INSEE/Sirene reports current administrative status; BODACC notices are historical publication evidence.\", … },\n        …\n      ]\n    }\n  },\n  \"freshness\": {\n    \"generated_at\": \"2026-09-29T05:18:42.961558Z\",\n    \"oldest_supporting_retrieved_at\": \"2026-09-29T05:18:42.087910Z\",\n    \"newest_supporting_retrieved_at\": \"2026-09-29T05:18:42.318612Z\",\n    \"stale\": false,\n    \"recheck_after\": null\n  },\n  \"receipt_id\": \"fr_952a43c43fba95f2ceeef5040132dd3206c4fe6a7dd0c7b5e1a8140a32012cef\",\n  \"state_fingerprint\": \"fs_6a25339254aa8ac976fa5987c691fa5b42bcfde8269457d850de135a04dd575e\",\n  \"generated_at\": \"2026-09-29T05:18:42.961558Z\"\n}\n```\n\nHow to read it:\n\n`authoritative`, and each points to an evidence entry (` insee-siren` or `insee-siret`) with a URL and a retrieval time.` legal_form`. It's listed under `fields_unresolved` with the reason `unsupported_fact`, and there's no value for it. Because one requested field is missing, the top-level `status` is `insufficient_evidence` and `coverage.level` is `partial`, rather than a success that quietly skips a field. An agent can pass that gap on to the user as it is.\n`authority_policies` state that for current status and legal name, the INSEE register takes precedence over BODACC publications. BODACC was queried (`source_status: \"available\"`, `truncated: true`), but no fact in this envelope was taken from it, and `conflicts` is empty.`stale` is `false`.\nWe then called `factrail_get_receipt` with the returned `receipt_id`. It returned the same envelope, with the same receipt ID and the same `state_fingerprint`, and no integrity error.\n\nWhat a receipt gives you:\n\nWhat it does **not** give you: a digital signature, a blockchain record, or proof that the facts are true. It proves the envelope hasn't changed since it was produced. Whether INSEE was right is a separate question.\n\nAs of 29/09/2026 (server 2.4.1, public beta):\n\n`factrail_assess`, `not_installed`), so there's no authoritative tariff coverage.\n**Fair use of the public endpoint:**\n\nFACTRAIL's public MCP endpoint ([https://mcp.factrail.online/mcp](https://mcp.factrail.online/mcp)) is free, read-only, and needs no account. To keep it fair for everyone, each client IP may make up to 60 MCP requests per minute, while traffic arriving through known MCP gateways such as Smithery shares a separate allowance of 600 requests per minute. Requests over the limit receive HTTP 429 with a `Retry-After` header, and clients that keep sending requests after being limited are paused for 5 minutes. If you need more capacity, please get in touch via [https://factrail.online/](https://factrail.online/) rather than working around the limits.\n\nIn practice: an agent that gets a `429` should wait for the number of seconds in `Retry-After` before retrying, instead of looping.\n\n**On Apify:** if your agents run on Apify, the Actor [`spherical_distinction/factrail-evidence`](https://apify.com/spherical_distinction/factrail-evidence) is a thin wrapper around this same endpoint, billed per event: $0.005 per successful verification and $0.03 per successful import assessment (Apify pricing checked on 29/09/2026). The direct MCP endpoint stays free.\n\nCall `factrail_capabilities` first: it reports the live scope, which may be newer than this article.\n\nThe server is a Python package. From the repository README:\n\n```\ngit clone https://github.com/baronsigma/factrail && cd factrail\npython3 -m venv .venv && . .venv/bin/activate\npip install -e '.[dev]'\ncp .env.example .env\n# Set INSEE_API_KEY in .env for live French company lookups\npython3 -m factrail.mcp_http_server   # POST http://localhost:8765/mcp\n```\n\nNotes from the README:\n\n`FACTRAIL_HOST` and `FACTRAIL_PORT` change the bind address and port. Behind a public hostname, set `FACTRAIL_ALLOWED_HOSTS`; by default only localhost is accepted.` FACTRAIL_ALLOWED_ORIGINS` controls browser access. When it's unset, requests that carry an `Origin` header are rejected; `*` allows any Origin with CORS (this is what the hosted endpoint does).`FACTRAIL_RATE_LIMIT_PER_MIN`, default 60, and related variables).` FACTRAIL_CACHE_PATH` to a persistent path if you want receipts to survive restarts (the default is `/tmp`).` python3 -m factrail.mcp_server`.\nLinks: [factrail.online](https://factrail.online) · [github.com/baronsigma/factrail](https://github.com/baronsigma/factrail) · endpoint `https://mcp.factrail.online/mcp`", "url": "https://wpnews.pro/news/give-your-ai-agent-a-way-to-say-i-don-t-know-evidence-envelopes-over-mcp", "canonical_source": "https://dev.to/baron_sigma_ed1b2652d6d89/give-your-ai-agent-a-way-to-say-i-dont-know-evidence-envelopes-over-mcp-1na", "published_at": "2026-09-29 05:39:28+00:00", "updated_at": "2026-09-29 05:46:32.491732+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "developer-tools"], "entities": ["FACTRAIL MCP", "factrail.online", "GitHub", "baronsigma"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/give-your-ai-agent-a-way-to-say-i-don-t-know-evidence-envelopes-over-mcp", "markdown": "https://wpnews.pro/news/give-your-ai-agent-a-way-to-say-i-don-t-know-evidence-envelopes-over-mcp.md", "text": "https://wpnews.pro/news/give-your-ai-agent-a-way-to-say-i-don-t-know-evidence-envelopes-over-mcp.txt", "jsonld": "https://wpnews.pro/news/give-your-ai-agent-a-way-to-say-i-don-t-know-evidence-envelopes-over-mcp.jsonld"}}