@tokenfactory/acc-mcp-server
v0.6.0
Published
Agent Control Center MCP server. Stdio-transport subprocess spawned by acc-runner alongside Claude Code; exposes the full acc-mcp registry catalog (src/lib/acc-mcp/registry.ts) with access/risk gating and telemeters every call to acc.mcp_tool_calls.
Downloads
680
Readme
@tokenfactory/acc-mcp-server
Agent Control Center MCP server. Stdio-transport subprocess spawned by
@tokenfactory/acc-runner alongside each Claude Code session. Exposes
the full acc-mcp registry catalog — all 49 tools in
src/lib/acc-mcp/registry.ts, the SAME catalog api/_lib/acc-mcp-bus.ts
enforces server-side for the chat lane — over the Model Context Protocol,
and telemeters every tool call to acc.mcp_tool_calls.
How agents reach it
The runner writes a .mcp.json into the task working directory before
spawning Claude Code:
{
"mcpServers": {
"acc": {
"command": "npx",
"args": [
"-y", "@tokenfactory/acc-mcp-server",
"--task-id", "T-12345",
"--runner-id", "local-priyas-mbp",
"--access-token", "<runner JWT>"
]
}
}
}Claude Code auto-discovers the server, lists the tools at session
start, and the agent can call any of them during a task. Each call
writes one row to acc.mcp_tool_calls (latency, request args, response
summary) via POST /api/mcp/record-tool-call — service-role only, so
the agent surface cannot fabricate telemetry.
Tool catalog
tools/list is a straight projection of src/lib/acc-mcp/registry.ts
(toolDescriptors()) — there is no second catalog to keep in sync, and
no parity test, because there is nothing left to drift. That registry
is bundled into this package's published dist/ at build time (see
"Build" below) rather than copied by hand.
Not every advertised tool has a runner-tier EXECUTOR, though — tools/call
only implements what api/_lib/acc-mcp-bus.ts's own dispatcher can run
directly today:
- every read (the CC-2A pack, the CC-2B integration reads,
query_reports/query_audit/query_lifecycle/query_repo_docs,ping/echo) search_repo_docs(RAG-1) — the one read that is NOT a PostgREST query. Ranking the repo-docs corpus needs the query embedded, the embedding provider key is a SERVER secret, and this process runs on the agent's own runner host. So the executor PROXIES: it POSTs to${ACC_PUBLIC_URL}/api/knowledge/agent-searchwith the access/delegation token it already holds, and the server embeds + callsacc.search_knowledge. No runner host ever needs an embedding key, andtests/search-repo-docs.test.tsasserts that absence from this package's own source rather than trusting convention.read_agent_memory/write_agent_memory(MEMORY-1) — the second proxied pair, and the only MUTATING tool here whose effect is an HTTP call rather than an RPC. What must not cross onto the agent's machine this time is not a secret but a POLICY decision:acc.record_agent_memory/acc.read_agent_memorytake ap_data_class, and MEMORY-1's whole contract is that no agent input can ever select anything butoperational. So both executors POST to${ACC_PUBLIC_URL}/api/mcp/agent-memory, which writes the class server-side. Neither forwards adata_classfield, and the agent id rides from this process's ownServerContextrather than from the tool arguments.- the CC-5A plan-write pair (
draft_plan/revise_plan, plus the purerender_gap_register/estimate_wavesreads) - the four low-risk MC-9 orchestration verbs (
pause_session/resume_session/steer_session/retry_task) - the six MC-7 board writes (
create_task/update_task/assign_task/set_priority/close_task/comment_task)
Mutating tools this tier DOES implement still go through the per-agent
approval gate (resolveApprovalMode in src/policy.ts), now driven by the
registry's own access.risk instead of a hand-maintained risk-tier map.
Operator-bus-only tools (NS MCP-1b)
The remaining 13 tools are listed (so an agent can discover their schema) but
refuse on tools/call with unsupported_at_runner_tier. NS MCP-1b reviewed
all 13 and ruled every one of them operator-bus-only, permanently — this is a
settled boundary, not a backlog of unported executors.
The reason is one property all 13 share rather than thirteen separate ones. Each
is risk: "elevated" with reversal: "none", so resolveConfirmRequirement
answers "confirm" in every org confirm-policy mode — relaxed may only
demote an elevated action that declares a reversal path. Not even the server tier
can execute one inline: it needs a DECIDED acc.proposed_actions row,
re-validated against the call's intent and the 0260 TOCTOU etag.
This tier has no proposal machinery at all. Its only gate is the per-agent
approval in acc.mcp_pending_calls, which is a strictly weaker artifact than
a decided proposal — no dedup key binding the approval to a specific target
value, no org-admin re-check at decide time, no ops-registry verb, no etag.
Implementing a runner-tier executor for any of the 13 would therefore not port a
feature; it would open a second, weaker path to exactly the effects the
confirmation gate exists to fence, reachable by an agent asking its own runner
instead of the operator.
| Tool | Domain | Confirmation verb | Applied at |
| --- | --- | --- | --- |
| set_automation_knob | config | automation_mode | POST /api/ops/config |
| set_cost_cap | config | budget_raise | POST /api/ops/config |
| set_confirm_policy | config | confirm_policy | POST /api/ops/config |
| set_repo_access_policy | config | repo_access_policy | POST /api/ops/config |
| set_turn_routing_policy | config | turn_routing | POST /api/ops/config |
| create_branch | repo | branch_create | POST /api/mcp/tool-call + decided action id |
| open_pr | repo | pr_open | POST /api/mcp/tool-call + decided action id |
| merge_pr | repo | pr_merge | POST /api/mcp/tool-call + decided action id |
| rerun_ci | repo | rerun_ci | POST /api/mcp/tool-call + decided action id |
| abort_session | orchestration | session_abort | POST /api/ops/orchestration |
| reassign_task | orchestration | task_reassign | POST /api/ops/orchestration |
| set_dependencies | orchestration | task_dependencies | POST /api/ops/orchestration |
| interrupt_for_incident | orchestration | task_interrupt | POST /api/ops/orchestration |
The two apply paths differ in kind. The config and elevated-orchestration verbs
have no bus executor at all — their effects live behind the decide routes.
The four repo writes do have one (api/_lib/repo-ops.ts), so once the
proposal is decided the same tool call is re-sent to /api/mcp/tool-call
carrying the action id.
Note the repo writes are not blocked merely because this subprocess lacks a GitHub App installation token — NS RUN-3 can mint one for a runner. The token is incidental; the confirmation is the gate. (The pre-MCP-1b docs said otherwise, which invited exactly the wrong conclusion once RUN-3 landed.)
The refusal is machine-readable so an agent's next step is "ask an operator to
approve <verb>" rather than a blind retry:
{
"error": "unsupported_at_runner_tier: merge_pr is an elevated, unreversible repo write: …",
"reason": "operator_bus_only",
"tool": "merge_pr",
"confirm_via": "pr_merge",
"apply_via": "POST /api/mcp/tool-call with confirmation.actionId, once the proposal is decided"
}Both sides of the split are derived, not listed. The gate set comes from the
registry via requiresDecidedProposal; "has an executor" comes from
RUNNER_TIER_EXECUTORS itself. A 14th always-confirm tool is gated the moment it
lands in the registry, and a tool that later declares a reversal path drops out
of the gate automatically. tests/unit/mcp1b-runner-tier-coverage.test.ts (root
suite, so CI's required unit job runs it) asserts the two sets partition the
registry's mutating tools exactly — a 50th tool cannot land in neither bucket and
silently become unreachable.
A mutating tool that is not proposal-gated and also has no executor here is a
wiring gap, and says so: no_runner_tier_executor. That state is unreachable
today (the partition test forbids it) and exists so the failure reads honestly if
it ever occurs.
Build
pnpm build (build.mjs) bundles src/cli.ts with esbuild rather than
running tsc — this package's runtime dependencies (the MCP SDK,
supabase-js, ws, zod) stay external node_modules imports, but the
registry and its pure src/lib/* dependencies are inlined into
dist/cli.js. That is what lets this package import the registry directly:
a plain tsc emit can't write output for a source file outside its
rootDir, and a hand-copied mirror is exactly the problem this catalog
exists to avoid.
Environment
| Var | Notes |
| --- | --- |
| SUPABASE_URL / ACC_SUPABASE_URL | Project URL |
| SUPABASE_ANON_KEY / ACC_SUPABASE_ANON_KEY | Anon key for the user-scoped client |
| ACC_PUBLIC_URL | ACC API host. Defaults to https://agent-control-center-sepia.vercel.app. Overridable via --public-url. |
| ACC_MCP_ACCESS_TOKEN | Runner JWT. Overridable via --access-token (visible in ps). |
Security notes
- The runner's JWT is passed as a CLI arg. Visible in
psoutput for the subprocess lifetime — acceptable for v0.5-C1 because the runner itself owns the process tree, but tracked for v0.6 hardening (env-only handoff or token-broker pattern). - Telemetry writes go through
POST /api/mcp/record-tool-call, which validates the JWT and uses the service-role to callacc.record_mcp_tool_call. Direct RPC access is denied.
Versioning
Pin @modelcontextprotocol/sdk exactly — MCP SDK churns on minor
releases. Update via the standard checklist in
docs/acc/MCP_SERVER.md (peer-dep cycle, schema-shape diff, parity
test against the latest Claude Code release).
