MCP client onboarding
The developerz.ai control plane is a plain MCP server at mcp.developerz.ai,
authenticated by a personal access token (PAT). Any MCP-capable client —
Claude Code, Cursor, tesote.ai, or your own script —
can drive it: dispatch a task, watch it run, read the result.
This page is the full external-client walkthrough: mint a token, connect your client, dispatch a task, and check the work. For the full tool surface see the MCP verb reference.
1. Mint a PAT with write:tasks + read:tasks
Sign in to the dashboard (GitHub SSO). The login
sets a dz_session cookie that the REST API accepts as a credential on every
/v1/* route — so your first token is one POST /v1/tokens call away. The
mint route requires write:account, which among dashboard roles only the
account owner holds, so mint from the owner's login.
The dashboard has no tokens screen yet — until one ships, the self-serve path
is the session call itself. From any page on app.developerz.ai, open the
browser console and run:
fetch('https://api.developerz.ai/v1/tokens', {
method: 'POST',
credentials: 'include', // sends the dz_session cookie
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scopes: ['write:tasks', 'read:tasks'] }),
}).then((r) => r.json());
// → { id: "…", token: "dev_pat_…", scopes: ["write:tasks", "read:tasks"] }
The API CORS-allowlists the dashboard origin for credentialed calls, so the
cookie rides along. Prefer the shell? Copy the dz_session value from devtools
→ Application → Cookies — but treat it as what it is: a full-power dashboard
credential at the owner-role ceiling, far stronger than the scoped PAT you're
minting. Pasted inline it lands in your shell history and the process table, so
keep it in an env var and use it once, in a throwaway shell:
read -s DZ_SESSION # paste the cookie value — no echo, nothing in history
curl https://api.developerz.ai/v1/tokens \
-H "Cookie: dz_session=$DZ_SESSION" \
-H "Content-Type: application/json" \
-d '{"scopes":["write:tasks","read:tasks"]}'
unset DZ_SESSION
If the value was ever exposed — shared screen, pasted into a chat or a file —
rotate it: signing out of the dashboard (POST /auth/logout) revokes the
session server-side, and the next sign-in mints a fresh cookie. The PAT you
already minted is an independent credential and keeps working.
A dispatch token needs exactly two scopes:
| Scope | Why |
|---|---|
write:tasks |
Dispatch work (task_create). |
read:tasks |
Watch it (task.list, task.run_status, task.run_tail). |
The plaintext (dev_pat_…) is returned once — copy it into your client's
config immediately. Scopes are subset-of-caller: a PAT can never hold more than
the credential that minted it, and asking for a scope the caller lacks fails
the mint. Omit scopes and the token inherits everything the caller holds
except admin:* and use:* — those land on a token only when explicitly
requested, and only when the caller holds them itself.
Already hold a token with write:account? Mint the scoped one with that bearer
instead of the session — read it into an env var first so it stays out of your
shell history:
read -s DEVELOPERZ_MINT_TOKEN; export DEVELOPERZ_MINT_TOKEN # paste the write:account token
curl https://api.developerz.ai/v1/tokens \
-H "Authorization: Bearer $DEVELOPERZ_MINT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scopes":["write:tasks","read:tasks"]}'
# → 201 { "id": "…", "token": "dev_pat_…", "scopes": ["write:tasks","read:tasks"] }
2. Point your client at mcp.developerz.ai
The endpoint is https://mcp.developerz.ai/v1/mcp with a single
Authorization: Bearer header. The transport is stateless single-request
JSON-RPC — one POST in, one JSON-RPC response out, no SSE stream — so
streamable-HTTP clients interop through the plain JSON responses and the
configs below work as written.
Claude Code
Paste the PAT at the read prompt so it stays out of your shell history, then
register the server. Be clear about what read -s does not cover, though:
claude mcp add has no env indirection — the header value is expanded into
the process argv at exec and stored verbatim, in plaintext, in
~/.claude.json. Same exposure class as the dz_session cookie in step 1,
except this credential is scoped (write:tasks + read:tasks) and revocable.
Treat ~/.claude.json as a live credential file from then on:
read -s DEVELOPERZ_API_KEY; export DEVELOPERZ_API_KEY # paste the dev_pat_… value
claude mcp add --transport http developerz \
https://mcp.developerz.ai/v1/mcp \
--header "Authorization: Bearer $DEVELOPERZ_API_KEY"
The committed .mcp.json route avoids the plaintext copy: it expands
${DEVELOPERZ_API_KEY} from each user's environment at launch, so the token
never lands in the file:
{
"mcpServers": {
"developerz": {
"type": "http",
"url": "https://mcp.developerz.ai/v1/mcp",
"headers": { "Authorization": "Bearer ${DEVELOPERZ_API_KEY}" }
}
}
}
Cursor
Cursor does not expand ${VAR} references in headers, so the token has to
sit inline — which makes the file choice load-bearing. Add it to the
user-level ~/.cursor/mcp.json only, and never commit that file. Do not
use the project-level .cursor/mcp.json for this server: that file is
routinely checked in, and an inline token there leaks to everyone with repo
access.
{
"mcpServers": {
"developerz": {
"url": "https://mcp.developerz.ai/v1/mcp",
"headers": { "Authorization": "Bearer dev_pat_…" }
}
}
}
tesote.ai (or any generic MCP client)
Add a remote MCP server with:
- URL:
https://mcp.developerz.ai/v1/mcp - Auth header:
Authorization: Bearer dev_pat_…
Clients without header support
Bridge through mcp-remote, which
injects the bearer for you. It takes the full endpoint URL and passes auth via
--header CLI args (there is no token env var it reads on its own). This
block goes in the client's user-level config — e.g. Claude Desktop's
claude_desktop_config.json — never a committed project file: the token sits
inline in the env block, and committing it leaks the PAT to everyone with
repo access.
{
"mcpServers": {
"developerz": {
"command": "npx",
"args": [
"mcp-remote",
"https://mcp.developerz.ai/v1/mcp",
"--header",
"Authorization: Bearer ${DEVELOPERZ_API_KEY}"
],
"env": { "DEVELOPERZ_API_KEY": "dev_pat_…" }
}
}
}
On Windows, Cursor and Claude Desktop mangle spaces inside args — drop the
space after the colon and put the whole value in the env var instead:
"Authorization:${DEVELOPERZ_AUTH_HEADER}" with
"env": { "DEVELOPERZ_AUTH_HEADER": "Bearer dev_pat_…" }. Same rule as above:
the header value is inline in env, so the file stays user-level only and
uncommitted.
Verify the connection from any client by calling whoami — it returns the
account, MCP tier, and granted scopes. With curl, token read from the
environment so it stays out of your shell history:
read -s DEVELOPERZ_API_KEY; export DEVELOPERZ_API_KEY # paste the dev_pat_… value
curl https://mcp.developerz.ai/v1/mcp \
-H "Authorization: Bearer $DEVELOPERZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"whoami","arguments":{}}}'
3. Dispatch a task
Call task_create with a scoped envelope. The server stamps the origin and the
caller identity — you supply the work spec:
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 2,
"params": {
"name": "task_create",
"arguments": {
"repo_full_name": "owner/my-repo",
"title": "Add pagination to the user listing endpoint",
"problem": "GET /users returns the full table; it times out past 10k rows.",
"acceptance_criteria": [
"GET /users accepts page and per_page params",
"Default per_page is 50, max 200",
"Existing clients without params get page 1 unchanged"
],
"files_touched": ["src/routes/users.ts"],
"forbidden": ["src/routes/billing.ts"],
"confidence": 0.9,
"sink": "direct"
}
}
}
Field notes:
sink—directorqueuedruns the task on your BYOVM fleet (queuedis the capacity-aware fan-out sink: a busy fleet queues until the org's max queue depth, then refuses the enqueue withqueue_full);ghopens a GitHub issue;linearfiles to your Linear.confidencebelow the repo policy'sconfidence_thresholdbecomes an escalation, not a task — a human is paged instead of the fleet.files_touchedis advisory;forbiddenis a ban-wrong-fixes list rendered into the coding agent's brief as "Do not touch" guidance — prompt-level, not an enforced filesystem gate.
A direct dispatch returns {task_id, sink, sink_ref, url}. A queued
dispatch returns {task_id, sink, queue_position, queue_depth} so you can watch
the backlog drain — unless the org is already at its max queue depth, in which
case the enqueue is refused: no task is created, and the result comes back as a
tool error (isError: true) whose text names the depth and the cap:
queue full: <queue_depth> tasks waiting (max <max_depth>); retry once the backlog drains
Handle both shapes in an automation: success JSON carrying a task_id, or the
queue_full refusal — back off and re-dispatch once task.list shows the
backlog draining. In natural-language clients (Claude Code, Cursor, tesote.ai)
you can simply ask: "use task_create to dispatch … to owner/my-repo".
4. Check the work
The same PAT reads the fleet back (read:tasks):
| Tool | Action | Returns |
|---|---|---|
task |
list |
The account's task queue, newest-first (filters.status, per_page). Each row carries its run_id once a runner claims it. |
task |
run_status |
One-row pulse for a run — last event kind, seq, finished flag. |
task |
run_tail |
Durable run events after since_seq, capped — feed next_since_seq back to keep polling. |
curl https://mcp.developerz.ai/v1/mcp \
-H "Authorization: Bearer $DEVELOPERZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"task","arguments":{"action":"run_status","run_id":"RUN_ID"}}}'
The run_id comes from task.list once the task is claimed. The dashboard's
fleet view shows the same run live, and every step — the dispatch, the claim,
each tool call, the PR — is in the audit log.
Hygiene
- Least privilege. A dispatch PAT needs only
write:tasks+read:tasks. Don't reuse a dashboard-grade token in an external client. - Never commit the token. Reference it from the environment
(
${DEVELOPERZ_API_KEY}) in committed configs — but only where the client genuinely expands it (a committed.mcp.jsondoes; Cursor's headers do not). Where expansion isn't supported, the token sits inline and the config file is user-level only, never committed. - Revoke when done. A dispatch PAT can't revoke itself — revocation needs
write:account, the same owner session that minted it. List tokens withGET /v1/tokens(read:account— any dashboard role), thenDELETE /v1/tokens/:idwith the owner session (the console-fetch pattern from step 1; re-revoking is idempotent). A credential that already holdswrite:accountcan also call the MCPtokenresource'srevokeaction.