Kernel over MCP
Kernel's MCP server lets a coding agent work a Kernel work item entirely through tools: read
its current work and a bounded context, record decisions and artifacts, report progress and
blockers, ask for an approval, hand off, and complete the work. It is a thin client of
/api/v1: it holds no database session and no privileged path of its own, so every
permission, scope and tenant rule is enforced exactly once, in the API, whichever way a call
arrives. admin:* tokens gain nothing extra over MCP, and organization, member, token, machine
and runtime management are deliberately absent.
There are two transports of the same server:
| transport | how it is reached | credential |
|---|---|---|
stdio — kernel-mcp |
the agent runtime starts it as a subprocess | KERNEL_TOKEN in its environment |
streamable HTTP — POST /mcp on the Kernel server |
a remote client, over the network | Authorization: Bearer on every request |
Both are authenticated by a Kernel API token. Over HTTP the server checks the bearer before a
request reaches the MCP transport at all: no header, a malformed token, a revoked or expired
one — 401 auth.invalid_token, the same answer the API gives, and the MCP layer never sees
the request. KERNEL_MCP_HTTP_ENABLED=0 removes the /mcp path entirely; the stdio server is
unaffected.
Managed sessions get it per invocation, never through your files
A session the daemon starts is handed Kernel's server for that one invocation, by the
adapter, with the session's own token — the same credential the daemon puts in the
session's environment as KERNEL_SESSION_TOKEN (D-054), never the daemon's device token. It
is attached only when the launch carries both a server URL and a session token; a launch the
daemon gave no credential is started without it.
- Claude Code gets a private
--mcp-configfile, written0600in a directory the adapter creates for the invocation and deletes with it, naming the serverkernel:kernel-mcpover stdio withKERNEL_URL,KERNEL_TOKEN,KERNEL_SESSION_IDandKERNEL_WORK_KEYin itsenvblock.--strict-mcp-configstays on, so that file is the only source of servers the session has — your own~/.claude.jsonservers, and any.mcp.jsonin the checkout, never load into a managed session (D-041). - Codex gets
-c mcp_servers.kernel.command=…,-c mcp_servers.kernel.args=[…]and-c mcp_servers.kernel.env_vars=["KERNEL_URL","KERNEL_TOKEN","KERNEL_SESSION_ID","KERNEL_WORK_KEY"]on the command line, after--ignore-user-config, on the launch and on every resume. The overrides name the program and the variables; the values are in the Codex process's environment and never on the command line, whichpsshows to every account on the machine.
Neither adapter ever edits your configuration. Not ~/.claude.json, not
~/.claude/settings.json, not ~/.codex/config.toml — a test per adapter asserts those files
are byte-identical before and after a launch, and a daemon that wrote to them would change
every session you ever run, on a machine, from a work item. The setup below is for people who
want Kernel's tools in their own interactive sessions, by their own choice; it is not how a
managed session gets them, and a managed session does not read it.
The program the daemon launches is kernel-mcp beside its own interpreter (the console script
of this package), else the one on PATH; a machine may name another with the kernel_mcp
setting in its daemon configuration — a program name or an argv prefix, machine-owned like
executable, never taken from a runtime definition. The server name kernel is reserved:
a runtime definition whose mcp_servers names one is refused at launch.
Setting it up for your own sessions
You need a token. A service token scoped to what you want the agent to do is the right shape. The session tokens the daemon mints read and write, but only their own work item (see What a session token reads, and what it does not), so an agent you drive yourself — one that must range across work items, or read the fleet — wants a service token instead.
$ kernel token create --name mcp \
--scope work:read --scope work:write \
--scope decisions:read --scope decisions:write \
--scope artifacts:read --scope artifacts:write \
--scope blockers:read --scope blockers:write \
--scope approvals:request --scope approvals:read \
--scope handoffs:read --scope handoffs:write \
--scope sessions:read --scope sessions:write \
--scope runtimes:read --scope org:read
That is the full set needed to drive all fourteen tools: list_work needs org:read and
handoff needs runtimes:read + handoffs:write, which a narrower "write my own work" set
omits. Grant less for an agent that does less — the read scopes let it read back what it wrote.
The token is shown once. Put it in the environment rather than on a command line
(export KERNEL_TOKEN=krn_…), and point KERNEL_URL at your server
(https://kernel.example.com, no trailing /api/v1). KERNEL_WORK_KEY is optional: set it
to pin get_current_work to one work item, or leave it unset and let the server resolve the
single running work item assigned to you.
Claude Code
Over stdio, in your user scope (-s user), the values passed as -e rather than typed into
the arguments:
$ claude mcp add kernel -s user \
-e KERNEL_URL="$KERNEL_URL" -e KERNEL_TOKEN="$KERNEL_TOKEN" -- kernel-mcp
Or over HTTP, against the Kernel server directly:
$ claude mcp add --transport http kernel "$KERNEL_URL/mcp" \
--header "Authorization: Bearer $KERNEL_TOKEN"
Either writes to ~/.claude.json; claude mcp list shows it, claude mcp remove kernel
takes it out. The tools appear as mcp__kernel__get_current_work and so on.
Codex
Over stdio:
$ codex mcp add kernel --env KERNEL_URL="$KERNEL_URL" --env KERNEL_TOKEN="$KERNEL_TOKEN" \
-- kernel-mcp
Or over HTTP, with the token read from an environment variable at start rather than stored in the file:
$ codex mcp add kernel --url "$KERNEL_URL/mcp" --bearer-token-env-var KERNEL_TOKEN
Both write [mcp_servers.kernel] into ~/.codex/config.toml; codex mcp list shows it and
codex mcp remove kernel takes it out.
Gemini CLI
In ~/.gemini/settings.json (or the project's .gemini/settings.json):
{
"mcpServers": {
"kernel": {
"command": "kernel-mcp",
"env": { "KERNEL_URL": "https://kernel.example.com", "KERNEL_TOKEN": "krn_…" }
}
}
}
or, over HTTP:
{
"mcpServers": {
"kernel": {
"httpUrl": "https://kernel.example.com/mcp",
"headers": { "Authorization": "Bearer krn_…" }
}
}
}
Recent Gemini CLI releases also take gemini mcp add kernel -e KERNEL_URL=… -e KERNEL_TOKEN=… -- kernel-mcp, which writes the same entry. Gemini is not a managed runtime in this build
(M9), so this is the only way it reaches Kernel.
The tools
Fourteen, named by the dossier. Every result is a compact JSON object with ids, a status and
a next_actions hint — never prose — except get_context, which returns the bounded context
document verbatim.
| tool | when to call it |
|---|---|
get_current_work() |
first: which work item this session is for (the session's, else KERNEL_WORK_KEY, else the one running work item assigned to the token's user, else an error saying which to set) |
get_work(work) |
one work item by key or id |
list_work(state?, limit=50) |
a page of work items |
get_context(work?) |
the bounded context: work, constraints, acceptance criteria, the newest 20 decisions and artifacts, open blockers, pending approvals, the last 5 handoffs, with truncated counters — the same document as GET /api/v1/work/{id}/context and the kernel://work/{key}/context resource |
update_work_status(work, command, reason?) |
make_ready, start, submit_review, rework, complete, cancel, reopen, send_back |
complete_work(work, reason?) |
end the work item successfully |
record_decision(work, statement, rationale="", supersedes_decision_id?) |
a decision, attributed to this session |
create_artifact(work, type, title, content? | external_uri?, filename?, content_type?) |
an artifact: inline text, or one held elsewhere |
report_progress(message, session_id?) |
a progress event on the session timeline |
report_blocker(work, severity, title, description="") |
something in the way |
resolve_blocker(blocker_id, resolution) |
it no longer is |
request_approval(work, action, reason="", required_role?) |
ask a person before doing something |
get_approval_status(approval_id) |
has anyone answered |
handoff(work, target_runtime, purpose, objective?) |
hand the work to another runtime; with an objective the package is compiled too |
Raw event and transcript history is never in a tool result; the API serves it.
A worked example
A managed Claude Code session, started by the daemon, has kernel attached and is briefed to
record what it decides. It calls the tool the way it calls any other — over MCP this is a
tools/call request:
{
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {
"name": "record_decision",
"arguments": {
"work": "KBK-1",
"statement": "Codex is told the names of the server's variables, not their values",
"rationale": "an argument list is readable by every process on the machine"
}
}
}
The server sends POST /api/v1/work/{id}/decisions with the session's bearer and
session_id set to the session, so the decision lands on the session's timeline exactly as
if the session had made the request itself. The result is the kind of object every tool
returns:
{
"id": "b3ac828b-9ed4-49d0-88ed-aed364bff7b5",
"work_id": "dcd533f6-3bb7-4f13-a781-ca197d15c9e2",
"session_id": "39f088d3-1dc8-414f-b75f-9191f6161b83",
"status": "recorded",
"next_actions": ["create_artifact", "report_progress", "complete_work"]
}
A refusal comes back as an MCP tool error carrying the API's problem code and detail — a
token without decisions:write calling record_decision is told
auth.permission_denied: decision.record requires the scope decisions:write, naming the
scope, and nothing is recorded.
What a session token reads, and what it does not
A managed session's token (token_service.SESSION_TOKEN_SCOPES) holds work:read/work:write,
sessions:write, and, for the work-object families it produces, both halves: it may request an
approval (approvals:request) and read one back (approvals:read), and write and read
artifacts, blockers and decisions (artifacts:read, blockers:read, decisions:read); it may
read a handoff (handoffs:read). So get_approval_status, and reading back a decision, an
artifact, a blocker or a handoff it recorded, all work over the tools (D-055/KBK-46). Every one
of those reads is confined by the API to the token's own work item: a row of another work
item answers exactly as a write to it does — 403 auth.session_scope inside the organization,
404 across organizations — so an agent sees its own Work and never the fleet.
The token still holds no sessions:read (D-035): it cannot read the session list or even
its own session row (GET /sessions/{id} answers 403), and it manages no tokens and touches
no organization. A caller that needs those reads uses a service token with the scopes it needs.
Limitations
- The HTTP transport needs this build's MCP package. A server built without
kernelos.mcp.server.http_transportstill refuses an unauthenticated request at/mcp(401) and tells an authenticated one the transport is unavailable (503mcp.transport_unavailable) rather than pretending the path does not exist. Usekernel-mcpover stdio there. - Managed Codex sessions get Kernel's server and nothing else. A runtime definition's own
mcp_serversare wired for Claude Code only; see docs/runtimes/codex.md.
Where this is tested
tests/unit/adapters/test_kernel_mcp.py— how the server is described, from which of the daemon's variables, and when it is not.tests/unit/adapters/test_claude_adapter.py,test_codex_adapter.py— the file and the overrides the process was really started with, the stubkernel-mcpstarted from that configuration the way the runtime would, the user's configuration byte-identical before and after a launch.tests/integration/daemon/test_runner_mcp_wiring.py— through the daemon's runner: the session's token reaches the server and no fragment of the device token does.tests/unit/api/test_mcp_http.py,tests/integration/api/test_mcp_http.py— the/mcpgate: what it refuses before asking a database, and who passes through to the transport.tests/contract/test_mcp_tools.py— the server itself, over stdio, against a live API.