Skip to content

ctx_add MCP tool requires provenance fields its own schema doesn't expose (task/decision/learning unusable over MCP) #190

Open

Description

@omergk28

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 serve

Root cause

Four links, all on main @ e3bac629:

  1. internal/entry/validate.go:44-58 — provenance (--session-id, --branch, --commit) is required for task, decision, learning; :88 raises errAdd.MissingFields.
  2. internal/rc/rc.go:508 (ProvenanceSessionRequired, and the Branch/Commit equivalents) — each returns true by default when .ctxrc is absent or has no provenance_required block.
  3. internal/mcp/server/extract/extract.go:80-87 — the MCP layer obtains those values only by reading them from the tool-call arguments:
    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 }
    It does not derive them from the active MCP session or from git.
  4. internal/mcp/server/def/tool/tool.go:39-72 — the ctx_add schema's properties are type, content, priority, section, plus EntryAttrProps(...) (context/rationale/consequence/lesson/application). There are no provenance properties, and Required is [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 into ctx_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.
  • branch and commit are trivially derivable server-side from the context directory via git 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: false

This 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

  • ctx built from main @ 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

  1. bilersan commented on Oct 24, 2026

    @bilersan
    Contributor

    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 your
    report (never pushed), so I've built on it: #199.

    What the PR does

    1. Schema. ctx_add and ctx_watch_update advertise session_id, branch,
      commit. ctx_watch_update also gains section/priority: validation
      requires section for tasks and that schema never offered it either — same
      bug class.
    2. Branch/commit are resolved server-side when both are omitted, with the
      same calls the hook relay makes (provenance.Emit →
      git.CurrentBranch/git.ShortHead), so MCP-written values match the CLI
      path exactly, including unknown when 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 a branch without a commit is a validation error rather
      than a branch from one checkout next to a commit from another. No
      per-session cache: it would freeze commit at session start — the
      staleness server-side resolution is meant to remove.
    3. Single-line fields are flattened at the MCP boundary. SanitizedOpts
      promised to sanitize every text field but passed priority, section,
      branch, and commit through raw, so a newline in any of them can forge a
      task line or a ### heading in TASKS.md. That is reachable on main
      already: a raw ctx_watch_update with all three provenance fields and
      "branch": "x\n- [x] forged" writes a separate - [x] forged line, and once
      the schema advertises branch/commit, schema-driven clients send them too.
      A new sanitize.Line strips every control character (sanitize.Reflect
      keeps \n) and truncates to 256 bytes — the cap
      specs/sanitize-hardening-followup.md deferred for these fields.
    4. session_id stays caller-supplied and required. Its description says:
      use the relayed Session: value, or "unknown" if you have none. The
      ctx-decision-add/ctx-learning-add prompt footers now ask for it too; their
      "all fields above" never covered it. This isn't a relaxation: omitting it
      still fails, only .ctxrc relaxes the gate, and "unknown" is what the relay
      itself emits without a session ID.

    Checked end to end with your JSON-RPC probe on a fresh ctx init: on main it
    fails as reported; on the branch it names only --session-id, with
    "session_id": "unknown" it returns Added learning to LEARNINGS.md, a task
    records the repo's real branch and commit, and branch without commit is
    rejected instead of being paired with the server's HEAD.

    Why not the session half of A. MCPSession has no identity:
    ctx_sessionevent(start) only resets governance counters
    (internal/mcp/handler/tool.go:531-533). Filling from the live session would
    mean 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 / a3048c5c are explicit: "no silent 'unknown'
    defaults", "agents cannot bypass".

    @josealekhine — one judgment call is yours: if you'd rather the server write
    "unknown" for a missing session_id (the probe in this issue would then pass
    unchanged), 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.Write passes session/branch/commit only to
    format.Task (internal/entry/write.go:93-97); format.Decision and
    format.Learning don't take them. Reproducible on main:
    ctx decision add "x" --session-id s1 --branch b1 --commit c1 --context c --rationale r --consequence q
    writes a DECISIONS.md entry with none of the three values, while
    ctx decision add --help says these flags "link the decision to the AI session
    and code state that created it". Only TASKS.md carries
    #session:/#branch:/#commit:, so today the gate buys no traceability for
    LEARNINGS.md/DECISIONS.md. Either those formats start recording it, or the
    gate narrows to tasks — your call; happy to open a separate issue.

    Two smaller notes: the validator reports CLI flag names (--session-id) in MCP
    responses because it's shared with the CLI; and ctx mcp serve skips the git
    gate (AnnotationSkipInit), which is the only reason a non-git context dir is
    reachable over MCP at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions