-
Notifications
You must be signed in to change notification settings - Fork 1
MCP
The agent-as-driver surface. Tep apps expose themselves as Model Context Protocol servers so Claude Code / OpenCode / Gravity CLI / any MCP client can discover and drive the catalog natively.
Battery 5 in docs/MCP-BATTERY.md.
require 'sinatra'
mcp_tool 'greet', "Say hi to someone" do
param :name, String, "person to greet"
on_call do |name:|
Tep::MCP.text("hello " + name)
end
endbin/tep build hello.rb -o ./hello
./hello -p 4567The translator emits:
-
POST /tools/greet— HTTP-direct endpoint for curl / non-MCP agents. -
POST /mcp— JSON-RPC 2.0 dispatcher (initialize / tools/list / tools/call). -
GET /llms.txt— markdown catalog. -
GET /openapi.json— OpenAPI 3.0.3 spec.
Test it:
curl http://127.0.0.1:4567/llms.txt
curl -X POST http://127.0.0.1:4567/tools/greet \
-H "Content-Type: application/json" \
-d '{"name":"world"}'
# -> hello worldmcp_tool 'name', "description" [, caps: [:sym]] do; param ...; on_call do; ... end; end.
mcp_tool 'add', "Add two integers" do
param :a, Integer, "left operand"
param :b, Integer, "right operand"
on_call do |a:, b:|
Tep::MCP.text((a + b).to_s)
end
endString, Integer, and Float are supported in v1. They map to
JSON Schema string / integer / number in the auto-published
catalog. Param values get decoded out of the JSON request body
(or the JSON-RPC params.arguments) before the on_call body
runs, so user code only ever sees properly-typed locals.
mcp_tool 'wipe_db', "Drop everything", caps: [:admin] do
on_call do
Tep::MCP.text("wiped")
end
endThe translator emits a req.identity.may?(:admin) check at the
top of the generated dispatch cmeth; failure returns
Tep::MCP.error("missing capability: admin") without running the
body. Identity comes from the regular Tep::Auth chain
(Tep::AuthBearerToken / Tep::AuthSessionCookie /
Tep::AuthOAuth2), so an MCP client authenticating with a JWT
that delegates :admin from a human gets the same allow/deny
treatment as any other request path.
Inside on_call, return one of:
Tep::MCP.text("plain text content block")
Tep::MCP.error("error message; sets isError:true in the envelope")JSON content + streaming progress notifications are 5.5+ follow-ups.
Read-only fetches. Same DSL shape as tools, with on_read
instead of on_call:
mcp_resource 'server/status', "Current server status" do
on_read do
Tep::MCP.resource_text("server/status", "uptime: " + uptime.to_s)
end
endgenerates:
GET /resources/server/status-
resources/listarm in/mcp(returns uri + name + description + mimeType per resource). -
resources/readarm in/mcp(looks up by URI, returnscontents:[{uri,mimeType,text}]).
URI templating ('experiments/{id}/log' with captured id) is
5.5+. For per-id reads today, use a tool that takes id as a
param.
Three co-exist, all auto-generated from the same DSL:
| URL | Format | Audience |
|---|---|---|
POST /mcp |
JSON-RPC 2.0 | MCP-native clients |
GET /llms.txt |
Markdown | LLM-readable catalog, humans skimming |
GET /openapi.json |
OpenAPI 3.0.3 | Non-MCP agents, Swagger UI |
The initialize response on /mcp advertises capabilities: {tools:{}, resources:{}} whenever the app declares any of
either kind.
Tep apps shipping MCP should ship an AGENTS.md at the repo
root describing the agent-facing surface:
- What the app does (one paragraph).
- Discovery URLs.
- Tool + resource catalog with capability requirements.
- Invariants the app maintains (state machines, id rules, append- only logs, ...).
- "Things you should NOT do" — guardrails an agent can't infer from the schema alone (concurrency caps, anti-patterns).
- Authorization model.
This file is not auto-generated. Claude Code reads it once per session before driving the app, so the prose-level invariants
- anti-patterns sit alongside the machine-readable catalog. See
examples/experiments/AGENTS.mdfor a worked example.
examples/experiments —
mock training-run manager driving the full agent-as-driver loop
in ~200 lines: 4 tools (2 capped on :run_experiments), 2
resources, AGENTS.md, README walking through both the MCP-native
and curl driver paths.
-
URI templating for resources (
'experiments/{id}/log'). Use a tool with anidparam meanwhile. -
Streaming via
stream do |out| ... endover SSE. Tools return a single result; long-running operations either return immediately and expose progress as a resource, or wait synchronously. - MCP progress notifications during long-running tool calls.
-
Resource subscriptions (
resources/subscribe+notifications/resources/updated).
All four are tracked for 5.5+. Apps with real streaming needs today can run the tool body, return a quick "started" text, then expose a resource for polling.