Skip to content

HTTP server

The tool service, its ASGI and WSGI apps, auth and problems. Guide: HTTP server.

agent_adapter.server

HTTP tool server: the registry over REST + SSE + OpenAPI, with MCP mounted beside it.

APPROVER_SCOPE = 'approvals:decide' module-attribute

Authenticator = Callable[[RequestInfo], Awaitable[Principal | None]] module-attribute

Authorizer = Callable[[Principal, ToolSpec, Mapping[str, Any] | None], Awaitable[bool]] module-attribute

DecisionAuthorizer = Callable[[Principal, ApprovalRecord], Awaitable[bool]] module-attribute

PROBLEM_MEDIA_TYPE = 'application/problem+json' module-attribute

Claim dataclass

new (run it; token completes or releases the lease), in_progress or done.

fingerprint names the request holding the key (in_progress / done); stored is the response to replay (done).

EventStream

Bases: EventSink, Protocol

An event sink that replays and follows the events of one run.

subscribe(run_id, *, after=0)

(seq, event) for every event of run_id with seq > after, then new ones as they arrive; seq starts at 1 per run.

IdempotencyStore

Bases: Protocol

Atomic per-key leases and replay entries shared by every worker of a service.

claim(key, fingerprint, lease_s) async

Take a lease of lease_s seconds on a free key, or report who holds it.

complete(key, token, stored, ttl_s) async

Replace the lease token holds with stored for ttl_s; False if it was lost.

release(key, token) async

Free the lease token holds (nothing is replayed).

update(key, stored, ttl_s) async

Replace a done entry with the same fingerprint (a decided approval's outcome).

MemoryEventStream

In-memory EventStream: the last max_events_per_run events of the last max_runs runs.

Events without a run_id are dropped. Subscribing never creates a run, so readers cannot evict the buffers of other runs.

emit(event)

Store event under its run and wake that run's subscribers; never raises.

subscribe(run_id, *, after=0) async

Replay the kept events after after, then follow the run until closed.

MemoryIdempotencyStore

In-process IdempotencyStore (one worker); oldest replay entries go first past max_entries, leases are never evicted.

Principal dataclass

An authenticated caller; id scopes every run, call and approval it touches.

Problem

Bases: BaseModel

An RFC 9457 problem; extensions (tool, errors, approval_id, ...) are extra fields.

Readiness

The /readyz state of one create_app app, found at app.state.readiness.

The lifespan marks it started, and draining when shutdown begins. Hosts that drain on their own (a SIGTERM handler, a parent app's lifespan) call drain() first.

draining property

True once drain() was called or the lifespan began shutting down.

ready property

True when new requests should be routed here.

drain()

Turn /readyz to 503; other routes keep serving in-flight and new requests.

response()

200 {"status": "ready", "version"}, else a 503 service-unavailable problem.

start()

The lifespan started: the app is ready (again).

RedisIdempotencyStore

IdempotencyStore on Redis (agent-adapter[redis]), shared by every worker.

client_factory() returns a redis.asyncio.Redis (one per event loop), e.g. lambda: Redis.from_url(url). Leases are SET key NX PX; completing, releasing and updating are one compare-and-set script, so a lost lease never overwrites another worker's entry. Backend errors propagate: the service answers 503 (fails closed).

aclose() async

Close the running loop's client.

RequestInfo dataclass

A framework-neutral view of one HTTP request (header names in any case).

header(name)

The value of header name, matched case-insensitively.

ServiceResponse dataclass

A framework-neutral response: JSON body, or an SSE stream of bytes.

Stored dataclass

A response kept for replay, with the fingerprint of the request that produced it.

ToolService

The HTTP tool server's behaviour behind handle(request, body).

Tools run through (executor or Executor(registry)).replace(...) with a DeferredApproval over approvals and a sink that also feeds events; the host's executor is never changed. Paths are relative to the mount point (no prefix).

__init__(registry, *, executor=None, approvals=None, events=None, ledger=None, idempotency=None, authenticate=None, authorize=None, authorize_decision=None, allow_anonymous=False, idempotency_ttl=86400, title='agent-adapter tools')

Validate the configuration and build the service's own executor view.

Raises:

Type Description
ConfigError

no authenticate and allow_anonymous=False (fail fast), or a non-positive idempotency_ttl.

ToolUsageError

registry, executor, approvals or events of the wrong type.

authenticate(request) async

The caller, ANONYMOUS when allowed, else Unauthorized.

Raises:

Type Description
Unauthorized

no principal and anonymous requests are not allowed.

Forbidden

the authenticator refused the caller outright.

handle(request, body) async

Route one request; errors become RFC 9457 problems, never exceptions.

openapi(*, server_url=None, dialect='3.1')

The OpenAPI document of every tool exported to http (see openapi_document).

bearer_auth(tokens)

An authenticator for Authorization: Bearer <token>.

Parameters:

Name Type Description Default
tokens Mapping[str, Principal] | TokenVerifier

A fixed {token: Principal} table (looked up by SHA-256, never by the raw token), or an async verify(token) -> Principal | None.

required

Raises:

Type Description
ToolUsageError

tokens is neither, or the table holds an empty token or a value that is not a Principal.

create_app(registry, *, mcp=True, mcp_path='/mcp', prefix='', **service_options)

A Starlette app for ToolService(registry, **service_options); MCP at prefix + mcp_path.

Host and anonymous options: docs/cross-language/http-server.md. MCP needs the lifespan. GET /readyz answers from app.state.readiness (503 before startup with MCP, and once shutdown or drain() begins).

Raises:

Type Description
ConfigError

bad paths, options or service config; anonymous on a public bind host.

MissingDependency

starlette (or, with mcp=True, mcp) is not installed.

create_wsgi_app(registry, **options)

a2wsgi.ASGIMiddleware(create_app(registry, **options)) with the app's lifespan running.

The lifespan (which starts the MCP session manager) runs on the middleware's own event loop thread until close() (or the end of a with block); without it, until the process ends. SSE responses are buffered by WSGI servers: follow run events over ASGI. An anonymous app without allow_anonymous_public=True is wrapped so a request whose REMOTE_ADDR is not loopback gets the 403 non-local-client problem.

Drain the WSGI server first (its worker-exit hook), then call close(). A request still in flight is cancelled: one whose response has not started gets a 503 problem, one that has is aborted. close() waits at most CLOSE_TIMEOUT_S for open responses, and a request after close() gets a 503 problem.

Raises:

Type Description
MissingDependency

a2wsgi (or starlette) is not installed.

is_invoker(principal, record)

True when principal made the call record waits on (from the record's metadata).

Anonymous invokers are marked metadata["anonymous"]; a record naming nobody has no invoker, so it matches no one (approvers only).

openapi_document(registry, *, title, version, server_url=None, dialect='3.1')

OpenAPI for the tools of registry exported to http and every server route.

Parameters:

Name Type Description Default
dialect Dialect

"3.1"; "3.0" is OpenAPI 3.0.3 with no oneOf/anyOf/allOf (Go and Rust generators); "3.0-gateway" is that, with only the invoke operations and no auth blocks, for gateways that import tools (AgentCore); it needs server_url.

'3.1'

Raises:

Type Description
ConfigError

an unknown dialect, or "3.0-gateway" without server_url.

problem_for(exc)

The problem for an exception: detail is ErrorInfo.message (the class name only for exceptions that are not AdapterError), plus tool, errors, approval_id.

serve(app, *, host='127.0.0.1', port=8000)

Run app with uvicorn (blocking); a create_app app learns its real bind host.

Raises:

Type Description
ConfigError

an anonymous create_app app on a non-local host that was built without allow_anonymous_public=True (nothing starts).

MissingDependency

uvicorn is not installed.

set_log_key(key)

Share the principal_ref key between workers so their logs correlate (>= 16 bytes).

Raises:

Type Description
ConfigError

a key shorter than 16 bytes.

agent_adapter.server.asgi

The Starlette app: ToolService routes under prefix, MCP at prefix + mcp_path.

PRINCIPAL_STATE = 'aa.principal' module-attribute

mcp_principal(ctx)

The principal _McpGate stored on the MCP request of a tool call's ctx, if any.

request_info(request, path)

The framework-neutral view of a Starlette request (path relative to the prefix).

Also exported here, documented under the canonical path: create_app, serve.

agent_adapter.server.service

ToolService: every route of the HTTP tool server, with no web framework imports.

ANONYMOUS = Principal(id='') module-attribute

encode_body(response)

The response body as UTF-8 JSON (b"" for no body).

Raises:

Type Description
ValueError

the body holds NaN, infinity or a non-JSON value.

Also exported here, documented under the canonical path: ServiceResponse, ToolService.