Observed since 20 Oct, 23:30 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
agent-surface
Project typed Python operations as agent-friendly CLIs, MCP tools, and schemas
Typed Python operations that become HATEOAS CLI and MCP surfaces. A response tells a person or agent the next valid thing to do, so callers follow concrete actions instead of guessing commands, routes, or object encodings.
Define an operation once with Pydantic. agent-surface projects it as a YAML-first Click CLI and
native MCP tools with bounded, concrete next_actions.
From a checkout of this repository, run the bookstore example:
uv sync --frozen --all-extras --dev
./examples/bookstore books search --query dune --limit 2
The relevant fields from its YAML envelope show the first page and its concrete next actions:
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
Follow the advertised CLI command verbatim:
./examples/bookstore books inspect --book book_dune
For MCP, call operation: books.inspect with bound: {book: book_dune} instead. The complete
search → inspect → reserve → cancel → delete trajectory is executable in the
bookstore tutorial.
Save this as hello.py:
import asyncio
import sys
from pydantic import BaseModel
from agent_surface import App
from agent_surface.adapters.click import build_click_group
from agent_surface.adapters.mcp import MCPAdapter
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)
mcp = MCPAdapter(app)
if __name__ == "__main__":
if sys.argv[1:] == ["--mcp"]:
asyncio.run(mcp.run_stdio())
else:
cli()
Install the package into your existing Python environment and run the CLI:
pip install 'agent-surface[mcp]'
python hello.py people greet --name Ada
The same typed operation is callable from Python, exposed through Click, and available as the exact
MCP tool people.greet.
For a local client, use stdio: the client starts hello.py --mcp and exchanges MCP messages
over standard input and output. Add this to ~/.codex/config.toml, replacing both absolute paths.
python must be the interpreter where you installed agent-surface:
[mcp_servers.hello]
command = "/absolute/path/to/.venv/bin/python"
args = ["/absolute/path/to/hello.py", "--mcp"]
Restart Codex, then use /mcp to inspect hello. For Claude Code, save the equivalent
project-local .mcp.json next to hello.py:
{
"mcpServers": {
"hello": {
"command": "/absolute/path/to/.venv/bin/python",
"args": ["/absolute/path/to/hello.py", "--mcp"]
}
}
}
Streamable HTTP is only for serving MCP remotely from a web application. See the MCP contract when you need that deployment.
Install the optional authoring skills for an agent that will build a HATEOAS CLI or MCP tool:
curl -fsSL https://raw.githubusercontent.com/allenday/agent-surface/main/src/agent_surface/skills/install.sh | sh
The agent-surface-authoring skill first checks how the current project manages Python before it
uses the library; it does not choose a virtual environment or install packages globally. You can
also read the skills directly:
Start with the documentation map if you are unsure which path fits.
next_actions, stable references, and explicit confirmation for writesMIT
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
clickEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.12",
"evaluation": "NOT_EVALUATED"
} | click<9,>=8.1 | runtime |
pydanticEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.12",
"evaluation": "NOT_EVALUATED"
} | pydantic<3,>=2.11 | runtime |
ruamel-yamlEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.12",
"evaluation": "NOT_EVALUATED"
} | ruamel-yaml<0.20,>=0.19.1 | runtime |
mcpEnvironment{
"extras": "",
"marker": "extra == \"mcp\"",
"requires_python": ">=3.12",
"evaluation": "NOT_EVALUATED"
} | mcp<3,>=2.0.0; extra == "mcp" | runtime |
bdist_wheel · 57.68 KB · Python py3
sdist · 200.42 KB · Python source
Observed since 20 Oct, 23:30 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:a94d2a4cd7356f6bf99357dd59c69cd9fbca5e31670b81c9d4f86eef0b7a450a