OpenCode Lazy Loader is a standalone OpenCode plugin that enables Skill-Embedded MCP Support. Skills (markdown files with YAML frontmatter) can define their own Model Context Protocol (MCP) servers, which are automatically discovered, lazily loaded, and managed by this plugin.
Key Value Proposition: Skills bring their own tools. No manual MCP server configuration required.
| Component | Technology |
|---|---|
| Runtime | Node.js >= 18 |
| Language | TypeScript (strict mode) |
| Module System | ESM ("type": "module") |
| Plugin SDK | @opencode-ai/plugin |
| MCP SDK | @modelcontextprotocol/sdk |
| YAML Parser | js-yaml |
src/
├── index.ts # Plugin entry point - registers tools, handles lifecycle
├── types.ts # TypeScript interfaces and types
├── skill-loader.ts # Skill discovery from filesystem
├── skill-mcp-manager.ts # MCP client lifecycle management
├── tools/
│ ├── skill.ts # `skill` tool - loads skill instructions
│ └── skill-mcp.ts # `skill_mcp` tool - invokes MCP operations
└── utils/
├── env-vars.ts # Environment variable expansion
└── frontmatter.ts # YAML frontmatter parsing
1. Plugin loads → discoverSkills() scans OpenCode's native skill roots
2. OpenCode's native skill tool loads instructions
3. User calls skill_mcp(mcp_name="Y", tool_name="Z") → invokes MCP tool
4. Session ends → disconnectSession() cleans up connections
5. Idle timeout (5min) → automatic connection cleanup
# Install dependencies
npm install
# Build TypeScript to dist/
npm run build
# Watch mode during development
npm run watch
# Clean build artifacts
npm run cleanSince this is a plugin loaded by OpenCode:
- Build:
npm run build - Restart OpenCode (or reload plugins if supported)
- Test with:
skill(name="some-skill")andskill_mcp(...)
# Verify package contents before publish
npm pack
tar -tzf opencode-lazy-loader-*.tgz | grep dist/
# Publish (prepack runs automatically)
npm publish- Strict mode enabled - no
any, no implicit types - Factory functions over classes - avoids
newkeyword issues in plugin sandbox - Explicit error types - use
error instanceof Error ? error.message : String(error)
// GOOD: Descriptive error with hints
throw new Error(
`MCP server "${name}" not found.\n\n` +
`Available servers:\n${list}\n\n` +
`Hint: Load the skill first using the 'skill' tool.`
)
// BAD: Generic error
throw new Error('Server not found')Skills define MCP servers in two ways:
1. YAML Frontmatter (in SKILL.md)
---
name: my-skill
description: Does something useful
mcp:
server-name:
command: ["npx", "-y", "@some/mcp-server"]
env:
API_KEY: "${MY_API_KEY}"
---2. Separate mcp.json file
{
"mcpServers": {
"server-name": {
"command": ["npx", "-y", "@some/mcp-server"]
}
}
}- Support
${VAR}and${VAR:-default}syntax - Essential vars are passed through:
PATH,HOME,USER,SHELL,TERM,NODE_ENV,TMPDIR,LANG - Custom vars from config are expanded and merged
| File | Responsibility |
|---|---|
index.ts |
Plugin registration, tool wiring, session lifecycle |
types.ts |
All TypeScript interfaces - single source of truth |
skill-loader.ts |
Filesystem scanning, SKILL.md parsing, mcp.json loading |
skill-mcp-manager.ts |
Client pooling, connection lifecycle, idle cleanup |
tools/skill.ts |
Legacy formatter; OpenCode's native skill tool is used at runtime |
tools/skill-mcp.ts |
skill_mcp tool - validates params, routes to MCP operations |
utils/env-vars.ts |
${VAR} expansion, clean environment creation |
utils/frontmatter.ts |
YAML frontmatter extraction |
This plugin automatically disables itself when oh-my-opencode is detected to avoid a duplicate skill_mcp tool.
Detection Logic:
- Call
client.config.get()via the OpenCode SDK to fetch the active config - Check if
oh-my-opencodeis in thepluginarray - If found → return empty hooks (plugin does nothing)
User's Config Setup:
| Mode | Launch Command | Config File | Plugins |
|---|---|---|---|
| Standard | opencode |
opencode.json |
opencode-lazy-loader (this plugin) |
| OMO | OPENCODE_CONFIG=~/.config/opencode/omo.json opencode |
omo.json |
oh-my-opencode |
Developer Override:
Set OPENCODE_LAZY_LOADER_FORCE=1 to force-enable the plugin even when oh-my-opencode is detected (for testing).
- Key format:
${sessionID}:${skillName}:${serverName} - Deduplication via
pendingConnectionsMap - Retry logic: if connection fails, cleanup and retry once
- Session cleanup:
disconnectSession(sessionID)onsession.deletedevent - Idle cleanup: 60-second interval checks, 5-minute timeout
- Process cleanup: SIGINT/SIGTERM handlers
| Priority | Location | Scope |
|---|---|---|
| 1 (highest) | .opencode/skills/ |
Project-specific |
| 2 | .claude/skills/ |
Project-specific |
| 3 | .agents/skills/ |
Project-specific |
| 4 | ~/.config/opencode/skills/ |
User global |
| 5 | ~/.claude/skills/ |
User global |
| 6 | ~/.agents/skills/ |
User global |
Project skills override global skills with the same name.
For a skill directory, checks in order:
SKILL.md{dirname}.md
Standalone .md files in skill directories are also supported.
Before releasing, verify:
-
npm run buildsucceeds with no errors -
npm packincludesdist/index.js - Fresh install works:
npm i opencode-lazy-loader@latest -
skill(name="...")loads skill content -
skill_mcp(...)invokes MCP tools - OpenCode doesn't hang on startup with plugin enabled
- Connections clean up after session ends
| Symptom | Cause | Fix |
|---|---|---|
| OpenCode hangs on startup | Missing dist/ in npm package |
Run npm run build before publish |
ERR_MODULE_NOT_FOUND |
Package published without build | Ensure prepack script exists |
| MCP connection fails | Command not found | Check PATH, ensure package installed |
| Skills not discovered | Wrong directory | Check .opencode/skills/ or ~/.config/opencode/skills/ |
| Env vars not expanded | Wrong syntax | Use ${VAR} not $VAR |
- Make changes in
src/ - Run
npm run buildto verify TypeScript compiles - Test manually with OpenCode
- Update types in
src/types.tsif data structures change - Update CHANGELOG.md