The add_lesson MCP tool — the registration, and the description that tells an agent what a lesson is
add_lesson — a new MCP tool: record a lesson for this project, so future plans get it.
A thin adapter over the service from MOTIR-3360, following lib/mcp/tools/addComment.ts exactly — resolve the context, call the service, toolOk / toToolError, no logic of its own.
The description is the deliverable, not the plumbing. An MCP tool's description is the entire briefing the calling agent gets; there is no onboarding, no docs it will read, no second chance. If it says "add a lesson to the project's lesson library", agents will add restatements of what just happened, one-off incidents, and things that are already in the plan — and the store this epic exists to make trustworthy fills with noise nobody can tell from signal. The description has to carry, in the schema's .describe() text and the tool description together:
- What a lesson is — something that went wrong and will go wrong again. Not a note, not a status update, not a summary of this task.
- That it is this project's only. It is never shared with other projects, and it is not how you change the product's own rules.
- What it is used for — it is retrieved and put in front of the planner on later plans for this project, so write it as an instruction to a future planner, not as a report about the past.
- The three axes are how it gets found.
kinds,types,phasesroute retrieval. Leaving one empty means applies to everything on that axis — which is occasionally right and usually just means the lesson turns up in plans it has nothing to do with. - When NOT to add one — a one-off, something already covered by an existing lesson (search first), or anything that is really a bug report.
The four text fields exist and are distinct; the description should say what each is for rather than let an agent put the same sentence in all of them: title (the takeaway in a line), body (what went wrong), why (why it matters), howToApply (the actionable second-person rule).
Not to be confused with log_planning_mistake. That is motir-ai's internal tool, called by the planner during a run, and it is untouched by this card. This one is the agent-facing path on motir-core's MCP surface.
Leave the schema strict. Since MOTIR-3342 (#2241), lib/mcp/strictInput.ts rewrites every tool's inputSchema to strict at the registration seam, so an unknown key is refused by name instead of silently dropped. .passthrough() and .catchall() are deliberately exempt from that rewrite — declaring either here would opt this tool back out of the guard, and quietly, which is how scope would find its way through.
Three files carry an MCP tool's argument shape and all three must move together: the zod inputSchema, that tool's section in docs/mcp.md, and lib/apiDocs/mcp.ts. The catalogue gate recomputes descriptionFingerprint from a live tools/list and fails naming the tool and printing the new hash — run tests/api-docs/ and paste it. Nothing in lib/api/v1/** is involved; the OpenAPI document does not describe MCP tools.
Acceptance criteria
lib/mcp/tools/addLesson.tswith aregister*export, one line inregisterMcpTools, and its name in the tool-name union.- An entry in
TOOL_PERMISSIONSnaming the lesson-library-change permission; the typedRecord<McpToolName, PermissionKey>makes an omission a compile error, and the runtime totality test passes unchanged. - The tool description and the per-field
.describe()text cover all five points above; reviewed as writing. docs/mcp.mdgains this tool's section, andlib/apiDocs/mcp.tsits summary; the catalogue fingerprint gate passes with the recomputed hash committed.- The three axes are arguments, typed to the enum members, so a mistyped value is refused at the schema rather than stored as a filter that never matches.
- The schema declares neither
.passthrough()nor.catchall(), so the strict-input rewrite applies. - A test asserts the tool refuses without the permission, before any upstream call.
- A test asserts a near-duplicate refusal reaches the caller with the existing lesson's id and title in the tool result.
- Whatever coverage tests the MCP surface already has — every tool registered, every tool documented, both surfaces conformant — pass with the new member and are not weakened.
- No new scope, transport or auth wiring; the tool goes through the existing permission, rate-limit and strict-input gates.
Context refs
motir-corelib/mcp/tools/addComment.ts— the adapter shape, verbatim, down totoolOkand the summary line.motir-corelib/mcp/registry.ts— the seam: import theregister*, add one line, touch neither the transport nor the auth gate.motir-corelib/mcp/strictInput.ts— the registration-time rewrite, and what.passthrough()opts out of.motir-corelib/mcp/toolPermissions.ts— the tool → permission map and its totality guarantee.motir-coredocs/mcp.md,lib/apiDocs/mcp.ts,lib/apiDocs/mcpFingerprint.ts— the two doc surfaces and the gate.motir-coretests/mcp/tool-coverage.test.ts,tests/mcp/two-surface-conformance.test.ts,tests/mcp/write-tools.test.ts— the guards that will notice a new tool.motir-aiprisma/schema.prisma— the enum members the axis arguments must mirror.- MOTIR-3360 — the service this adapts.