Repository navigation
ctx_add MCP tool requires provenance fields its own schema doesn't expose (task/decision/learning unusable over MCP) #190
Description
Summary
The ctx_add MCP tool rejects type=task|decision|learning with:
learning missing required fields: --session-id, --branch, --commit
…but its own published input schema does not expose session_id, branch, or commit, so an MCP client has no way to supply them. In any project without a .ctxrc that relaxes provenance, those three entry types are unreachable over MCP.
type=convention works, because it is not provenance-gated.
This is not client-specific — I reproduced it with a raw JSON-RPC stdio client, not through any particular editor integration.
Reproduction
Clean build of main @ e3bac629, fresh ctx init, no .ctxrc:
ctx_sessionevent(start) → ok "Session started for probe. Context: …/.context"
ctx_status → ok context loaded
ctx_add type=learning → ERROR missing required fields: --session-id, --branch, --commit
ctx_add type=convention → ok "Added convention to CONVENTIONS.md"
Note the session is created server-side, and the error persists anyway.
Minimal probe (no editor involved):
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ctx_add","arguments":{"type":"learning","content":"probe","context":"c","lesson":"l","application":"a"}}}' \
| ctx mcp serveRoot cause
Four links, all on main @ e3bac629:
internal/entry/validate.go:44-58— provenance (--session-id,--branch,--commit) is required fortask,decision,learning;:88raiseserrAdd.MissingFields.internal/rc/rc.go:508(ProvenanceSessionRequired, and the Branch/Commit equivalents) — each returnstrueby default when.ctxrcis absent or has noprovenance_requiredblock.internal/mcp/server/extract/extract.go:80-87— the MCP layer obtains those values only by reading them from the tool-call arguments:It does not derive them from the active MCP session or from git.if v, ok := args[field.SessionID].(string); ok { opts.SessionID = v } if v, ok := args[field.Branch].(string); ok { opts.Branch = v } if v, ok := args[field.Commit].(string); ok { opts.Commit = v }
internal/mcp/server/def/tool/tool.go:39-72— thectx_addschema's properties aretype,content,priority,section, plusEntryAttrProps(...)(context/rationale/consequence/lesson/application). There are no provenance properties, andRequiredis[type, content].
So the validator demands three fields the schema never offers. ctx_watch_update shares the same entity.EntryOpts path and is affected identically.
Signals that point at the intended design
ctx_sessionevent(type="start")creates a session successfully, and the server holds it — it simply is not threaded intoctx_add. That reads like auto-fill was intended and the wiring is missing, rather than a deliberate "caller must pass provenance" contract.- The error surfaces CLI flag names (
--session-id) in an MCP response, where flags do not exist. branchandcommitare trivially derivable server-side from the context directory viagit rev-parse.
Possible fixes
A — Auto-fill provenance in the MCP handler. Populate SessionID from the live MCP session, and Branch/Commit via git rev-parse on the context dir; fall back to any explicitly-passed args. Keeps provenance meaningful, needs no schema change, and makes ctx_sessionevent(start) load-bearing as it appears designed to be. Costs a git shell-out per add (cacheable per session), and needs a defined behaviour when the context dir is not a git repo.
B — Expose the three fields in the ctx_add / ctx_watch_update schemas. Smallest diff and explicit. But it pushes work onto every client, and an agent cannot know a meaningful session id — it would end up synthesising one, which makes the provenance recorded in LEARNINGS.md decorative rather than real.
C — Exempt the MCP path from the provenance gate (treat it like convention, or default provenance_required to false when the caller is MCP). Unblocks immediately, but silently drops provenance for MCP-written entries, so LEARNINGS.md gains entries that can't be traced back to a session or commit.
My read is that A is the only one that preserves the feature's intent, with B as a complement for callers that genuinely do have a session id. C seems like it trades away the thing provenance exists for.
@josealekhine — which direction would you prefer? Happy to put up a PR for whichever you pick; I didn't want to guess at the intended semantics for the non-git and no-active-session cases, since those drive the shape of A.
Workaround
A project-level .ctxrc relaxing the gate:
provenance_required:
session_id: false
branch: false
commit: falseThis also disables provenance for the CLI, so I've been using ctx learning add --session-id … --branch … --commit … directly instead and leaving the gate on.
Environment
ctxbuilt frommain@e3bac629(2026-10-05)- go1.26.2, darwin/arm64
- Also reproduces on an older Sep-14 build, so this is not a recent regression
Activity
bilersan commentedon Oct 24, 2026
Thanks for the careful write-up, @omergk28 — the four links reproduce exactly on
current
main(d56e7c3d). I had a schema fix sitting locally from before yourreport (never pushed), so I've built on it: #199.
What the PR does
ctx_addandctx_watch_updateadvertisesession_id,branch,commit.ctx_watch_updatealso gainssection/priority: validationrequires
sectionfor tasks and that schema never offered it either — samebug class.
same calls the hook relay makes (
provenance.Emit→git.CurrentBranch/git.ShortHead), so MCP-written values match the CLIpath exactly, including
unknownwhen git can't answer. Explicit values win(an agent in a worktree may know better than the server's cwd), and the pair
is resolved as a whole: the server fills it only when each half is omitted or
"unknown", so abranchwithout acommitis a validation error ratherthan a branch from one checkout next to a commit from another. No
per-session cache: it would freeze
commitat session start — thestaleness server-side resolution is meant to remove.
SanitizedOptspromised to sanitize every text field but passed
priority,section,branch, andcommitthrough raw, so a newline in any of them can forge atask line or a
###heading inTASKS.md. That is reachable onmainalready: a raw
ctx_watch_updatewith all three provenance fields and"branch": "x\n- [x] forged"writes a separate- [x] forgedline, and oncethe schema advertises
branch/commit, schema-driven clients send them too.A new
sanitize.Linestrips every control character (sanitize.Reflectkeeps
\n) and truncates to 256 bytes — the capspecs/sanitize-hardening-followup.mddeferred for these fields.session_idstays caller-supplied and required. Its description says:use the relayed
Session:value, or"unknown"if you have none. Thectx-decision-add/ctx-learning-addprompt footers now ask for it too; their"all fields above" never covered it. This isn't a relaxation: omitting it
still fails, only
.ctxrcrelaxes the gate, and"unknown"is what the relayitself emits without a session ID.
Checked end to end with your JSON-RPC probe on a fresh
ctx init: onmainitfails as reported; on the branch it names only
--session-id, with"session_id": "unknown"it returnsAdded learning to LEARNINGS.md, a taskrecords the repo's real branch and commit, and
branchwithoutcommitisrejected instead of being paired with the server's HEAD.
Why not the session half of A.
MCPSessionhas no identity:ctx_sessionevent(start)only resets governance counters(
internal/mcp/handler/tool.go:531-533). Filling from the live session wouldmean minting an ID that matches no journal file — the decorative-provenance
problem again. Meanwhile, in Claude Code the hook relay already hands the agent
the real session ID on every prompt, so B records real provenance there.
Why not C.
b4dba771/a3048c5care explicit: "no silent 'unknown'defaults", "agents cannot bypass".
@josealekhine — one judgment call is yours: if you'd rather the server write
"unknown"for a missingsession_id(the probe in this issue would then passunchanged), it's a one-line flip in the PR.
A bigger finding along the way. For decisions and learnings, provenance is
validated but never persisted:
entry.Writepasses session/branch/commit only toformat.Task(internal/entry/write.go:93-97);format.Decisionandformat.Learningdon't take them. Reproducible onmain:ctx decision add "x" --session-id s1 --branch b1 --commit c1 --context c --rationale r --consequence qwrites a
DECISIONS.mdentry with none of the three values, whilectx decision add --helpsays these flags "link the decision to the AI sessionand code state that created it". Only
TASKS.mdcarries#session:/#branch:/#commit:, so today the gate buys no traceability forLEARNINGS.md/DECISIONS.md. Either those formats start recording it, or thegate narrows to tasks — your call; happy to open a separate issue.
Two smaller notes: the validator reports CLI flag names (
--session-id) in MCPresponses because it's shared with the CLI; and
ctx mcp serveskips the gitgate (
AnnotationSkipInit), which is the only reason a non-git context dir isreachable over MCP at all.