create-module
/create-module
Step-by-step house pattern for building your own connector module under `modules/` — 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. For the widget path see the add-widget skill; for per-user settings see add-settings-section.
Assessment and security
- Files in this package
- 1
- Scripts
- 0
- HTML comments
- 0
- URLs in SKILL.md
- 1
No signals detected. Repository changed on 27.10.2026, without references to retired models or APIs.
MIT, open: use and modify while retaining author attribution.
What it does and when to use it
Step-by-step house pattern for building your own connector module under `modules/` — 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. For the widget path see the add-widget skill; for per-user settings see add-settings-section.
Triggers from the description
Original author description
Step-by-step house pattern for building your own connector module under `modules/` — 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. For the widget path see the add-widget skill; for per-user settings see add-settings-section.
How it works
- Step 1 — scaffold the package
- Step 2 — the data layer
- Step 3 — the module definition (src/module.ts)
- Step 4 — catalogue and tools
- Step 5 — toolsets (optional)
- Step 6 — wire the module into the server
- Step 7 — verify
- Anti-patterns
SKILL.md preview
---
name: create-module
description: Step-by-step house pattern for building your own connector module under `modules/` — 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. For the widget path see the add-widget skill; for per-user settings see add-settings-section.
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
---
# 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, the server
never reaches into your module). Modules are peers: they never import each other; anything shared goes
through `@miragon/pulse-widget-shell` or the app-wired `SharedResources`.
**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, settings,
paged lists), the published Miragon modules ship their sources under
`node_modules/@miragon/pulse-{camunda7,analytics}-connector/src/`.
## Step 1 — scaffold the package
```bash
cp -R modules/mcp-notes modules/mcp-<name>
```
`pnpm-workspace.yaml` already covers `modules/*` — no workspace edit needed.
Then adjust:
- `package.json`: name it `@<your-scope>/mcp-<name>` and keep the three-entry
`exports` map exactly — `.` (compiled server code), `./widgets` (pointing at
**TypeScript source**, compiled by the server's Vite build), `./definition`.
Keep `react` an optional peer: server-side consumers must not need it.
- Keep both 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).
- Do a global rename `notes` → `<name>` across `src/` (module name, tool
prefixes, widget ids, data types).
## 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 — 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.
## Step 3 — the module definition (`src/module.ts`)
The contract, field by field (see `notesModule`):
- `name` — the key used in `MCP_ACTIVE_MODULES` and as the plugin's app name.
- `configFromEnv(env)` — **pure** env → raw-config mapping; no I/O, no
`process.env` reads outside the argument, no validation here.
- `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 — your module gets the same typo coverage as the
built-in ones.
- `toolsets` — leave it out unless you implement Step 5 (the notes module has
none); without it a `<name>:<toolset>` suffix is ignored with a warning.
- `createPlugin(config, shared)` — validate `config` with your zod schema
(**this** is where validation lives) and return the `AppPlugin`: construct
your data source/client once and pass it to both `registerTools` and
`registerWidgetTools`. `shared` is the app-wired cross-module resource bag;
type the fields you consume structurally (e.g.
`profileStore?: ProfileSource`) and tolerate their absence.
- Optional `bootWarnings(env)` — return actionable hints for misconfigured
deployments (see the analytics module's Prometheus-URL warning).
- Optional `instructions(config, { authenticated })` — 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.
## Step 4 — catalogue and tools
- `src/definition.ts` — the module catalogue: `name`, `steps: []`, and one
entry per widget (start with `widgets: []` if you have none yet).
- `src/tools.ts` — plain tools **for the model**, registered through
`createToolRegistrar(server, store, { strictInput: true })` (from
`@miragon/mcp-toolkit-core/tools`) — the second argument is threaded into
every handler as its first parameter; `strictInput` refuses unknown keys.
Raw `server.tool()` 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.
- Every input field gets a `.describe()`.
- Annotations: reads get `{ readOnlyHint: true, idempotentHint: true }`
(+ `openWorldHint: true` when they talk to an external system); writes drop
`readOnlyHint`; 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 (optional)
Skip this until someone needs to run your module with a narrowed tool surface
(`MCP_ACTIVE_MODULES=<name>:read-only`). Then declare a vocabulary and put it
on the module definition — the composition resolves ONE concrete toolset per
boot, logs it, and hands it to `createPlugin` as `config.toolset`:
```ts
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 durable
write). No suffix on a boot without `MCP_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 `MCP_OAUTH` (which
`server/src/app.ts` wires through `oauthFromEnv`) — 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.
- Filter registrar tools by their annotations (read-only ⇔
`readOnlyHint: true`), and gate durable writes registered outside the
registrar on `myToolsets.allowsDurableWrites(myToolsets.resolve(config.toolset))`
— never a `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:
`node_modules/@miragon/pulse-analytics-connector/src/toolsets.ts`.
## Step 6 — wire the module into the server
Four edits, plus the env documentation. The failure modes of the middle two
are **silent** (tools work, widgets render empty) — which is why the last one
exists:
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/widget-registry.ts` — spread your `<name>Widgets` map
(needed once you have widgets; harmless before).
4. `server/test/widget-registry.test.ts` — add your `definition` to **both**
describe blocks, so the guard actually covers your module.
Tailwind needs no edit: `server/src/ui/globals.css` scans every workspace
module through the `modules/*/src` glob — only an npm-installed module
package 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 (undocumented
var, or a documented var no module reads).
The wire-contract e2e test (`server/test/widget-contract.e2e.test.ts`) needs no
edit: it boots the composed server in-process and covers your module by naming
convention.
## Step 7 — verify
```bash
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 call your tools in the
inspector at `http://localhost:8400/mcp/inspector` — or headless via the
mcp-use client/screenshot commands in `CLAUDE.md` → Verification. Also verify
the selection path: `MCP_ACTIVE_MODULES=<name> pnpm dev` must expose exactly
your module's tools — if you declared `toolsets`, its read-only floor without
`MCP_OAUTH` and its `authenticatedDefault` with it (the boot log's
`Toolsets —` line says which).
## Anti-patterns
- Importing another module (or the server) instead of conforming structurally —
peer isolation is the architecture.
- Validating config in `configFromEnv` (belongs in `createPlugin`) or reading
`process.env` anywhere else in the module.
- Registering plain tools with raw `server.tool()` — 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 self-fetch through
`*_data` feeds only (see the add-widget skill).
Versions
| Repository | Score | Licence |
|---|---|---|
| This page · miragon/pulse | 100 | MIT |
| mx-space/core | 100 | MIT |
| monarch-initiative/dismech | 100 | BSD-3-Clause |
| velimirmueller/ai-development-skills | 100 | MIT |
| marko-php/marko | 100 | MIT |
Database snapshot: 13.11.2026 · package version 2243e86adec6 (SHA-256 digest of the entire package)
Downloads in the last 30 days: 0 · ranking
Report a problem with this skill: harmful content, incorrect descriptions or an author’s removal request.
Similar skills
- 100add-settings-section
Step-by-step house pattern for giving your module persisted per-user settings — a schema under `profile.modules.<module>`, the show/data/save tool triple, and a settings widget. Use whenever adding or changing persisted user preferences ("make X configurable per user", "add a… - 100add-settings-section
Step-by-step house pattern for giving a module its own user-settings slice — a schema under `profile.modules.<module>`, the show/data/save tool triple, and a settings-page section widget. Use whenever adding or changing persisted user preferences ("make X configurable per user",… - 95module-scaffold
Use when creating a brand new module file in modules/ for a new tool category. Generates the boilerplate with correct array prefix, install_module_<name>() function, and ensures it integrates with install.sh, profiles, and MCP server. Triggers on "new module", "add a module for… - 85{setup-skill-name}
Sets up {module-name} module in a project. Use when the user requests to 'install {module-code} module', 'configure {module-name}', or 'setup {module-name}'. - 100module-client
Create or use a <name>-client package (modules/<name>/client): the typed tRPC hooks another module's browser reads. Use when someone says 'read another module's data in the browser', 'prompt-client', 'dataset-client', 'a client package', 'createModuleApi', 'ContractApiMap',… - 95rust-server
SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust.