Logo

Original language: English.

Agent documentation — the landline

How an AI agent finds a business in the Tunnel knowledge base, verifies who is behind it, and opens a two-way conversation with the human who runs it. Written for the developer building the agent, not for the merchant.

Last updated: July 26, 2026

1.What this is

Most business data an agent can reach is a dead end: it can read a listing but cannot ask it anything. The landline is the missing half — a durable conversation between an agent and the person behind a profile. The agent posts a message, it lands in that business's inbox, and the reply comes back on the same conversation, possibly hours later.

It covers businesses that have no website at all. A merchant selling through Instagram or TikTok can be listed, verified and reachable, which is the case ordinary web crawling cannot serve.

Reading the knowledge base needs no credentials. Only messaging a business does.

2.Connect over MCP

The knowledge base is exposed as an MCP server over Streamable HTTP. It is stateless: no session to keep and no server-initiated stream — GET and DELETE on the endpoint return 405 by design.

POST https://api.tunnelpowered.com/api/mcp
Content-Type: application/json
Accept: application/json
Authorization: Bearer <token>      # only needed for messaging tools

{"jsonrpc":"2.0","id":1,"method":"initialize",
 "params":{"protocolVersion":"2025-06-18"}}
Server name: tunnel-knowledge-base. Supported protocol versions: 2025-06-18, 2025-03-26, 2024-11-05.

3.The tools

ToolAuthWhat it does
search_businessesNoneSearch by name, topic or place. Words are matched independently against name, location, description, offerings and FAQ; accents are ignored.
get_businessNoneFull profile for one slug: identity, contact, socials, offerings, FAQ, verification, machine-readable endpoints.
check_merchant_verificationNoneLive check of what a merchant is verified to and by whom — the level, the signed attestation, the expiry and the transparency-log position.
contact_businessBearerOpen a conversation. Returns a conversation id and a secret token — keep both.
check_repliesBearerPoll a conversation for replies from the business.
send_followupBearerSend another message on an open conversation.

Read verification.level rather than inferring anything from the presence of a record. "human" means a Tunnel employee checked identity, channel control and that the service is real; "automated" means machines proved only that the business controls the channels its record cites; null means neither, which is the common case. The separate humanReview object is about whether a person edited the knowledge-base entry — it is not a statement about the business.

Search matches words, not meanings. A business calling itself a *cofetărie* is not returned for bakery. An empty result set comes back as a normal answer with completeness "empty" and a hint, never as an error — it means "not in this index", which is not the same as "not in business".

4.The response envelope

Every tool result is wrapped. The wrapper exists so an agent never has to guess how much of what it is holding is real.

{
  "tunnel_mcp": "1.0",
  "tool": "get_business",
  "retrieved_at": "2026-07-27T12:00:00.000Z",
  "completeness": "partial",
  "missing": ["offerings", "address"],
  "freshness": { "source": "crawl", "observed_at": "…", "age_days": 3.2, "stale": false },
  "data": { … }
}
Everything before `data` describes the answer; `data` is the answer.
  • tunnel_mcp — schema version. Minor bumps are additive: ignore fields you do not recognise rather than failing on them.
  • retrieved_at — when we answered. Not when the data was observed.
  • freshness — when it *was* observed, how old that is in days, and whether we consider it stale. source is crawl, record, index, live or unknown; we say unknown rather than defaulting to now.
  • completeness and missing"full", "partial" or "empty", with the absent fields named. An empty field means we do not hold that information, not that the business lacks it. If that distinction affects your answer, say which one you mean.

Partial data with stated gaps is returned in preference to a 404. An unknown slug comes back with candidate slugs in did_you_mean rather than a bare failure, because a 404 only tells an agent to give up.

5.Errors you can act on

A failed tool call returns the same envelope with an error object in place of data, and isError set on the MCP result. Branch on code, which is stable; the message is for a human reading a log.

codeWhat it means
unknown_toolNo such tool. available_tools lists what does exist.
missing_argumentA required argument was absent. required and accepted list the parameters.
invalid_argumentA value was outside the allowed set. valid_values lists them.
unknown_slugNothing is listed under that slug. did_you_mean carries candidates when there are any.
auth_requiredA messaging tool was called with no bearer token. Reading needs none.
invalid_credentialsA token was sent and did not authenticate. We do not silently downgrade you to anonymous.
rate_limited_ip, rate_limited_declared, rate_limited_agentSlow down. retry_after_seconds says how long.
internal_errorOur fault. Retry once, then tell us.

Every error also carries fix: one imperative sentence naming the call to make instead. A refusal that does not tell you what would have worked is a bug on our side, not yours.

6.Registering your agent

Messaging a real business is rate-limited and attributable, so it needs a registered agent. Register once, then exchange the credentials for a token:

POST https://api.tunnelpowered.com/api/v1/agents/register     → client_id, client_secret
POST https://api.tunnelpowered.com/api/v1/agents/token        → access_token  (client_credentials)

Both endpoints are discoverable rather than hand-configured — we publish RFC 8414 authorization-server metadata and RFC 9728 protected-resource metadata:

GET https://api.tunnelpowered.com/.well-known/oauth-authorization-server
GET https://api.tunnelpowered.com/.well-known/oauth-protected-resource

7.Opening a conversation

Send agent_name honestly — "Claude, on behalf of a user" is the right shape. The person on the other end decides how to reply based on who is asking, and a business that discovers it was talking to an undisclosed bot is a business that leaves.

POST https://api.tunnelpowered.com/api/kb/entities/{slug}/messages
POST https://api.tunnelpowered.com/api/kb/websites/{slug}/messages

{ "agent_name": "Claude, on behalf of a user",
  "subject":    "Table for four on Friday?",
  "message":    "…",                     // max 4000 characters
  "reply_to":   "user@example.com" }     // optional, out of band

→ { "conversation_id": 123, "token": "…" }   // the token is shown once

Then poll, and follow up on the same conversation:

GET  https://api.tunnelpowered.com/api/kb/conversations/{id}?token=…
POST https://api.tunnelpowered.com/api/kb/conversations/{id}/messages

Replies are asynchronous and human. Poll on the order of minutes, not seconds, and tell your user the answer is coming from a person who may be asleep.

8.Checking verification

There are two levels and they are not interchangeable. Read the level field on the verification response rather than inferring anything from the presence of a badge.

levelBadgeWhat was actually established
"human"verifiedByAHumanA Tunnel employee confirmed the identity of the person, their control of the channel, and that the service described is real. A second employee approved it.
"automated"verifiedAutomatedMachines confirmed the merchant controls the channels the record cites — a DNS record or file on the domain, a token in the public social bio, a code to the listed inbox. Nothing about identity or service reality.
nullnoneNeither. Treat the record as self-reported.

Neither level is an endorsement of quality, solvency or licensing, and you should not present either as one. An "automated" response carries a limitations array — identity_not_verified, service_reality_not_verified, no_human_review — inside the signed payload, so the caveat travels with the signature rather than alongside it.

Every verification is a signed attestation with a position in a public transparency log, and the signing keys are published so you can verify offline:

GET https://api.tunnelpowered.com/.well-known/tunnel-trust.json   # key id, rotation metadata
GET https://api.tunnelpowered.com/.well-known/jwks.json           # the same keys as RFC 7517 JWKS

For anything high-stakes, treat a verification result older than five minutes as stale and re-check it live. Verification can be withdrawn.

To be clear about the state of the world: no AI vendor consults this registry today, because no cross-vendor trust registry exists. It is a signal your agent can choose to check, not one anybody checks for you.

9.Without MCP

Everything above is plain HTTP and works from curl. The read surface needs no credentials at all:

GET https://api.tunnelpowered.com/api/kb                      # index
GET https://api.tunnelpowered.com/api/kb/llms.txt             # the guide, written for an LLM
GET https://api.tunnelpowered.com/api/kb/search?q={name}
GET https://api.tunnelpowered.com/api/kb/entities/{slug}      # add ?format=md for markdown
GET https://api.tunnelpowered.com/api/kb/websites/{slug}

10.Connect over A2A

The same three read skills are also reachable over Agent2Agent v1.0 (JSONRPC binding). The Agent Card is at the path the spec fixes:

GET  https://api.tunnelpowered.com/.well-known/agent-card.json
POST https://api.tunnelpowered.com/api/a2a

{"jsonrpc":"2.0","id":1,"method":"SendMessage",
 "params":{"message":{"role":"ROLE_USER",
                      "parts":[{"text":"cakes in Chisinau"}]}}}
Protocol version 1.0. Skills: search_businesses, get_business, check_merchant_verification.

Two things trip up clients written against older material. The method is SendMessage — v0.3 called it message/send, and v1.0 renamed every operation; send the old name and you get a -32601 that names the new one. And a text part is {"text":"…"}, the protobuf oneof, not the v0.3 {"kind":"text","text":"…"}.

It returns a `Message`, never a `Task`. SendMessageResponse allows either, and these three skills are synchronous lookups — there is nothing to poll. So GetTask is not implemented, and answers -32601 explaining that rather than returning an empty task and letting you conclude yours was lost. Plain text is treated as a search; to choose a skill explicitly, send a data part such as {"skill":"get_business","slug":"…"}.

Read-only, on purpose. Messaging a business is not exposed over A2A. An agent reaching a merchant on the strength of a self-declared identity would be committing a person to a conversation on a claim nobody checked; that waits until there is a mandate model behind it. Reads are safe to open to anyone, and they are.

The card is unsigned. Cards *may* carry a JWS over the JCS canonicalization and we already publish a JWKS, but a subtly wrong canonicalization produces a signature that fails verification, which looks like tampering and is worse than none. The signatures field is absent rather than empty, because an empty array would claim we signed it zero times.

11.The API as a contract

The whole public surface is also described as OpenAPI 3.1, served from the origin it describes so that a cached copy of this site can never disagree with the live API:

GET https://api.tunnelpowered.com/openapi.json

It covers exactly what this page covers, and nothing else: the knowledge base, the landline, verification and the transparency log, agent registration, and the MCP endpoint. Endpoints absent from it are internal and may change without notice. That boundary is deliberate — describing an endpoint in a machine-readable contract is an undertaking to keep it working, and we only want to make that undertaking about the surface we already ask you to depend on.

The per-website artifact routes under /api/public/ are excluded for a different reason: they need a ?t= token that belongs to a customer, so an agent has no way to call them and calling them "public" would be misleading.

The MCP endpoint appears as a single path. Its tools, their arguments and the response envelope are described by initialize and tools/list at runtime rather than copied into the spec, because a second copy is a second thing that can drift.

In the official MCP registry the server is com.tunnelpowered/knowledge-base. Verify the identifier there rather than trusting a descriptor file that claims it.

12.Rate limits and etiquette

Public endpoints are rate-limited per IP in a fixed window. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a refusal carries Retry-After in seconds. Read those headers rather than guessing — the limit is configurable and we will not silently change behaviour under you.

The MCP endpoint meters reads on three keys. The IP ceiling is the hard one and is checked first. Inside it, a self-declared identity — an MCP clientInfo.name or a descriptive User-Agent — gets its own narrower share, so one noisy integration cannot consume an address's whole budget. A registered agent holding a bearer token gets a higher ceiling of its own.

Declaring who you are never raises a limit — only proving it does. A name you type about yourself is not checkable, so it buys you nothing and costs you nothing; there is no incentive either way. Please send one anyway: it is how we tell agent traffic apart from noise when we report on who uses this.

Within tools/call, a rate-limit refusal comes back as a normal tool result with isError and a rate_limited_* code, not as an HTTP error — a model driving the tool reads the result, not the status line. Other methods get a JSON-RPC error and a 429.

Three things we ask of any agent using the landline: identify yourself, do not open a conversation you will not read the reply to, and do not use it to broadcast. A business that gets three useless agent messages will stop answering the fourth, and the channel is only worth having if the humans on it stay engaged.

13.Disputes

If an interaction goes wrong — a business misrepresents itself, or a verification looks incorrect — an authenticated agent can contest it:

POST https://api.tunnelpowered.com/api/kb/conversations/{id}/dispute

A dispute is reviewed by a person. It is the mechanism that keeps verification from being a rubber stamp, so please use it.

Questions, or a use case that does not fit: contact@tunnelpowered.com. Related reading: how the visibility score is computed.