LIMEDocs

For agent developers

Agent integration: passport and calling MCP

This guide shows how a deployed agent keeps its Agent Token, mints and refreshes an MCP passport, and calls a service's tools. We cover minting the passport, calling tools, and refreshing it before expiry. For AI agents: /docs/all.en.md.

At a glance#

FactValue
Effort~15 minutes
TypeAgent-side SDK, recurring loop
DeliveryAgent runtime → MCP service

How it works#

  1. The agent keeps its Agent Token in the environment and never sends it to a service.
  2. The agent mints a short-lived aud=mcp passport per service domain.
  3. The agent calls tools with Authorization: Bearer and re-mints before expiry.

What you need#

Create an agent in the LIME portal and keep its Agent Token in a .env file.

Step 1. Mint the MCP passport#

The service should not have to call LIME to learn who the agent is, so the agent gets a passport in advance. The SDK sends the Agent Token in the X-Agent-Token header and the service domain in the JSON body.

env
# .env — Agent Token lives here, not in the code
LIME_AGENT_TOKEN="<AGENT_TOKEN>"
import asyncio

from lime_agents import LimeAgent

agent = LimeAgent(agent_token="<AGENT_TOKEN>")  # replace with your Agent Token


async def main() -> None:
    passport = await agent.get_mcp_access_token("<SERVICE_DOMAIN>")
    print(passport.access_token)
    await agent.aclose()


if __name__ == "__main__":
    asyncio.run(main())

Result: 200 with a passport issued for 300 seconds, carrying aud=mcp and sub=<AGENT_ID>.

Step 2. Call the tools over MCP#

Send the passport in the Authorization: Bearer header. The service verifies the signature locally via JWKS and reads the agent_id from the sub claim.

Python
import asyncio

from lime_agents import LimeAgent

agent = LimeAgent(agent_token="<AGENT_TOKEN>")  # replace with your Agent Token


async def main() -> None:
    print(await agent.list_tools("https://<SERVICE_DOMAIN>/mcp"))
    await agent.aclose()


if __name__ == "__main__":
    asyncio.run(main())

Result: the service accepts the call and logs the agent_id; a call without Authorization: Bearer returns 401.

Step 3. Let the SDK refresh the passport#

The SDK handles refresh for you: it caches the passport per domain, lazily re-mints it ~30 s before expiry (mcp_token_refresh_skew), and re-mints once on a 401 from a service.

Python
import asyncio

from lime_agents import LimeAgent

agent = LimeAgent(agent_token="<AGENT_TOKEN>")  # replace with your Agent Token


async def main() -> None:
    passport = await agent.get_mcp_access_token("<SERVICE_DOMAIN>")
    print(passport.access_token)
    await agent.aclose()


if __name__ == "__main__":
    asyncio.run(main())

Result: the client reuses one passport per domain and re-mints it before expiry — no cache class to maintain.

Example response (JSON)#

The mint call returns the passport and its lifetime in seconds.

json
{
  "access_token": "<PASSPORT>",
  "token_type": "Bearer",
  "expires_in": 300
}

MCP protocol#

LIME does not host MCP servers — the service does, and it picks the transport. The SDK speaks it for you; a hand-rolled client talks MCP over Streamable HTTP with JSON-RPC 2.0 at the server endpoint (the demo server uses https://demo.lime.pics/mcp). The demo RS is stateless: call tools/list and tools/call directly — no initialize handshake and no session.

tools = await agent.list_tools("https://demo.lime.pics/mcp")
result = await agent.call_tool("https://demo.lime.pics/mcp", "who_am_i", {})

The transport is stateless: the server issues no mcp-session-id and needs no initialize handshake — call tools/list / tools/call directly, carrying only Authorization: Bearer. (A stateful MCP server does require an initialize first and may return a session id to echo.) The tools/list result carries a tools array that names each tool:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": { "tools": [ { "name": "<TOOL>", "description": "<TEXT>", "inputSchema": { "type": "object" } } ] }
}

tools/call returns the tool's own result; the passport rides in Authorization: Bearer on every call, and the server verifies it locally without calling LIME. A call returns the tool's content plus structured data:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [ { "type": "text", "text": "{\"verified\":true,\"agent_id\":\"<AGENT_ID>\"}" } ],
    "structuredContent": { "verified": true, "agent_id": "<AGENT_ID>" },
    "isError": false
  }
}

A protocol failure is HTTP 200 with an error object; a missing or invalid passport is HTTP 401:

json
{ "jsonrpc": "2.0", "id": 3, "error": { "code": -32602, "message": "invalid params", "data": null } }

Verify it worked#

CheckExpectedNegative
First mintaud=mcp, sub=<AGENT_ID>, lifetime 300 sa wrong X-Agent-Token returns invalid_client (401)
list_tools with the passportthe service returns tools and logs the agent_ida call without Authorization: Bearer returns 401 from the service
Loop longer than the TTLthe cache re-mints before expiry; no gapminting on every call returns RATE_LIMIT_EXCEEDED (429)
Reuse the passport after expirythe service rejects it and the SDK re-mintsan expired passport returns 401

Codes and errors#

The mint returns the same errors as the quickstart; the full public catalogue is in the value reference. The OAuth token mint reports RFC 6749 errors as a raw body {error,error_description} (no ok envelope) — for example invalid_client on a bad X-Agent-Token; LIME's other routes use the envelope {ok:false,error:{code,message,detail}}. The refresh loop also meets these two:

CodeHTTPCauseAction
RATE_LIMIT_EXCEEDED429A mint loop hits the limitCache the passport per domain and back off with jitter; the OAuth mint 429 does not carry Retry-After
AGENT_INACTIVE403The agent is inactive and cannot mint passportsEnable the agent in the LIME portal

Next steps#

Was this page helpful?

Agent integration: passport and calling MCP · LIME