Repository navigation
Route Capability and REST
Five lines of web.php serve the capabilities to programs instead of browsers: one capability at one REST address
(Route::capability), all of them for the JS client (Route::capabilities), all of them as tools for AI agents
(Route::mcp), and two framework sets of routes, the pages of notification emails (Route::notifications) and the data
routes of the Watson bot apps (Route::bots).
Route::capability('GET', '/api/books', 'list_books'); // one route, one capability, JSON
Route::capability('GET', '/api/books/{id}', 'get_book'); // {id} becomes the input "id"
Route::capability('POST', '/api/books', 'create_book');
Route::capabilities('/capabilities'); // GET lists, POST /capabilities/{name} calls
Route::mcp('/mcp'); // AI agents: the capabilities their token allows
Route::notifications(); // the confirm and unsubscribe pages of notification emails
Route::bots(); // the data routes of the Watson bot apps/caligrafy-quill:new-capability create_book books:write: also as POST /api/books, for the JS client, and for AI agents
Check before you say OK:
- each REST route names a capability, never a closure;
-
Route::mcpis there when agents should use the app, and a token is created with only the scopes they need; -
php caligrafer.php clientwas run again after adding or changing a capability.
| Line | Declares | Who calls |
|---|---|---|
Route::capability($method, $route, $name) |
$method $route (GET, POST, PUT or DELETE; anything else stops with an error) |
programs with a personal access token, or a page of the app with the session |
Route::capabilities($prefix = '/capabilities') |
GET {prefix} (what the caller may call, with the input schemas and the session's CSRF token) and POST {prefix}/{name}
|
the JS client of the Vue apps and the SPA |
Route::mcp($route = '/mcp') |
POST {route} (one JSON-RPC message per request); GET and DELETE answer 405
|
AI agents, with a personal access token only |
Route::notifications() |
GET /notify/confirm/{token}, GET and POST /notify/unsubscribe
|
the links of notification emails (2.15.0) |
Route::bots() |
POST /__bots__/{appName}/communicate, POST /__bots__/{appName}/{appId}, DELETE /__bots__/{appName}
|
the Watson bot apps of public/ (2.21.0) |
curl -s http://localhost:8080/api/books/3
# {"data":{"id":3,"title":"Kindred","published_year":1979,...}} 200: get_book is public
curl -s -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-d '{"title": "X", "published_year": 1971, "author_id": 42}' http://localhost:8080/api/books
# {"error":{"code":"invalid","message":"Some of the inputs are invalid","fields":{"author_id":"There is no such author"}}} 422-
Requests: a JSON body, form fields, or the query string for
GET. Then the route variables, which win (/books/3?id=9givesid = 3, 2.21.2). -
Responses:
200with{"data": ...},201when aPOSTchanges data, or{"error": {"code", "message", "fields"}}with401(andWWW-Authenticate: Bearer),403,404,409or422. -
Who calls:
Authorization: Bearer <token>, or the session of a signed-in person. A session call that changes data needs theX-CSRF-Tokenheader (403"Missing or invalid CSRF token" otherwise). - A
POSTwith neither a token norAccept: application/jsonis taken for a form and answers403[ERROR 3000]. - Files: a
filerule takes the uploaded file (File uploads).
docker compose exec dev-box php caligrafer.php client public/js/capabilities.jswrites one JavaScript function per capability, calling /capabilities. In a page of the app it uses the session (the
cookie, and the CSRF token it reads once from GET /capabilities); from Node or another site, a token:
configure({ baseUrl: 'http://localhost:8080', token: process.env.CALIGRAFY_TOKEN }).
GET /capabilities answers {"data": [{"name", "description", "scope", "level", "readOnly", "destructive", "public", "input"}, ...], "csrf": "<the session's CSRF token>"}. More:
The JS client.
Every capability the token allows is a tool, with its description, its input schema and the hints readOnlyHint and
destructiveHint. A refused call is a tool result with "isError": true, so the agent can correct itself.
claude mcp add --transport http bookshelf http://localhost:8080/mcp --header "Authorization: Bearer cq_..."Without a token it answers 401 with WWW-Authenticate (2.19.1). MCP addresses skip the form CSRF check: the token is
the proof. More: MCP.
-
Route::capability(),Route::capabilities(),Route::mcp(),Route::notifications(),Route::bots() -
Caligrafy\Rest,Caligrafy\Client,Caligrafy\Mcp\Http,Caligrafy\Mcp\Server
-
A JSON write with the session and no
X-CSRF-Token:403. The JS client sends it;fetch()by hand must too. -
Data work in a closure route (
Route::post('/x', function () {...})): it has no permission rules and no CSRF check for JSON calls. UseRoute::capability. -
MCP with the legacy API key: refused (
401); agents use personal access tokens. -
A
webOnlycapability behind these routes: it answers "There is no capability"; onlyRoute::actionreaches it. -
A webhook's capability is served by these routes too: guard it with
$context->actorType() !== 'webhook'(Webhooks).
Caligrafy: Describe it. Approve it. Ship it. Your idea turned into a complete app: every feature built once, for web pages, Vue apps, a REST API and AI agents, and online.
Why Caligrafy: the benefits, the measurements, and what it is not yet
-
Quickstart with Claude: describe it, approve it, ship it
or the Quickstart: the same three steps by hand, in 5 minutes
- How Caligrafy works: the ideas in 5 minutes
- Your first capability: build a feature, page included
-
Capabilities in depth
-
#[Capability]: every option - Scheduled jobs: capabilities on a timer
- Notifications: web push and email
- Background jobs: capabilities run later
- Migrations: database changes, in order
- The cache
-
Testing your app
- Dependency injection
- Validation rules
-
Web pages
- Views and page components
- Design styles: another look, one setting
- Declarative routes: every request, in one line
-
Route::page·Route::form·Route::auth·Route::webhook·Route::download·Route::redirect·Route::action·Route::snap· capability routes - How to export a list as CSV
- Controllers: moving one to a declared route · how to, step by step
- Forms and helpers
-
Vue apps
-
Mobile and tablet
-
REST API
-
AI agents
-
Snap Cards and the assistant
- Payments: Stripe and crypto
- Face detection and face login
- OpenAI: chat and images · Assistants
- Open models: Llama, Mistral...
- Chatbots with Watson
- Machine learning in the browser
- Metadata and social cards · search and analytics
-
Putting it online: the production image, the database at every start
-
API reference: every class and method
-
Routing ·
Route·AuthRoutes· pages ·Controller· capabilities -
Data ·
Model·Query· sign-in · jobs ·Notify· payments · AI -
Interfaces · Snap ·
Snap\Cards·Snap\Conversation· testing · tooling · helper functions
-
Routing ·
-
Caligrafer: every command
- Project · build · data · agents · jobs and tests · maintenance · deploy
- Installation, configuration and Caligrafer
- Routing · Request · Controllers · Views
- Vue: flow of information · routes · validations
- Security and upgrading
- Release notes: 2.26 · 2.25 · 2.24 · 2.23 · 2.22 · 2.21 · 2.20 · 2.19 · 2.18 · 2.17 · 2.16 · 2.15 · 2.14 · 2.13 · 2.12 · 2.11 · 2.10 · 2.9 · 2.8 · 2.7 · 2.6 · 2.5 · 2.4 · 2.3 · 2.1 · October 2026 · changelog
- Testing Caligrafy (contributors)