-
Notifications
You must be signed in to change notification settings - Fork 0
Contributing
github-actions[bot] edited this page May 30, 2026
·
3 revisions
Contributions are welcome. Please read this page before opening a PR.
All work happens on branches — never commit directly to main.
git checkout -b feat/my-new-tool # or fix/, chore/, docs/, refactor/
# make changes
npm test # must pass before pushing
git push -u origin feat/my-new-tool
# open a PR targeting main — squash mergeBranch naming uses kebab-case prefixes: feat/, fix/, chore/, docs/, refactor/.
Commit messages follow Conventional Commits: feat(scope): summary, fix: summary, etc.
- Create a file in
src/domain/tools/(or add to an existing one). - Define the tool using the
ToolDefinitioninterface — see the pattern inCLAUDE.mdor any existing tool file. - Export it and add it to
allToolsinsrc/domain/tools/index.ts. - Add unit tests in
test/unit/covering metadata, input schema, and all handler paths. - Document it in both of these files (lockstep tests enforce this — CI fails if either is missing):
-
docs/wiki/Available-Tools.md— add a section with a parameter table and example prompt. -
skills/mikromcp/references/tool-map.md— add the tool name to the appropriate category row.
-
- Add a line to
CHANGELOG.mdunder[Unreleased] → Added.
Minimal tool skeleton:
import { z } from "zod";
import type { ToolDefinition, ToolContext, ToolResult } from "./tool-definition.js";
import { enrichError } from "../errors/error-enricher.js";
import { MikroMCPError } from "../errors/error-types.js";
import { createLogger } from "../../observability/logger.js";
const log = createLogger("my-tools");
const myInputSchema = z.object({
routerId: z.string().describe("Target router identifier from the router registry"),
// all params need .describe() for AI clients
}).strict(); // always .strict()
const myTool: ToolDefinition = {
name: "my_tool",
title: "My Tool",
description: "What this tool does. Mention idempotency and dry-run if applicable.",
inputSchema: myInputSchema,
annotations: {
readOnlyHint: true, // true → auto-retry enabled in tool-registry
destructiveHint: false, // true → circuit breaker trips on failure
idempotentHint: true,
openWorldHint: false,
},
async handler(params: Record<string, unknown>, context: ToolContext): Promise<ToolResult> {
const parsed = myInputSchema.parse(params);
log.info({ routerId: context.routerId }, "Doing thing");
try {
const records = await context.routerClient.get("ros/path", {});
return {
content: `Found ${records.length} things.`,
structuredContent: { routerId: context.routerId, records },
};
} catch (err) {
if (err instanceof MikroMCPError) throw err;
throw enrichError(err, { routerId: context.routerId, tool: "my_tool" });
}
},
};
export const myTools: ToolDefinition[] = [myTool];-
Idempotency first — write tools must check existing state before acting and return
already_exists/no_changewhen nothing needs to be done. -
Always support
dryRunon write tools — no exceptions. -
Never log credentials — they are resolved by
tool-registry.tsand never reach handlers. -
Always use
.strict()on Zod schemas — reject extra fields. -
Enrich errors — use
enrichError()in every catch block; throwMikroMCPErrordirectly for domain errors (NOT_FOUND,CONFLICT,VALIDATION). -
No retry/circuit-breaker logic in handlers — that is handled by
tool-registry.ts. -
ESM imports with
.jsextensions —from "../../adapter/rest-client.js"even for.tssource. - Run
npm run format && npm run lintbefore pushing.
-
npm testpasses (vitest + tsc + eslint + doc-accuracy guards + skill tool-map lockstep) - New tool:
docs/wiki/Available-Tools.mdupdated with parameter table and example prompt - New tool:
skills/mikromcp/references/tool-map.mdupdated (lockstep test will fail otherwise) - New tool:
CHANGELOG.md[Unreleased]section updated - Write tool:
dryRunsupported - Write tool: idempotency check included
- Write tool:
snapshotPathsset if the tool modifies a RouterOS path that should be rollback-able - PR title follows Conventional Commits format