Skip to content
Skills Library
Programming · Tool integration

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.

Repository
pulse ↗
Licence
MIT
Last repository change
27.10.2026

Assessment and security

100/ 100Class: good
Format25 / 25
Frontmatter with name and description, valid name, description up to 1,024 characters, name matching the folder.
Discoverability25 / 25
Description explains when to use the skill, has a reasonable length and uses third person.
Content35 / 35
At least 150 words and at most 500 lines, headings, step lists, examples or code.
Hygiene15 / 15
No placeholders such as TODO, no Windows paths, with a licence.
Riskno signals detected in the scan
Files in this package
1
Scripts
0
HTML comments
0
URLs in SKILL.md
1
Deprecation

No signals detected. Repository changed on 27.10.2026, without references to retired models or APIs.

Licence

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

build a connector for Xadd tools for our APInew module
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

  1. Step 1 — scaffold the package
  2. Step 2 — the data layer
  3. Step 3 — the module definition (src/module.ts)
  4. Step 4 — catalogue and tools
  5. Step 5 — toolsets (optional)
  6. Step 6 — wire the module into the server
  7. Step 7 — verify
  8. Anti-patterns
Original SKILL.md headings · 182 lines, 1,298 words
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

RepositoryScoreLicence
This page · miragon/pulse100MIT
mx-space/core100MIT
monarch-initiative/dismech100BSD-3-Clause
velimirmueller/ai-development-skills100MIT
marko-php/marko100MIT

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.

Feedback and reports

Your message goes to Bartosz Sokoliński. An email address is needed only for a reply. See the privacy policy.