Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ Set `OPENCODE_LAZY_LOADER_FORCE=1` to force-enable the plugin even when oh-my-op

| Priority | Location | Scope |
|----------|----------|-------|
| 1 (highest) | `.opencode/skill/` | Project-specific |
| 2 | `~/.config/opencode/skill/` | User global |
| 1 (highest) | `.opencode/skills/` | Project-specific |
| 2 | `~/.config/opencode/skills/` | User global |

Project skills override global skills with the same name.

Expand Down Expand Up @@ -217,7 +217,7 @@ Before releasing, verify:
| 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/skill/` or `~/.config/opencode/skill/` |
| Skills not discovered | Wrong directory | Check `.opencode/skills/` or `~/.config/opencode/skills/` |
| Env vars not expanded | Wrong syntax | Use `${VAR}` not `$VAR` |

## Contributing
Expand Down
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

This is the OpenCode plugin that lazy-loads skill-embedded MCP servers. It lets skills bundle their own MCP servers so they can be loaded on-demand instead of being configured globally.

Note: This package was renamed from `opencode-embedded-skill-mcp` to `opencode-lazy-loader`. If you still have the old package, upgrade to the new name.

This is a standalone OpenCode plugin that enables skills to bundle and manage their own [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, then lazy-load them on demand.

This allows skills to bring their own tools, resources, and prompts without requiring manual server configuration in `opencode.json`.
Expand All @@ -30,21 +28,19 @@ This allows skills to bring their own tools, resources, and prompts without requ

## Installation

Deprecation notice: The package was renamed from `opencode-embedded-skill-mcp` to `opencode-lazy-loader`. If you installed the old name, update to the new package.

Add the plugin to your `opencode.json`:

```json
{
"plugin": ["opencode-lazy-loader"]
"plugin": ["@orionpax/opencode-lazy-mcp"]
}
```

Or install it locally:

```json
{
"plugin": ["./path/to/opencode-lazy-loader"]
"plugin": ["./path/to/@orionpax/opencode-lazy-mcp"]
}
```

Expand All @@ -62,15 +58,15 @@ Then use the embedded MCP:
skill_mcp(mcp_name="playwright", tool_name="browser_navigate", arguments='{"url": "https://example.com"}')
```

See [`.opencode/skill/playwright-example/SKILL.md`](.opencode/skill/playwright-example/SKILL.md) for the full example.
See [`.opencode/skills/playwright-example/SKILL.md`](.opencode/skills/playwright-example/SKILL.md) for the full example.

## Usage

### 1. Create a Skill with Embedded MCP

You can define MCP servers in the skill's YAML frontmatter:

**`~/.config/opencode/skill/my-skill/SKILL.md`**
**`~/.config/opencode/skills/my-skill/SKILL.md`**

```markdown
---
Expand All @@ -88,7 +84,7 @@ This skill provides browser automation tools via the `playwright` MCP.

Alternatively, place an `mcp.json` file in the skill directory:

**`~/.config/opencode/skill/browser-automation/mcp.json`**
**`~/.config/opencode/skills/browser-automation/mcp.json`**

```json
{
Expand Down Expand Up @@ -184,7 +180,7 @@ interface McpServerConfig {

## Example Skill

Here's a complete example of a skill with an embedded MCP server (from [`.opencode/skill/playwright-example/SKILL.md`](.opencode/skill/playwright-example/SKILL.md)):
Here's a complete example of a skill with an embedded MCP server (from [`.opencode/skills/playwright-example/SKILL.md`](.opencode/skills/playwright-example/SKILL.md)):

```markdown
---
Expand Down
9 changes: 7 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencode-lazy-loader",
"name": "@orionpax/opencode-lazy-mcp",
"version": "1.0.3",
"description": "OpenCode plugin for lazy-loading skill-embedded MCP servers",
"description": "OpenCode plugin for skill-embedded MCP support - skills can define their own MCP servers that are automatically discovered and lazily loaded",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -19,20 +19,19 @@
"watch": "npx tsc --watch",
"clean": "rm -rf dist",
"test": "vitest run",
"test:watch": "vitest",
"prepack": "npm run clean && npm run build"
"test:watch": "vitest"
},
"keywords": [
"opencode",
"plugin",
"mcp",
"skill"
],
"author": "keybrdist",
"author": "orionpax",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/keybrdist/opencode-lazy-loader.git"
"url": "git+https://github.com/orionpax/opencode-lazy-mcp.git"
},
"files": [
"dist",
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,6 @@ export const OpenCodeEmbeddedSkillMcp: Plugin = async ({ client }) => {
export default OpenCodeEmbeddedSkillMcp

// Re-export types for external use
export type { LoadedSkill, McpServerConfig, SkillScope } from './types.js'
export type { LoadedSkill, McpServerConfig, LocalMcpServerConfig, RemoteMcpServerConfig, SkillScope } from './types.js'
export { discoverSkills } from './skill-loader.js'
export { createSkillMcpManager } from './skill-mcp-manager.js'
19 changes: 11 additions & 8 deletions src/skill-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { promises as fs } from 'fs'
import { promises as fs, Dirent } from 'fs'
import { join, basename } from 'path'
import { homedir } from 'os'
import type { LoadedSkill, McpServerConfig, SkillScope, LazyContent } from './types.js'
Expand Down Expand Up @@ -45,12 +45,15 @@ export async function loadMcpJsonFromDir(
return parsed.mcp as Record<string, McpServerConfig>
}

// Support direct { serverName: { command: ... } } format
// Support direct { serverName: { command: ... } } or { serverName: { type: "remote", url: ... } } format
if (parsed && typeof parsed === 'object' && !('mcpServers' in parsed) && !('mcp' in parsed)) {
const hasCommandField = Object.values(parsed).some(
(v) => v && typeof v === 'object' && 'command' in (v as Record<string, unknown>)
)
if (hasCommandField) {
const hasRemoteConfig = Object.values(parsed).some(
(v) => v && typeof v === 'object' && 'type' in (v as Record<string, unknown>) && (v as Record<string, unknown>).type === 'remote'
)
if (hasCommandField || hasRemoteConfig) {
return parsed as unknown as Record<string, McpServerConfig>
}
}
Expand Down Expand Up @@ -132,7 +135,7 @@ export async function loadSkillsFromDir(
skillsDir: string,
scope: SkillScope
): Promise<LoadedSkill[]> {
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => [])
const entries: Dirent<string>[] = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => [])
const skills: LoadedSkill[] = []

for (const entry of entries) {
Expand Down Expand Up @@ -191,18 +194,18 @@ export async function loadSkillsFromDir(
}

/**
* Discover skills from opencode global directory (~/.config/opencode/skill/)
* Discover skills from opencode global directory (~/.config/opencode/skills/)
*/
export async function discoverOpencodeGlobalSkills(): Promise<LoadedSkill[]> {
const opencodeSkillsDir = join(homedir(), '.config', 'opencode', 'skill')
const opencodeSkillsDir = join(homedir(), '.config', 'opencode', 'skills')
return loadSkillsFromDir(opencodeSkillsDir, 'opencode')
}

/**
* Discover skills from opencode project directory (.opencode/skill/)
* Discover skills from opencode project directory (.opencode/skills/)
*/
export async function discoverOpencodeProjectSkills(): Promise<LoadedSkill[]> {
const opencodeProjectDir = join(process.cwd(), '.opencode', 'skill')
const opencodeProjectDir = join(process.cwd(), '.opencode', 'skills')
return loadSkillsFromDir(opencodeProjectDir, 'opencode-project')
}

Expand Down
Loading