Working on the MCP server
How to keep @mapomodule/mcp accurate as Mapo changes, and how to extend it with recipes, prompts, resources and tools. What the server does for app developers is in @mapomodule/mcp; this page is about its source.
What the server is made of
| Part | Source | Kept in sync by |
|---|---|---|
| Docs search | docs/**/*.md, indexed into dist/knowledge/docs.json | Rebuilding the package after a docs change |
| Component APIs | uikit and form .vue files, read with vue-component-meta | Rebuilding; JSDoc on props and events |
| Field types | form registry and descriptor interfaces | Rebuilding; JSDoc on attrs |
| Composables | Every module's addImports([...]) calls | Rebuilding |
| Overview | docs/guide/concepts.md and src/runtime/core/instructions.ts | Editing them by hand when the architecture changes |
| Recipes | src/runtime/core/recipes.ts | Editing it by hand; the tests check every link |
| Prompts | src/runtime/core/prompts.ts | Editing it by hand |
Everything under "Rebuilding" follows the code by itself, as long as the package is rebuilt (pnpm build does it). The rest is written by people and drifts unless someone updates it.
What to update when Mapo changes
| You changed | Also update |
|---|---|
| A docs page path or a heading | The recipes that point at it (the MCP tests fail until you do) |
| A feature that makes a new task possible | A recipe for the task; a prompt if the workflow takes several tools in a fixed order |
| The package list, the config keys, the request flow | docs/guide/concepts.md and, if the summary changes, src/runtime/core/instructions.ts |
| A component's props, events or slots | Nothing by hand: keep the JSDoc accurate and rebuild |
| A field type | src/runtime/core/openapi.ts if a schema shape should map to it |
| A composable | Nothing by hand, as long as it is registered with addImports |
| A tool, a prompt or a resource | README.md of the package, docs/modules/mcp.md and the CLI test (see below) |
Then verify:
pnpm --filter @mapomodule/mcp build
pnpm --filter @mapomodule/mcp test
pnpm --filter @mapomodule/mcp typecheckAdding a recipe
A recipe maps a task, phrased the way a developer asks for it, to the page that answers it. Keyword search cannot infer that intent, so the recipes are curated by hand in src/runtime/core/recipes.ts:
{
id: "form-draft",
task: "Autosave form drafts",
keywords: ["draft", "autosave", "useFormDraft", "restore", "unsaved"],
doc: "uikit/form/draft.md",
seeAlso: ["howto/crud-detail.md"],
summary:
"useFormDraft keeps unsaved edits in localStorage and offers to restore them on the next visit.",
}idis unique;keywordshas at least three entries;summaryis one sentence.doc,headingand everyseeAlsomust exist in the generated knowledge base:src/__tests__/knowledge.test.tschecks them against the real docs.- Add a case to
src/__tests__/recipes.test.tswith a question the recipe must win, and run the suite: new keywords can steal another recipe's questions.
Adding a prompt
Prompts are slash-commands: the model receives them as if the user had typed them. Each one names the tools to call and their order.
Add a
MapoPrompttosrc/runtime/core/prompts.tsand to thePROMPTSarray. Every tool it names must exist.Add the Nitro wrapper
src/runtime/mcp/handlers/mapo/prompts/<name>.ts:tsimport { myPrompt } from "../../../../core/prompts.js"; import { toMcpPrompt } from "../../../../nitro/to-mcp-prompt.js"; export default toMcpPrompt(myPrompt);
The stdio CLI registers everything in PROMPTS on its own.
Adding a resource
- Write the render function in
src/runtime/core/resources.ts, so both transports share it. - Add the Nitro definition in
src/runtime/mcp/handlers/mapo/resources/<name>.tswithdefineMcpResource. - Register it in
registerResources()insrc/cli.ts, unless it needs the running app (asmapo://app/manifestdoes). - Test the render function in
src/__tests__/resources.test.ts, and the listing insrc/__tests__/cli.test.ts.
Adding a tool
Write the spec in
src/runtime/core/tools/<area>.tswithdefineMapoTool: an explicitArgsinterface, a zodinputSchema, annotations,inputExamples, and arunthat returns markdown.Export it from the area's tool array.
Add a one-line Nitro wrapper in
src/runtime/mcp/handlers/mapo/tools/<name>.ts:tsimport { myTool } from "../../../../core/tools/<area>.js"; import { toMcpTool } from "../../../../nitro/to-mcp-tool.js"; export default toMcpTool(myTool);If the tool works without the dev server, add it to
staticTools()insrc/cli.ts, and update the expected list insrc/__tests__/cli.test.ts.Mention it in the instructions (
src/runtime/core/instructions.ts) if assistants should reach for it unprompted.
Tools return markdown on purpose: assistants read it directly, and it costs fewer tokens than JSON wrapped in prose. Names start with mapo_, and only a tool that writes files may drop readOnlyHint.
Rules the toolkit imposes
src/runtime/mcp/contains nothing buthandlers/. The toolkit loads every top-level file of a scanned path as a handler, so a helper placed there breaks the Nitro build.- The build-time extractors live in
src/build/, a separate bundle entry, sovue-component-meta,typescriptandjitinever reach the runtime. - Writing is guarded by
src/runtime/core/write.ts: development only, inside the project root, never over an existing file withoutoverwrite: true.
Trying it by hand
Over stdio, against the reference app, from the repository root:
node packages/@mapomodule/mcp/bin/mapo-mcp.mjs serve --root apps/example-e2eOver HTTP, with pnpm dev:example-e2e running, at http://localhost:3000/mcp/mapo. This transport also answers mapo_inspect_app and mapo_doctor. The commands are in Testing.