[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6ENA321FCXRTT8X06D0K4A0":3,"skill-same:01M6ENA321FCXRTT8X06D0K4A0":49},{"skillNo":4,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":8,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":14,"categories":15,"keywords":16,"publishedAt":20,"linkOnly":21,"markdownContent":22,"markdownBody":23,"frontMatter":24,"refName":25,"commitSha":26,"skillDirectory":27,"forksCount":28,"repositoryHtmlUrl":29,"repositoryHomepageUrl":30,"ownerHtmlUrl":31,"ownerBlogUrl":32,"ownerDescription":33,"updatedAt":34,"install":35,"resources":48},"01M6ENA321FCXRTT8X06D0K4A0","create-module",null,"Miragon\u002Fpulse","Miragon","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F58698887?v=4","01M6EN8W7TCK4BRJB1S2GEW8C3","01M6EN8WC4V4QYAV5C8FMYZ4R9",8,"MIT","https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse\u002Fblob\u002Fmain\u002Ftemplates\u002Fcomposed-server\u002F.claude\u002Fskills\u002Fcreate-module\u002FSKILL.md",[],[17,18,19],"create module","module","create","2026-11-10 10:12:30",false,"---\nname: create-module\ndescription: Step-by-step house pattern for building your own connector module under `modules\u002F`, a self-contained package exporting a module definition plus tools, wired into the server composition. Use whenever integrating a new system (\"build a connector for X\", \"add tools for our API\", \"new module\"), or wiring a module into the server. Not for engine operations (start a process, correlate a message, job retries, variables): the engine module's `engine_*` tools do those already. For the widget path see the add-widget skill.\nallowed-tools: Read, Edit, Write, Glob, Grep, Bash\n---\n\n# create-module: your own connector module\n\nA module is a workspace package that exports a module-definition object\nconforming **structurally** to the server's port (the `ModuleDefinition` type\nin `server\u002Fsrc\u002Fsetup.ts`). Your module never imports the server, and the\nserver never reaches into your module. Modules are peers: they never import\neach other; anything shared goes through `@miragon\u002Fpulse-ui` (the design kit),\n`@miragon\u002Fpulse-widget-shell` or the server-wired `SharedResources` (today empty).\n\n**Reference implementation: `modules\u002Fmcp-notes`.** Copy it rather than\ninventing a new shape; every step below points at the file that shows the\npattern. For patterns the notes module doesn't cover (toolsets, paged\nlists), the published engine module ships its sources under\n`server\u002Fnode_modules\u002F@miragon\u002Fpulse-engine-connector\u002Fsrc\u002F`\n(a gitignored symlink: search that package directory, not the repo root; see\n`CLAUDE.md` → Reference code).\n\n**A module is for your other systems, not for the engine.** The engine\nmodule already registers the generic engine operations as tools\n(`engine_start_process_instance`, `engine_correlate_message`,\n`engine_set_job_retries` and more), so \"start our leasing process\" needs no\nnew tool. A listing shows only the running toolset: these writes appear with\nOAuth or under `MCP_ACTIVE_MODULES=engine:operations,\u003Cyours>`, never\nin the default read-only boot. List them in the MCP Inspector `pnpm dev`\nstarts, or headless from `server\u002F` with its CLI\n(`pnpm exec mcp-inspector --cli --server-url http:\u002F\u002F127.0.0.1:8400\u002Fmcp --transport http --method tools\u002Flist`). To give the model your processes'\nkeys and variables, put them into your module's `instructions` (Step 3)\nwithout naming `engine_*` tools: the engine module may be off.\n\n## Step 1: scaffold the package\n\n```bash\ncp -R modules\u002Fmcp-notes modules\u002Fmcp-\u003Cname>\n```\n\n`pnpm-workspace.yaml` already covers `modules\u002F*`, so no workspace edit is\nneeded. Then adjust:\n\n- `package.json`: name it `@\u003Cyour-scope>\u002Fmcp-\u003Cname>` and keep the two-entry\n  `exports` map exactly: `.` (compiled server code) and `.\u002Fwidgets` (pointing\n  at **TypeScript source**, compiled by the server's Vite build). Keep `react`\n  an optional peer: server-side consumers must not need it.\n- Keep the three tsconfigs: `tsconfig.json` excludes `src\u002Fwidgets` from the server\n  build; `tsconfig.widgets.json` type-checks the widget `.tsx` (wired into the\n  `typecheck` script, the only check that covers widget code); and\n  `src\u002Fwidgets\u002Ftsconfig.json` extends it, so editors, which read only files named\n  `tsconfig.json`, check the widgets with the same settings.\n- A module without widgets (its views are finished HTML documents, the `add-widget`\n  skill) deletes `src\u002Fwidgets` and drops the `.\u002Fwidgets` entry, `tsconfig.widgets.json`\n  and the second half of `typecheck`, which would fail on no inputs (`TS18003`).\n- Do a global rename `notes` → `\u003Cname>` across `src\u002F` (module name, plugin\n  name, tool prefixes and thus view names, query keys), `src\u002Fmodule.test.ts`\n  and the widget test included.\n\nThe copy carries its own pins of `@miragon\u002Fpulse-ui`,\n`@miragon\u002Fpulse-widget-shell` and the shared libraries, and they\nmove with the server's: an upgrade bumps them in every module too.\n`server\u002Ftest\u002Fpins.test.ts` names each pin that differs from the installed\n`@miragon\u002Fpulse-widget-shell`.\n\n## Step 2: the data layer\n\n`src\u002Fnotes-store.ts` shows the shape: tools and widgets only ever see an\ninterface; the implementation behind it is yours (an HTTP client, an SDK, a\nDB). Keep the client code in this layer, so handlers stay thin. If the client\ngrows real weight (generated SDK, query library), split it into its own\npackage later: extract on the second consumer, not speculatively.\n\nThe client talks to your system, never to a Camunda engine: a second engine\nclient built from `ENGINE_BASE_URL` skips what the engine module does on every\ncall (the engine pick by `engine` and `ENGINE_DEFAULT`, each engine's auth\nincluding passthrough, its error rules). If the engine tools do not cover a\nneed, stop and ask: custom modules have no supported engine seam yet.\n\n## Step 3: the module definition (`src\u002Fmodule.ts`)\n\nThe contract, field by field (see `notesModule`):\n\n- `name`: the key used in `MCP_ACTIVE_MODULES` and the name of its plugin.\n- `configFromEnv(env)`: a **pure** env → raw-config mapping; no I\u002FO, no\n  `process.env` reads outside the argument. It may check the config and\n  throw one plain `Error` that names the variable, as the engine module\n  does: a wrong setting then fails the boot before its\n  `Toolsets:` line.\n- `knownEnvVars`: every env var your module reads. This feeds the server's\n  boot-time typo warner, and each var contributes its prefix (`ACME_URL` →\n  `ACME_`) to the watched set, so your module gets the same typo coverage as\n  the built-in ones. A variable whose name your own settings carry (the engine\n  module's profiles name `ENGINE_SECRET_*` variables) goes into the optional\n  `referencedEnvVars(env)` instead, which never throws.\n- `toolsets`: required as soon as your module registers a tool without\n  `readOnlyHint: true` and the server may run without OAuth (Step 5). A\n  module of reads only, like notes, leaves it out; a `\u003Cname>:\u003Ctoolset>`\n  suffix on it is ignored with a warning.\n- `createPlugin(config, shared)`: validate `config` with your zod schema\n  (also after a check in `configFromEnv`) and return a `ModulePlugin` (the type\n  from `@miragon\u002Fpulse-widget-shell\u002Fserver`), `{ name, register(server) }`.\n  `server` is a `ModuleServer`: the official MCP SDK's own\n  `registerTool`\u002F`registerPrompt`\u002F`registerResource`, with the signatures its docs\n  (https:\u002F\u002Fts.sdk.modelcontextprotocol.io\u002Fv2\u002F) show. The boot serves your views\n  from the bundle, so a module needs no resource for a widget; a finished HTML\n  view is the exception (the `add-widget` skill, \"A finished HTML view\").\n  Construct your data source\u002Fclient once in `createPlugin`; `register` hands\n  it to `registerTools`, then to `registerWidgetTools`. `name` is the module's\n  name: the boot refuses a tool, prompt or resource name or a resource URI two\n  plugins register and names both owners, and an input schema that would let an\n  unknown key through. The toolset filter never sees a resource, so every client\n  that reaches the server can read it: register one that only a wider toolset\n  may read inside that toolset's branch. `shared` is the port for what the\n  modules of this server share, built by `sharedResources` in\n  `server\u002Fsrc\u002Fsetup.ts`, which hands nothing through today: a field you add\n  to `SharedResources` there, your module types structurally on its side and\n  tolerates its absence.\n- Optional `bootWarnings(env)`: return actionable hints for misconfigured\n  deployments (see the engine module's warning when no engine is configured).\n- Optional `instructions(config)`: a short snippet the\n  server appends to its MCP `instructions` (the rules that hold for EVERY tool\n  of your module: routing, argument conventions, which tool answers what), so\n  tool descriptions stay one-liners. `config` carries the effective `toolset`;\n  return `undefined` to add nothing.\n\n`src\u002Findex.ts`, the package root, exports the module definition and only what\na root wires (`notesModule`); `createPlugin`, the config and the data types stay\ninside the package, as in the Miragon connectors.\n\n`src\u002Fmodule.test.ts` checks the port the way the boot uses it: the plugin's\nname and the tools its `register` adds, recorded by `recordingModuleServer()`\nfrom `@miragon\u002Fpulse-widget-shell\u002Ftesting`, which also calls a handler and reads\na resource (`recording.resource(uri).read()`) the way the SDK does. Keep it in\nstep with your tools.\n\n## Step 4: tools\n\n`src\u002Ftools.ts` holds the plain tools **for the model**, registered through\n`createToolRegistrar(server, store)` (from\n`@miragon\u002Fpulse-widget-shell\u002Fserver`). The second argument is threaded into\nevery handler as its first parameter; the registrar refuses unknown keys.\nA raw `server.registerTool()` is reserved for the widget path (`widget-tools.ts`).\n\nTool conventions (they carry the contract, so they are not cosmetic):\n\n- Names are `\u003Cmodule>_\u003Cverb>_\u003Cnoun>` in snake_case. **Never** use `_show_` or a\n  `_data` suffix for a plain tool: the server's wire-contract e2e test asserts\n  widget `_meta` on every `*_show_*` tool and app-only visibility on every\n  `*_data` feed, by name, across all composed modules including yours.\n- Describe an input field only where it means more than its name; a rule every tool of your\n  module shares goes once into `instructions` (Step 3), which the server sends with every\n  session.\n- Annotations: reads get `{ readOnlyHint: true, idempotentHint: true }`\n  (+ `openWorldHint: true` when they talk to an external system); writes drop\n  `readOnlyHint` and need toolsets (Step 5); every write states\n  `destructiveHint` explicitly (MCP reads an absent hint as `true`): `true`\n  for irreversible operations, `false` otherwise.\n- Write tools that only flip state return a small `{ success: true, … }`\n  object, not the raw (often empty) upstream response.\n\n## Step 5: toolsets (as soon as a tool writes)\n\nA module without toolsets registers every tool whenever it runs. Without OAuth\nnobody signs in, so each of its writes is open to anyone who reaches the\nport; the boot warns once per tool without `readOnlyHint: true`, naming the\ntool and your module. So take this step as soon as your module registers a\ntool without `readOnlyHint: true` and the server may run without OAuth; a\nmodule of reads only skips it. Declare a vocabulary and put it\non the module definition. The boot resolves ONE concrete toolset per\nboot, logs it, and hands it to `createPlugin` as `config.toolset`:\n\n```ts\nimport { createToolsetVocabulary } from \"@miragon\u002Fpulse-widget-shell\u002Fserver\"\n\nexport const MY_TOOLSETS = [\"read-only\", \"standard\"] as const\nexport const myToolsets = createToolsetVocabulary(\"\u003Cname>\", MY_TOOLSETS, \"read-only\", {\n  authenticatedDefault: \"standard\",\n})\n\u002F\u002F src\u002Fmodule.ts: `toolsets: myToolsets,` on the module definition\n```\n\n- Accept it in your config schema (`toolset: z.string().optional()`) and\n  resolve it once in `createPlugin` with `myToolsets.resolve(config.toolset)`.\n- The third argument is your READ-ONLY FLOOR (it must permit no write). No suffix on a boot without OAuth, an empty or unknown suffix\n  (with a warning), and a direct `createPlugin` caller passing no toolset all\n  land there.\n- `authenticatedDefault` is the no-suffix default under OAuth, which the boot\n  installs from `MCP_OAUTH_*`, so every OAuth boot\n  that names no suffix runs it: your standard non-admin toolset. Never make it\n  your widest one: an admin-like toolset must only be reachable by naming it.\n- Write ONE rule function for the module, `isToolInToolset(config, toolset)`\n  over the toolset `resolve` returned: admin-only names first, then the\n  annotations (read-only ⇔ `readOnlyHint: true`). Apply it through ONE\n  registrar wrapper, `withToolsetFilter`, so every tool, writes included,\n  registers through the registrar and the filter decides it. Never a scattered\n  `toolset === \"read-only\"` compare (fails open for every name you add later)\n  or a `toolset === undefined` shortcut (an absent toolset is the floor, not\n  \"everything\"). Reference:\n  `server\u002Fnode_modules\u002F@miragon\u002Fpulse-engine-connector\u002Fsrc\u002Flib\u002Fmcp\u002Ftoolsets.ts`.\n\n## Step 6: wire the module into the server\n\nFour edits, plus the env documentation:\n\n1. `server\u002Fpackage.json`: add `\"@\u003Cyour-scope>\u002Fmcp-\u003Cname>\": \"workspace:*\"`,\n   then `pnpm install`.\n2. `server\u002Fsrc\u002Fsetup.ts`: add your module to `MODULES`.\n3. `server\u002Fsrc\u002Fui\u002Fview-registry.ts`: spread your `\u003Cname>Views` map into\n   `viewRegistry` (needed once you have show tools; harmless before).\n4. `MCP_ACTIVE_MODULES` in `.env.example` and in your `.env`: add `\u003Cname>` to\n   the list. A module the list does not name stays off and its tools are absent\n   (the boot log's `Toolsets:` line shows what runs).\n   `server\u002Ftest\u002Fenv-example.test.ts` holds `.env.example` to `MODULES`; nothing\n   checks your `.env`, so a stale copy boots without your module.\n\nA missed spread is **silent** (tools work, the view says it can't show\nitself), which is why `server\u002Ftest\u002Fview-registry.test.ts` covers you\nwithout an edit: it boots the real composition (edit 2) and fails naming a\nview-bound tool without a view. A module whose views are finished HTML documents\nhas nothing to spread: it skips edit 3 and the Tailwind line below.\n\nTailwind needs no edit: `server\u002Fsrc\u002Fui\u002Fglobals.css` scans every workspace\nmodule through the `modules\u002F*\u002Fsrc` glob. Only an npm-installed module\npackage with widget sources would need its own `@source` line there.\n\nThen document every var from `knownEnvVars` in `.env.example`.\n`server\u002Ftest\u002Fenv-example.test.ts` fails on both drift directions (an\nundocumented var, or a documented var no module reads).\n\nThe other guard tests in `server\u002Ftest\u002F` need no edit either: they boot the\ncomposed server in-process and derive what they check from `MODULES` and the\ntools it lists, so they cover your module by naming convention and on every\ntoolset it declares. Only `server\u002Ftest\u002Fexample-notes.test.ts` names the\nexample module, and it goes with it (see \"Remove the example module\").\n\n## Step 7: verify\n\n```bash\npnpm build && pnpm typecheck && pnpm test\n```\n\nThen boot it for real: `pnpm dev`, check the boot log (your module's env-var\ntypo coverage and boot warnings show up here, and a warning names each write\na module without toolsets leaves open), and call your tools in the MCP\nInspector `pnpm dev` starts (open the URL it prints), or headless with its CLI\n(`CLAUDE.md` → Verification). Also verify\nthe selection path: `MCP_ACTIVE_MODULES=\u003Cname> pnpm dev` must expose exactly\nyour module's tools (the shell's value beats `.env`). If you declared `toolsets`, that is its read-only floor\nwithout OAuth and its `authenticatedDefault` with it (the boot log's\n`Toolsets:` line says which).\n\n## Remove the example module\n\n`modules\u002Fmcp-notes` is an example to copy, not part of your server. Once your\nown module stands, take the example out in one change:\n\n1. `server\u002Fsrc\u002Fsetup.ts`: drop `notesModule` from `MODULES` and its import.\n2. `server\u002Fsrc\u002Fui\u002Fview-registry.ts`: drop the `notesViews` spread and its\n   import.\n3. `server\u002Fpackage.json`: drop the `@acme\u002Fmcp-notes` dependency and the notes\n   in `description`; delete `modules\u002Fmcp-notes`, then `pnpm install`.\n4. `.env.example`: drop the Notes block (`NOTES_TITLE`), `notes` from the live\n   `MCP_ACTIVE_MODULES` value and every other `notes` mention (that variable's\n   comment). Take it out of your `.env` too: the boot refuses a name the list\n   still holds once `MODULES` no longer does.\n5. `server\u002Fsrc\u002Fapp.ts`: `SERVER_INSTRUCTIONS` tells the model the server holds\n   team notes; say what yours does.\n6. Delete `server\u002Ftest\u002Fexample-notes.test.ts`, the one test that names the\n   example.\n7. The smoke checks that name `notes_show_notes` (`CLAUDE.md` → Commands and\n   Verification, the `setup-server` skill's Step 1, the README's Quickstart)\n   take a show tool of yours, and the README's Configuration names your\n   module where it names notes (the toolset prose and the variable table).\n8. Every module list that still names `notes` names your module: the README's\n   Quickstart (`MCP_ACTIVE_MODULES=engine,notes`),\n   `CLAUDE.md` (`engine:operations,notes` and the comment on\n   `cp .env.example .env`) and the `setup-server` skill (Steps 1, 2, 3 and 5,\n   Troubleshooting). Copied as it stands, such a value fails the boot.\n   `git grep -n notes` shows what is left, including the comment in\n   `server\u002Fsrc\u002Fsetup.ts` that names the deleted test.\n\nThen run Step 7's bar. The other guard tests follow `MODULES`, so they need no\nedit.\n\n## Anti-patterns\n\n- Importing another module (or the server) instead of conforming\n  structurally: peer isolation is the architecture.\n- Reading `process.env` anywhere in the module but `configFromEnv`'s\n  argument, or letting a check there throw a ZodError: the boot prints its\n  schema path (`engines[0].id`), not the variable to fix.\n- Registering plain tools with a raw `server.registerTool()`: the registrar is\n  the contract; the raw path is for widget tools only.\n- Naming a plain tool `*_data` or `*_show_*`: the e2e naming contract will\n  fail it, correctly.\n- Talking to your backend from widget code: widgets read through `*_data`\n  feeds only, via the view runtime's query hooks (see the add-widget skill).\n","# create-module: your own connector module\n\nA module is a workspace package that exports a module-definition object\nconforming **structurally** to the server's port (the `ModuleDefinition` type\nin `server\u002Fsrc\u002Fsetup.ts`). Your module never imports the server, and the\nserver never reaches into your module. Modules are peers: they never import\neach other; anything shared goes through `@miragon\u002Fpulse-ui` (the design kit),\n`@miragon\u002Fpulse-widget-shell` or the server-wired `SharedResources` (today empty).\n\n**Reference implementation: `modules\u002Fmcp-notes`.** Copy it rather than\ninventing a new shape; every step below points at the file that shows the\npattern. For patterns the notes module doesn't cover (toolsets, paged\nlists), the published engine module ships its sources under\n`server\u002Fnode_modules\u002F@miragon\u002Fpulse-engine-connector\u002Fsrc\u002F`\n(a gitignored symlink: search that package directory, not the repo root; see\n`CLAUDE.md` → Reference code).\n\n**A module is for your other systems, not for the engine.** The engine\nmodule already registers the generic engine operations as tools\n(`engine_start_process_instance`, `engine_correlate_message`,\n`engine_set_job_retries` and more), so \"start our leasing process\" needs no\nnew tool. A listing shows only the running toolset: these writes appear with\nOAuth or under `MCP_ACTIVE_MODULES=engine:operations,\u003Cyours>`, never\nin the default read-only boot. List them in the MCP Inspector `pnpm dev`\nstarts, or headless from `server\u002F` with its CLI\n(`pnpm exec mcp-inspector --cli --server-url http:\u002F\u002F127.0.0.1:8400\u002Fmcp --transport http --method tools\u002Flist`). To give the model your processes'\nkeys and variables, put them into your module's `instructions` (Step 3)\nwithout naming `engine_*` tools: the engine module may be off.\n\n## Step 1: scaffold the package\n\n```bash\ncp -R modules\u002Fmcp-notes modules\u002Fmcp-\u003Cname>\n```\n\n`pnpm-workspace.yaml` already covers `modules\u002F*`, so no workspace edit is\nneeded. Then adjust:\n\n- `package.json`: name it `@\u003Cyour-scope>\u002Fmcp-\u003Cname>` and keep the two-entry\n  `exports` map exactly: `.` (compiled server code) and `.\u002Fwidgets` (pointing\n  at **TypeScript source**, compiled by the server's Vite build). Keep `react`\n  an optional peer: server-side consumers must not need it.\n- Keep the three tsconfigs: `tsconfig.json` excludes `src\u002Fwidgets` from the server\n  build; `tsconfig.widgets.json` type-checks the widget `.tsx` (wired into the\n  `typecheck` script, the only check that covers widget code); and\n  `src\u002Fwidgets\u002Ftsconfig.json` extends it, so editors, which read only files named\n  `tsconfig.json`, check the widgets with the same settings.\n- A module without widgets (its views are finished HTML documents, the `add-widget`\n  skill) deletes `src\u002Fwidgets` and drops the `.\u002Fwidgets` entry, `tsconfig.widgets.json`\n  and the second half of `typecheck`, which would fail on no inputs (`TS18003`).\n- Do a global rename `notes` → `\u003Cname>` across `src\u002F` (module name, plugin\n  name, tool prefixes and thus view names, query keys), `src\u002Fmodule.test.ts`\n  and the widget test included.\n\nThe copy carries its own pins of `@miragon\u002Fpulse-ui`,\n`@miragon\u002Fpulse-widget-shell` and the shared libraries, and they\nmove with the server's: an upgrade bumps them in every module too.\n`server\u002Ftest\u002Fpins.test.ts` names each pin that differs from the installed\n`@miragon\u002Fpulse-widget-shell`.\n\n## Step 2: the data layer\n\n`src\u002Fnotes-store.ts` shows the shape: tools and widgets only ever see an\ninterface; the implementation behind it is yours (an HTTP client, an SDK, a\nDB). Keep the client code in this layer, so handlers stay thin. If the client\ngrows real weight (generated SDK, query library), split it into its own\npackage later: extract on the second consumer, not speculatively.\n\nThe client talks to your system, never to a Camunda engine: a second engine\nclient built from `ENGINE_BASE_URL` skips what the engine module does on every\ncall (the engine pick by `engine` and `ENGINE_DEFAULT`, each engine's auth\nincluding passthrough, its error rules). If the engine tools do not cover a\nneed, stop and ask: custom modules have no supported engine seam yet.\n\n## Step 3: the module definition (`src\u002Fmodule.ts`)\n\nThe contract, field by field (see `notesModule`):\n\n- `name`: the key used in `MCP_ACTIVE_MODULES` and the name of its plugin.\n- `configFromEnv(env)`: a **pure** env → raw-config mapping; no I\u002FO, no\n  `process.env` reads outside the argument. It may check the config and\n  throw one plain `Error` that names the variable, as the engine module\n  does: a wrong setting then fails the boot before its\n  `Toolsets:` line.\n- `knownEnvVars`: every env var your module reads. This feeds the server's\n  boot-time typo warner, and each var contributes its prefix (`ACME_URL` →\n  `ACME_`) to the watched set, so your module gets the same typo coverage as\n  the built-in ones. A variable whose name your own settings carry (the engine\n  module's profiles name `ENGINE_SECRET_*` variables) goes into the optional\n  `referencedEnvVars(env)` instead, which never throws.\n- `toolsets`: required as soon as your module registers a tool without\n  `readOnlyHint: true` and the server may run without OAuth (Step 5). A\n  module of reads only, like notes, leaves it out; a `\u003Cname>:\u003Ctoolset>`\n  suffix on it is ignored with a warning.\n- `createPlugin(config, shared)`: validate `config` with your zod schema\n  (also after a check in `configFromEnv`) and return a `ModulePlugin` (the type\n  from `@miragon\u002Fpulse-widget-shell\u002Fserver`), `{ name, register(server) }`.\n  `server` is a `ModuleServer`: the official MCP SDK's own\n  `registerTool`\u002F`registerPrompt`\u002F`registerResource`, with the signatures its docs\n  (https:\u002F\u002Fts.sdk.modelcontextprotocol.io\u002Fv2\u002F) show. The boot serves your views\n  from the bundle, so a module needs no resource for a widget; a finished HTML\n  view is the exception (the `add-widget` skill, \"A finished HTML view\").\n  Construct your data source\u002Fclient once in `createPlugin`; `register` hands\n  it to `registerTools`, then to `registerWidgetTools`. `name` is the module's\n  name: the boot refuses a tool, prompt or resource name or a resource URI two\n  plugins register and names both owners, and an input schema that would let an\n  unknown key through. The toolset filter never sees a resource, so every client\n  that reaches the server can read it: register one that only a wider toolset\n  may read inside that toolset's branch. `shared` is the port for what the\n  modules of this server share, built by `sharedResources` in\n  `server\u002Fsrc\u002Fsetup.ts`, which hands nothing through today: a field you add\n  to `SharedResources` there, your module types structurally on its side and\n  tolerates its absence.\n- Optional `bootWarnings(env)`: return actionable hints for misconfigured\n  deployments (see the engine module's warning when no engine is configured).\n- Optional `instructions(config)`: a short snippet the\n  server appends to its MCP `instructions` (the rules that hold for EVERY tool\n  of your module: routing, argument conventions, which tool answers what), so\n  tool descriptions stay one-liners. `config` carries the effective `toolset`;\n  return `undefined` to add nothing.\n\n`src\u002Findex.ts`, the package root, exports the module definition and only what\na root wires (`notesModule`); `createPlugin`, the config and the data types stay\ninside the package, as in the Miragon connectors.\n\n`src\u002Fmodule.test.ts` checks the port the way the boot uses it: the plugin's\nname and the tools its `register` adds, recorded by `recordingModuleServer()`\nfrom `@miragon\u002Fpulse-widget-shell\u002Ftesting`, which also calls a handler and reads\na resource (`recording.resource(uri).read()`) the way the SDK does. Keep it in\nstep with your tools.\n\n## Step 4: tools\n\n`src\u002Ftools.ts` holds the plain tools **for the model**, registered through\n`createToolRegistrar(server, store)` (from\n`@miragon\u002Fpulse-widget-shell\u002Fserver`). The second argument is threaded into\nevery handler as its first parameter; the registrar refuses unknown keys.\nA raw `server.registerTool()` is reserved for the widget path (`widget-tools.ts`).\n\nTool conventions (they carry the contract, so they are not cosmetic):\n\n- Names are `\u003Cmodule>_\u003Cverb>_\u003Cnoun>` in snake_case. **Never** use `_show_` or a\n  `_data` suffix for a plain tool: the server's wire-contract e2e test asserts\n  widget `_meta` on every `*_show_*` tool and app-only visibility on every\n  `*_data` feed, by name, across all composed modules including yours.\n- Describe an input field only where it means more than its name; a rule every tool of your\n  module shares goes once into `instructions` (Step 3), which the server sends with every\n  session.\n- Annotations: reads get `{ readOnlyHint: true, idempotentHint: true }`\n  (+ `openWorldHint: true` when they talk to an external system); writes drop\n  `readOnlyHint` and need toolsets (Step 5); every write states\n  `destructiveHint` explicitly (MCP reads an absent hint as `true`): `true`\n  for irreversible operations, `false` otherwise.\n- Write tools that only flip state return a small `{ success: true, … }`\n  object, not the raw (often empty) upstream response.\n\n## Step 5: toolsets (as soon as a tool writes)\n\nA module without toolsets registers every tool whenever it runs. Without OAuth\nnobody signs in, so each of its writes is open to anyone who reaches the\nport; the boot warns once per tool without `readOnlyHint: true`, naming the\ntool and your module. So take this step as soon as your module registers a\ntool without `readOnlyHint: true` and the server may run without OAuth; a\nmodule of reads only skips it. Declare a vocabulary and put it\non the module definition. The boot resolves ONE concrete toolset per\nboot, logs it, and hands it to `createPlugin` as `config.toolset`:\n\n```ts\nimport { createToolsetVocabulary } from \"@miragon\u002Fpulse-widget-shell\u002Fserver\"\n\nexport const MY_TOOLSETS = [\"read-only\", \"standard\"] as const\nexport const myToolsets = createToolsetVocabulary(\"\u003Cname>\", MY_TOOLSETS, \"read-only\", {\n  authenticatedDefault: \"standard\",\n})\n\u002F\u002F src\u002Fmodule.ts: `toolsets: myToolsets,` on the module definition\n```\n\n- Accept it in your config schema (`toolset: z.string().optional()`) and\n  resolve it once in `createPlugin` with `myToolsets.resolve(config.toolset)`.\n- The third argument is your READ-ONLY FLOOR (it must permit no write). No suffix on a boot without OAuth, an empty or unknown suffix\n  (with a warning), and a direct `createPlugin` caller passing no toolset all\n  land there.\n- `authenticatedDefault` is the no-suffix default under OAuth, which the boot\n  installs from `MCP_OAUTH_*`, so every OAuth boot\n  that names no suffix runs it: your standard non-admin toolset. Never make it\n  your widest one: an admin-like toolset must only be reachable by naming it.\n- Write ONE rule function for the module, `isToolInToolset(config, toolset)`\n  over the toolset `resolve` returned: admin-only names first, then the\n  annotations (read-only ⇔ `readOnlyHint: true`). Apply it through ONE\n  registrar wrapper, `withToolsetFilter`, so every tool, writes included,\n  registers through the registrar and the filter decides it. Never a scattered\n  `toolset === \"read-only\"` compare (fails open for every name you add later)\n  or a `toolset === undefined` shortcut (an absent toolset is the floor, not\n  \"everything\"). Reference:\n  `server\u002Fnode_modules\u002F@miragon\u002Fpulse-engine-connector\u002Fsrc\u002Flib\u002Fmcp\u002Ftoolsets.ts`.\n\n## Step 6: wire the module into the server\n\nFour edits, plus the env documentation:\n\n1. `server\u002Fpackage.json`: add `\"@\u003Cyour-scope>\u002Fmcp-\u003Cname>\": \"workspace:*\"`,\n   then `pnpm install`.\n2. `server\u002Fsrc\u002Fsetup.ts`: add your module to `MODULES`.\n3. `server\u002Fsrc\u002Fui\u002Fview-registry.ts`: spread your `\u003Cname>Views` map into\n   `viewRegistry` (needed once you have show tools; harmless before).\n4. `MCP_ACTIVE_MODULES` in `.env.example` and in your `.env`: add `\u003Cname>` to\n   the list. A module the list does not name stays off and its tools are absent\n   (the boot log's `Toolsets:` line shows what runs).\n   `server\u002Ftest\u002Fenv-example.test.ts` holds `.env.example` to `MODULES`; nothing\n   checks your `.env`, so a stale copy boots without your module.\n\nA missed spread is **silent** (tools work, the view says it can't show\nitself), which is why `server\u002Ftest\u002Fview-registry.test.ts` covers you\nwithout an edit: it boots the real composition (edit 2) and fails naming a\nview-bound tool without a view. A module whose views are finished HTML documents\nhas nothing to spread: it skips edit 3 and the Tailwind line below.\n\nTailwind needs no edit: `server\u002Fsrc\u002Fui\u002Fglobals.css` scans every workspace\nmodule through the `modules\u002F*\u002Fsrc` glob. Only an npm-installed module\npackage with widget sources would need its own `@source` line there.\n\nThen document every var from `knownEnvVars` in `.env.example`.\n`server\u002Ftest\u002Fenv-example.test.ts` fails on both drift directions (an\nundocumented var, or a documented var no module reads).\n\nThe other guard tests in `server\u002Ftest\u002F` need no edit either: they boot the\ncomposed server in-process and derive what they check from `MODULES` and the\ntools it lists, so they cover your module by naming convention and on every\ntoolset it declares. Only `server\u002Ftest\u002Fexample-notes.test.ts` names the\nexample module, and it goes with it (see \"Remove the example module\").\n\n## Step 7: verify\n\n```bash\npnpm build && pnpm typecheck && pnpm test\n```\n\nThen boot it for real: `pnpm dev`, check the boot log (your module's env-var\ntypo coverage and boot warnings show up here, and a warning names each write\na module without toolsets leaves open), and call your tools in the MCP\nInspector `pnpm dev` starts (open the URL it prints), or headless with its CLI\n(`CLAUDE.md` → Verification). Also verify\nthe selection path: `MCP_ACTIVE_MODULES=\u003Cname> pnpm dev` must expose exactly\nyour module's tools (the shell's value beats `.env`). If you declared `toolsets`, that is its read-only floor\nwithout OAuth and its `authenticatedDefault` with it (the boot log's\n`Toolsets:` line says which).\n\n## Remove the example module\n\n`modules\u002Fmcp-notes` is an example to copy, not part of your server. Once your\nown module stands, take the example out in one change:\n\n1. `server\u002Fsrc\u002Fsetup.ts`: drop `notesModule` from `MODULES` and its import.\n2. `server\u002Fsrc\u002Fui\u002Fview-registry.ts`: drop the `notesViews` spread and its\n   import.\n3. `server\u002Fpackage.json`: drop the `@acme\u002Fmcp-notes` dependency and the notes\n   in `description`; delete `modules\u002Fmcp-notes`, then `pnpm install`.\n4. `.env.example`: drop the Notes block (`NOTES_TITLE`), `notes` from the live\n   `MCP_ACTIVE_MODULES` value and every other `notes` mention (that variable's\n   comment). Take it out of your `.env` too: the boot refuses a name the list\n   still holds once `MODULES` no longer does.\n5. `server\u002Fsrc\u002Fapp.ts`: `SERVER_INSTRUCTIONS` tells the model the server holds\n   team notes; say what yours does.\n6. Delete `server\u002Ftest\u002Fexample-notes.test.ts`, the one test that names the\n   example.\n7. The smoke checks that name `notes_show_notes` (`CLAUDE.md` → Commands and\n   Verification, the `setup-server` skill's Step 1, the README's Quickstart)\n   take a show tool of yours, and the README's Configuration names your\n   module where it names notes (the toolset prose and the variable table).\n8. Every module list that still names `notes` names your module: the README's\n   Quickstart (`MCP_ACTIVE_MODULES=engine,notes`),\n   `CLAUDE.md` (`engine:operations,notes` and the comment on\n   `cp .env.example .env`) and the `setup-server` skill (Steps 1, 2, 3 and 5,\n   Troubleshooting). Copied as it stands, such a value fails the boot.\n   `git grep -n notes` shows what is left, including the comment in\n   `server\u002Fsrc\u002Fsetup.ts` that names the deleted test.\n\nThen run Step 7's bar. The other guard tests follow `MODULES`, so they need no\nedit.\n\n## Anti-patterns\n\n- Importing another module (or the server) instead of conforming\n  structurally: peer isolation is the architecture.\n- Reading `process.env` anywhere in the module but `configFromEnv`'s\n  argument, or letting a check there throw a ZodError: the boot prints its\n  schema path (`engines[0].id`), not the variable to fix.\n- Registering plain tools with a raw `server.registerTool()`: the registrar is\n  the contract; the raw path is for widget tools only.\n- Naming a plain tool `*_data` or `*_show_*`: the e2e naming contract will\n  fail it, correctly.\n- Talking to your backend from widget code: widgets read through `*_data`\n  feeds only, via the view runtime's query hooks (see the add-widget skill).\n",{},"main","ff711bb6e1eb952767e56e3bdd4d7c249ced5b68","templates\u002Fcomposed-server\u002F.claude\u002Fskills\u002Fcreate-module",0,"https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse","https:\u002F\u002Fmiragon.github.io\u002Fpulse\u002F","https:\u002F\u002Fgithub.com\u002FMiragon","https:\u002F\u002Fwww.miragon.io","We enable organizations to become truly digital.","2026-11-14 03:34:19",{"source":7,"skillName":5,"commands":36,"prompt":46,"zipAvailable":47},[37,40,43],{"tool":38,"command":39},"npx","npx skills add Miragon\u002Fpulse --skill create-module",{"tool":41,"command":42},"bunx","bunx skills add Miragon\u002Fpulse --skill create-module",{"tool":44,"command":45},"pnpm","pnpm dlx skills add Miragon\u002Fpulse --skill create-module","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse\u002Fblob\u002Fmain\u002Ftemplates\u002Fcomposed-server\u002F.claude\u002Fskills\u002Fcreate-module\u002FSKILL.md",true,[],[50,88,115,142,167,191],{"skillNo":51,"name":52,"nameZh":6,"description":53,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":8,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":54,"categories":55,"keywords":67,"publishedAt":20},"01M6ENA6VEQJXWWXWE52B9JPZK","setup-server","Set up, configure, run, brand, and deploy this composed MCP server. Use whenever connecting a Camunda 7 \u002F CIB seven engine, selecting modules or toolsets (`MCP_ACTIVE_MODULES`), renaming the `@acme` scope, building the Docker image, running behind an MCP gateway, or troubleshooting boot warnings or inspector issues.","https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse\u002Fblob\u002Fmain\u002Ftemplates\u002Fcomposed-server\u002F.claude\u002Fskills\u002Fsetup-server\u002FSKILL.md",[56,61,64],{"categoryNo":57,"categoryName":58,"taxonomyType":59,"taxonomyTypeDesc":60},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":62,"categoryName":63,"taxonomyType":59,"taxonomyTypeDesc":60},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":65,"categoryName":66,"taxonomyType":59,"taxonomyTypeDesc":60},"01JBSEEDCAT000000000000023","Skill与智能体开发",[68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87],"setup server","server","setup","modules","SET","gateway","prometheus","brand","scope","docker","up","issues","MCP","use","boot","configure","image","Bash","mcp server","running",{"skillNo":89,"name":90,"nameZh":6,"description":91,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":8,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":92,"categories":93,"keywords":97,"publishedAt":20},"01M6EN9ZEDJZMEAJH3RP50NV5S","add-widget","Step-by-step house pattern for adding an interactive widget to a module (the React component built from the design kit `@miragon\u002Fpulse-ui` and widget-shell's view runtime, the `show_*` widget tool, the app-only `*_data` feed, the view map keyed by the show tool's name, and a widget test under `TestViewHost`). Use whenever adding or changing widgets, `show_*` tools, `*_data` feeds, in-widget data fetching, or when debugging a widget that renders empty, unstyled, or hangs on \"Loading…\".","https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse\u002Fblob\u002Fmain\u002Ftemplates\u002Fcomposed-server\u002F.claude\u002Fskills\u002Fadd-widget\u002FSKILL.md",[94],{"categoryNo":95,"categoryName":96,"taxonomyType":59,"taxonomyTypeDesc":60},"01JBSEEDCAT000000000000009","前端开发",[98,99,100,101,102,103,104,18,105,81,106,85,107,108,109,110,111,112,113,114],"add widget","widget","add","app","feeds","show","tool","only","debugging","view","for","loading","runtime","design","kit","map","when",{"skillNo":116,"name":117,"nameZh":6,"description":118,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":8,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":119,"categories":120,"keywords":121,"publishedAt":20},"01M6EN9PRZFNEA99X41BWPYVAB","pulse-cli","Inspect and operate Camunda 7, Operaton and CIB seven process engines through their REST API with the Pulse CLI (deployments, process instances, user tasks, external tasks, incidents, jobs, history, variables, decisions), and drive process instances with its workflow commands (inspect, wait, advance, retry, deploy, status). Use it whenever a task needs to read or change the state of such an engine.","https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse\u002Fblob\u002Fmain\u002Fapps\u002Fcli\u002Fskills\u002Fpulse-cli\u002FSKILL.md",[],[122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141],"pulse cli","pulse","cli","instances","history","advance","deployments","workflow","tasks","api","operate","retry","inspect","incidents","Variables","change","read","commands","status","wait",{"skillNo":143,"name":144,"nameZh":6,"description":145,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":8,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":146,"categories":147,"keywords":148,"publishedAt":20},"01M6EN9J8S9R51AP99F0YBBQM7","shadcn","Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn\u002Fui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for \"shadcn init\", \"create an app with --preset\", or \"switch to --preset\".","https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fshadcn\u002FSKILL.md",[],[144,101,149,150,151,152,153,154,155,156,106,157,158,159,160,19,161,162,163,164,165,108,114,166],"projects","component docs","switch","project","init","triggers","codes","false","file","components","examples","preset","component","json","project context","styling","usage","with",{"skillNo":168,"name":169,"nameZh":6,"description":170,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":8,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":171,"categories":172,"keywords":175,"publishedAt":20},"01M6EN9ETKR0TQJRVBVEHW17VM","mcp-apps-builder","Where to look before changing MCP server code in this repo (tools, resources, prompts, widgets, auth). Points to the house skills, the CLAUDE.md invariants, widget-shell's view runtime and the official MCP SDK's typings and docs; it teaches no framework API itself.","https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fmcp-apps-builder\u002FSKILL.md",[173,174],{"categoryNo":57,"categoryName":58,"taxonomyType":59,"taxonomyTypeDesc":60},{"categoryNo":65,"categoryName":66,"taxonomyType":59,"taxonomyTypeDesc":60},[176,80,177,178,179,69,180,181,182,183,184,81,86,185,107,110,186,187,99,131,188,189,190],"builder","apps","mcp apps builder","mcp apps","resources","widgets","auth","repo","look","code","shell","invariants","official","mcp sdk","sdk",{"skillNo":192,"name":193,"nameZh":6,"description":194,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":8,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":195,"categories":196,"keywords":200,"publishedAt":20},"01M6EN9BKN04C3NG1Q6CXBR92T","create-ticket","Create or update a GitHub issue for Pulse using the `gh` CLI. Use when the user asks to \"file a bug\", \"create a feature request\", \"open a GitHub issue\", or \"update an existing one\". Supports feature, bug, and refactor issue types with structured templates; shows a draft for confirmation before creating or editing; looks up existing issues by number or URL for updates.","https:\u002F\u002Fgithub.com\u002FMiragon\u002Fpulse\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fcreate-ticket\u002FSKILL.md",[197],{"categoryNo":198,"categoryName":199,"taxonomyType":59,"taxonomyTypeDesc":60},"01JBSEEDCAT000000000000007","代码审查",[201,19,202,203,204,205,78,79,206,207,81,208,85,157,209,108,210,211,212,213,114],"ticket","create ticket","types","feature","editing","open","gh","confirmation","draft","Existing","updates","number","URL"]