ESM TypeScript plugin (@opencode-ai/plugin) that discovers skills from the filesystem, loads their MCP server configs, and lazily connects/manages MCP clients per session.
| Cmd | What |
|---|---|
npm run build |
npx tsc → dist/ |
npm test |
vitest run (picks up src/**/*.test.ts) |
npm run clean |
rm -rf dist (PowerShell: works but may warn) |
npm run watch |
npx tsc --watch |
npm pack |
Verify dist/index.js is included before publish |
npm publish |
Prepack runs clean && build automatically |
src/
├── index.ts # Plugin factory, OMO conflict detection, session lifecycle
├── types.ts # All interfaces (McpServerConfig, LoadedSkill, etc.)
├── skill-loader.ts # Scan .opencode/skills/ + ~/.config/opencode/skills/
├── skill-mcp-manager.ts # StdioClientTransport pool per session:skill:server
├── tools/
│ ├── skill.ts # Load skill body + discover MCP capabilities
│ └── skill-mcp.ts # Route to tool/resource/prompt on a loaded MCP server
└── utils/
├── env-vars.ts # ${VAR}/${VAR:-default} expansion, normalizeCommand, normalizeEnv
└── frontmatter.ts # YAML frontmatter extraction (js-yaml)
Plugin entry: default export OpenCodeEmbeddedSkillMcp, also named export. Both required for compatibility.
- Scans
.opencode/skills/(project) and~/.config/opencode/skills/(global) — both plural. ⚠️ Current repo mismatch: the example skill lives at.opencode/skill/playwright-example/(singular) — it is not discovered by the code. An agent should be aware of this.- Project skills override globals by name (Map merge, global first then project).
- Per directory: tries
SKILL.md→{dirname}.md→ standalone.mdfiles. mcp.jsonin skill dir takes priority over YAML frontmattermcp:block.mcp.jsonsupports{ mcpServers: {...} },{ mcp: {...} }, or bare{ serverName: { command: ... } }.
commandaccepts array (["npx", "-y", "@pkg"]) or string + args syntax. If array,argsfield is ignored.envaccepts object ({ KEY: "val" }) or array (["KEY=val"]). Array splits on first=.environmentfield deprecated;envtakes priority when both present.- Essential vars forwarded to child process:
PATH,HOME,USER,SHELL,TERM,NODE_ENV,TMPDIR,LANG,LC_ALL,npm_config_registry,npm_config_cache. - Supports both
${VAR}and${VAR:-default}.
- Connection key format:
${sessionID}:${skillName}:${serverName}. - Idle cleanup: 60s interval check, 5min timeout. Interval is
unref()'d (won't keep process alive). - Session cleanup on
session.deletedevent. - Retry-once: if
getOrCreateClientfails for an existing key, it removes the dead entry and retries. - Process cleanup:
SIGINT,SIGTERM, andSIGBREAK(Win32 only).
- Fast path: reads
$OPENCODE_CONFIGfile directly (sync) to check foroh-my-opencode/@code-yeongyu/oh-my-opencode/ any/oh-my-opencodeentry. - Fallback:
client.config.get()with 1s timeout. - Override:
OPENCODE_LAZY_LOADER_FORCE=1skips all conflict checks.
OPENCODE_LAZY_LOADER_DEBUG=1→ appends timestamps to/tmp/opencode-lazy-loader.log.
- Requires exactly one of
tool_name,resource_name,prompt_name— rejects zero or >1. argumentsis a JSON string (not an object), parsed internally.grepparam filters output lines by case-insensitive regex.
- Single test file:
src/__tests__/normalize-command.test.tscoversnormalizeCommandandnormalizeEnv. - Run:
npm test.