docs/api-reference.md
API Reference
HTTP endpoints, auth, MCP transport, and operation dispatch.
agent-fs exposes a single HTTP API. All file operations go through one dispatch endpoint.
Base URL
http://localhost:7433Authentication
All endpoints (except /health and /auth/register) require a Bearer token:
Authorization: Bearer <api-key>Get an API key by registering:
curl -X POST http://localhost:7433/auth/register \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com"}'Endpoints
GET /health
Health check. No auth required.
curl http://localhost:7433/health# {"ok":true,"version":"0.1.1"}POST /auth/register
Register a new user. Returns user ID, org ID, drive ID, and API key. The key is shown only once — it isn't stored anywhere but the user's own config.json.
curl -X POST http://localhost:7433/auth/register \ -H "Content-Type: application/json" \ -d '{"email": "agent@example.com"}'Set AGENT_FS_REGISTER_ALLOWED_DOMAINS (comma-separated, e.g. acme.com,acme.io) to restrict sign-ups by email domain. The domain after the last @ must exactly match an entry, case-insensitive. Other domains get 403 {"error":"REGISTRATION_DOMAIN_NOT_ALLOWED"}; an email with no domain gets 400 {"error":"VALIDATION_ERROR"}. Unset means open registration.
agent-fs auth reset-key rotates your own key while you still hold the current one — it calls /auth/reset-key, which sits behind the same auth middleware as every other endpoint, so it cannot help if the key is genuinely lost. Recovering a lost key requires an org admin to run agent-fs member reset-key <email> on the locked-out user's behalf. Either path invalidates the old key immediately.
GET /auth/me
Get current user info with default org/drive context.
curl http://localhost:7433/auth/me \ -H "Authorization: Bearer <api-key>"# {"userId":"...","email":"...","defaultOrgId":"...","defaultDriveId":"..."}ALL /mcp
MCP endpoint (Streamable HTTP transport). Accepts JSON-RPC requests from MCP clients. Stateless — each request creates a fresh MCP server instance.
curl -X POST http://localhost:7433/mcp \ -H "Authorization: Bearer <api-key>" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'In practice, use agent-fs mcp (stdio proxy) rather than calling /mcp directly. The proxy handles the MCP lifecycle (initialize, tools/list, tool calls) automatically.
POST /orgs/{orgId}/ops
Dispatch any file operation. The op field determines which operation runs.
curl -X POST http://localhost:7433/orgs/<orgId>/ops \ -H "Authorization: Bearer <api-key>" \ -H "Content-Type: application/json" \ -d '{"op": "write", "path": "/hello.md", "content": "# Hello"}'An optional driveId field targets a specific drive. The drive must belong to `{orgId}` — a driveId from another org returns 404 NOT_FOUND, the same response as a nonexistent drive, so drive IDs cannot be probed across tenants. Each op requires a minimum drive role (see Access control).
See the OpenAPI spec for the full schema of each operation, or browse it interactively:
- Live endpoint:
GET /docs/openapi.json(when server is running) - Static file: `docs/openapi.json` (committed to repo)
Import either into Swagger Editor or Scalar for interactive exploration.
Raw file bytes
Use the raw file route for binary-safe uploads and downloads:
curl -X PUT http://localhost:7433/orgs/<orgId>/drives/<driveId>/files/assets/logo.png/raw \ -H "Authorization: Bearer <api-key>" \ -H "Content-Type: application/octet-stream" \ --data-binary @logo.png
curl http://localhost:7433/orgs/<orgId>/drives/<driveId>/files/assets/logo.png/raw \ -H "Authorization: Bearer <api-key>" \ -o logo.pngThe raw route preserves bytes exactly. Text indexing runs only for valid, indexable UTF-8 payloads.
PUT /raw requires the editor role (or better) on the target drive — viewers get 403 PERMISSION_DENIED, matching the JSON write op. GET /raw is viewer-accessible. As with the ops route, the driveId in the path must belong to the orgId in the path; mismatches return 404.
Access control
All routes authenticate via API key and authorize against explicit memberships:
- Strict drive membership — a drive is only visible and accessible to users with an explicit drive membership row. Creating a drive grants the creator an admin membership automatically.
- Per-op role gates — read ops (
ls,cat,search,signed-url, ...) requireviewer; write ops (write,edit,append,rm,mv,cp,revert,comment-add, ...) requireeditor;reindexrequiresadmin. - Member management is admin-only — inviting, listing, updating, and removing org members requires org
admin. Drive member routes require driveadminor admin of the owning org. - No existence oracle — requests that reference an org or drive you have no access to return
404, identical to the response for IDs that don't exist. - Scoped comment IDs — comment IDs only resolve within the org/drive context they were created in; cross-tenant IDs return
404.
Signed URLs are bearer secrets
The signed-url op is viewer-accessible and RBAC is checked only at generation time. The returned URL is a presigned S3 URL: it requires no authentication and grants download access to anyone who has it until it expires (default 24h, max 7 days). Treat signed URLs like bearer tokens — don't log them, don't post them anywhere you wouldn't post a credential, and use the shortest expiry that works (expiresIn).
Share links are bearer secrets
share-create mints a public GET /share/{token} page on the API host. The token (256 random bits) is the only credential: anyone who has the link can open it until it expires (default 24h, max 7 days), is revoked with share-revoke, or reaches maxViews. Only the SHA-256 of the token is stored. The response's url is absolute when the server knows its address, and urlSource says how it was built: "configured" from AGENT_FS_PUBLIC_URL (hand out url as is), "request" from the request's Host (build <the address you reached the API at> + sharePath). The page is server-rendered under a strict Content-Security-Policy (nosniff, no-store) that runs only the page's own two inline scripts, allowed by hash, plus, when a markdown file needs them, version-pinned mermaid, KaTeX and highlight.js files loaded with Subresource Integrity; markdown is rendered with raw HTML and images stripped (frontmatter card, table of contents, callouts, footnotes, math and mermaid diagrams, light/dark theme), SVG files are download-only, and the page shows no API keys, org names or member details. /share/* sits before authentication and has its own per-IP rate limit (AGENT_FS_SHARE_RATE_LIMIT, default 120/min). Expired, revoked, used-up and unknown links all render the same "link expired" page. See the Sharing row below.
HTML files. A shared .html or .htm file renders in a sandboxed <iframe> on its page, with an "Open full page" link. Both load GET /share/{token}/raw, which always proxies the page (never a presigned redirect, at most 25 MB, else 413) with Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-modals; frame-ancestors 'self'. The page runs in an opaque origin. Only that file is shared: relative assets next to it return 404. Share the folder (a site share, below) when a page needs its assets. Download still sends application/octet-stream.
Site shares. share-create on a folder returns kind: "site" and a /site/{token}/ URL instead of a /share/{token} page. GET /site/{token}/{path} serves the folder's files as they are, so a published HTML page loads its relative CSS, scripts, images and fetch calls. An empty path or a trailing slash serves index.html. A folder with no index.html gets a 302 to its only .html/.htm file, or else an HTML listing of its HTML pages (names escaped, relative links). Other files and sub-folders are never listed. A folder with no HTML page returns 404. A sub-folder without its trailing slash gets a 301 to {path}/ when that serves a page. The path is decoded once and a ., .. or NUL segment is rejected (400 or 404), so a request never reaches outside the shared folder. Every response is proxied by the daemon (no presigned redirects, at most 25 MB per file, else 413) and carries Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-modals; frame-ancestors *. With no allow-same-origin the page runs in an opaque origin: it has no localStorage, sessionStorage or cookies and cannot read the API host's storage. External fonts and CDNs still load. Responses also send Referrer-Policy: no-referrer, X-Content-Type-Options: nosniff, X-Robots-Tag: noindex, Cache-Control: no-store and Access-Control-Allow-Origin: *, and OPTIONS answers CORS preflights. A page can read every file under its folder while the link is valid, so do not publish a folder that also holds secrets. Site shares have no view limits (maxViews is rejected for folders) and record no share_viewed events; they end on expiry or share-revoke (path accepts the folder), after which they return 410. GET /share/{token} with a site token redirects to /site/{token}/. /site/* sits before authentication with its own per-IP rate limit (AGENT_FS_SITE_RATE_LIMIT, default 600/min, because one page load makes many requests). Servers that support site shares list html-sites in the /health features array.
Comments on HTML pages. With ?__afs_comments=1, GET /site/{token}/{path} appends a small script to a text/html file. The web app uses it to comment on the rendered page: the script reports the page text and the reader's selection to the parent window over postMessage and paints the highlights the parent sends back. It holds no credentials and receives no comment text, and it does nothing unless a parent window answers its handshake. Headers, the CSP and the sandbox are unchanged, and every other response is the stored bytes as they are. Comments are stored as usual, with a quote and, when the text is in the source, its lines. Servers that support this list html-comments in the /health features array.
Operations
All 26 operations are dispatched through POST /orgs/{orgId}/ops. Each expects {"op": "<name>", ...params}.
| Category | Operations |
|---|---|
| Content | write, cat, edit, append, tail |
| Navigation | ls, stat, tree, glob, find |
| File Management | rm, mv, cp |
| Version Control | log, diff, revert |
| Search | grep, fts, search |
| Maintenance | recent, reindex |
| Comments | comment-add, comment-list (supports pathPrefix), comment-get, comment-update, comment-delete, comment-resolve |
| Drive Members | drive-members |
| Favorites | favorite-add, favorite-remove, favorite-list (per user, see below) |
| Sharing | signed-url, share-create (a file gets a /share/{token} page, a folder gets a /site/{token}/ HTML site), share-revoke |
Favorites. Each user stars files and folders for themselves: favorite-add {path}, favorite-remove {path} and favorite-list {}. The user is always the one the API key belongs to; the ops take no user id, and one user never sees another's favorites. Favorites are kept per drive and need viewer role. favorite-add returns 404 for a path that does not exist. Moving or renaming a file moves every user's star with it, deleting a file removes the stars on it, and a folder's star is removed once the folder is gone. Servers that support favorites list favorites in the /health features array.
For parameter details, see the OpenAPI spec.