AGENT SKILL

create-module

暂无简介

License 许可
MIT
所属仓库
Miragon/pulse
最近更新
2026-11-14
Stars
★ 8
Forks
0

03 SKILL.md 正文

create-module: your own connector module

A module is a workspace package that exports a module-definition object conforming structurally to the server's port (the ModuleDefinition type in server/src/setup.ts). Your module never imports the server, and the server never reaches into your module. Modules are peers: they never import each other; anything shared goes through @miragon/pulse-ui (the design kit), @miragon/pulse-widget-shell or the server-wired SharedResources (today empty).

Reference implementation: modules/mcp-notes. Copy it rather than inventing a new shape; every step below points at the file that shows the pattern. For patterns the notes module doesn't cover (toolsets, paged lists), the published engine module ships its sources under server/node_modules/@miragon/pulse-engine-connector/src/ (a gitignored symlink: search that package directory, not the repo root; see CLAUDE.md → Reference code).

A module is for your other systems, not for the engine. The engine module already registers the generic engine operations as tools (engine_start_process_instance, engine_correlate_message, engine_set_job_retries and more), so "start our leasing process" needs no new tool. A listing shows only the running toolset: these writes appear with OAuth or under MCP_ACTIVE_MODULES=engine:operations,<yours>, never in the default read-only boot. List them in the MCP Inspector pnpm dev starts, or headless from server/ with its CLI (pnpm exec mcp-inspector --cli --server-url http://127.0.0.1:8400/mcp --transport http --method tools/list). To give the model your processes' keys and variables, put them into your module's instructions (Step 3) without naming engine_* tools: the engine module may be off.

Step 1: scaffold the package

cp -R modules/mcp-notes modules/mcp-<name>

pnpm-workspace.yaml already covers modules/*, so no workspace edit is needed. Then adjust:

  • package.json: name it @<your-scope>/mcp-<name> and keep the two-entry exports map exactly: . (compiled server code) and ./widgets (pointing at TypeScript source, compiled by the server's Vite build). Keep react an optional peer: server-side consumers must not need it.
  • Keep the three tsconfigs: tsconfig.json excludes src/widgets from the server build; tsconfig.widgets.json type-checks the widget .tsx (wired into the typecheck script, the only check that covers widget code); and src/widgets/tsconfig.json extends it, so editors, which read only files named tsconfig.json, check the widgets with the same settings.
  • A module without widgets (its views are finished HTML documents, the add-widget skill) deletes src/widgets and drops the ./widgets entry, tsconfig.widgets.json and the second half of typecheck, which would fail on no inputs (TS18003).
  • Do a global rename notes → <name> across src/ (module name, plugin name, tool prefixes and thus view names, query keys), src/module.test.ts and the widget test included.

The copy carries its own pins of @miragon/pulse-ui, @miragon/pulse-widget-shell and the shared libraries, and they move with the server's: an upgrade bumps them in every module too. server/test/pins.test.ts names each pin that differs from the installed @miragon/pulse-widget-shell.

Step 2: the data layer

src/notes-store.ts shows the shape: tools and widgets only ever see an interface; the implementation behind it is yours (an HTTP client, an SDK, a DB). Keep the client code in this layer, so handlers stay thin. If the client grows real weight (generated SDK, query library), split it into its own package later: extract on the second consumer, not speculatively.

The client talks to your system, never to a Camunda engine: a second engine client built from ENGINE_BASE_URL skips what the engine module does on every call (the engine pick by engine and ENGINE_DEFAULT, each engine's auth including passthrough, its error rules). If the engine tools do not cover a need, stop and ask: custom modules have no supported engine seam yet.

Step 3: the module definition (src/module.ts)

The contract, field by field (see notesModule):

  • name: the key used in MCP_ACTIVE_MODULES and the name of its plugin.
  • configFromEnv(env): a pure env → raw-config mapping; no I/O, no process.env reads outside the argument. It may check the config and throw one plain Error that names the variable, as the engine module does: a wrong setting then fails the boot before its Toolsets: line.
  • knownEnvVars: every env var your module reads. This feeds the server's boot-time typo warner, and each var contributes its prefix (ACME_URL → ACME_) to the watched set, so your module gets the same typo coverage as the built-in ones. A variable whose name your own settings carry (the engine module's profiles name ENGINE_SECRET_* variables) goes into the optional referencedEnvVars(env) instead, which never throws.
  • toolsets: required as soon as your module registers a tool without readOnlyHint: true and the server may run without OAuth (Step 5). A module of reads only, like notes, leaves it out; a <name>:<toolset> suffix on it is ignored with a warning.
  • createPlugin(config, shared): validate config with your zod schema (also after a check in configFromEnv) and return a ModulePlugin (the type from @miragon/pulse-widget-shell/server), { name, register(server) }. server is a ModuleServer: the official MCP SDK's own registerTool/registerPrompt/registerResource, with the signatures its docs (https://ts.sdk.modelcontextprotocol.io/v2/) show. The boot serves your views from the bundle, so a module needs no resource for a widget; a finished HTML view is the exception (the add-widget skill, "A finished HTML view"). Construct your data source/client once in createPlugin; register hands it to registerTools, then to registerWidgetTools. name is the module's name: the boot refuses a tool, prompt or resource name or a resource URI two plugins register and names both owners, and an input schema that would let an unknown key through. The toolset filter never sees a resource, so every client that reaches the server can read it: register one that only a wider toolset may read inside that toolset's branch. shared is the port for what the modules of this server share, built by sharedResources in server/src/setup.ts, which hands nothing through today: a field you add to SharedResources there, your module types structurally on its side and tolerates its absence.
  • Optional bootWarnings(env): return actionable hints for misconfigured deployments (see the engine module's warning when no engine is configured).
  • Optional instructions(config): a short snippet the server appends to its MCP instructions (the rules that hold for EVERY tool of your module: routing, argument conventions, which tool answers what), so tool descriptions stay one-liners. config carries the effective toolset; return undefined to add nothing.

src/index.ts, the package root, exports the module definition and only what a root wires (notesModule); createPlugin, the config and the data types stay inside the package, as in the Miragon connectors.

src/module.test.ts checks the port the way the boot uses it: the plugin's name and the tools its register adds, recorded by recordingModuleServer() from @miragon/pulse-widget-shell/testing, which also calls a handler and reads a resource (recording.resource(uri).read()) the way the SDK does. Keep it in step with your tools.

Step 4: tools

src/tools.ts holds the plain tools for the model, registered through createToolRegistrar(server, store) (from @miragon/pulse-widget-shell/server). The second argument is threaded into every handler as its first parameter; the registrar refuses unknown keys. A raw server.registerTool() is reserved for the widget path (widget-tools.ts).

Tool conventions (they carry the contract, so they are not cosmetic):

  • Names are <module>_<verb>_<noun> in snake_case. Never use _show_ or a _data suffix for a plain tool: the server's wire-contract e2e test asserts widget _meta on every *_show_* tool and app-only visibility on every *_data feed, by name, across all composed modules including yours.
  • Describe an input field only where it means more than its name; a rule every tool of your module shares goes once into instructions (Step 3), which the server sends with every session.
  • Annotations: reads get { readOnlyHint: true, idempotentHint: true } (+ openWorldHint: true when they talk to an external system); writes drop readOnlyHint and need toolsets (Step 5); every write states destructiveHint explicitly (MCP reads an absent hint as true): true for irreversible operations, false otherwise.
  • Write tools that only flip state return a small { success: true, … } object, not the raw (often empty) upstream response.

Step 5: toolsets (as soon as a tool writes)

A module without toolsets registers every tool whenever it runs. Without OAuth nobody signs in, so each of its writes is open to anyone who reaches the port; the boot warns once per tool without readOnlyHint: true, naming the tool and your module. So take this step as soon as your module registers a tool without readOnlyHint: true and the server may run without OAuth; a module of reads only skips it. Declare a vocabulary and put it on the module definition. The boot resolves ONE concrete toolset per boot, logs it, and hands it to createPlugin as config.toolset:

import { createToolsetVocabulary } from "@miragon/pulse-widget-shell/server"

export const MY_TOOLSETS = ["read-only", "standard"] as const
export const myToolsets = createToolsetVocabulary("<name>", MY_TOOLSETS, "read-only", {
  authenticatedDefault: "standard",
})
// src/module.ts: `toolsets: myToolsets,` on the module definition
  • Accept it in your config schema (toolset: z.string().optional()) and resolve it once in createPlugin with myToolsets.resolve(config.toolset).
  • 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 (with a warning), and a direct createPlugin caller passing no toolset all land there.
  • authenticatedDefault is the no-suffix default under OAuth, which the boot installs from MCP_OAUTH_*, so every OAuth boot that names no suffix runs it: your standard non-admin toolset. Never make it your widest one: an admin-like toolset must only be reachable by naming it.
  • Write ONE rule function for the module, isToolInToolset(config, toolset) over the toolset resolve returned: admin-only names first, then the annotations (read-only ⇔ readOnlyHint: true). Apply it through ONE registrar wrapper, withToolsetFilter, so every tool, writes included, registers through the registrar and the filter decides it. Never a scattered toolset === "read-only" compare (fails open for every name you add later) or a toolset === undefined shortcut (an absent toolset is the floor, not "everything"). Reference: server/node_modules/@miragon/pulse-engine-connector/src/lib/mcp/toolsets.ts.

Step 6: wire the module into the server

Four edits, plus the env documentation:

  1. server/package.json: add "@<your-scope>/mcp-<name>": "workspace:*", then pnpm install.
  2. server/src/setup.ts: add your module to MODULES.
  3. server/src/ui/view-registry.ts: spread your <name>Views map into viewRegistry (needed once you have show tools; harmless before).
  4. MCP_ACTIVE_MODULES in .env.example and in your .env: add <name> to the list. A module the list does not name stays off and its tools are absent (the boot log's Toolsets: line shows what runs). server/test/env-example.test.ts holds .env.example to MODULES; nothing checks your .env, so a stale copy boots without your module.

A missed spread is silent (tools work, the view says it can't show itself), which is why server/test/view-registry.test.ts covers you without an edit: it boots the real composition (edit 2) and fails naming a view-bound tool without a view. A module whose views are finished HTML documents has nothing to spread: it skips edit 3 and the Tailwind line below.

Tailwind needs no edit: server/src/ui/globals.css scans every workspace module through the modules/*/src glob. Only an npm-installed module package with widget sources would need its own @source line there.

Then document every var from knownEnvVars in .env.example. server/test/env-example.test.ts fails on both drift directions (an undocumented var, or a documented var no module reads).

The other guard tests in server/test/ need no edit either: they boot the composed server in-process and derive what they check from MODULES and the tools it lists, so they cover your module by naming convention and on every toolset it declares. Only server/test/example-notes.test.ts names the example module, and it goes with it (see "Remove the example module").

Step 7: verify

pnpm build && pnpm typecheck && pnpm test

Then boot it for real: pnpm dev, check the boot log (your module's env-var typo coverage and boot warnings show up here, and a warning names each write a module without toolsets leaves open), and call your tools in the MCP Inspector pnpm dev starts (open the URL it prints), or headless with its CLI (CLAUDE.md → Verification). Also verify the selection path: MCP_ACTIVE_MODULES=<name> pnpm dev must expose exactly your module's tools (the shell's value beats .env). If you declared toolsets, that is its read-only floor without OAuth and its authenticatedDefault with it (the boot log's Toolsets: line says which).

Remove the example module

modules/mcp-notes is an example to copy, not part of your server. Once your own module stands, take the example out in one change:

  1. server/src/setup.ts: drop notesModule from MODULES and its import.
  2. server/src/ui/view-registry.ts: drop the notesViews spread and its import.
  3. server/package.json: drop the @acme/mcp-notes dependency and the notes in description; delete modules/mcp-notes, then pnpm install.
  4. .env.example: drop the Notes block (NOTES_TITLE), notes from the live MCP_ACTIVE_MODULES value and every other notes mention (that variable's comment). Take it out of your .env too: the boot refuses a name the list still holds once MODULES no longer does.
  5. server/src/app.ts: SERVER_INSTRUCTIONS tells the model the server holds team notes; say what yours does.
  6. Delete server/test/example-notes.test.ts, the one test that names the example.
  7. The smoke checks that name notes_show_notes (CLAUDE.md → Commands and Verification, the setup-server skill's Step 1, the README's Quickstart) take a show tool of yours, and the README's Configuration names your module where it names notes (the toolset prose and the variable table).
  8. Every module list that still names notes names your module: the README's Quickstart (MCP_ACTIVE_MODULES=engine,notes), CLAUDE.md (engine:operations,notes and the comment on cp .env.example .env) and the setup-server skill (Steps 1, 2, 3 and 5, Troubleshooting). Copied as it stands, such a value fails the boot. git grep -n notes shows what is left, including the comment in server/src/setup.ts that names the deleted test.

Then run Step 7's bar. The other guard tests follow MODULES, so they need no edit.

Anti-patterns

  • Importing another module (or the server) instead of conforming structurally: peer isolation is the architecture.
  • Reading process.env anywhere in the module but configFromEnv's argument, or letting a check there throw a ZodError: the boot prints its schema path (engines[0].id), not the variable to fix.
  • Registering plain tools with a raw server.registerTool(): the registrar is the contract; the raw path is for widget tools only.
  • Naming a plain tool *_data or *_show_*: the e2e naming contract will fail it, correctly.
  • Talking to your backend from widget code: widgets read through *_data feeds only, via the view runtime's query hooks (see the add-widget skill).

04 同仓库更多 Skills

Miragon
Miragon/pulse

setup-server

Set up, configure, run, brand, and deploy this composed MCP server. Use whenever connecting a Camunda 7 / 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.

后端与接口运维与部署
★ 8
Miragon
Miragon/pulse

add-widget

Step-by-step house pattern for adding an interactive widget to a module (the React component built from the design kit `@miragon/pulse-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…".

前端开发
★ 8
Miragon
Miragon/pulse

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.

★ 8
Miragon
Miragon/pulse

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/ui, 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".

★ 8
Miragon
Miragon/pulse

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.

后端与接口Skill与智能体开发
★ 8
Miragon
Miragon/pulse

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.

代码审查
★ 8