agent-surface
Define a typed Python operation once. Invoke it directly, project it as a YAML-first Click CLI or native MCP server, and publish bounded instructions for what an agent can validly do next.
agent-surface is agent-first and developer-friendly: Pydantic remains the source of truth, output
is compact and inspectable, errors explain how to recover, and no adapter contains business logic.
[!NOTE] Typed operations, adaptive YAML/JSON rendering, references, bounded actions, and generated Click CLIs and MCP v2 servers work today. The public API may change before 1.0.
Five-minute bookstore
Clone the repository and create the locked Python 3.12+ environment:
git clone git@github.com:allenday/agent-surface.git
cd agent-surface
uv sync --frozen --all-extras --dev
./examples/bookstore books search --query dune --limit 2
The complete bookstore source is consumer-owned domain code wrapped by one
integration boundary. It includes an async search, stable book references, a generated Click CLI,
bounded actions, and confirmed mutations. Books are seeded domain data; holds use a small SQLite
store so create, read, cancel, and delete remain visible across CLI and MCP processes. Set
AGENT_SURFACE_BOOKSTORE_DB to choose the database path.
For use in your own project:
pip install agent-surface
pip install 'agent-surface[mcp]'
A trajectory through application state
Suppose an agent knows only one entry command: search the bookstore. The response contains data and the exact valid transitions from that state.
./examples/bookstore books search --query dune --limit 2
schema_version: '1'
ok: true
command:
raw:
- ./examples/bookstore
- books
- search
- --query
- dune
- --limit
- '2'
parsed:
path: [books, search]
args: {}
options: {query: dune, limit: 2}
flags: []
result:
query: dune
items:
- ref: {value: book_dune}
title: Dune
author: Frank Herbert
- ref: {value: book_dune_messiah}
title: Dune Messiah
author: Frank Herbert
total: 3
returned: 2
truncated: true
next_cursor: book_dune_messiah
next_actions:
items:
- rel: inspect
description: Inspect the first returned book
command: [./examples/bookstore, books, inspect, --book, book_dune]
operation: books.inspect
bound: {book: book_dune}
slots: {}
- rel: next-page
description: Continue this search
command:
- ./examples/bookstore
- books
- search
- --query
- dune
- --cursor
- book_dune_messiah
- --limit
- '2'
operation: books.search
bound: {query: dune, cursor: book_dune_messiah, limit: 2}
slots: {}
total: 2
returned: 2
truncated: false
The agent chooses the returned inspect command without guessing a route or reconstructing a shell
string:
./examples/bookstore books inspect --book book_dune
schema_version: '1'
ok: true
command:
raw: [./examples/bookstore, books, inspect, --book, book_dune]
parsed:
path: [books, inspect]
args: {}
options: {book: book_dune}
flags: []
result:
ref: {value: book_dune}
title: Dune
author: Frank Herbert
available: true
next_actions:
items:
- rel: reserve
description: Reserve this available book
command: [./examples/bookstore, holds, create, --book, book_dune, --confirm]
operation: holds.create
bound: {book: book_dune, confirm: true}
slots: {}
total: 1
returned: 1
truncated: false
The next response advertises a confirmed write. The adapter enforces --confirm before calling the
handler:
./examples/bookstore holds create --book book_dune --confirm
schema_version: '1'
ok: true
command:
raw: [./examples/bookstore, holds, create, --book, book_dune, --confirm]
parsed:
path: [holds, create]
args: {}
options: {book: book_dune}
flags: [confirm]
result:
id: hold_book_dune
book: {value: book_dune}
status: active
next_actions:
items:
- rel: get
description: Read this hold
command: [./examples/bookstore, holds, get, --hold, hold_book_dune]
operation: holds.get
bound: {hold: hold_book_dune}
slots: {}
- rel: cancel
description: Cancel this hold
command: [./examples/bookstore, holds, cancel, --hold, hold_book_dune, --confirm]
operation: holds.cancel
bound: {hold: hold_book_dune, confirm: true}
slots: {}
- rel: delete
description: Delete this hold
command: [./examples/bookstore, holds, delete, --hold, hold_book_dune, --confirm]
operation: holds.delete
bound: {hold: hold_book_dune, confirm: true}
slots: {}
total: 3
returned: 3
truncated: false
The hold can now be read with holds.get, transitioned to cancelled with holds.cancel, or
physically removed with holds.delete. The same SQLite state is available to MCP clients through
the examples/bookstore-mcp stdio server. The
bookstore integration guide shows the
Codex and Claude Code configuration.
That is HATEOAS—Hypermedia as the Engine of Application State—in practical terms: the response tells the caller what it can validly do next. An agent follows those exact links and command arrays through application state instead of memorizing an undocumented command tree. Read the plain-language HATEOAS explanation or continue the complete bookstore tutorial.
Define and project an operation
The domain model stays independent of Click:
from pydantic import BaseModel
from agent_surface import App
from agent_surface.adapters.click import build_click_group
class GreetRequest(BaseModel):
name: str
class Greeting(BaseModel):
message: str
app = App("hello")
@app.operation("people.greet", summary="Greet one person", read_only=True)
def greet(request: GreetRequest) -> Greeting:
return Greeting(message=f"Hello, {request.name}!")
cli = build_click_group(app)
Invoke app.invoke(...) from Python or mount cli beneath an existing Click group. Both paths use
the same request model, handler, result model, and stable OperationError semantics. See the
Python API guide, CLI contract, or
existing-application adoption guide.
Project the same registry through MCP
MCP is a sibling adapter, not a wrapper around Click:
from agent_surface.adapters.mcp import MCPAdapter
mcp = MCPAdapter(app)
mcp.server is the native low-level MCP server for embedding and tests. Run it over stdio with
await mcp.run_stdio(), or obtain its ASGI application with mcp.streamable_http_app(). Tools keep
their exact dotted operation names, Pydantic schemas, safety annotations, structured outcomes, and
bounded discovery cursors. Pass the same references= and action_provider= integrations used by
Click when your operations use stable object references or advertise next actions.
In MCP responses, an advertised action's operation and bound fields are the next tool name and
arguments. The complete search → inspect → reserve journey is executable in the
bookstore tutorial; protocol details are in the
MCP contract.
Bounded output by construction
YAML with adaptive flow style is the default. Small leaf collections stay on one line; larger and multiline structures remain block-oriented. JSON and explicit styles are presentation choices:
from agent_surface import BoundedCollection, RenderOptions, render, render_envelope
print(render(value))
print(render(value, options=RenderOptions(yaml_style="flow")))
print(render(value, options=RenderOptions(format="json")))
The default OutputBudget permits 20 returned items and 65,536 UTF-8 bytes. BoundedCollection
requires a concrete continuation whenever it truncates. render_envelope converts an oversized
success document into a complete structured error when possible. Nothing silently disappears, and
ellipsis is never an omission protocol.
References and action discovery
Stable identity is separate from display text. A ReferenceCodec implements encode, decode, and
display; ReferenceRegistry performs exact-type lookup and never falls back to str(object).
Action candidates come only from registered operations or explicitly @action-decorated methods.
AllowActions or another explicit policy authorizes publication. ActionCatalog returns bounded,
cursor-addressable pages with one immediate continuation instead of serializing the reachable graph.
See references and actions.
Choose your path
- Learn by doing: bookstore tutorial
- Understand the model: HATEOAS and bounded discovery
- Adopt incrementally: existing application guide and the original adoption boundary
- Integrate precisely: Python API and CLI envelope, discovery, and exits, or the MCP contract
- Contribute or release: CONTRIBUTING.md and release guide
Design principles
- one typed operation registry; sibling transport adapters
- YAML-first structured output with compact flow style for small values
- HATEOAS responses with a bounded relevant
next_actionsfrontier - stable references instead of incidental stringification
- explicit policy and confirmation gates for actions and writes
- original argv boundaries, repair-oriented errors, and deterministic discovery
- excellent developer experience without weakening agent contracts
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_surface-0.1.0.tar.gz.
File metadata
- Download URL: agent_surface-0.1.0.tar.gz
- Upload date:
- Size: 164.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d55636ef97c7ac16b345feacaf8c0e32b8405c4b687e5359111943b191cf3625
|
|
| MD5 |
0ab80993425903dd2abb284f4688887f
|
|
| BLAKE2b-256 |
22e88b67d6b17eaf278e2cd3892c090c8deed3b7e2ceaba035057ca5a9368bd6
|
Provenance
The following attestation bundles were made for agent_surface-0.1.0.tar.gz:
Publisher:
release.yml on allenday/agent-surface
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_surface-0.1.0.tar.gz -
Subject digest:
d55636ef97c7ac16b345feacaf8c0e32b8405c4b687e5359111943b191cf3625 - Sigstore transparency entry: 2513031910
- Sigstore integration time:
-
Permalink:
allenday/agent-surface@790d15ae4b476efa7bcf6bfd2ec2677970a2f900 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/allenday
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@790d15ae4b476efa7bcf6bfd2ec2677970a2f900 -
Trigger Event:
release
-
Statement type:
File details
Details for the file agent_surface-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agent_surface-0.1.0-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bf45795fd4f9fd2b7e5f0dbbdea703b16cdc51943cfc61500bead6ee5dcb923
|
|
| MD5 |
cbf5cdade24b12cb529b79318858883a
|
|
| BLAKE2b-256 |
ad430185d49d79005fc73afa9f2324ac3d8f38d3eb161e0a25a654ad4e6c8bed
|
Provenance
The following attestation bundles were made for agent_surface-0.1.0-py3-none-any.whl:
Publisher:
release.yml on allenday/agent-surface
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_surface-0.1.0-py3-none-any.whl -
Subject digest:
6bf45795fd4f9fd2b7e5f0dbbdea703b16cdc51943cfc61500bead6ee5dcb923 - Sigstore transparency entry: 2513032046
- Sigstore integration time:
-
Permalink:
allenday/agent-surface@790d15ae4b476efa7bcf6bfd2ec2677970a2f900 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/allenday
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@790d15ae4b476efa7bcf6bfd2ec2677970a2f900 -
Trigger Event:
release
-
Statement type: