A toy todo CLI in Typer. Highlights mark the agent-friendly bits.
todo.py
from json import dumpsimport typerapp = typer.Typer(rich_markup_mode=None) # plain text: no box art for agentsTODOS = [{"id": 1, "title": "Write slides"}]@app.command("list")def list_todos(json: bool= typer.Option(False, "--json")):"""Show all todos."""print(dumps(TODOS) if json else"\n".join(t["title"] for t in TODOS))@app.command()def delete(todo_id: int, yes: bool= typer.Option(False, "--yes")):"""Delete a todo by id."""if todo_id notin {t["id"] for t in TODOS}: typer.echo(f"error: no todo {todo_id}; run `todo list`", err=True)raise typer.Exit(3)ifnot yes: typer.echo("error: deleting needs --yes", err=True)raise typer.Exit(4)print(dumps({"deleted": todo_id}))app()
A stub: delete validates and reports but stores nothing. Typer turns type hints and docstrings into --help. Exit code 2 already means a usage error, so domain errors start at 3. Tested with Typer 0.27.3.
The same CLI, seen by an agent
Human mode
terminal
$ todo listWrite slides
Agent mode
terminal
$ todo list --json[{"id": 1, "title": "Write slides"}]
Failures that teach the agent
terminal
$ todo delete 7;echo"exit $?"error: no todo 7;run`todo list`exit 3$ todo delete 1;echo"exit $?"error: deleting needs --yesexit 4$ todo delete 1 --yes{"deleted": 1}
Each error tells the agent what went wrong and what to try next.
# JMESPath query + TSV output: values only, no table art$ az vm list --show-details\--query"[?powerState=='VM running'].name"-o tsv# No prompt; --no-wait returns once started$ az group delete --name rg-demo --yes--no-wait$ az group wait --name rg-demo --deleted# Managed identity: on an Azure VM or app$ az login --identity# Escape hatch: any Azure REST API, already authenticated$ az rest --method get --url"<management-endpoint>"
--query filters before output
-o json, tsv, or table everywhere
--yes, --no-wait for unattended runs, then wait to confirm
Pattern: human-friendly by default, machine-friendly by flag, with the full API one command away.
from mcp.server import MCPServermcp = MCPServer("Todo")@mcp.tool()def add_todo( title: str, due: str|None=None) ->dict:"""Create a todo. due: ISO date."""return {"id": 42, "title": title}if__name__=="__main__": mcp.run("streamable-http")
tools/list: the agent sees
{"name":"add_todo","description":"Create a todo. due: ISO date.","inputSchema":{"type":"object","properties":{"title":{"type":"string"},"due":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["title"]}}
Type hints and docstrings become the interface. Write them for a reader with no context.
A stub: add_todo stores nothing and ignores due. Tested with the MCP Python SDK 2.3.0, which speaks protocol 2026-07-28; schema output abridged.
No sessions, so any replica can take the request. Cache discovery and reads by their hints; keep app state in a shared store.
Sent with curl to the minimal server.py from the previous slide (MCP Python SDK 2.3.0); without the headers the server returns 400. Response abridged. Specs: Streamable HTTP, caching.
CLI or MCP?
Prefer a CLI when
the agent has a terminal (coding agents)
one developer, their own credentials
output needs piping and scripting
humans use the same tool
Prefer MCP when
the agent has no shell (chat apps)
many users need per-user OAuth
you need central governance and audit
you want hosted, zero-install access
Design beats protocol
A naive MCP server exposes one tool per endpoint. Cloudflare’s API has 2,500+ endpoints.
~1.17Mtokens of tool definitions: one tool per endpoint
~1,000tokens of tool definitions: search() + execute()
Expose discoverable, task-shaped tools, not raw endpoints. Let the agent search for detail on demand.
These vendors’ users span terminals and chat apps, so they ship all three. Choose yours from your users’ workflows.
Teach the agent: ship a skill
skills/todo-cli/SKILL.md
---name: todo-clidescription: Manage todos with the `todo` CLI. Use when asked to list or delete todos.---# Using the todo CLI- Run `todo list --json` and parse stdout.- Exit code 3 means "not found": run `todo list` to get valid ids.- Deleting needs `--yes`; ask the user first.
Agent Skills are folders of instructions an agent loads only when relevant.
AGENTS.md does the same job for a repository.
Tools ship them too: Astral publishes an official uv skill.
plugins:- llmstxt:markdown_description:| You can use uv to install Python dependencies, run scripts, ...sections:Getting started:- getting-started/installation.mdGuides:- guides/scripts.md
llms.txt: the build writes
# uv> uv is an extremely fast Python package> and project manager, written in Rust.You can use uv to install Python ...## Getting started- [Installation](https://docs.astral.sh/uv/ getting-started/installation/index.md)## Guides- [Running scripts](https://docs.astral.sh/uv/ guides/scripts/index.md)
Add your repo at context7.com and steer the parser with context7.json. Agents then call resolve-library-id and query-docs over MCP.
what the agent gets back
# query-docs("/astral-sh/uv",# "add a dependency to a project")# → "Adding a dependency with uv add"# (docs/guides/projects.md)uv add requestsuv add --dev pytestuv add --group lint ruff
Ask Context7 your users’ top five questions. No answers? Your docs are hard to find there.
Source: Upstash, Inside Context7’s quality stack. uv figures from Context7 on 7 Oct 2026. Scores are LLM-judged signals, not a certificate of correctness or security.
Agents act with your users’ credentials
🔑 Least privilege
Fine-grained, scoped, short-lived tokens. Read-only by default.
🧪 Dry runs, safe retries
--dry-run first; idempotency keys so retries never write twice.
🛑 Explicit intent
A flag for anything destructive; consent comes from the user.
🧾 Audit trail
Identify agent traffic and log every action it takes.
Treat tool output as untrusted input: text in the data can carry instructions that hijack the agent (prompt injection).
Not taking part is completely legitimate. Pick deliberately, then make the signals match.
robots.txt is a request, not a lock: enforce with bot management or a WAF. Content signals state a preference; Disallow is what crawlers without signal support obey. Content-Signal values must sit on one line in a real file. Sources: contentsignals.org, Cloudflare managed robots.txt
The old rules still apply
Agent extras: llms.txt, Markdown, MCP
Structured data and metadata
SEO: crawlable, titled, described, in a sitemap
Fast, accessible, semantic HTML over HTTPS
“All existing SEO fundamentals continue to be worthwhile.”
Google Search Central
“Agents review the accessibility tree to identify interactive elements.”
Never advertise a capability you don’t have. A false signal is worse than none.
For a server at /mcp, that is /.well-known/oauth-protected-resource/mcp; WWW-Authenticate can also point to it. The metadata names your authorisation server; that server, often an external identity provider, publishes /.well-known/oauth-authorization-server. Sources: Cloudflare, isitagentready.com check list; MCP authorisation
Level 4: WebMCP, tools inside the page
search.html
<form toolname="search_flights" tooldescription="Search flights by route and date"><input name="origin" required><input name="destination" required><input name="date" type="date" required><button>Search</button></form>
Experimental
A browser agent calls search_flights directly instead of guessing which box to type in.
WebMCP is a Chrome origin trial; API names are still changing. Watch, don’t bet the roadmap.
Both tools turn failures into a prompt for your coding agent. Review the diff like any other PR.
Scan from the terminal and CI
terminal
# Vercel's Is Agentic scanner, from the command linenpx is-agentic example.com --json# Or read a stored report over HTTPcurl-s'https://is-agentic.com/api/v1/report?url=example.com'# Catch regressions in CI: set the threshold from your own baselinenpx @vercel/agent-readability audit https://example.com --min-score 80
Chrome Lighthouse has an experimental Agentic Browsing category: accessibility tree, layout shift, llms.txt, WebMCP.
The scanners practise what they preach: they offer CLIs, APIs, and MCP servers of their own.