cofinance.land MCP server: developer reference

Looking for the short version? Start at /docs/mcp. Writing an agent? Read the guide for AI agents.

A hosted, read-only Model Context Protocol server over our curated 318-page wiki on European cohousing finance. Seven free tools that need no API key and cost nothing, plus one paid tool (ask_wiki) that reads the wiki for you and charges credits.

Read this before you connect

Most of what follows is a list of things this server is not. That is deliberate. A knowledge tool that oversells its coverage is worse than no tool, because a model will trust it.

  • The corpus is narrow: 318 pages today, about 596.2k characters. You can read the whole thing end to end. It is hand-written and actively being extended, so every figure on this page is a snapshot of the corpus as it stands rather than a ceiling — re-read it, or call wiki_list, instead of caching what it says today.
  • Coverage varies by jurisdiction and is still growing. There are 196 funding programmes on file, alongside legal-form and financing notes. Germany, Italy and Spain, including Catalonia, have country-specific coverage; 147 of the 318 pages are German. The jurisdictions on file right now are CH, DE, DE-BB, DE-BE, DE-BW, DE-BW-FR, DE-BW-HD, DE-BW-TU, DE-BY, DE-BY-M, DE-HB, DE-HE, DE-HE-F, DE-HH, DE-MV, DE-NI, DE-NW, DE-NW-K, DE-RP, DE-SH, DE-SL, DE-SN, DE-SN-DD, DE-SN-L, DE-ST, DE-TH, ES, ES-AN, ES-AR, ES-AR-ZAR, ES-AS, ES-CB, ES-CE, ES-CL, ES-CM, ES-CN, ES-CT, ES-CT-B, ES-CT-B-BCN, ES-CT-B-MAN, ES-CT-GI-PAL, ES-CT-L-LLE, ES-EX, ES-GA, ES-IB, ES-MC, ES-MD, ES-MD-MAD, ES-ML, ES-NC, ES-PV, ES-PV-ALA-VIT, ES-RI, ES-VC, EU, IT, IT-21, IT-23, IT-25, IT-32, IT-32-BZ, IT-32-TN, IT-34, IT-36, IT-42, IT-45, IT-52, IT-55, IT-57, IT-62, IT-65, IT-67, IT-72, IT-75, IT-77, IT-78, IT-82, IT-88, NO. Jurisdictions absent from that list are not covered yet. Read that as a gap in a growing wiki, not as a verdict; and do not read it the other way either, as a commitment that any particular country is on its way.
  • An empty result means “not covered here”. It does not mean “no such funding exists”. A query about French cooperative housing finance correctly returns nothing from this server today. That is the honest meaning of openWorldHint: false, and it is the single most important thing for a model to get right here: someone making a real financing decision must not read our gap as the absence of a programme. Say the wiki does not cover it yet — do not substitute a figure recalled from training data.
  • Freshness is wildly uneven. 316 of 318 pages carry a lastVerified date, but the dates span 2015-11-09 to 2026-10-05 — two of them are eleven years old — and 257 pages are flagged volatile, meaning their figures are known to move. Near total date coverage is not the same as currency. Read lastVerified as a date, not as a badge. Every tool result states it, or states that the page has never been verified — the field is never quietly dropped.
  • One tool spends money. Exactly one. The free wiki tools are anonymous and cost nothing — no model call happens behind them. ask_wiki is the single exception: it requires a credential (an OAuth sign-in or an API key) and charges credits to that account, roughly one chat turn's worth, and every successful call states what it cost. It is labelled _meta.tier: "paid" in tools/list, so a client can see the difference before calling rather than at the paywall. Nothing else on this server can spend your credits.
  • Anonymous rate limiting is best-effort, not a security boundary. See rate limits.
  • Both protocol eras are served, but they are not the same server to a client. A modern 2026-07-28 caller gets server/discover, tools/list and tools/call and no ping; a legacy caller gets the initialize handshake exactly as before. See protocol version.

What it is good at

This is a bounded, curated, source-cited corpus, not an open-web scraper. That is why every free tool declares openWorldHint: false: the answer set is closed, so an empty result genuinely means “this wiki does not cover it” rather than “the search failed”. Every page carries its sources and its own freshness verdict, so a model can cite the primary source instead of citing us — and can tell you when a number is too old to trust.

Endpoint and transport

HTTP methods supported at the MCP endpoint
MethodBehaviour
POST /mcpThe MCP endpoint. One JSON-RPC 2.0 message per request. Requests answer 200 with a JSON-RPC body; notifications answer 202 with no body.
GET /mcp405 Method Not Allowed. There is no SSE stream — see below.
OPTIONS /mcp204 CORS preflight, against a strict origin allowlist. Never echoes a wildcard.
DELETE /mcp204 no-op. The server is stateless, so there is no session to tear down.

The transport is Streamable HTTP, in its request/response form only.

  • No SSE streaming. A GET /mcp — which is a client asking to open a server-to-client stream — returns 405. The spec explicitly permits this, and well-behaved clients fall back to plain POST. There are no server-initiated messages to stream.
  • Stateless: no Mcp-Session-Id. The server never issues one, so clients never send one back. Every request is authorised independently. initialize may be re-sent at any time; it is a pure capability handshake.
  • No JSON-RPC batching. An array body is rejected with -32600, in both eras and on every supported revision.
  • No self-hosting. The server is not published as a runnable package; the hosted endpoint is the only way to use it today.
  • Request bodies are capped at 256 KB, checked before parsing. Larger bodies get 413.
  • Content-Type must be application/json, compared by media type rather than by substring. Anything else — including an absent header, which is the other un-preflighted cross-site body shape — is 415. Accept is ignored.
  • A request carrying an Origin header that is not on the server’s allowlist is rejected with 403. An absent Origin is fine — that is the normal non-browser MCP client.

Protocol version and negotiation

This server is dual-era

It implements MCP revision 2026-07-28 (modern, per-request metadata) and revisions 2025-06-18 / 2025-03-26 (legacy, initialize handshake), on the one /mcp endpoint. The era is chosen per request, from the message itself: an initialize request selects legacy; a request carrying _meta["io.modelcontextprotocol/protocolVersion"], or a modern MCP-Protocol-Version header, or the method server/discover, selects modern.

Legacy handshake clients

You get exactly what you got before, byte for byte.

  • initialize negotiates 2025-06-18 or 2025-03-26 — never 2026-07-28, because a legacy client cannot speak it. An unsupported or absent params.protocolVersion is answered with our latest legacy revision rather than an error; the lifecycle spec requires a version in the reply, not a failure.
  • If the MCP-Protocol-Version header is absent, the server assumes 2025-03-26, per the transport spec.
  • ping, tools/list and tools/call all work. Results carry no resultType, and a JSON-RPC error comes back at HTTP 200.
  • Successful responses echo the negotiated revision in the MCP-Protocol-Version response header.

Modern stateless clients

Send MCP-Protocol-Version: 2026-07-28, Mcp-Method, Mcp-Name (on tools/call) and a _meta carrying the required io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities (clientInfo optional).

  • server/discover — the mandatory modern RPC. Returns supportedVersions: ["2026-07-28"], capabilities: { tools: { listChanged: false }, extensions: {} }, instructions, and _meta["io.modelcontextprotocol/serverInfo"].
  • tools/list carries resultType, ttlMs: 300000 and cacheScope: "public"; tools/call carries resultType: "complete" with our serverInfo merged into _meta.
  • initialize and ping are not available to you. ping is 404 / -32601; the revision removed it. initialize is still answered, under legacy semantics, because the spec makes it the era selector.
  • We advertise no extensions. There is no SSE, no session and no Mcp-Session-Id; GET is 405 and DELETE is a 204 no-op (legacy teardown).

Unknown-version clients

A revision we serve in neither era is HTTP 400:

Unsupported protocol version
{
  "jsonrpc": "2.0",
  "id": <id or null>,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": {
      "supported": ["2026-07-28", "2025-06-18", "2025-03-26"],
      "requested": "<echoed, sanitised>"
    }
  }
}

It is checked twice: a cheap pre-parse header screen, which cannot know the request id and so answers id: null, and again after the parse, where the id is carried.

Other modern-only errors: -32020 (header mismatch) at 400 when MCP-Protocol-Version, Mcp-Method or Mcp-Name is missing or does not match the body; -32602 at 400 for a missing required _meta field. -32001 (auth required) and -32002 (rate limited) are unchanged — still 200 and 429 respectively, in both eras.

Advertised capabilities are { "tools": { "listChanged": false } } and nothing else: no prompts, no resources, no logging, no completions. The tool registry is static per deploy, so notifications/tools/list_changed is never emitted.

Methods this server implements at all: initialize, ping, server/discover, tools/list, tools/call — but the reachable set is per era, as above, and a method that exists in the other era is -32601 just like one that exists nowhere. Well-formed notifications (including notifications/initialized) are accepted and answered 202 with no body.

Authentication

You do not need an API key for the wiki tools. All of these are free and work anonymously. A credential raises your rate limit, makes your usage attributable and revocable, and is the only way to reach ask_wiki, which charges credits. Anonymous callers get a -32001 for that tool and are never billed.

Two credentials are accepted on the same Authorization: Bearer header, told apart by shape:

  • OAuth 2.1 — for hosts such as Claude.ai that sign a user in. The server is a resource server only; Clerk is the authorization server. Discovery follows RFC 9728 at /.well-known/oauth-protected-resource, which every 401 names in WWW-Authenticate. Scopes are mcp:read and mcp:ask; dynamic client registration and PKCE are supported. Tokens are verified locally against the issuer JWKS, including the aud claim, which must name this endpoint.
  • API key — for CLIs and scripts. Shape cfl_live_ + 64 hex.

Create one in Settings → API keys. The plaintext secret is shown exactly once and is never stored in a readable form; only its SHA-256 digest is kept, so a database leak does not hand anyone a working credential. Keys look like cfl_live_…, are limited to 10 active per account, and revocation takes effect on the very next request.

Authorization header
Authorization: Bearer cfl_live_7f3a…
How the Authorization header is treated
What you sendWhat happens
No Authorization headerAnonymous. Free tools work. This is the documented way to use the server, and it is not an error.
A malformed, unknown or revoked key401 with JSON-RPC code -32001 and one generic message, “Invalid or revoked API key”. All three failure modes share that message on purpose, so the endpoint cannot be used to enumerate valid keys. The response carries WWW-Authenticate: Bearer realm="cofinance-land-mcp", error="invalid_token".
A valid keyAuthenticated. Your rate-limit bucket is keyed on the key id, and usage is attributable to it.

A supplied credential that does not work is an error

The server never silently downgrades a bad key to anonymous. If it did, a user whose key had been revoked would keep getting 200s from the free tools and reasonably conclude they were still authenticated.

Tool reference

Eight tools. The seven free tools are read-only, idempotent, non-destructive and closed-world (openWorldHint: false), and all read the same compiled-in corpus: no database round-trip, no model call, no per-request cost. The eighth, ask_wiki, is the paid exception and is described last.

Shared behaviour worth knowing before you read the schemas: German spelling is folded three ways, so Mietshauser, Mietshaeuser and Mietshäuser all reach the same pages; compounds partially match, so Genossenschaft reaches Wohnungsgenossenschaft. limit is clamped to 25 rather than rejected. The whole response text is capped at 16,000 characters, and results are dropped from the end with a notice rather than the cap being breached — check _meta.hasMore and page with offset.

The public corpus has three page types — funding-program, entity-form and legal-topic. A fourth internal type documents the product itself and is withheld from this server and from the public wiki alike, so you will never see it in a result.

wiki_search — ranked keyword search

Returns a relevance-centred snippet per hit (max 320 characters) plus the id needed to read the page in full.

inputSchema
{
  "type": "object",
  "properties": {
    "query":       { "type": "string" },
    "jurisdiction":{ "type": "string" },
    "type":        { "type": "string",
                     "enum": ["funding-program","entity-form","legal-topic"] },
    "legalForms":  { "type": "array", "items": { "type": "string" } },
    "tags":        { "type": "array", "items": { "type": "string" } },
    "limit":       { "type": "integer", "minimum": 1, "maximum": 25 },
    "offset":      { "type": "integer", "minimum": 0 }
  },
  "required": ["query"],
  "additionalProperties": false
}

Default limit is 10. An unknown tag or legalForm simply matches nothing.

How the jurisdiction filter behaves

A country page matches only that country or a subdivision of it. Pass a country to include its regions — DE shows the Saxony page — and pass a region to narrow to it. EU-level pages match EU member states only, not CH or NO; Switzerland and Norway are EEA/EFTA, and this corpus records nothing about which EU instruments reach them, so silence is the honest answer. An unrecognised code returns nothing rather than a broadened result.

Concretely: funding_programs({ jurisdiction: "FR" }) returns nothing. It used to return four German programmes under a header claiming they matched France — a fabricated answer for someone making a real financing decision. The filter now prefers a false negative, which you recover in one call by dropping the filter, over a wrongly-asserted match, which you cannot recover from at all.

wiki_get — one page in full

inputSchema
{
  "type": "object",
  "properties": {
    "id": { "type": "string" }
  },
  "required": ["id"],
  "additionalProperties": false
}

Ids are stable lowercase slugs (kfw-134, syndikat-direktkredit, eg-cooperative). An unknown id returns isError: true with the full list of valid ids, so a model can correct itself in one step. Bodies over 2,000 characters are cut with the cut marked inline and a link to the public page.

wiki_list — browse the index

inputSchema
{
  "type": "object",
  "properties": {
    "jurisdiction": { "type": "string" },
    "type":         { "type": "string",
                      "enum": ["funding-program","entity-form","legal-topic"] },
    "legalForms":   { "type": "array", "items": { "type": "string" } },
    "tags":         { "type": "array", "items": { "type": "string" } },
    "limit":        { "type": "integer", "minimum": 1, "maximum": 25 },
    "offset":       { "type": "integer", "minimum": 0 }
  },
  "required": [],
  "additionalProperties": false
}

One compact line per page — id, title, type, jurisdiction, freshness, link — and no body prose. With no arguments it returns the first 25 pages and sets _meta.hasMore with a nextOffset, so page through it to see the whole index. Once you have, treat that as the corpus boundary.

funding_programs — programmes, freshness first

inputSchema
{
  "type": "object",
  "properties": {
    "jurisdiction": { "type": "string" },
    "legalForms":   { "type": "array", "items": { "type": "string" } },
    "limit":        { "type": "integer", "minimum": 1, "maximum": 25 },
    "offset":       { "type": "integer", "minimum": 0 }
  },
  "required": [],
  "additionalProperties": false
}

The same pages wiki_list would give you with type: "funding-program", but with freshness leading every entry and a header stating how many of the shown entries are volatile or unverified. This is the highest-churn part of the corpus.

wiki_get_many — up to eight pages in one round trip

inputSchema
{
  "type": "object",
  "properties": {
    "ids": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["ids"],
  "additionalProperties": false
}

Bodies share one response budget, so eight ids return eight shorter bodies rather than eight full ones, and anything shortened is named. An id that does not exist is reported alongside the ones that do — the call does not fail because one id was wrong — and the result is flagged _meta.degraded so a missing page cannot quietly become a hole in your answer.

affordability — equity into a price ceiling

inputSchema
{
  "type": "object",
  "properties": {
    "jurisdiction":   { "type": "string" },
    "equityTotal":    { "type": "number" },
    "households":     { "type": "integer", "minimum": 1 },
    "propertyKind":   { "type": "string",
                        "enum": ["einfamilienhaus","eigentumswohnung",
                                 "mehrfamilienhaus","land"] },
    "buyerKind":      { "type": "string",
                        "enum": ["natural-persons","entity"] },
    "agentUsed":      { "type": "boolean" },
    "maxLoanToPrice": { "type": "number" },
    "interestRate":   { "type": "number" },
    "repaymentRate":  { "type": "number" }
  },
  "required": ["jurisdiction", "equityTotal"],
  "additionalProperties": false
}

Germany only. Returns the purchase-price ceiling the equity can carry, the Kaufnebenkosten broken out by line at the rate for that Land, and an indicative monthly payment. Every figure carries its own source URL and verified-on date, the same as a wiki page.

Two refusals are deliberate. A bare DE is rejected: Grunderwerbsteuer runs 3.5 % in Bayern to 6.5 % in four Länder, a €9,000 spread on a €300,000 purchase, so there is no honest national average to fall back on. And interestRate and repaymentRate have no defaults — omit them and you get the ceiling with no monthly figure, because a rate nobody has been offered is not a fact and this tool will not supply one. Whatever you do pass is echoed back labelled as an assumption.

conflicts — can these routes be combined?

inputSchema
{
  "type": "object",
  "properties": {
    "ids":              { "type": "array", "items": { "type": "string" } },
    "entityForm":       { "type": "string" },
    "hasChildUnder18":  { "type": "boolean" },
    "willOwnerOccupy":  { "type": "boolean" }
  },
  "required": ["ids"],
  "additionalProperties": false
}

Takes programme ids and structural route ids together — the forks that matter here are mostly between the two. Reports mutually exclusive pairs, hard eligibility exclusions for the applicant you describe, and separately the gates you have not answered, so a model asks rather than assumes.

An empty conflict list is not clearance, and the tool says so in its own output: it returns what it checked and what it did not. Budget exhaustion, application deadlines and the Kumulierungsverbote written into individual Richtlinien are outside this corpus entirely.

ask_wiki — a grounded answer, paid

inputSchema
{
  "type": "object",
  "properties": {
    "question": { "type": "string" }
  },
  "required": ["question"],
  "additionalProperties": false
}

The one tool that runs a model. It reads the wiki and writes a prose answer that names the jurisdiction of every programme it cites and carries each page’s freshness through. Requires a credential (OAuth scope mcp:ask, or an API key) and spends credits from that account, roughly one chat turn per call; the cost is appended to every answer. Tagged _meta.tier: "paid" in tools/list. Not idempotent. It answers only from the corpus and says so when the wiki is silent.

A worked call

Request
POST https://mcp.cofinance.land/mcp
Content-Type: application/json
MCP-Protocol-Version: 2025-06-18

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "wiki_get",
    "arguments": { "id": "kfw-134" }
  }
}
Response (abridged)
HTTP/1.1 200 OK
Content-Type: application/json
MCP-Protocol-Version: 2025-06-18

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "# KfW Programm 134 …\n- Freshness: VOLATILE — figures, rates and deadlines on this page change; re-verify against the listed source before relying on it. Last verified 2026-06-06.\n…"
      }
    ],
    "_meta": {
      "results": [
        {
          "pageId": "kfw-134",
          "lastVerified": "2026-06-06",
          "volatile": true,
          "sources": ["https://www.kfw.de/…"],
          "truncated": false
        }
      ],
      "corpusPageCount": 318,
      "totalMatches": 1,
      "truncated": false,
      "hasMore": false,
      "nextOffset": null
    }
  }
}

Provenance rides in _meta and is restated in words inside content[0].text. That duplication is deliberate: most MCP clients never surface _meta to the model, so the freshness caveat has to survive in the text the model actually reads.

A tool that runs and fails returns isError: true inside a successful JSON-RPC result, per the tools spec, so the model can see what went wrong and adapt. Protocol-level failures — unknown tool, bad params — come back as JSON-RPC errors instead.

Client setup

Claude Code

Anonymous (every free tool)
claude mcp add --transport http cofinance-land https://mcp.cofinance.land/mcp
With an API key
claude mcp add --transport http cofinance-land https://mcp.cofinance.land/mcp \
  --header "Authorization: Bearer cfl_live_YOUR_KEY_HERE"

Claude Desktop

Add this to your claude_desktop_config.json and restart the app.

claude_desktop_config.json
{
  "mcpServers": {
    "cofinance-land": {
      "type": "http",
      "url": "https://mcp.cofinance.land/mcp"
    }
  }
}
claude_desktop_config.json — with an API key
{
  "mcpServers": {
    "cofinance-land": {
      "type": "http",
      "url": "https://mcp.cofinance.land/mcp",
      "headers": {
        "Authorization": "Bearer cfl_live_YOUR_KEY_HERE"
      }
    }
  }
}

Raw HTTP — the initialize handshake

curl
curl -sS -X POST https://mcp.cofinance.land/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-06-18' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-06-18",
      "capabilities": {},
      "clientInfo": { "name": "curl", "version": "1.0.0" }
    }
  }'
Expected result
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": {
      "name": "cofinance-land",
      "version": "1.0.0",
      "title": "Cofinance Land"
    },
    "instructions": "Read-only tools over cofinance.land's small, hand-curated wiki …"
  }
}

Then tools/list to enumerate, and tools/call to invoke. There is no session to establish; notifications/initialized is accepted (202) but not required.

Raw HTTP — the modern server/discover

The 2026-07-28 equivalent. No handshake: the metadata rides on every request, and the mirrored headers must agree with the body or you get a -32020.

curl
curl -sS -X POST https://mcp.cofinance.land/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "server/discover",
    "params": {
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientCapabilities": {}
      }
    }
  }'

Rate limits

Rate limit buckets
CallerSustainedBurstKeyed on
Anonymous120 / hour20Client IP
With an API key1,200 / hour60API key id

Exceeding a bucket returns 429 with a Retry-After header, JSON-RPC code -32002, and error.data.retryAfterSeconds carrying the same number for clients that only read the JSON body.

Anonymous per-IP limiting is best-effort, not a security gate

The client IP is taken from the first value of X-Forwarded-For, which in principle a caller could forge to mint unlimited distinct buckets. We tested it rather than guessing: thirty requests carrying the same forged header exhausted a bucket, and twelve carrying twelve different forged headers were all rejected rather than each getting a fresh allowance. Forging the header does not work. What we still have not confirmed is whether that shared bucket is keyed on your real IP or on a single global fallback — so we describe the limit as best-effort rather than promising you an allowance nobody else can spend.

What actually contains the anonymous surface is that it is cheap, read-only, and spends no credits: every tool reads a corpus compiled into the bundle, with no database write, no model call and no third-party API behind it. Bulk-reading it costs us bandwidth and nothing else — and you can read the whole corpus on the public wiki anyway.

NAT’d offices share an IP and therefore share the anonymous bucket. If you hit it, create an API key and move to the authenticated bucket.

Status and error codes

HTTP status codes and their JSON-RPC error codes
HTTPJSON-RPC codeWhen
200—A request was answered (including tool results with isError: true).
202—A notification was accepted. No body.
400-32700Body was not valid JSON.
400-32600Not a valid single JSON-RPC 2.0 message; batches; unreadable body.
400-32022Unsupported protocol version, in either era. Carries error.data.supported and the echoed error.data.requested. Screened once on the header before parsing (id: null) and once after (id carried).
400-32020Modern era only: MCP-Protocol-Version, Mcp-Method or Mcp-Name missing, or not matching the body.
400-32602Modern era only: a required _meta field is missing.
401-32001Authorization header present but malformed, unknown or revoked.
403-32600Origin header present and not on the allowlist.
404-32601Modern era only: a method that exists in the legacy era but not this one — ping, and any unknown method.
405-32600GET — no SSE stream is offered here.
413-32600Body over 256 KB.
415-32600Content-Type is not application/json.
429-32002Rate limited. Retry-After header and error.data.retryAfterSeconds.
500-32603Internal error. Carries a correlation reference; details are logged server-side only.
503-32603The MCP service is disabled by its kill-switch.

In the legacy era every JSON-RPC error is returned with 200: -32601 for an unknown method, -32602 for an unknown tool or invalid params, and -32001 if a tool requiring authentication is called without a verified key. In the modern era the protocol-level failures above are lifted to their HTTP status — 400 for -32020 and -32022, 404 for -32601 — while everything else, including -32001, stays at 200.

Error messages never echo internal detail. A handler that throws is logged in full server-side against a correlation id, and you receive a generic failure carrying only that reference.

Pricing and credits

The four wiki tools are free

wiki_search, wiki_get, wiki_list and funding_programs declare no authentication requirement, read only from a corpus compiled into the deploy, and write no usage event. There is no model call behind them. Calling them costs you nothing and costs us nothing per call.

cofinance.land itself runs on a credits model for its paid agent runs (1 credit = €0.01), and ask_wiki bills against that same wallet — the one belonging to the API key's owner. It is metered like any other agent run: actual model tokens, times the configured markup, floored at the per-action minimum. A single question is roughly the cost of one chat turn, and the exact figure is appended to every successful answer rather than left for you to reconstruct from an invoice. There is no subscription and no plan to buy. If the wallet is empty the call is refused before any model runs, with a top-up link and a reminder that the free tools still work. Paid tools are tagged _meta.tier: "paid" in tools/list, so a client can tell the tiers apart before calling rather than after.

Freshness semantics

Two fields travel with every page, in _meta and in the result text. Neither is a quality score, and the absence of one is never a positive signal:

Freshness fields
FieldMeaning
lastVerifiedISO date a human last checked the page against its source, or null. Never omitted — an absent key would be indistinguishable from a serialisation bug, whereas an explicit null says “we looked, there is no date”. In text it renders as never verified. Read the date itself: a 2015 check and a 2026 check are not the same claim.
volatileTrue when the page carries figures, rates or deadlines that move. Re-verify against the cited source before quoting any number from a volatile page. volatile: false does not mean “stable” — it means nobody flagged it as fast-changing. That is the absence of an editorial judgement, not a positive one, and it says nothing about whether the page has ever been checked.

As of now: 316 of 318 pages have a date at all, 257 are flagged volatile, and recorded verifications run from 2015-11-09 to 2026-10-05. Those numbers are computed from the corpus at build time, not typed in, so this paragraph cannot drift away from the truth.

Explicitly not supported

  • SSE / server-initiated streaming (GET /mcp → 405).
  • Sessions (Mcp-Session-Id is never issued).
  • JSON-RPC batching (arrays are rejected).
  • MCP resources, prompts, logging, sampling and completions.
  • Self-hosting or an on-prem build.
  • Write operations of any kind. Every tool is read-only.
  • Open-web lookup. The corpus is closed and curated by hand.
  • ping and initialize for a modern 2026-07-28 caller — the revision removed the first and replaced the second with server/discover. Legacy callers keep both.
  • MCP extensions. We advertise none.

Found something wrong, or a figure that has moved? Tell us — corrections to a 318-page corpus land fast.