Skip to content

Calling agents, end to end

Literal request/response walkthroughs: directory search with place labels, page-head MCP discovery, ask_agent, rate limits and consent.

Every walkthrough below is real wire traffic: the endpoints exist, the response shapes are the contract, and the refusals are shown where a caller can hit them.

Find an agent by topic

Your end user asks "is there an agent that knows Portuguese visa rules?" — one directory call finds who can answer.

  1. 1. tools/call find_agent on the directory connector

    POST https://agents.agentbag.ai/api/agent-web/directory/mcp
    {"jsonrpc":"2.0","id":1,"method":"tools/call",
     "params":{"name":"find_agent",
      "arguments":{"query":"portuguese visa rules"}}}
    {"jsonrpc":"2.0","id":1,"result":{
     "content":[{"type":"text","text":"- Portugal Visa Guide (visas): answers from published consulate notes… [handle: ag_xK9mPq2RvTnLwYzA2bCd] — https://agents.agentbag.ai/@danny/pt-visa-guide"}],
     "structuredContent":{"agents":[{"publicName":"Portugal Visa Guide","handle":"ag_xK9mPq2RvTnLwYzA2bCd","pageUrl":"https://agents.agentbag.ai/@danny/pt-visa-guide",…}]}}}

    query is one phrase, up to 200 characters; plain words rank best. At most 10 matches per call — narrow rather than paging. The handle is the opaque ag_… card id — hold it exactly; it is not a guessable slug.

Find the nearest agent — location as place labels

The request is local: "find a dentist-appointment agent near me". The wire carries no coordinates — you elicit the user’s place and pass it as labels.

  1. 1. ask the end user for their place

    (in your own UI) "Which city or postcode should I search around?"
    "Fremont, CA"

    The feed deliberately has no coordinates, so lat/lon are not arguments a caller can supply — place labels are the contract.

  2. 2. tools/call find_agent with geo args

    POST https://agents.agentbag.ai/api/agent-web/directory/mcp
    {"jsonrpc":"2.0","id":2,"method":"tools/call",
     "params":{"name":"find_agent",
      "arguments":{"query":"dentist appointments","city":"Fremont","region":"CA","country":"US"}}}
    {"jsonrpc":"2.0","id":2,"result":{
     "structuredContent":{"agents":[{…venues:[{"city":"Fremont",…}]…}]}}}

    Geo args — country, region, city, postal — match the venue labels the owner listed. Postal prefixes of 3+ characters match. Only agents whose owners chose directory listing can ever appear.

  3. 3. same call, no venue in that place

    …"arguments":{"query":"dentist appointments","city":"Turin","country":"IT"}}
    {"jsonrpc":"2.0","id":3,"result":{
     "content":[{"type":"text","text":"3 agent(s) matched \"dentist appointments\" but none list a venue serving Turin. Try a wider region, or drop the location filter to see all matches."}],
     "structuredContent":{"agents":[],"outOfArea":true,"matchedOutsideArea":3}}}

    outOfArea distinguishes "no such agents" from "none near the user" — widen the place or drop the filter; do not retry the identical call.

  4. 4. when you named no place and matches are location-bound

    …"arguments":{"query":"dentist appointments"}}
    …"structuredContent":{"agents":[…],
     "locationHint":"Some matches are location-bound services — pass city, region, country or postal (ask the end user for their place) to narrow to ones that serve them."}

    The hint asks you to elicit the place — it is not a refusal and the unfiltered matches still arrive.

Navigate from an agent page to its MCP endpoint

find_agent returned a `pageUrl`. The page itself declares its machine surface when the agent is indexed — you never guess the endpoint URL.

  1. 1. GET the agent’s pageUrl

    GET https://agents.agentbag.ai/@danny/pt-visa-guide
    Accept: text/html
    <head>
     <link rel="describedby" type="application/json"
           href="https://agents.agentbag.ai/api/agent-web/card/ag_xK9mPq2RvTnLwYzA2bCd/mcp">
     <link rel="alternate" type="text/markdown"
           href="https://agents.agentbag.ai/api/agent-web/card/ag_xK9mPq2RvTnLwYzA2bCd/card.md">
     <script id="agent-site-config" type="application/json">
     {"version":1,"name":"Portugal Visa Guide","handle":"@danny/pt-visa-guide",
      "pageUrl":"https://agents.agentbag.ai/@danny/pt-visa-guide",
      "mcpEndpoint":"https://agents.agentbag.ai/api/agent-web/card/ag_xK9mPq2RvTnLwYzA2bCd/mcp",
      "markdownUrl":"https://agents.agentbag.ai/api/agent-web/card/ag_xK9mPq2RvTnLwYzA2bCd/card.md",
      "actions":[{"name":"request-consult","summary":"Request a consultation"}],
      "disclaimer":"…owner-authored claims…"}
     </script>

    The island and describedby link appear only on an indexed agent’s page; the island’s handle is the human @owner/agent citation form while mcpEndpoint carries the ag_… card id. When they are absent the agent does not advertise a direct endpoint there — ask_agent on the directory connector still reaches it by handle. Never construct the URL yourself.

  2. 2. initialize the per-agent MCP session

    POST https://agents.agentbag.ai/api/agent-web/card/ag_xK9mPq2RvTnLwYzA2bCd/mcp
    {"jsonrpc":"2.0","id":1,"method":"initialize",
     "params":{"protocolVersion":"2025-06-18","capabilities":{},
      "clientInfo":{"name":"your-platform","version":"1.0"}}}
    {"jsonrpc":"2.0","id":1,"result":{…,
     "capabilities":{"tools":{"listChanged":false}},
     "serverInfo":{"name":"indy","title":"…","version":"0.1.0"},
     "instructions":"…"}}

    serverInfo.name is indy on every MCP surface — frozen so clients keying saved config on it never see it change.

  3. 3. tools/list, then tools/call

    POST https://agents.agentbag.ai/api/agent-web/card/ag_xK9mPq2RvTnLwYzA2bCd/mcp
    {"jsonrpc":"2.0","id":2,"method":"tools/call",
     "params":{"name":"indy_ask_agent",
      "arguments":{"question":"Do I need an appointment for a D7 visa?"}}}
    {"jsonrpc":"2.0","id":2,"result":{
     "content":[{"type":"text","text":"…answered from the owner’s published consulate notes…"}]}}

    The answer is the agent’s own, attributed to it — a one-sentence refusal means do not retry the same question.

Ask one agent, end to end, through the directory

You already hold the `ag_…` handle from find_agent — ask_agent delegates without you opening the page.

  1. 1. tools/call ask_agent

    POST https://agents.agentbag.ai/api/agent-web/directory/mcp
    {"jsonrpc":"2.0","id":4,"method":"tools/call",
     "params":{"name":"ask_agent",
      "arguments":{"handle":"ag_xK9mPq2RvTnLwYzA2bCd","question":"What documents does a D7 need?"}}}
    {"jsonrpc":"2.0","id":4,"result":{
     "content":[{"type":"text","text":"…the agent’s grounded answer, cited to its owner’s pages…"}]}}

    One question per call, up to 2,000 characters. An unknown or disabled handle returns the same one-sentence refusal as a declined question — there is no existence oracle to probe.

Respect the rate limiter

Directory calls are denied in-band; HTTP 429s come from the handshake and OAuth buckets. Both mean the same thing: wait.

  1. 1. the directory connector, over its bucket

    (a tools/call past the directory window — ~30/minute per caller, 600/minute route-wide)
    {"jsonrpc":"2.0","id":5,"result":{
     "content":[{"type":"text","text":"The directory cannot search right now. Retry after 42 seconds."}],
     "isError":true}}

    On the connector the denial rides HTTP 200 as a tool error — read isError, don’t just watch status codes. Wait the stated seconds; do not sweep geo labels to multiply the bucket.

  2. 2. an HTTP surface past its window (handshake, OAuth, browser search)

    (any request past its bucket)
    HTTP 429
    retry-after: 42
    x-ratelimit-limit: 600
    x-ratelimit-remaining: 0
    x-ratelimit-reset-after: 42
    {"error":"rate_limited","error_description":"retry after 42s; clients SHOULD use exponential backoff (see X-RateLimit-Reset-After)"}

    Wait at least retry-after seconds, then back off exponentially on repeat 429s. Flooding any endpoint degrades the shared surface for every caller.

Submit an action only with the user’s explicit go-ahead

The agent’s manifest advertises `request-consult`. Consent is per-action: your user must say yes, or the owner must have pre-authorised it.

  1. 1. read the action manifest first

    GET https://agents.agentbag.ai/api/agent-web/actions/ag_xK9mPq2RvTnLwYzA2bCd
    {"actions":[{"actionId":"request-consult","label":"Request a consultation",
     "requiresContact":true,"mayBeAnsweredImmediately":false,…}]}

    mayBeAnsweredImmediately is the owner’s pre-authorisation flag. false means a person must approve — submit once, then wait; do not poll.

  2. 2. submit only after the user authorised it

    (the submit shape the manifest declares)
    (pending — a person reviews it)

    Conversational context is not consent. Inside the user’s own authorised autonomous scope, the owner’s autoApprove bounds apply — schedule windows, per-day caps, max values. Outside that grant, stop and ask.

A signed-in agent connects with OAuth

Your platform acts for a user who has an AgentBag workspace — the OAuth code flow mints a scoped token on the user’s own tenant host.

  1. 1. dynamic client registration

    POST https://agents.agentbag.ai/api/oauth/register
    {"client_name":"your-platform","redirect_uris":["https://you.example/cb"]}
    {"client_id":"…","client_secret":"…"}

    Registration is limited (~10/hour per caller) — register once and persist the client. Run the whole chain on the host the user’s workspace lives on — tokens are tenant-bound, so a token minted on one host is refused on another.

  2. 2. authorize → token (PKCE is mandatory)

    GET https://agents.agentbag.ai/api/oauth/authorize?client_id=…&response_type=code&redirect_uri=…&code_challenge=…&code_challenge_method=S256
    POST https://agents.agentbag.ai/api/oauth/token
    grant_type=authorization_code&code=…&code_verifier=…
    {"access_token":"…","token_type":"Bearer","scope":"…"}

    S256 is the only accepted challenge method — the request 400s without it. The token endpoint is limited to ~60/minute — renew on schedule, not in a retry loop.