Building Plugins
A plugin is an npm package that exports a descriptor — the same object built-in targets use — telling AgentsMesh how to generate, import, lint, and detect config files for an AI tool. Plugins get full parity: generation, import, linting, global mode, conversions, and everything else.
The pipeline is straightforward:
.agentsmesh/rules/*.md descriptor.generateRules().agentsmesh/commands/*.md → descriptor.generateCommands() → .foo-ide/rules/*.md.agentsmesh/mcp.json descriptor.generateMcp() .foo-ide/mcp.json (canonical source) (your plugin) (generated output)Your plugin sits in the middle — it receives parsed canonical files and returns { path, content } pairs that AgentsMesh writes to disk. The reverse direction (import) reads tool-native files and writes canonical files back to .agentsmesh/.
Your First Plugin
-
Create the package
Terminal window mkdir agentsmesh-target-foo-idecd agentsmesh-target-foo-idenpm init -ySet
"type": "module"and declareagentsmeshas a peer dependency inpackage.json:{"name": "agentsmesh-target-foo-ide","version": "1.0.0","type": "module","main": "index.js","exports": {".": "./index.js"},"peerDependencies": {"agentsmesh": ">=0.6"}} -
Write the descriptor
Create
index.jswith a single named export:index.js export const descriptor = {// ---- Identity ----id: 'foo-ide',// ---- User-facing metadata (required) ----// Drives display names in `agentsmesh matrix`, the support-matrix doc,// and any UI rendering that lists targets. Plugins missing `metadata`// are rejected at load time.metadata: {displayName: 'Foo IDE',category: 'ide', // 'cli' | 'ide' | 'agent-platform'officialUrl: 'https://example.test/foo-ide',shortDescription: 'Foo IDE — a fictional editor with native rule support.',},// ---- Generation ----generators: {name: 'foo-ide',generateRules(canonical) {return canonical.rules.map((rule) => ({path: `.foo-ide/rules/${rule.slug}.md`,content: rule.body,}));},async importFrom() { return []; },},// ---- Capabilities: what this tool supports ----capabilities: {rules: 'native',additionalRules: 'none', commands: 'none', agents: 'none',skills: 'none', mcp: 'none', hooks: 'none',ignore: 'none', permissions: 'none',},// ---- Layout: where outputs live on disk ----project: {managedOutputs: {dirs: ['.foo-ide/rules'],files: [],},paths: {rulePath: (slug) => `.foo-ide/rules/${slug}.md`,commandPath: () => null,agentPath: () => null,},},// ---- Plumbing ----emptyImportMessage: 'No Foo IDE config found.',lintRules: null,buildImportPaths: async () => {},detectionPaths: ['.foo-ide'],};Every non-
nonecapability must have a matching generator. Themetadatablock must be present (all four subfields are required). AgentsMesh validates this at load time with a Zod schema and warns (or hard-errors underAGENTSMESH_STRICT_PLUGINS=1) if something is missing. -
Test locally
Terminal window agentsmesh plugin add ./path/to/agentsmesh-target-foo-ideagentsmesh generate --dry-run # preview without writingagentsmesh generate # generate for realagentsmesh matrix # confirm it appears -
Publish to npm
Terminal window npm publishUsers install with:
Terminal window agentsmesh plugin add agentsmesh-target-foo-ideagentsmesh generate
You can also export a descriptors array (for multi-target packages) or a default export — AgentsMesh checks all three.
Adding More Features
The minimal plugin above only generates rules. Most AI tools support commands, agents, MCP servers, and more. Each feature follows the same pattern: declare a capability, add a generator, and register the output path.
Commands and Agents
Add generateCommands to generators, change capabilities.commands to 'native', and register the output path:
generators: { // ...existing rules generator... generateCommands(canonical) { return canonical.commands.map((cmd) => ({ path: `.foo-ide/commands/${cmd.name}.md`, content: `# ${cmd.name}\n\n${cmd.description}\n\n${cmd.body}`, })); },},capabilities: { // ... commands: 'native', // was 'none'},project: { managedOutputs: { dirs: ['.foo-ide/rules', '.foo-ide/commands'], // add the new dir files: [], }, paths: { // ... commandPath: (name) => `.foo-ide/commands/${name}.md`, // was () => null },},Agents follow the same pattern with generateAgents, capabilities.agents, and agentPath.
Skills
Skills are directory-based — each skill is a folder with a SKILL.md file and optional supporting files:
generators: { generateSkills(canonical) { return canonical.skills.map((skill) => ({ path: `.foo-ide/skills/${skill.name}/SKILL.md`, content: `# ${skill.name}\n\n${skill.body}`, })); },},capabilities: { skills: 'native',},project: { skillDir: '.foo-ide/skills', managedOutputs: { dirs: ['.foo-ide/skills'], // add skills dir files: [], },},MCP, Permissions, Hooks, and Ignore
These four features are “settings-backed” — they can be satisfied either by a dedicated generator or by emitScopedSettings if the tool uses a single settings file.
With a dedicated generator:
generators: { generateMcp(canonical) { if (!canonical.mcp) return []; return [{ path: '.foo-ide/mcp.json', content: JSON.stringify(canonical.mcp, null, 2), }]; }, generateIgnore(canonical) { if (canonical.ignore.length === 0) return []; return [{ path: '.fooignore', content: canonical.ignore.join('\n'), }]; },},capabilities: { mcp: 'native', ignore: 'native',},If your tool reads all settings from a single file, see Scoped Settings in the Advanced section.
Capability Levels
| Level | Meaning |
|---|---|
'native' | The tool has first-class support for this feature |
'embedded' | AgentsMesh projects the feature with metadata for round-trip |
'partial' | Supported with limitations |
'none' | Not supported — generator is skipped |
Capabilities can also be objects with an optional flavor field:
capabilities: { agents: { level: 'embedded', flavor: 'markdown' },},Adding Import Support
Import is the reverse pipeline — agentsmesh import --from foo-ide reads the tool’s native files and writes canonical files to .agentsmesh/.
Add an importer block to the descriptor. The shared runner walks each feature in canonical order, resolves scope-specific source paths, and dispatches to the right helper:
export const descriptor = { // ...other fields... importer: { rules: { feature: 'rules', mode: 'directory', source: { project: ['.foo-ide/rules'], global: ['.foo-ide/rules'] }, canonicalDir: '.agentsmesh/rules', extensions: ['.md'], preset: 'rule', }, mcp: { feature: 'mcp', mode: 'mcpJson', source: { project: ['.foo-ide/mcp.json'] }, canonicalDir: '.agentsmesh', canonicalFilename: '.agentsmesh/mcp.json', }, ignore: { feature: 'ignore', mode: 'flatFile', source: { project: ['.fooignore'] }, canonicalDir: '.agentsmesh', canonicalFilename: '.agentsmesh/ignore', }, }, generators: { // ... async importFrom(projectRoot, options) { const { runDescriptorImport } = await import('agentsmesh/targets'); return runDescriptorImport(descriptor, projectRoot, options?.scope ?? 'project'); }, },};Features whose source omits the active scope are automatically skipped — no if (scope === 'global') branches in your code.
Importer modes:
| Mode | Use case | Required fields |
|---|---|---|
singleFile | Root rule with fallback chain | canonicalRootFilename, optional markAsRoot |
directory | Recursive scan of a directory | extensions, preset (rule/command/agent) or custom map |
flatFile | Verbatim copy (e.g. ignore file) | canonicalFilename |
mcpJson | Parse mcpServers JSON | canonicalFilename |
Write a custom importFrom that reads files from disk and returns ImportResult[]:
generators: { async importFrom(projectRoot, options) { const { readFileSync, existsSync } = await import('node:fs'); const { join } = await import('node:path'); const results = [];
const mcpPath = join(projectRoot, '.foo-ide/mcp.json'); if (existsSync(mcpPath)) { const content = readFileSync(mcpPath, 'utf-8'); const { writeMcpWithMerge } = await import('agentsmesh/targets'); await writeMcpWithMerge(projectRoot, '.agentsmesh/mcp.json', JSON.parse(content).mcpServers); results.push({ fromTool: 'foo-ide', fromPath: mcpPath, toPath: '.agentsmesh/mcp.json', feature: 'mcp', }); }
return results; },},Use imperative import when the target format requires non-trivial parsing — embedded rule splitters, TOML/YAML native configs, or multi-file merges.
Project Layout
You have already seen project.paths and project.managedOutputs in the minimal plugin. Here are the other layout fields:
| Field | Purpose |
|---|---|
managedOutputs | Directories and files that AgentsMesh fully manages — stale artifacts are deleted on generate |
rootInstructionPath | Path to the primary root instruction file (e.g. .foo-ide/ROOT.md) |
skillDir | Target-native skills directory for skill generation and import |
outputFamilies | Declared output path families for reference rewriting between targets |
Output families enable AgentsMesh to rewrite internal file references from canonical paths to target-relative paths:
project: { outputFamilies: [ { id: 'rules', kind: 'primary', pathPrefix: '.foo-ide/rules/' }, { id: 'commands', kind: 'additional', pathPrefix: '.foo-ide/commands/' }, ],},Testing and Validation
AgentsMesh validates every plugin descriptor with a Zod schema at load time. Invalid descriptors are warned and skipped — one bad plugin does not block other targets.
Common validation errors:
idmust match^[a-z][a-z0-9-]*$metadata.displayName,metadata.category,metadata.officialUrl, andmetadata.shortDescriptionare all required;categorymust be one of'cli' | 'ide' | 'agent-platform'generators.generateRulesandgenerators.importFromare required- Non-
nonecapabilities must have their matching generator (oremitScopedSettingsfor settings-backed features)
For CI release jobs that must not silently skip a broken plugin, set strict: true on the plugin entry or run with AGENTSMESH_STRICT_PLUGINS=1. See the plugin CLI reference for the full contract.
agentsmesh generate --dry-run # preview generated outputagentsmesh matrix # verify capabilities appear correctlyagentsmesh lint # run lint hooks across all targetsPublishing
- Ensure
"type": "module"and"peerDependencies"are set inpackage.json - Export
descriptor(named),descriptors(array), ordefaultfrom your entry point - Name your package
agentsmesh-target-<id>by convention - Publish:
npm publishUsers install and generate:
agentsmesh plugin add agentsmesh-target-foo-ideagentsmesh generateAdvanced Features
The features below are used by targets with complex requirements. Most plugins will not need them. Each section is self-contained.
Global Mode
To support agentsmesh generate --global, add globalSupport — a single object bundling global capabilities, detection paths, and layout:
globalSupport: { capabilities: { rules: 'native', additionalRules: 'none', commands: 'embedded', agents: 'embedded', skills: 'native', mcp: 'native', hooks: 'none', ignore: 'native', permissions: 'none', }, detectionPaths: ['.foo-ide', '.foo-ide/ROOT.md'], layout: { rootInstructionPath: '.foo-ide/ROOT.md', skillDir: '.foo-ide/skills', paths: { rulePath: (slug) => `.foo-ide/rules/${slug}.md`, commandPath: (name) => `.foo-ide/commands/${name}.md`, agentPath: (name) => `.foo-ide/agents/${name}.md`, }, rewriteGeneratedPath(path) { return path; }, },},Feature Conversions
If your target lacks native support for commands or agents but can represent them as skills, declare supportsConversion:
supportsConversion: { commands: true, agents: true },capabilities: { commands: 'embedded', agents: 'embedded',},When declared, conversion defaults to enabled. Users can override in agentsmesh.yaml:
conversions: commands_to_skills: foo-ide: true agents_to_skills: foo-ide: project: true global: falseSee the Conversions reference for full syntax.
Per-Feature Lint Hooks
Add target-specific validation for individual features:
lint: { commands(canonical, options) { const issues = []; for (const cmd of canonical.commands) { if (!cmd.description) { issues.push({ level: 'warning', file: cmd.source || '', target: 'foo-ide', message: `Command '${cmd.name}' is missing a description`, }); } } return issues; },},The options parameter includes { scope: 'project' | 'global' }. Available hooks: commands, mcp, permissions, hooks, ignore.
Scoped Settings
If your tool reads all config from a native settings file, use emitScopedSettings instead of individual generators for mcp, hooks, ignore, and permissions:
emitScopedSettings(canonical, scope) { const settings = { version: 1, featureCount: canonical.rules.length, }; return [{ path: '.foo-ide/settings.json', content: JSON.stringify(settings, null, 2) }];},Hook Post-Processing
Generate additional files alongside hooks (like wrapper scripts) with postProcessHookOutputs:
async postProcessHookOutputs(projectRoot, canonical, outputs) { const processed = [...outputs]; for (const output of outputs) { if (output.path.endsWith('.json')) { processed.push({ path: output.path.replace('.json', '.wrapper.sh'), content: '#!/bin/sh\nexec "$@"\n', }); } } return processed;},Scope Extras
Generate additional global outputs beyond the standard feature loop with globalSupport.scopeExtras:
globalSupport: { // ...capabilities, detectionPaths, layout... async scopeExtras(canonical, projectRoot, scope, enabledFeatures) { if (scope === 'global' && enabledFeatures.has('rules')) { return [{ path: '.foo-ide/scope-info.txt', content: `scope=${scope}` }]; } return []; },},Shared Artifacts
If your target generates files into a path shared with other targets, declare ownership:
sharedArtifacts: { '.foo-ide/skills/': 'owner',},Use 'owner' if your target controls writes to that prefix, or 'consumer' if you only read from it. Only one target can be the owner of a given prefix — AgentsMesh fails fast at startup if two targets claim the same one.
Content Merging
If your target needs to merge generated content into an existing file rather than overwriting, use mergeGeneratedOutputContent:
mergeGeneratedOutputContent(existingContent, pendingContent, newContent, path) { if (path.endsWith('settings.json')) { const existing = JSON.parse(existingContent || '{}'); const update = JSON.parse(newContent); return JSON.stringify({ ...existing, ...update }, null, 2); } return newContent;},Descriptor Reference
Required Fields
| Field | Type | Purpose |
|---|---|---|
id | string | Unique target ID (^[a-z][a-z0-9-]*$) |
metadata | TargetMetadata | User-facing metadata (displayName, category: 'cli' | 'ide' | 'agent-platform', officialUrl, shortDescription). Drives matrix display and target lists |
generators | TargetGenerators | Feature generators (generateRules, importFrom required; others per capability) |
capabilities | TargetCapabilities | Support levels for all 9 features |
emptyImportMessage | string | Shown when import --from finds nothing |
lintRules | function | null | Rule linter callback, or null to skip |
project | TargetLayout | Project-scope layout (paths, managed outputs) |
buildImportPaths | function | Populates import reference map |
detectionPaths | string[] | Paths for target detection during init |
Optional Fields
| Field | Type | Purpose |
|---|---|---|
globalSupport | GlobalTargetSupport | Global-scope support (capabilities, detection, layout, scopeExtras) |
supportsConversion | { commands?, agents? } | Enables user-configurable feature projection |
conversionDefaults | { commandsToSkills?, agentsToSkills? } | Built-in defaults for commands_to_skills / agents_to_skills projection. undefined falls back to caller-supplied default |
excludeFromStarterInit | boolean | When true, agentsmesh init skips this target in the default bulk starter scaffold; explicit --target <id> still works |
preservesManualActivation | boolean | When true, the target preserves manual-only activation semantics (e.g. Cursor’s alwaysApply: false); other targets get a lint warning on trigger: 'manual' rules |
importer | TargetImporterDescriptor | Declarative importer for scan + map orchestration |
lint | TargetLintHooks | Per-feature lint hooks |
sharedArtifacts | Record<string, 'owner' | 'consumer'> | Shared output path ownership |
emitScopedSettings | function | Emit native settings sidecar files |
mergeGeneratedOutputContent | function | Custom merge for generated outputs |
postProcessHookOutputs | function | Async post-pass for hook outputs |
What’s Next
- Plugin CLI commands —
plugin add,plugin remove,plugin list - Generation Pipeline — how the engine processes your descriptor
- Supported Tools Matrix — see how built-in targets declare capabilities
- Conversions reference — user-facing conversion config syntax
- Extending AgentsMesh — scaffold a new built-in target (contributing upstream)
- Rich plugin test fixture — comprehensive example exercising every descriptor field