opencoding-agent 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,31 @@ opencoding-agent is an OpenCode plugin that provides specialized agents and a su
5
5
  ## Features
6
6
 
7
7
  - **Custom Agents**: Replaces default agents with specialized `plan` and `build` modes.
8
+ - **Dynamic MCP Injection**: Injects powerful Model Context Protocol (MCP) servers into your environment.
8
9
  - **Subagent Catalog**: Browse and install subagents from the [awesome-opencode-subagents](https://github.com/j5hjun/awesome-opencode-subagents) repository.
9
10
 
11
+ ## Built-in MCPs
12
+
13
+ The following MCP servers are automatically injected and granted full access for `opencoding-plan` and `opencoding-build` agents:
14
+
15
+ - **websearch**: Real-time web search powered by Exa AI.
16
+ - **context7**: Official documentation lookup for libraries.
17
+ - **grep_app**: Ultra-fast code search across GitHub repositories.
18
+
19
+ ## Configuration
20
+
21
+ You can customize the plugin by creating a configuration file at `~/.config/opencode/opencoding-agent.json` or `.opencoding-agent.json` in your project root.
22
+
23
+ ### Disable specific MCPs
24
+
25
+ If you want to disable specific MCP servers, add them to the `disabled_mcps` list:
26
+
27
+ ```json
28
+ {
29
+ "disabled_mcps": ["grep_app"]
30
+ }
31
+ ```
32
+
10
33
  ## Installation
11
34
 
12
35
  Add this plugin to your `opencode.json`:
@@ -1 +1 @@
1
- export declare const injectAgents: (config: any) => Promise<void>;
1
+ export declare const injectAgents: (opencodeConfig: any) => Promise<void>;
@@ -1,20 +1,22 @@
1
1
  import { planAgent } from "./plan";
2
2
  import { buildAgent } from "./build";
3
- export const injectAgents = async (config) => {
4
- config.agent = {
5
- ...config.agent,
3
+ import { deepMerge } from "../config";
4
+ export const injectAgents = async (opencodeConfig) => {
5
+ const existingAgents = (opencodeConfig.agent ?? {});
6
+ opencodeConfig.agent = {
7
+ ...existingAgents,
6
8
  // Disable default agents
7
9
  "build": { disable: true },
8
10
  "plan": { disable: true },
9
11
  // Inject our opencoding- prefixed agents
10
- "opencoding-plan": {
11
- ...planAgent,
12
+ "opencoding-plan": deepMerge(planAgent, {
13
+ ...(existingAgents["opencoding-plan"] ?? {}),
12
14
  disable: false
13
- },
14
- "opencoding-build": {
15
- ...buildAgent,
15
+ }),
16
+ "opencoding-build": deepMerge(buildAgent, {
17
+ ...(existingAgents["opencoding-build"] ?? {}),
16
18
  disable: false
17
- }
19
+ })
18
20
  };
19
- config.default_agent = "opencoding-plan";
21
+ opencodeConfig.default_agent = "opencoding-plan";
20
22
  };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Parse a list with wildcard and exclusion syntax.
3
+ * Validates against allAvailable and removes duplicates.
4
+ */
5
+ export declare function parseList(items: string[], allAvailable: string[]): string[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Parse a list with wildcard and exclusion syntax.
3
+ * Validates against allAvailable and removes duplicates.
4
+ */
5
+ export function parseList(items, allAvailable) {
6
+ if (!items || items.length === 0) {
7
+ return [];
8
+ }
9
+ const allow = items.filter((i) => !i.startsWith("!"));
10
+ const deny = items.filter((i) => i.startsWith("!")).map((i) => i.slice(1));
11
+ if (deny.includes("*")) {
12
+ return [];
13
+ }
14
+ let result;
15
+ if (allow.includes("*")) {
16
+ result = allAvailable.filter((item) => !deny.includes(item));
17
+ }
18
+ else {
19
+ result = allow.filter((item) => !deny.includes(item) && allAvailable.includes(item));
20
+ }
21
+ return [...new Set(result)];
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { expect, test, describe } from "bun:test";
2
+ import { parseList } from "./mcp-parser";
3
+ describe("parseList", () => {
4
+ const allAvailable = ["a", "b", "c"];
5
+ test("returns empty list for empty items", () => {
6
+ expect(parseList([], allAvailable)).toEqual([]);
7
+ });
8
+ test("returns specific items", () => {
9
+ expect(parseList(["a", "b"], allAvailable)).toEqual(["a", "b"]);
10
+ });
11
+ test("returns all items with *", () => {
12
+ expect(parseList(["*"], allAvailable)).toEqual(["a", "b", "c"]);
13
+ });
14
+ test("excludes specific items", () => {
15
+ expect(parseList(["*", "!b"], allAvailable)).toEqual(["a", "c"]);
16
+ });
17
+ test("excludes all with !*", () => {
18
+ expect(parseList(["a", "!*"], allAvailable)).toEqual([]);
19
+ });
20
+ test("handles only exclusions", () => {
21
+ expect(parseList(["!b"], allAvailable)).toEqual([]);
22
+ });
23
+ });
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  export type AgentInfo = {
2
3
  name: string;
3
4
  description: string;
@@ -6,4 +7,20 @@ export type AgentInfo = {
6
7
  permission: Record<string, string>;
7
8
  prompt?: string;
8
9
  };
10
+ /**
11
+ * opencoding-agent Configuration Schema
12
+ */
13
+ export declare const PluginConfigSchema: z.ZodObject<{
14
+ disabled_mcps: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
+ }, z.core.$strip>;
16
+ export type PluginConfig = z.infer<typeof PluginConfigSchema>;
9
17
  export declare const PLUGIN_NAME = "opencoding-agent";
18
+ /**
19
+ * Loads the plugin configuration from the home directory or project directory.
20
+ * Path: ~/.config/opencode/opencoding-agent.json or <project_dir>/.opencoding-agent.json
21
+ */
22
+ export declare function loadPluginConfig(projectDir?: string): PluginConfig;
23
+ /**
24
+ * Basic deep merge for configuration objects.
25
+ */
26
+ export declare function deepMerge(target: any, source: any): any;
@@ -1 +1,54 @@
1
+ import { z } from "zod";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ /**
6
+ * opencoding-agent Configuration Schema
7
+ */
8
+ export const PluginConfigSchema = z.object({
9
+ disabled_mcps: z.array(z.string()).optional(),
10
+ // Add other config items as needed
11
+ });
1
12
  export const PLUGIN_NAME = "opencoding-agent";
13
+ /**
14
+ * Loads the plugin configuration from the home directory or project directory.
15
+ * Path: ~/.config/opencode/opencoding-agent.json or <project_dir>/.opencoding-agent.json
16
+ */
17
+ export function loadPluginConfig(projectDir) {
18
+ const globalConfigPath = path.join(os.homedir(), ".config", "opencode", `${PLUGIN_NAME}.json`);
19
+ const projectConfigPath = projectDir ? path.join(projectDir, `.${PLUGIN_NAME}.json`) : null;
20
+ const configPaths = [projectConfigPath, globalConfigPath].filter(Boolean);
21
+ for (const configPath of configPaths) {
22
+ if (fs.existsSync(configPath)) {
23
+ try {
24
+ const rawContent = fs.readFileSync(configPath, "utf-8");
25
+ const json = JSON.parse(rawContent);
26
+ return PluginConfigSchema.parse(json);
27
+ }
28
+ catch (error) {
29
+ console.error(`[${PLUGIN_NAME}] Failed to load config from ${configPath}:`, error);
30
+ }
31
+ }
32
+ }
33
+ return {};
34
+ }
35
+ /**
36
+ * Basic deep merge for configuration objects.
37
+ */
38
+ export function deepMerge(target, source) {
39
+ if (typeof target !== "object" || target === null || typeof source !== "object" || source === null) {
40
+ return source;
41
+ }
42
+ const result = { ...target };
43
+ for (const key in source) {
44
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
45
+ if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key])) {
46
+ result[key] = deepMerge(target[key] || {}, source[key]);
47
+ }
48
+ else {
49
+ result[key] = source[key];
50
+ }
51
+ }
52
+ }
53
+ return result;
54
+ }
@@ -3,6 +3,7 @@ import type { Plugin } from "@opencode-ai/plugin";
3
3
  * opencoding-agent Plugin
4
4
  *
5
5
  * Replaces default OpenCode agents with core-identical 'plan' and 'build' modes.
6
+ * Now with dynamic MCP injection!
6
7
  */
7
8
  declare const OpencodingAgentPlugin: Plugin;
8
9
  export default OpencodingAgentPlugin;
package/dist/src/index.js CHANGED
@@ -1,20 +1,63 @@
1
1
  import { injectAgents } from "./agents";
2
2
  import { catalogTools } from "./tools/catalog";
3
+ import { createBuiltinMcps } from "./mcp";
4
+ import { loadPluginConfig } from "./config";
3
5
  /**
4
6
  * opencoding-agent Plugin
5
7
  *
6
8
  * Replaces default OpenCode agents with core-identical 'plan' and 'build' modes.
9
+ * Now with dynamic MCP injection!
7
10
  */
8
11
  const OpencodingAgentPlugin = async (ctx) => {
12
+ const pluginConfig = loadPluginConfig(ctx.directory);
13
+ const mcps = createBuiltinMcps(pluginConfig.disabled_mcps);
14
+ const mcpNames = Object.keys(mcps);
9
15
  return {
16
+ name: "opencoding-agent",
10
17
  // Config hook: Injected once during initialization
11
- config: async (config) => {
12
- await injectAgents(config);
18
+ // Note: 'any' is used because the Config type is not exported by the plugin SDK
19
+ config: async (opencodeConfig) => {
20
+ // 1. Inject specialized agents (opencoding-plan, opencoding-build)
21
+ await injectAgents(opencodeConfig);
22
+ // 2. Merge MCP configs (careful not to overwrite user settings)
23
+ if (!opencodeConfig.mcp) {
24
+ opencodeConfig.mcp = { ...mcps };
25
+ }
26
+ else {
27
+ const existingMcp = opencodeConfig.mcp;
28
+ for (const [name, config] of Object.entries(mcps)) {
29
+ if (!(name in existingMcp)) {
30
+ existingMcp[name] = config;
31
+ }
32
+ }
33
+ }
34
+ // 3. Grant full permissions to opencoding- agents for these MCPs
35
+ const agentsToGrant = ["opencoding-plan", "opencoding-build"];
36
+ const agentConfig = opencodeConfig.agent;
37
+ agentsToGrant.forEach((agentName) => {
38
+ const agent = agentConfig[agentName];
39
+ if (!agent)
40
+ return;
41
+ if (!agent.permission) {
42
+ agent.permission = {};
43
+ }
44
+ for (const mcpName of mcpNames) {
45
+ // MCP tools are prefixed with sanitized mcp server name
46
+ const sanitizedMcpName = mcpName.replace(/[^a-zA-Z0-9_-]/g, "_");
47
+ const permissionKey = `${sanitizedMcpName}_*`;
48
+ // Force allow unless already defined
49
+ if (!(permissionKey in agent.permission)) {
50
+ agent.permission[permissionKey] = "allow";
51
+ }
52
+ }
53
+ });
13
54
  },
14
55
  // Register custom tools
15
56
  tool: {
16
- ...catalogTools
17
- }
57
+ ...catalogTools,
58
+ },
59
+ // Register MCPs
60
+ mcp: mcps,
18
61
  };
19
62
  };
20
63
  export default OpencodingAgentPlugin;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test, mock } from "bun:test";
2
+ import OpencodingAgentPlugin from "./index";
3
+ // Mock config load for test
4
+ mock.module("./config", () => {
5
+ return {
6
+ loadPluginConfig: () => ({ disabled_mcps: [] }),
7
+ PLUGIN_NAME: "opencoding-agent"
8
+ };
9
+ });
10
+ describe("OpencodingAgentPlugin", () => {
11
+ const mockCtx = {
12
+ directory: "/mock/dir"
13
+ };
14
+ test("config hook injects agents and mcps and permissions", async () => {
15
+ const plugin = await OpencodingAgentPlugin(mockCtx);
16
+ const opencodeConfig = {
17
+ agent: {},
18
+ mcp: {}
19
+ };
20
+ if (plugin.config) {
21
+ await plugin.config(opencodeConfig);
22
+ }
23
+ // Agents should be injected
24
+ expect(opencodeConfig.agent["opencoding-plan"]).toBeDefined();
25
+ expect(opencodeConfig.agent["opencoding-build"]).toBeDefined();
26
+ // MCPs should be merged
27
+ expect(opencodeConfig.mcp["websearch"]).toBeDefined();
28
+ expect(opencodeConfig.mcp["context7"]).toBeDefined();
29
+ expect(opencodeConfig.mcp["grep_app"]).toBeDefined();
30
+ // Permissions should be granted to opencoding- agents
31
+ expect(opencodeConfig.agent["opencoding-plan"].permission["websearch_*"]).toBe("allow");
32
+ expect(opencodeConfig.agent["opencoding-build"].permission["websearch_*"]).toBe("allow");
33
+ });
34
+ });
@@ -0,0 +1,6 @@
1
+ import type { RemoteMcpConfig } from './types';
2
+ /**
3
+ * Context7 - official documentation lookup for libraries
4
+ * @see https://context7.com
5
+ */
6
+ export declare const context7: RemoteMcpConfig;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Context7 - official documentation lookup for libraries
3
+ * @see https://context7.com
4
+ */
5
+ export const context7 = {
6
+ type: 'remote',
7
+ url: 'https://mcp.context7.com/mcp',
8
+ headers: process.env.CONTEXT7_API_KEY
9
+ ? { CONTEXT7_API_KEY: process.env.CONTEXT7_API_KEY }
10
+ : undefined,
11
+ oauth: false,
12
+ };
@@ -0,0 +1,6 @@
1
+ import type { RemoteMcpConfig } from './types';
2
+ /**
3
+ * grep.app - ultra-fast code search across GitHub repositories
4
+ * @see https://grep.app
5
+ */
6
+ export declare const grep_app: RemoteMcpConfig;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * grep.app - ultra-fast code search across GitHub repositories
3
+ * @see https://grep.app
4
+ */
5
+ export const grep_app = {
6
+ type: 'remote',
7
+ url: 'https://mcp.grep.app',
8
+ oauth: false,
9
+ };
@@ -0,0 +1,7 @@
1
+ import type { McpConfig } from './types';
2
+ export type { LocalMcpConfig, McpConfig, RemoteMcpConfig } from './types';
3
+ /**
4
+ * Creates MCP configurations, excluding disabled ones.
5
+ * Supports wildcard (*) and exclusion (!) syntax via parseList.
6
+ */
7
+ export declare function createBuiltinMcps(disabledMcps?: readonly string[]): Record<string, McpConfig>;
@@ -0,0 +1,21 @@
1
+ import { context7 } from './context7';
2
+ import { grep_app } from './grep-app';
3
+ import { websearch } from './websearch';
4
+ import { parseList } from '../config/mcp-parser';
5
+ const allBuiltinMcps = {
6
+ websearch,
7
+ context7,
8
+ grep_app,
9
+ };
10
+ /**
11
+ * Creates MCP configurations, excluding disabled ones.
12
+ * Supports wildcard (*) and exclusion (!) syntax via parseList.
13
+ */
14
+ export function createBuiltinMcps(disabledMcps = []) {
15
+ const allNames = Object.keys(allBuiltinMcps);
16
+ // By default, all builtin MCPs are allowed.
17
+ // We prepend '*' to the list and then apply the disabled list as exclusions.
18
+ const items = ["*", ...disabledMcps.map(name => `!${name}`)];
19
+ const enabledNames = parseList(items, allNames);
20
+ return Object.fromEntries(Object.entries(allBuiltinMcps).filter(([name]) => enabledNames.includes(name)));
21
+ }
@@ -0,0 +1,12 @@
1
+ export type RemoteMcpConfig = {
2
+ type: 'remote';
3
+ url: string;
4
+ headers?: Record<string, string>;
5
+ oauth?: false;
6
+ };
7
+ export type LocalMcpConfig = {
8
+ type: 'local';
9
+ command: string[];
10
+ environment?: Record<string, string>;
11
+ };
12
+ export type McpConfig = RemoteMcpConfig | LocalMcpConfig;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { RemoteMcpConfig } from './types';
2
+ /**
3
+ * Exa AI web search - real-time web search
4
+ * @see https://exa.ai
5
+ */
6
+ export declare const websearch: RemoteMcpConfig;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Exa AI web search - real-time web search
3
+ * @see https://exa.ai
4
+ */
5
+ export const websearch = {
6
+ type: 'remote',
7
+ url: 'https://mcp.exa.ai/mcp?tools=web_search_exa',
8
+ headers: process.env.EXA_API_KEY
9
+ ? { 'x-api-key': process.env.EXA_API_KEY }
10
+ : undefined,
11
+ oauth: false,
12
+ };
@@ -1,33 +1,2 @@
1
- import { z } from "zod";
2
- export declare const catalogTools: {
3
- "subagent-catalog:list": {
4
- description: string;
5
- args: {};
6
- execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
7
- };
8
- "subagent-catalog:search": {
9
- description: string;
10
- args: {
11
- query: z.ZodString;
12
- category: z.ZodOptional<z.ZodString>;
13
- };
14
- execute(args: {
15
- query: string;
16
- category?: string | undefined;
17
- }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
18
- };
19
- "subagent-catalog:fetch": {
20
- description: string;
21
- args: {
22
- name: z.ZodString;
23
- scope: z.ZodDefault<z.ZodEnum<{
24
- global: "global";
25
- local: "local";
26
- }>>;
27
- };
28
- execute(args: {
29
- name: string;
30
- scope: "global" | "local";
31
- }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
32
- };
33
- };
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ export declare const catalogTools: Record<string, ToolDefinition>;
@@ -40,7 +40,8 @@ export const catalogTools = {
40
40
  query: z.string().describe("Search term"),
41
41
  category: z.string().optional().describe("Filter by category")
42
42
  },
43
- execute: async ({ query, category }) => {
43
+ execute: async (args) => {
44
+ const { query, category } = args;
44
45
  try {
45
46
  const url = `${REPO_URL}/catalog.json?t=${Date.now()}`;
46
47
  const response = await fetch(url);
@@ -75,7 +76,8 @@ export const catalogTools = {
75
76
  name: z.string().describe("Name of the agent to fetch"),
76
77
  scope: z.enum(["global", "local"]).default("global").describe("Installation scope (global: all projects, local: current project only)")
77
78
  },
78
- execute: async ({ name, scope }, { directory }) => {
79
+ execute: async (args, { directory }) => {
80
+ const { name, scope } = args;
79
81
  try {
80
82
  const catalogUrl = `${REPO_URL}/catalog.json?t=${Date.now()}`;
81
83
  const catalogResponse = await fetch(catalogUrl);
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "opencoding-agent",
3
- "version": "1.0.2",
4
- "description": "OpenCode specialized agents and subagent catalog manager",
5
- "type": "module",
3
+ "version": "1.0.3",
6
4
  "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
5
+ "devDependencies": {
6
+ "@opencode-ai/plugin": "^1.0.0",
7
+ "@types/bun": "^1.2.0",
8
+ "@types/node": "^22.0.0",
9
+ "typescript": "^5.7.0"
10
+ },
11
+ "peerDependencies": {
12
+ "@opencode-ai/plugin": "^1.0.0"
13
+ },
14
+ "description": "OpenCode specialized agents and subagent catalog manager",
8
15
  "files": [
9
16
  "dist",
10
17
  "README.md",
@@ -16,21 +23,14 @@
16
23
  "plugin"
17
24
  ],
18
25
  "license": "MIT",
19
- "peerDependencies": {
20
- "@opencode-ai/plugin": "^1.0.0"
21
- },
22
- "devDependencies": {
23
- "@opencode-ai/plugin": "^1.0.0",
24
- "@types/bun": "^1.2.0",
25
- "@types/node": "^22.0.0",
26
- "typescript": "^5.7.0"
26
+ "publishConfig": {
27
+ "access": "public"
27
28
  },
28
29
  "scripts": {
29
30
  "clean": "rm -rf dist",
30
31
  "build": "npm run clean && tsc",
31
32
  "prepublishOnly": "npm run build"
32
33
  },
33
- "publishConfig": {
34
- "access": "public"
35
- }
34
+ "type": "module",
35
+ "types": "dist/index.d.ts"
36
36
  }