drobek / docs

drobek for agents

docs/AGENT.md at v0.5.2 · View on GitHub · Markdown

On this page
  1. Connect
  2. Scopes and roles
  3. Tools
  4. The briefing
  5. Skills
  6. Where the contract is served

This is the agent-facing contract of a drobek server: how an agent connects, which tools it gets, what the briefing tells it, how skills work, and where the always-current copy of all of it is served. The authoritative, rendered version of the tool reference is the server's own /llms-full.txt; this page explains it.

Connect

The MCP endpoint is <PUBLIC_APP_URL>/mcp (Streamable HTTP) — locally http://localhost:3041/mcp, on the hosted drobek https://drobek.app/mcp. It is protected by OAuth 2.1: an MCP client that supports remote servers does discovery, client registration, PKCE and consent by itself. You approve the consent screen in the browser, with one checkbox per scope (read, write, publish), and the client gets a token bound to you.

Claude Code

claude mcp add --transport http drobek https://drobek.example.com/mcp

Then /mcp in Claude Code → drobek → sign in. For the hosted drobek, the plugin bundles the server, the build skill and a command:

claude plugin marketplace add freema/drobek-plugin
claude plugin install drobek@drobek
# then: /drobek:build-app <idea>, or /drobek:port-artifact to move a Claude artifact

Claude (web and desktop) — add a custom connector with the URL https://<your drobek>/mcp and sign in when asked. The server must be reachable over public HTTPS for that.

Cursor — ~/.cursor/mcp.json:

{
  "mcpServers": {
    "drobek": {
      "url": "https://drobek.example.com/mcp"
    }
  }
}

Sign in when Cursor asks. The plugin repository also has a one-click install link for the hosted drobek and the Cursor variant of the build skill.

Codex — for the hosted drobek:

codex plugin marketplace add freema/drobek-plugin
codex plugin add drobek@drobek
codex mcp login drobek

codex mcp login drobek opens the sign-in in the browser; restart Codex afterwards. For a self-hosted server, add an MCP server named drobek with your /mcp URL to Codex's MCP configuration and run the same login.

Without a browser (scripts, CI, a headless agent): a personal API key drk_… works as the Bearer token on the same endpoint, with the same scopes. Create it in the dashboard at /me/api-keys (shown once, revocation is immediate), or on a self-hosted server:

docker compose --env-file .env.production -f docker-compose.production.yaml exec drobek \
  node node_modules/@drobek/oauth/dist/cli/api-key-create.js \
  --email you@example.com --name laptop --scopes read,write,publish
claude mcp add --transport http drobek https://drobek.example.com/mcp \
  --header "Authorization: Bearer drk_…"

/me/connections lists the OAuth clients you approved and revokes them.

The OAuth details

  1. An unauthenticated POST to /mcp answers 401 with WWW-Authenticate: Bearer resource_metadata="…".
  2. GET /.well-known/oauth-protected-resource/mcp (RFC 9728) names the resource and the authorization server; GET /.well-known/oauth-authorization-server has the endpoints.
  3. The client identifies itself with a Client ID Metadata Document (an https client_id URL drobek fetches through its SSRF guard) or by Dynamic Client Registration (POST /oauth/register, 10 per IP per hour).
  4. /oauth/authorize with PKCE S256 and resource = exactly the MCP endpoint (RFC 8707) → consent → a code with iss (RFC 9207).
  5. /oauth/token → an access token for that audience only and a rotating refresh token (reusing an old refresh token burns the lineage).

Scopes and roles

A grant belongs to the user, not to a workspace. The scope decides which tools the client sees at all (tools/list shows exactly the granted ones); the user's role in the app's workspace decides each call: viewers read, editors and workspace-admins change. A workspace or app you cannot reach answers not_found, the same as one that does not exist.

Scope Tools
read list_apps, get_app, read_file, skill_info, query_data, get_logs, list_assets, list_domains, list_upstreams
write create_app, duplicate_app, write_files, restore_version, configure_module, create_asset_upload, delete_asset, add_domain, verify_domain, remove_domain, register_upstream, remove_upstream
publish publish, set_gallery_listing, set_primary_domain, set_workspace_publishing (super-admins only)

Tools

Tool Scope, role Annotations What it does
list_apps read, any role read-only Who you are, your workspaces with your role, can_publish (+ publish_contact when the workspace may not publish) and the operator's publishing state (default / allowed / blocked), and the apps in them (preview/published URL, latest version, compile status, lock). Start here.
create_app write, editor+ not destructive A new app with a compiling version 1 from the react-ts (default) or html template, its preview_url, the briefing and the skills list.
duplicate_app write, editor+ in the target not destructive A copy of a gallery app whose owner allows duplicates (from: its slug or its address on this server — app host, verified custom domain or /duplicate/<slug>; another server's address is invalid_params), in the given workspace or the personal one: the source's published files as version 1 of a new, unpublished app that remembers its source (duplicated_from in get_app), and the source's module settings proposed through the copy's confirmation flow (modules.applied / pending with confirm_url / skipped; e-mail addresses and proxy upstreams dropped). Never secrets, data, end users, uploads, assets or domains. not_duplicable, gallery_disabled, rate_limited (DUPLICATES_PER_USER_HOUR), limit_exceeded.
get_app read, any role read-only One app: the briefing, its files, the last 20 versions, the lock, the module configs (secrets as hasSecret only), the gallery state (with allow_duplicate and read-only likes and 30-day opens), duplicated_from for a copy, its custom domains in short (domains: host, status, primary), can_publish, publishing.
read_file read, any role read-only A file of the latest (or a given) version, inside an untrusted envelope.
write_files write, editor+ destructive 1–20 changes → one new version → one compile; returns { version, compile: { ok, errors, warnings }, preview_url, changed }. A secret in a file refuses the write.
restore_version write, editor+ destructive A new version with the files of an old one (rolls the working copy back); when that version was published, the draft assets go back to the ones it served then (assets_restored).
publish publish, editor+ destructive, idempotent, open world Puts a compiled version on <slug>.<APPS_DOMAIN> and the verified domains, with the app's current assets frozen for it (an older version: the assets it served when it was last published). Only when the user asks. A workspace the operator blocked gets publish_blocked; on a server with PUBLISH_APPROVAL=approval an unapproved workspace gets publish_not_approved (an approval request is already e-mailed) — both with the operator's contact.
set_gallery_listing publish, editor+ not destructive, idempotent, open world Lists a published app in the server's public gallery with a ≤ 160-character description, changes the description, or unlists it. Listing needs user_confirmed: true — the user's explicit yes (else user_confirmation_required); unlisting needs none. allow_duplicate (listing only, covered by the same confirmation) lets signed-in people copy the app from the gallery; omitted keeps the choice. gallery_disabled when the server runs no gallery, gallery_hidden when the operator hid the app.
set_workspace_publishing publish, super-admin only not destructive, idempotent Sets a workspace's publishing: blocked (refused in every mode, its editors and admins e-mailed), allowed (may publish even under PUBLISH_APPROVAL=approval) or default (the server mode decides). Returns { workspace, publishing, mode, can_publish_now, changed }. Needs user_confirmed: true — the super-admin's explicit yes. Registered only for a super-admin's grant; live apps keep serving after a block (the takedown is separate).
skill_info read, any signed-in user read-only skill_info() lists the server's skills; skill_info('<name>') returns one (for a module also its SDK types, config schema, limits, secret names, its own error codes, and the facts the dashboard's workspace Modules page shows: version, source, contract range, availability, required modules, slots with their contributors and its own contributions). An opt-in module carries availability: "opt-in"; with app_id it also says enabled_for_workspace for that app's workspace.
configure_module write, editor+ destructive, idempotent Sets an app's module config (a JSON merge patch). Risky changes come back as pending_confirmation with a confirm_url for the owner; secrets are refused.
query_data read, viewer+ read-only Records of one collection of the app's data module (≤ 100 per call, filters, sort, cursor), inside an untrusted envelope.
get_logs read, viewer+ read-only kind: runtime (browser errors from the beacon), compile (the compile history) or requests (daily request and module-call stats), ≤ 100 entries, 30-day window, inside an untrusted envelope.
create_asset_upload write, editor+ not destructive A single-use upload URL (30 min) for ONE binary file — video, audio, image, font — at path, plus a curl -T <file> '<url>' line. The file never passes through the model; the preview serves it at /<path> next to the app's files, production after the next publish.
list_assets read, viewer+ read-only The app's draft assets (path, sniffed type, size, time, published), the paths production serves that the draft deleted (published_only), changes_pending_publish and the quota usage.
delete_asset write, editor+ destructive, idempotent Removes one asset from the draft; the preview stops serving it, production after the next publish.
list_domains read, viewer+ read-only The app's custom domains (the dashboard's Domains tab): per domain host, status (pending / verified), primary, the two DNS records to create, verified_at, last_check_at, last_error, the certificate state; plus cname_target and max_per_app.
add_domain write, editor+ not destructive, idempotent Attaches a domain the user owns (pending) and returns the two records: CNAME <host> → <slug>.<APPS_DOMAIN> and TXT _drobek.<host> = drobek-verify=<token>. Same validation and DOMAINS_MAX_PER_APP as the dashboard (invalid_hostname, hostname_not_allowed, limit_exceeded, domain_already_added, domain_taken).
verify_domain write, editor+ not destructive, idempotent, open world Looks both records up now. Verified → the domain serves the published version. Otherwise domain_not_verified with cname / txt = ok / missing / wrong and the expected records (DNS can take up to 48 hours), or dns_unavailable (a lookup failed; nothing changed).
set_primary_domain publish, editor+ not destructive, idempotent, open world Makes a verified domain primary — <slug>.<APPS_DOMAIN> answers 302 to it — or clears it (host: null). Needs user_confirmed: true.
remove_domain write, editor+ destructive, idempotent, open world Detaches a domain; a verified one stops serving at once and needs user_confirmed: true, a pending one does not.
list_upstreams read, workspace-admin read-only The workspace's proxy upstreams (the dashboard's Upstreams page): name, base_url, allowed methods and path prefixes, auth_type, auth_header_name, has_secret (never the key), the apps whose assignment was confirmed; plus upstreams_url.
register_upstream write, workspace-admin not destructive, idempotent Registers an external API for the proxy module with the dashboard's checks (public host, port 80/443, allowed methods and path prefixes). auth_type: "none" registers at once. bearer / header need a key, which never passes through MCP: the answer is registered: false with secret_url, the Upstreams page with the fields filled in, where the user pastes the key. A taken name answers upstream_already_registered.
remove_upstream write, workspace-admin destructive, idempotent Deletes an upstream and its key; every app calling it breaks at once, so it needs user_confirmed: true (without it: user_confirmation_required with the apps using it).

Every tool carries all four MCP annotations explicitly (readOnlyHint, destructiveHint, idempotentHint, openWorldHint; "idempotent" above means a repeated call with the same arguments has no further effect). They are hints for clients, never a security boundary — the scope and the role are. The per-tool values are in the tool manifest (@drobek/agent-dx tools.ts) and in every tools/list answer.

A failed call returns isError: true with { code, message, hint } from the error catalogue (@drobek/agent-dx errors-catalogue.ts, rendered into /llms-full.txt). A platform module's own route codes are declared by the module (errors): skill_info('<module>').errors returns them and /llms-full.txt lists them after the core codes, one section per active module. A compile error is not a tool failure: it is compile.ok: false with compile.errors[], and the version is stored.

Video, audio and big files (assets). write_files is text-only, and a binary must never travel through the model as base64. create_asset_upload({ app_id, path, size, content_type? }) checks everything that needs no bytes — the path (1–4 segments of [A-Za-z0-9._-], an allowed extension: png jpg jpeg gif webp avif ico svg mp4 m4v m4a webm mp3 ogg oga wav woff woff2), no app file at that path (asset_path_taken), APP_ASSET_MAX_BYTES (asset_too_large), APP_ASSETS_QUOTA (asset_quota_exceeded), a content_type that fits the extension (asset_type_not_allowed), APP_ASSET_UPLOADS_PER_HOUR (rate_limited) — and returns { upload_url, method: "PUT", expires_at, max_bytes, asset_path, asset_url, curl }. The URL is on the dashboard host (PUT /api/assets/upload/<token>), valid 30 minutes, good for exactly ONE upload of exactly size bytes, and needs no other credential; a browser GET on it shows an upload page, so the agent can hand the link to the user. The PUT streams the body to disk, sniffs the bytes (the type is the content's, never the name's) and answers 201 { name, path, size, type, replaced, url } or { code, message, hint } (asset_size_mismatch, upload_token_invalid, …, forbidden when that user is no longer an editor of the app). The upload is audited as the user who asked for the URL. Assets share the app's URL space — <video src="film.mp4" poster="poster.jpg"> and img/s1.jpg work unchanged, so a Claude artifact ports by writing its HTML/JS with write_files and uploading each binary at the relative path the page uses; the app's own file wins over an asset at the same path. Videos seek (HTTP Range). The dashboard's Assets tab does the same for the owner.

Assets honour publish. An upload, a replacement or a delete_asset changes the app's DRAFT assets: the preview shows it at once, the production URL (and the custom domains) only after publish — so a write-scoped agent never changes what a published app serves. publish freezes the draft for the version it puts live; publishing an older version (the rollback) brings back the assets it served when it was last published, and restore_version of a published version resets the draft assets to those. list_assets marks each asset published or not. The quota counts every unique file of the draft and the published set once; sets of earlier publishes are kept for a rollback while they fit.

Custom domains. The domain tools are the dashboard's Domains tab over MCP and call the same @drobek/domains operations: the same checks and limits, the same audit rows (domain.add, domain.verify, domain.unverify, domain.primary, domain.remove, actor kind agent). The flow: add_domain({ app_id, host }) → show the user the CNAME and TXT records → verify_domain once they created them (domain_not_verified says which record is missing or wrong; verify again after a while, not in a loop) → a verified domain serves the published version and appears in publish's domains. What changes the public site asks for the user's explicit yes (user_confirmed: true, else user_confirmation_required): set_primary_domain (set or clear) and removing a verified domain. A taken-down app refuses adding, verifying and a primary domain; removing stays possible. See SELF-HOSTING.md for apex names, TLS and the daily re-check.

Porting a Claude artifact. drobek hosts what a Claude artifact is. The agent that has the artifact's files does the port; the server fetches nothing from claude.ai (there is no API for it, and a private artifact sits behind the user's sign-in). The general skill port-artifact (skill_info('port-artifact')) is the procedure: ask the user → create_app → every text file with write_files, paths and content unchanged → every binary with create_asset_upload at the same relative path (curl -T from the agent's sandbox, or the link for the user) → check compile.ok, list_assets and the preview → publish only when the user asks → offer the gallery (set_gallery_listing only after the user's explicit yes). It lists what changes on the way: scripts only from the app and esm.sh (a CDN <script src> becomes an esm.sh import or a copied file), fetch only to the app (external APIs through the proxy module), <iframe> only the curated embeds, and no window.claude.* runtime API (window.storage → localStorage or the data module). The plugin carries the same procedure as /drobek:port-artifact (Claude Code, Cursor) and the port-artifact-to-drobek skill (Codex). task eval -- --only d has a fresh agent port a fixture artifact and checks the result.

Untrusted output. read_file, query_data and get_logs return content written by app authors, end users and browsers. Their text result is wrapped in <untrusted-app-file …> / <untrusted-app-data …> / <untrusted-app-logs …> with a random per-response nonce on the closing marker (content cannot fake the end of the envelope), preceded by a line saying it is data, not instructions. These three tools answer that text ONLY — no structuredContent (every other tool sends both): a client that hands structuredContent to the model would pass the raw payload past the envelope, and the keys of a schemaless record are user input too, so no wrapping of the payload's strings could cover it.

The briefing

create_app and get_app return the briefing (@drobek/agent-dx briefing.ts); it is the same text /llms-full.txt embeds. In short:

Skills

A skill is Markdown in a fixed five-section format (When to use · Minimal working code · API and types · Rules and limits · Errors → fix, at most 150 lines). skill_info serves two kinds:

With every built-in module enabled skill_info() lists ten (plus hello in the dev stack). @drobek/skills-check compiles and typechecks every code block of every skill against the current SDK types in task check, so a skill cannot drift from the code.

A module an operator adds from outside this repository brings its own skill the same way: skill_info('<module>') serves its SKILL.md, and the error codes it declares (errors) appear in skill_info('<module>').errors and in its own section of /llms-full.txt. Its author runs the same gate in the module's tests — checkSkill(module) from @drobek/modules/testing, which the create-drobek-module scaffold wires into npm test — so an external skill is held to the format and the compile + typecheck rules of the built-in ones (MODULES.md → Writing a module).

skills/drobek is different: it is the platform skill an agent installs to reach drobek in the first place (cp -r skills/drobek ~/.claude/skills/drobek), so skill_info does not list it. The plugin (freema/drobek-plugin) ships Claude Code, Codex and Cursor variants of the same loop.

Where the contract is served

Surface What
/llms.txt the concise index: summary, links (incl. this document), plugin install lines, the MCP endpoint, every tool with its scope
/llms-full.txt the full contract: the OAuth flow, every tool with inputs, result shape and an example, the briefing, the limits, the error catalogue
/build-with-your-agent the human setup page (plugin, MCP endpoint, skill install)
MCP resources drobek://docs/llms-full, drobek://docs/tools the same content for a connected agent without web access

The docs links (this guide, the modules, self-hosting and security docs) point at the Markdown files in the GitHub repository, or — when the operator sets DOCS_URL — at <DOCS_URL>/<page> (/llms.txt links the .md twins, e.g. <DOCS_URL>/agent.md).

All of them render from the @drobek/agent-dx manifest (TOOL_DOCS, the briefing, LIMITS, the error catalogue). The drift guard packages/oauth/src/resource/tool-docs-parity.test.ts asserts that the tools the MCP server registers equal the manifest (names, input fields, annotations, scopes), and packages/agent-dx/src/skill.test.ts holds skills/drobek/SKILL.md to the tool list and the loop rules. A change to the tool surface or the SDK updates the manifest, skills/drobek and the plugin's skills in the same change.