MCP Server
Connect external MCP clients to DorkOS, and render MCP Apps from other servers inline in chat
MCP Server
DorkOS exposes an external MCP server at /mcp so agents running outside DorkOS, standalone Claude Code, Cursor, Windsurf, or any MCP-compatible client, can call the same tools and read the same data DorkOS's own agents use. Separately, DorkOS also acts as an MCP host: when a tool from any MCP server returns an interactive ui:// app (the MCP Apps extension, SEP-1865), DorkOS renders it inline in the chat, sandboxed.
These are two directions of the same protocol, and fittingly symmetric ones: DorkOS as a server other agents connect to, and DorkOS as a host that renders apps other servers ship. This page covers both.
Quick win: flip on Settings → Tools → External MCP Server, copy the Claude Code snippet it
generates, and paste it into ~/.claude.json. That's the whole setup for the common case. The
rest of this page is the reference.
Connecting to the DorkOS MCP server
The endpoint is /mcp on your DorkOS server (http://localhost:4242/mcp by default). It speaks Streamable HTTP and is stateless: every request gets a fresh server instance. There's no session to keep alive and no GET/DELETE lifecycle to manage (both return 405).
Manage it from Settings → Tools → External MCP Server: an enable/disable toggle, the live endpoint URL, current auth mode, rate limiting, and copy-pasteable setup snippets for Claude Code, the Claude Code CLI, Cursor, and Windsurf.
A screenshot of the Settings → Tools → External MCP Server card would help here (toggle, endpoint URL, auth mode, rate limit, and snippets in one view). Flagged for the media pipeline.
Don't add this to agents already running inside DorkOS. DorkOS gives its own managed agents these tools internally. Configuring the external MCP endpoint for one of those same agents registers every tool twice: the Anthropic API rejects the duplicate names with an HTTP 400 ("Tool names must be unique") and every tool call fails. External MCP access is for agents running outside DorkOS.
Authentication
How you authenticate depends on whether you've turned on login.
Login off (the default). DorkOS protects the tools that change things on your machine with a local token it makes for you. You paste this token into your MCP client once, as a Bearer header. Read-only checks (listing tools, ping, get_server_info) still work with no token, so a quick health probe needs no setup:
# Read-only: no token needed
curl -X POST http://localhost:4242/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'Any tool that changes state (creating an agent, sending a Relay message, installing from the marketplace) needs the token. Without it, the call comes back with a 401 that tells you where to find the token:
# A tool that changes things: token required
curl -X POST http://localhost:4242/mcp \
-H "Authorization: Bearer dork_mcp_local_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_agent","arguments":{}}}'Find your token in Settings → Tools → External MCP Server: click Reveal token, and the setup snippets fill in with it so you can copy one and paste it straight into your client. It also lives in a file called mcp-local-token in your DorkOS data folder (~/.dork/ by default). Rotating the token from Settings makes a new one and turns off the old one right away, so any client you already set up stops working until you paste in the new token.
One honest note on what this protects: it stops tokenless calls, keeps the token out of reach of anything that can't open a local connection, and guards the endpoint anywhere the port is reachable from outside (a tunnel or proxy). While login is off, though, a program running on your computer can still ask DorkOS for the token the same way the app does. If that matters for your machine, turn on login — that is the boundary that closes it.
A bare GET to /mcp always returns 405, with or without a token. To confirm auth is working,
POST a real JSON-RPC request like the ones above.
Login on. Once you turn on login, MCP clients authenticate with a personal API key instead, created and revoked from Settings → Security → API keys and sent as a Bearer token. The local token steps aside; per-user keys take over. See Securing Your Instance for the full flow.
Headless deployments (CI, servers no one signs into): set the MCP_API_KEY environment variable. It's a static override that every client uses as its Bearer token; it takes priority over both the local token and per-user keys, and it can't be revoked from the UI.
Client configuration
Example for Claude Code's ~/.claude.json (the same shape Settings generates for you, with your key filled in):
{
"mcpServers": {
"dorkos-external": {
"type": "http",
"url": "http://localhost:4242/mcp",
"headers": { "Authorization": "Bearer dork_..." }
}
}
}Or register it from the CLI:
claude mcp add-json dorkos-external '{"type":"http","url":"http://localhost:4242/mcp","headers":{"Authorization":"Bearer dork_..."}}'Cursor and Windsurf use the same url + headers shape under their own config file (mcp.json, mcp_config.json). See Settings for the exact snippet.
Rate limiting is on by default (60 requests per 60-second window) and configurable from the same settings card. Changes take effect after a server restart.
What it exposes
Connecting gives a client DorkOS's full external tool surface: 48 tools in total, covering sessions, Tasks, Relay messaging, agent bindings, Mesh discovery, agent/extension management, and the marketplace. Six read-only resources round it out, for browsing sessions, agents, and skills without a tool round-trip.
| Domain | Tools | Examples |
|---|---|---|
| Core | 4 | ping, get_server_info, get_session_count, get_agent |
| Tasks | 5 | tasks_list, tasks_create, tasks_update, tasks_delete, tasks_get_run_history |
| Relay (messaging) | 13 | relay_send, relay_inbox, relay_send_and_wait, relay_list_adapters, relay_get_metrics |
| Bindings | 3 | binding_list, binding_create, binding_delete |
| Mesh (discovery) | 8 | mesh_discover, mesh_register, mesh_list, mesh_status, mesh_query_topology |
| Agents & extensions | 7 | create_agent, list_extensions, create_extension, test_extension |
| Marketplace | 8 | marketplace_search, marketplace_install, marketplace_uninstall, marketplace_create_package |
The 8 marketplace_* tools only appear when Relay is enabled and extensions have initialized
successfully. Without that, the marketplace branch is silently skipped and the server still boots
with everything else. marketplace_install and marketplace_uninstall require an out-of-band
confirmation resolved in the DorkOS UI before they complete, the same as installing from the
Marketplace tab.
Every tool's full name, description, and input schema is visible from any connected MCP client's tool list. This table is a map of the territory, not the source of truth.
Tool annotations & structured output
Every tool is registered with MCP tool annotations: readOnlyHint, destructiveHint, idempotentHint, and openWorldHint. A client can reason about what a tool does before calling it:
readOnlyHint: the tool only reads state; it never mutates anything.destructiveHint: the tool deletes or unregisters a resource.idempotentHint: calling it again with the same input converges rather than compounding (a secondmesh_registercall for the same path still creates a new agent record, so registration is not idempotent; a secondmesh_denyfor the same path is).openWorldHint: the tool reaches an external system (a configured marketplace source, an external adapter) rather than only local DorkOS state.
Most tools return plain text content, matching the underlying handler. A handful of read/list tools also declare a Zod outputSchema and return matching structuredContent, so a client can parse the result directly instead of scraping text:
get_agentmesh_listmesh_statusmesh_inspectmesh_query_topologyrelay_get_metricstasks_list
Resources
Alongside tools, the server registers six read-only dorkos:// resources: three list views and three id-scoped templates.
| URI | Kind | Returns |
|---|---|---|
dorkos://sessions | List | Session metadata (id, title, runtime, cwd, updatedAt) across every registered runtime, with per-runtime degradation warnings. No transcript content. |
dorkos://sessions/{id} | Template | Metadata for one session by id. |
dorkos://agents | List | Every agent manifest registered with Mesh, the same data mesh_list returns. |
dorkos://agents/{id} | Template | A single agent manifest (.dork/agent.json content) by agent ULID. |
dorkos://skills | List | Authored skills under the server's default project's .agents/skills: name, description, kind only. |
dorkos://skills/{name} | Template | Full SKILL.md frontmatter and body for one skill, by name. |
Session and skill resources are scoped to the server's default working directory; the external surface has no per-request cwd parameter to key on. Both list resources are the complete enumeration, so the item templates aren't re-listed separately. A client reads them once it already has an id or name.
The server is stateless per request (a fresh instance is built for every /mcp call), so it never
advertises resources/listChanged or supports resources/subscribe. There's no long-lived
connection to push a notification down.
MCP Apps host
Separately from the tools above, DorkOS can act as an MCP Apps host, which is a fancier way of saying: when a tool from any connected MCP server returns a ui:// app reference, DorkOS renders that app inline in the chat as a live, sandboxed panel. No configuration needed beyond having the MCP server connected to your session.
Works with Claude Code sessions today. The Codex and OpenCode runtimes don't surface MCP Apps yet. Sessions on those runtimes never show the renderer, regardless of what the connected MCP server ships.
How it renders
The first app from a given MCP server shows a consent card before anything renders:
Interactive app provided by
<server name>. This runs sandboxed code from the MCP server. It cannot access your session, files, or credentials. Render it?
A screenshot of this consent card, plus a rendered app's fullscreen control, would make the behavior obvious at a glance. Flagged for the media pipeline.
Clicking Render app remembers that choice for the server (stored per-server, so a trusted server never asks twice) and renders the app inline in the message. Leaving the card alone simply skips the app for now. A fullscreen control on a rendered app pops it out to the full canvas for apps that need more room. A pop out control keeps a live app in view while you move around DorkOS: on a computer it floats in a small window that stays on top, and on a phone it docks to a bottom sheet you can drag up to use, or drag down to tuck into a small bar at the bottom; tap the bar to bring it back. These are all backed by the same sandboxed frame.
Security model
Apps run in a strict-sandbox iframe (sandbox="allow-scripts", deliberately never allow-same-origin), so they get an opaque origin and can only talk to DorkOS through a validated postMessage bridge:
- No access to your session, files, cookies, or credentials. The opaque origin can't reach host storage or the parent DOM.
- A Content-Security-Policy is enforced: the app's own declared policy if it ships one, otherwise a locked-down default (
default-src 'none', inline scripts/styles from'self'only, no network, no framing). - External links go through the same confirmation dialog used everywhere else in DorkOS before a link opens in a new tab.
- Apps cannot run tools. A
tools/callrequest from an app is refused with a spec-compliant JSON-RPC error: v1 is render-only. - Feature permissions (camera, microphone, geolocation, clipboard) are opt-in per app, granted only for what the app explicitly declared, and otherwise absent from the frame entirely.
The app's HTML itself is never trusted from the tool-call text the model sees. DorkOS's server opens its own short-lived MCP connection to fetch the resource directly by URI, recovering the correct mime type, CSP, and permissions before anything reaches the iframe. That fetch is exposed as its own endpoint; see the API reference for the request/response shape.