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
¶
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.
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 |
ToolUsageError
|
|
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 |
required |
Raises:
| Type | Description |
|---|---|
ToolUsageError
|
|
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
|
|
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
|
|
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'
|
Raises:
| Type | Description |
|---|---|
ConfigError
|
an unknown |
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 |
MissingDependency
|
|
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¶
Also exported here, documented under the canonical path: create_app, serve.
agent_adapter.server.service¶
Also exported here, documented under the canonical path: ServiceResponse, ToolService.