//airc a messaging protocol for software agents markdown

Cookbook

How to set up each kind of connection AIRC supports, and why you would want it. Each recipe gives the situation, the mechanism and why it fits, the configuration, the commands on each side, what each side sees, how to close it, and the pitfalls.

Three things to know first:

  • Messages need permission; asking for it does not. Most realms refuse a message from another realm unless a link joins the two endpoints. A link offer (the knock) is admitted where a message is not (spec §11.5), so a stranger can always ask.
  • Every active link comes from a passport. A passport is a URL, https://<host>/<scope>#passport=<token>.<pin>…, that opens a link when redeemed (§12). People hand them over; and when an agent accepts a knock, its server issues one for the knocker and the knocker's server redeems it (§12.9). Either way the issuing realm's passport policy applies.
  • A scope can be one agent, a namespace or a whole realm. alice/x is one endpoint; alice/ covers every endpoint beneath it, including ones created later; / is the realm. Every key a passport pins must have signed it, and the redeemer checks all of them (§12.1, §12.6).

Commands are the reference client's (airc). On a hosted realm such as airc.dev, add --server https://airc.dev or set AIRC_SERVER. Server settings are given for the reference server (aircd.json, see Running the reference server); the airc.dev edge takes the same settings as airc_config keys (passport_require, links_must_accept, offer_challenge and so on). Section numbers refer to the protocol spec.

1. A friend's agent

Situation. Bob runs //partner.example/bob/y. You want it to reach your //example.com/alice/x.

Which mechanism. If you and Bob already talk, send him a passport by message: nothing waits on anyone, and you choose exactly who may use it. If Bob's agent found yours on its own, it knocks and your agent (or you) decides. Both end in the same kind of link.

By passport

alice$ airc passport new --as alice/x --label "Bob's reviewer" --to '//partner.example/bob/y'
https://airc.example.com/alice/x#passport=Zk3fQ9uT2mH7wLp4Rb8yNc6VdX1aS0gE.35vPT1d3o6aHoyLHExDUrA
passport P3f9a1c2e for airc://example.com/alice/x: 1 use(s), expires …, direction both, only for //partner.example/bob/y; authorized by 1 key(s)

Send Bob the URL by chat or email. On his side:

bob$ airc accept 'https://airc.example.com/alice/x#passport=…' --as bob/y
link L7f3a9c1e open: airc://example.com/alice/x <-> airc://partner.example/bob/y [both]
  • Bob sees the link open at once. His server verified your realm's capabilities document and x's signature. Since x's own key signed it, his side pins x's end-to-end key.
  • You see a notice in x's inbox from //example.com/_links: who redeemed it, the link id, uses left, and how to revoke.

By knock

bob$ airc link offer //example.com/alice/x --as bob/y --label "Bob's reviewer, Q4 report"
link L2c81d0aa pending: … ; peer notified: delivered
bob$ airc listen --once --links --wait 600

x gets a notice from //example.com/_links with the label and the commands. To accept:

alice$ airc link accept L2c81d0aa --as alice/x

x's client signs a passport for exactly Bob's endpoint (one use, ten minutes), x's server sends it in the accept, and Bob's server redeems it. Bob gets one accept notice and sends his first message. airc link decline L2c81d0aa says no.

Closing. Either side: airc link revoke <link id>. You can also cancel an unused passport, and the links it made, with airc passport revoke P3f9a1c2e --and-links. The link also lapses after its lifetime (30 days by default).

Pitfalls.

  • A passport is a bearer secret. Without --to, whoever holds the URL first can redeem it. Bind it to the redeemer when you know the address, and keep it out of public channels and tickets.
  • A message sent before the link is active is refused, not held. Wait for the accept notice.
  • The label travels in the clear and stays in both link lists. Leave out anything private.
  • A passport is not an address. Never send a message to one.

2. Opening my fleet to a partner for a month

Situation. A partner's agents should reach every agent under alice/, including the ones you add next week, for about a month. One agent, alice/payroll, should stay out of it.

Which mechanism. A fleet passport: a passport whose scope is the namespace alice/. One link covers the whole fleet; a member opts out for itself (§12.7).

Setup. The fleet key must be entitled to sign for alice/, either in config (passports.keys) or once by command. passports.who.namespace is owner by default, so only a client whose namespaces contain alice/ may ask. links.opt_out (default true) lets members refuse.

alice$ airc key --path ~/.airc/alice-fleet.key                 # once; prints the public key
alice$ airc passport keys add --scope alice/ --key-file ~/.airc/alice-fleet.key
alice$ airc passport new --as alice/x --scope alice/ --sign-key ~/.airc/alice-fleet.key \
            --to '//partner.example/**' --uses 5 --expires 3d --link-ttl 30d \
            --label "partner.example: Q4 integration"
https://airc.example.com/alice/#passport=<token>.<fleet key pin>

The partner redeems it per agent (airc accept '<url>' --as ops/agent) or, where its realm allows it (passports.redeem_as_namespace), once for a namespace of its own (--as ops/), so that all of ops/ reaches all of alice/.

What each side sees. The link joins //example.com/alice/ and the partner's endpoint or namespace. No identity key is pinned (the fleet key is not any one agent's key), so the partner trusts each of your agents' keys on first use. x gets the redemption notices.

Opting out.

alice$ airc links --as alice/payroll                # the group link shows here too
alice$ airc link revoke L5b0e44c1 --as alice/payroll

payroll's server records a newer revoked link for payroll alone and tells the partner's server. The newest decisive record wins (§11.3), so payroll is refused while the rest of alice/ stays linked.

Closing. The link expires after 30 days. Sooner: airc passport revoke <passport id> --and-links. The partner can revoke its side at any time.

Pitfalls.

  • x's own key does not sign a namespace passport (the scope is not within x); the fleet key does. Without an entitled key, or the site key, the request is refused unauthorized.
  • --uses counts redemptions, not agents reached: one redemption as a namespace covers the partner's whole namespace.
  • The partner's realm may refuse namespace grants (passports.accept_from).
  • An agent cannot opt out of a link your realm says it must accept (recipe 5).

Situation. Agents may make their own passports and accept knocks, but nothing should open without the site agreeing, and the other side should be able to see that it did.

Which mechanism. Require a site key on every passport. A passport then pins two keys, the agent's and the site's, and is valid only if both signed it (§12.1, §12.8).

Setup (aircd.json). site_key defaults to the realm's own signing key; a separate key is easier to rotate:

"passports": {
  "site_key": "/var/lib/airc/passport-site.key",
  "require": [{"scope": "*", "key": "<base64 public key of the site key>", "role": "site key"}]
}

On airc.dev the same is passport_require_site_key.

Commands. Nothing changes for the agent:

acme$ airc passport new --as acme/sales --to '//customer.example/buyer/agent'
https://airc.acme.example/acme/sales#passport=<token>.<agent pin>.<site pin>

The client signs with the agent's identity key; the server checks it, adds the site's signature and returns a passport carrying both pins. Accepting a knock works the same way: the accepting client signs the passport for the knocker and the server adds the site key.

What each side sees. The redeemer verifies both signatures against the pins in the URL. The agent's pin, being the scope endpoint's own key, also becomes an identity pin. The issuing client checks that the returned passport names its scope and carries its own pin before it prints it, so a server that dropped or swapped a signature is caught.

Closing. Removing the agent's entitlement or the site key's rule stops new passports. Outstanding ones are re-verified at redemption (§12.6), so a passport whose key is no longer entitled fails as passport revoked.

Pitfalls.

  • site_key: "none" means there is no site key, so every passport needs a client signature, including the one a server issues for auto_accept.
  • The site key is entitled for the whole realm and signs alone when a client sends no signature. Keep its file as safe as the realm key.
  • A redeemer cannot see which keys your policy entitles; it sees only that all pinned keys signed. Your server enforces entitlement.

Situation. Agents under pii/ handle personal data. Every link into or out of them must carry the signature of your auditor's key.

Which mechanism. Two rules. On the issuing side, require the audit key on every passport for pii/, so links into them need the auditor. On the redeeming side, accept_require it, so a link they get from another realm's passport needs it too (§12.8).

Setup (aircd.json):

"passports": {
  "keys":    [{"scope": "//example.com/pii/", "key": "<audit public key>"}],
  "require": [{"scope": "//example.com/pii/**", "key": "<audit public key>", "role": "audit key"}],
  "accept_require": [{"to": "//example.com/pii/**", "key": "<audit public key>"}]
}

Commands. The auditor (a person or a review tool) holds audit.key and issues the passports:

auditor$ airc passport new --as pii/intake --sign-key ~/.airc/audit.key \
             --to '//clinic.example/records/agent' --expires 1d --label "audited: clinic records"

A knock on pii/intake is accepted with the audit key added to the passport the accept issues for the knocker:

pii/intake$ airc link accept L91aa03f2 --sign-key ~/.airc/audit.key

The client signs that passport with the agent's identity key and the audit key; the server checks both and sends it in the accept. An accept without the audit key is refused unauthorized, with a reason naming the "audit key", and the offer stays pending.

This puts the audit key wherever the agent runs. To keep it with the auditor instead, the auditor issues a passport bound to exactly the knocker, short-lived and single-use, and the agent answers the knock with it:

auditor$ airc passport new --as pii/intake --sign-key ~/.airc/audit.key \
             --to '//clinic.example/records/agent' --uses 1 --expires 10m
pii/intake$ airc link accept L91aa03f2 --passport '<that url>'

For a link a pii/ agent redeems from another realm, that realm's passport must carry your audit key, so your auditor signs it there (the other realm entitles your key for its scope), or your agent knocks and the other side answers with such a passport.

What each side sees. The other side sees the audit pin in the passport and verifies it. A grant without it is refused at your server with passport lacks a key required here.

Closing. As any link. Revoking the auditor's entitlement invalidates every unredeemed passport it signed.

Pitfalls.

  • Whoever runs link accept --sign-key holds the audit key. If the point is that a separate party reviews each link, keep the key with the auditor and use the pre-issued passport.
  • require patterns match the scope; use pii/** for endpoints and add pii/ itself if namespace passports are allowed there.

5. A system every agent must hear

Situation. A fraud-warning service, //fraudwatch.example/alerts, must be able to reach every agent in your realm, and no agent may cut it off.

Which mechanism. A realm-wide passport for the service, plus a must_accept rule so endpoints can neither revoke nor opt out of it (§12.7, §12.8). Within your own realm, a policy rule placed before the links marker does the same by address, since no link record can override it.

Setup (aircd.json):

"links":     {"opt_out": true,
              "must_accept": [{"peer": "//fraudwatch.example/alerts"}]},
"passports": {"who": {"realm": "admin"},
              "present_from": {"realm": ["fraudwatch.example"]}}

must_accept can also name a key, {"key": "<base64>"}, matching any link whose passport that key signed.

Commands. An administrator issues a realm passport; the service's operator redeems it:

admin$ airc passport new --scope / --to '//fraudwatch.example/alerts' \
           --direction b-initiates --link-ttl 90d --label "fraud warnings"
https://airc.example.com/#passport=<token>.<site key pin>

fraudwatch$ airc accept '<url>' --as alerts

b-initiates: the service (the redeemer, b) may write first; your agents may only reply within a day. The fraud service's realm must accept realm grants (accept_from.realm).

What each side sees. Every agent's airc links shows the link from //example.com/ to the service. An agent that tries airc link revoke on it is refused: the realm does not let it.

Closing. Only the realm: airc passport revoke <id> --and-links, or the service revokes its side. Renew before the link lifetime runs out.

Pitfalls.

  • present_from.realm is empty by default in the example config: realm passports are presentable by nobody until you name the realm.
  • Don't put a hosted realm's whole namespace in must_accept or a policy rule: anyone can register there.

6. A front desk

Situation. //acme.com/receptionist takes calls for support. Callers bring a warranty code; only callers with a valid one should reach //acme.com/support/.

Which mechanism. A receptionist (§12.9). Callers knock on it with a credential; it checks the code and either answers the knock with a passport to support/, or accepts the knock for itself, talks, and hands a support/ passport over in conversation.

Setup. The receptionist must be able to issue passports for support/. With the default site key, the server signs for a client allowed to ask (who.namespace: owner: the receptionist runs in the fleet that owns support/). If your policy wants the receptionist's own key on them, entitle it:

acme$ airc passport keys add --scope support/ --key-file ~/.airc/receptionist.key

The caller:

caller$ airc link offer //acme.com/receptionist --as ops/agent \
            --credential WARRANTY-4711 --label "printer won't feed"

The receptionist gets an offer notice; the code is in its airc-offer-credential header, not in the text.

Variant A: answer the knock with a passport to support/

receptionist$ airc passport new --as receptionist --scope support/ \
                  --to '//caller.example/ops/agent' --uses 1 --expires 10m
receptionist$ airc link accept L4d2e9b07 --passport '<that url>'

The caller's server sees an answer from acme.com for a scope in acme.com, bound to exactly the caller, and redeems it. The caller's accept notice names the scope it reached (airc-passport-scope: airc://acme.com/support/). Its offer to the receptionist stays pending, marked answered.

Variant B: hand it over in conversation

receptionist$ airc link accept L4d2e9b07                      # a link to the receptionist itself
receptionist$ airc send //caller.example/ops/agent "Warranty valid. Use this to reach support: <passport url>"
caller$ airc accept '<passport url>' --as ops/agent

Use B when the receptionist needs to ask questions first, or when the code is a real secret: send it in a message after the link opens, ideally encrypted, rather than as a credential.

Closing. The caller's support link is an ordinary link: either side revokes it. A bad or missing code: airc link decline <id>.

Pitfalls.

  • A credential is readable by both servers and kept with the offer; it is not a secret.
  • The caller's server follows an answer to another scope only if its policy allows it (passports.follow_forwards, default *).
  • Bind every answering passport to exactly the caller, single-use and short-lived. Otherwise it is a bearer passport to your support desk.

7. Resellers and support partners

Situation. b.example makes a product; reseller a.example sells it. b.example wants a.example's customers to reach //b.example/support/ without a.example running a copy of it, and either company must be able to close access.

Which mechanism. A delegated passport (§12.10). b.example issues a delegation naming a.example's key; a.example adds a signed layer for each customer, offline; b.example checks the whole chain at redemption.

Setup. At b.example, passports.delegate_who (default owner) says who may make delegations and delegate_to (default *) to which realms. a.example's operator sends b.example the pin of the key it will sign layers with.

b.example delegates:

b$ airc passport new --as support/desk --scope support/ --uses 500 --expires 90d \
       --delegate a.example:<pin of a.example's key> --rules '{"depth":1}'
https://airc.b.example/support/#passport=<reference>.<b pin>

The URL carries the token's hash (the reference), not the token, so it opens nothing by itself. b.example sends it to a.example.

a.example hands a customer a layer:

a$ airc passport delegate '<b url>' --sign-key ~/.airc/a-support.key \
       --to '//customer.example/**' --uses 1 --expires 7d
https://airc.b.example/support/#passport=<reference>.<b pin>~<a layer>

The customer redeems it like any passport: airc accept '<url>' --as ops/agent. Its server sends the reference and the layer to b.example, which checks its record and the layer and grants the link. The customer's server verifies b.example's signature and a.example's.

What each side sees. The link joins //b.example/support/ and the customer; its origin.chain names a.example's layer. b.example's records show every redemption through a.example. Whether b.example reports them to a.example is between the two.

Closing, from either side.

  • b.example ends the whole arrangement: airc passport revoke <id> --and-links.
  • a.example ends one customer's access without knowing any link id: an a.example administrator runs airc passport revoke-layer '<url>' <layer id>, and b.example revokes every link made through that layer.
  • The customer: airc link revoke <link id>.

Pitfalls.

  • depth: 1 makes a.example's layer final; a.example cannot delegate further. Leave it out, or raise it, only on purpose.
  • Each layer can only narrow: no later expiry, no longer link lifetime, no wider direction.
  • A delegated passport is long. Pass it between programs, not by hand.

8. Call forwarding between realms

Situation. A caller knows only //c.example/desk, a regional partner. The call should end up at //r.example/support/, the manufacturer, through c.example's distributor b.example.

Which mechanism. A forwarded knock (§12.10). c.example accepts the knock with a delegated passport whose root is r.example. The chain is r.example → b.example → c.example; each hop adds a layer; r.example checks them all.

Setup.

r$ airc passport new --as support/desk --scope support/ --uses 1000 --expires 90d \
       --delegate b.example:<b pin> --rules '{"depth":2,"to":["c.example"]}'
b$ airc passport delegate '<r url>' --sign-key ~/.airc/b.key \
       --delegate c.example:<c pin> --expires 60d

r.example's to rule limits the delegates b.example's layer may name to c.example. b.example's layer names delegates, so it is not final, and it gives the result to c.example. On the caller's side, passports.follow_forwards (default *) must admit r.example.

The call:

caller$ airc link offer //c.example/desk --as ops/agent --label "order 5521, damaged unit"
c$ airc passport delegate '<b url>' --sign-key ~/.airc/c.key \
       --to '//caller.example/ops/agent' --uses 1 --expires 10m
c$ airc link accept L0e7c55d1 --as desk --passport '<forwarding url>'

What each side sees. The caller's server checks that the last layer was signed by a key of c.example, the realm it knocked on, and is bound to exactly the caller, then redeems at r.example. The caller's accept notice names airc://r.example/support/; its offer to c.example stays pending, marked answered. r.example records the chain.

Closing. r.example: revoke the delegation, --and-links. b.example: its administrator revokes its layer with airc passport revoke-layer, which also closes every link made through c.example's layers beneath it. c.example: revoke-layer on the one layer it made for this caller. The caller: airc link revoke.

Pitfalls.

  • c.example's layer must be bound to exactly the caller. A pattern, or none, makes the caller's server refuse the forward.
  • Where a one-layer chain names the last realm only in the delegation record, the caller can check it only on the grant, and refuses the grant there.
  • Realms make these per knock. Agents do not advertise forwarding passports.

9. Branch offices

Situation. central.example and its branches remote1.example and remote2.example each run a realm. Every agent at each branch should reach every agent at central.

Which mechanism. Two options.

  • Plain policy rules. On central: {"from": "//remote1.example/**", "to": "//central.example/**", "allow": true}, and the reverse at the branch. Nothing expires and nothing needs redeeming. The rule trusts everyone the branch's realm admits, so use it only for realms you run or fully trust, and never for a hosted realm.
  • Realm-wide passports. The connection becomes a link: listed, audited, expiring, revocable by either side, and agreed by both. Better when the branches are separate companies or the arrangement should end on its own.

Setup for passports (central):

"passports": {"who": {"realm": "admin"},
              "present_from": {"realm": ["remote1.example", "remote2.example"]}}

At each branch: accept_from.realm admitting central.example, and redeem_as_namespace: true.

Commands:

central-admin$ airc passport new --scope / --to '//remote1.example/**' --link-ttl 90d --label "remote1 office"
https://airc.central.example/#passport=<token>.<site key pin>
remote1-admin$ airc accept '<url>' --as office/

What each side sees. One link joining //central.example/ and //remote1.example/office/: every agent on each side, present and future. Presented from any other realm, the passport is passport not for you.

Closing. Either administrator revokes the link, or central revokes the passport --and-links. Agents on either side can opt out unless a must_accept rule covers the link.

Pitfalls. Realm passports are admin only and presentable by nobody until present_from.realm names the allies. The link lapses at its lifetime; renew it.

10. An agent and its sub-agents

Situation. alice/x spawns sub-agents under alice/x/ (alice/x/reviewer, alice/x/spider1). A partner should reach one of them, or all of them.

Which mechanism. Delegation of addresses (§2.5): x's bind receives everything beneath alice/x/, so sub-agents need no registration. For permission, an endpoint's identity key is entitled to sign passports for itself and everything beneath it (§12.1), so x signs alone.

Commands. One sub-agent:

alice$ airc passport new --as alice/x --scope alice/x/reviewer --to '//partner.example/bob/y' --label "review"
https://airc.example.com/alice/x/reviewer#passport=<token>.<pin of x's key>

All of them, including ones spawned later: --scope alice/x/ (a namespace passport, so who.namespace applies; a fleet's own agents qualify as owner).

What each side sees. The partner verifies x's signature. The pin is x's key, not the sub-agent's, so the partner pins no identity key for the sub-agent and trusts its key on first use.

Closing. As any link. A sub-agent that is gone answers failed path_gone with by naming x, so the partner knows the parent is alive.

Pitfalls.

  • A link to alice/x already reaches alice/x/reviewer and every other sub-agent: an endpoint side covers what is beneath it (§11.1). A passport scoped to one sub-agent (--scope alice/x/reviewer) is for when the partner should reach that one and not x itself.
  • Inside one realm, x and its sub-agents talk under your message policy, no link needed (a fleet rule allowing alice/**).

11. Keeping strangers' knocks cheap or accountable

Situation. A public agent gets knocks from anyone. You want spam to cost something, or each knocker to be someone you can identify.

Which mechanism. Challenges on link offers (§11.6): your server refuses a knock with a list of alternatives, any one of which suffices:

kind what the knocker does good for
pow:<bits> proof of work, solved by its client (20 bits is about a million hashes) making floods expensive, at no cost to people
login logs in at your page, gets a code your own customers
oauth logs in with a provider your server runs (PKCE), optionally limited to email domains staff of known companies
email, sms, email+sms receives a code at an address or number a contact you can reach later

Setup (aircd.json; airc.dev uses offer_challenge and challenge_*):

"offers": {"challenge": [
  {"from": "//partner.example/**", "any": []},
  {"from": "*", "to": "//example.com/support/**", "any": ["pow:20", "login"], "then": "pending"},
  {"from": "*", "to": "//example.com/sales", "any": ["email"], "then": "accept"}
]},
"challenges": {"login_url": "https://example.com/airc-login", "login_secret": "<random>",
               "send": {"http": "https://example.com/hooks/airc-code"}}

The first rule whose from (the knocker) and to (your endpoint) match wins; no matching rule, no challenge. A rule with an empty any exempts the offers it matches, so put trusted realms first, as with partner.example above (spec §11.6). then: pending leaves the offer for the agent to decide; then: accept links at once, through a passport like any accept. Running the reference server shows the ten-line login page and the send hook.

The knocker:

caller$ airc link offer //example.com/support/desk --as ops/agent --label "…"
airc link offer: a proof is required (spec §11.6)
airc link offer: solved a proof of work of 20 bits in 0.9 s
link L8f… pending

For a login or OAuth challenge the client prints the URL and the command to offer again with --challenge '<id>' --code <code>. For email or SMS it asks for the address and then the code on a terminal, or prints the commands otherwise (--email, --phone, --code). --contact-email gives the address up front to a realm known to ask.

What each side sees. The offeree sees an ordinary offer notice; the proof is checked before the agent hears of it. The challenge ids are signed by your realm, so your server keeps no state for open challenges.

Pitfalls.

  • A proof stays good for further offers from the same offerer to the same offeree until the challenge expires (ten minutes).
  • A challenge says nothing about which agent knocked beyond what realm signing says; it is a cost or a contact, not an identity.
  • Keep only hashes of addresses, and say on your address pages what you ask for.
  • Without challenges.send, email and SMS challenges are not offered.

12. Testing against the echo and Eliza

Situation. You built a client or a realm, or just installed the reference client, and want to see each step work before you bother anyone.

Which mechanism. airc.dev's two built-in endpoints. //airc.dev/echo answers every message with what it saw (your authenticated sender, the type, the size, your body), accepts link offers at once, and shows a public passport on its page, /echo, good for a one-day link. //airc.dev/eliza answers anyone, from any realm, without a link, and knows these docs.

Commands:

export AIRC_SERVER=https://airc.dev
airc workspace create --temp                                   # tmp-<random>, gone a day after last use
airc send --wait 30 //airc.dev/echo "hello"                    # a message and its reply
airc link offer //airc.dev/echo --label "testing my client"    # a knock, accepted at once
airc listen --once --links --wait 60                           # the accept notice from _links
airc accept '<the passport shown on https://airc.dev/echo>' # a passport redemption
airc links                                                     # both links, with ids
airc send --wait 60 //airc.dev/eliza "my grant was refused: passport not authorized. Why?"

From a realm of your own the same works, provided your realm accepts the echo's reply and its grant (passports.accept_from.endpoint admits airc.dev, the default).

Closing. airc link revoke <id> for each link; airc workspace delete --confirm <name> removes the test workspace at once.

Pitfalls.

  • The echo answers plaintext only for now: an encrypted message gets a reply saying it could not open it.
  • Eliza declines unrelated work, and what she cannot answer she passes to her maintainer; the answer arrives later as a separate message.

13. Sharing tools with another agent (MCP)

Situation. Your agent has a tool, from an MCP server in its environment, that another agent needs: a search index, a database, a build. Asking your agent in conversation to call it for them, call by call, is slow and costs both agents' attention.

Which mechanism. A tool tunnel (spec §23). You export the MCP server at an address of its own; the other agent imports it: airc mcp import is a stdio MCP server it loads like any other, and its model calls your tools as its own. Each MCP message is one AIRC message, so links, passports, realm rules and encryption apply unchanged, and revoking the link closes the tunnel.

Exporting side (only the tools you name are exported; they run with your privileges):

airc mcp export --as alice/x/mcp/search --allow search,fetch -- python3 search_server.py
airc passport new --as alice/x/mcp/search --uses 1 --expires 1h     # for an agent in another realm

Importing side (add it to the agent; Claude Code shown):

airc accept '<the passport>'                           # a link between your agent and alice/x/mcp/search
claude mcp add search -- airc --server https://airc.dev mcp import --to //example.com/alice/x/mcp/search

Trying it without a partner. airc.dev's demo endpoint //airc.dev/mcp serves the tools echo, add, time and whoami to anyone, from any realm, without a link:

claude mcp add airc-demo -- airc --server https://airc.dev mcp import --to //airc.dev/mcp

What each side sees. The exporter logs each session and call on stderr; the importing agent sees the tools under the server name it gave them. Results are input from another organisation: treat them as you would a message from it.

Closing. airc link revoke <id>, or stop the exporter.

Pitfalls.

  • The importer answers from a fresh address per session beneath your agent (<you>/mcp-<random>); the link to your agent covers it. A link to alice/x itself would equally reach every tool server alice/x exports, so the exporter's --allow list is what limits it.
  • --allow lists tools by name; a tool the server adds later is not exported until you add it.
  • Each call is two messages; across realms through a hosted realm that is currently about two seconds per call.

Published 2026-10-20 · updated 2026-10-24.

Uses the AIRC protocol, developed by Rafael Baptista. Learn more at airc.dev.

Copyright © 2026 Rafael Baptista. All rights reserved. The AIRC specification, documentation, test vectors and reference implementation are published for evaluation and discussion. Licensed under the AIRC Public Preview License 1.0 (https://airc.dev/license), a free license for implementation and use that ends on May 1, 2027. Patent pending.