Agent-facing CLI layer for Python. JSON envelopes, command tree discovery, NDJSON streaming — works with any CLI framework.
Biology: porins are beta-barrel proteins in outer membranes that form structured channels for selective bidirectional passage of molecules.
pip install porin
from porin import ok, err, action, emit_ok, emit_err
# Build envelopes (returns dict)
result = ok("mytool status", {"id": "abc", "state": "running"}, [
action("mytool logs abc", "Fetch output logs"),
action("mytool cancel abc", "Cancel if stuck"),
])
# Write to stdout as JSON line
emit_ok("mytool list", {"count": 3, "items": [...]})
# Structured errors with fix suggestions
emit_err("mytool status bad-id", "Not found", "NOT_FOUND",
fix="Verify the ID with: mytool list")Envelope helpers — ok(), err(), action() build the JSON envelope. emit_ok(), emit_err() write them to stdout.
{"ok": true, "command": "mytool status abc", "result": {"state": "running"}, "next_actions": [...]}
{"ok": false, "command": "mytool status bad", "error": {"message": "Not found", "code": "NOT_FOUND"}, "fix": "Verify with: mytool list", "next_actions": [...]}Command tree — declarative registry for agent self-discovery:
from porin import CommandTree
tree = CommandTree("mytool")
tree.add_command("list", description="List items", params=[
{"name": "--count", "type": "integer", "default": 10},
])
tree.add_command("status", description="Get status", params=[
{"name": "item_id", "type": "string", "required": True},
])
# Bare invocation returns this as JSON — agent learns your full API in one call
emit_ok("mytool", tree.to_dict())NDJSON streaming — for long-running operations:
from porin import stream_event, stream_finish
stream_event("progress", {"step": 1, "total": 3})
stream_event("progress", {"step": 2, "total": 3})
stream_finish("mytool run", {"completed": True})
# Last line is always the standard envelope — backwards compatibleTool annotations — declare behavior for agent safety decisions:
tree.add_command("deploy", description="Deploy service", params=[...],
annotations={"readonly": False, "destructive": True, "idempotent": False})
tree.add_command("list", description="List items", params=[...],
annotations={"readonly": True})
# Annotations appear in to_dict() and to_schema()
# Omitted entirely when not provided (no empty dicts)Input examples — improve agent accuracy by 18pp (Anthropic data):
tree.add_command("status", description="Get status", params=[...],
examples=[
{"args": "status abc-123", "description": "Check a specific item"},
{"args": "status --all", "description": "List all statuses"},
])
# Examples appear in to_schema() only (not to_dict() — keeps discovery lightweight)Pagination — prevent context overflow on large result sets:
from porin import paginated_ok, emit_paginated_ok
emit_paginated_ok("mytool list", items=[...], total=100, next_cursor="pg2")
# {"ok": true, "command": "mytool list", "result": {"items": [...], "total": 100, "has_more": true}, "next_cursor": "pg2", ...}Envelope version — contract stability for agents:
emit_ok("mytool status", {"id": "abc"}, version="1.2.0")
# {"ok": true, ..., "version": "1.2.0"}
# Omitted when not provided — backwards compatibleConcise envelopes — cut token usage by ~66%:
from porin import ok_concise
ok_concise("mytool status", {"state": "running"})
# {"ok": true, "command": "mytool status", "result": {"state": "running"}}
# No next_actions — minimal footprint for simple queriesExit codes — semantic contract:
from porin import EXIT_OK, EXIT_ERROR, EXIT_USAGE, EXIT_NOT_FOUND, EXIT_PERMISSION, EXIT_CONFLICTBased on converging patterns from three independent sources:
- JSON is the only output format
- Every response includes
next_actions(HATEOAS for CLIs) - Errors include a
fixfield with recovery instructions - Bare invocation returns the command tree
- Exit codes are a semantic contract
- NDJSON for streaming operations
Turn any porin CLI into an MCP server with one call:
pip install porin[mcp]
from porin import CommandTree
from porin.mcp_bridge import serve
tree = CommandTree("mytool")
tree.add_command("list", description="List items", params=[
{"name": "--count", "type": "integer", "default": 10},
], annotations={"readonly": True})
tree.add_command("deploy", description="Deploy service", params=[
{"name": "service_name", "type": "string", "required": True},
], annotations={"destructive": True})
async def dispatch(command: str, args: dict) -> dict:
if command == "list":
return {"items": [...], "count": args.get("count", 10)}
if command == "deploy":
return {"deployed": args["service_name"]}
raise ValueError(f"Unknown: {command}")
serve(tree, dispatch) # starts stdio MCP serverPorin annotations map to MCP hints automatically (readonly -> readOnlyHint, destructive -> destructiveHint, etc.).
For more control, use create_mcp_server() which returns the FastMCP instance:
from porin.mcp_bridge import create_mcp_server
server = create_mcp_server(tree, dispatch, server_name="my-server")
# Add custom resources, prompts, etc. to the server
server.run()The core library has zero dependencies. It works with any CLI framework:
# cyclopts
from cyclopts import App
from porin import emit_ok, CommandTree
app = App(help_flags=[])
@app.default
def main():
emit_ok("mytool", tree.to_dict())
# Click, Typer, argparse — same patternMIT