Skip to main content

agent-surface

CI PyPI Python License: MIT

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

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_actions frontier
  • 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

agent_surface-0.1.0.tar.gz (164.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_surface-0.1.0-py3-none-any.whl (40.7 kB view details)

Uploaded Python 3

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

Hashes for agent_surface-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d55636ef97c7ac16b345feacaf8c0e32b8405c4b687e5359111943b191cf3625
MD5 0ab80993425903dd2abb284f4688887f
BLAKE2b-256 22e88b67d6b17eaf278e2cd3892c090c8deed3b7e2ceaba035057ca5a9368bd6

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_surface-0.1.0.tar.gz:

Publisher: release.yml on allenday/agent-surface

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for agent_surface-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6bf45795fd4f9fd2b7e5f0dbbdea703b16cdc51943cfc61500bead6ee5dcb923
MD5 cbf5cdade24b12cb529b79318858883a
BLAKE2b-256 ad430185d49d79005fc73afa9f2324ac3d8f38d3eb161e0a25a654ad4e6c8bed

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_surface-0.1.0-py3-none-any.whl:

Publisher: release.yml on allenday/agent-surface

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page