opencode-toolbox 0.4.0 → 0.5.0

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.
Files changed (3) hide show
  1. package/README.md +156 -7
  2. package/dist/index.js +226 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,7 +63,7 @@ Create `~/.config/opencode/toolbox.jsonc`:
63
63
 
64
64
  ## Usage
65
65
 
66
- The plugin exposes three tools:
66
+ The plugin exposes four tools:
67
67
 
68
68
  ### toolbox_search_bm25
69
69
 
@@ -135,6 +135,89 @@ Toolbox: { "datetime": "2026-01-07T02:15:00+09:00", "timezone": "Asia/Tokyo" }
135
135
  LLM: "The current time in Tokyo is 2:15 AM on January 7, 2026."
136
136
  ```
137
137
 
138
+ ### toolbox_status
139
+
140
+ Get toolbox status including plugin health, MCP server connections, and tool counts:
141
+
142
+ ```
143
+ toolbox_status({})
144
+ ```
145
+
146
+ Returns a comprehensive status object:
147
+
148
+ ```json
149
+ {
150
+ "plugin": {
151
+ "initialized": true,
152
+ "configPath": "/Users/username/.config/opencode/toolbox.jsonc",
153
+ "uptime": 123.45,
154
+ "searches": 23,
155
+ "executions": 15,
156
+ "successRate": "93%"
157
+ },
158
+ "servers": {
159
+ "total": 3,
160
+ "connected": 2,
161
+ "failed": 1,
162
+ "connecting": 0,
163
+ "connectionRatio": "2/3",
164
+ "details": [
165
+ {
166
+ "name": "time",
167
+ "status": "connected",
168
+ "type": "local",
169
+ "toolCount": 5,
170
+ "error": null,
171
+ "healthy": true
172
+ },
173
+ {
174
+ "name": "github",
175
+ "status": "connected",
176
+ "type": "local",
177
+ "toolCount": 12,
178
+ "error": null,
179
+ "healthy": true
180
+ },
181
+ {
182
+ "name": "weather",
183
+ "status": "error",
184
+ "type": "remote",
185
+ "toolCount": 0,
186
+ "error": "Failed to connect: timeout",
187
+ "healthy": false
188
+ }
189
+ ]
190
+ },
191
+ "tools": {
192
+ "total": 17,
193
+ "available": 17,
194
+ "serversWithTools": 2
195
+ },
196
+ "health": {
197
+ "status": "degraded",
198
+ "message": "1 server(s) failed to connect"
199
+ }
200
+ }
201
+ ```
202
+
203
+ **Health Status:**
204
+ - `healthy`: All servers connected successfully
205
+ - `degraded`: Some servers failed to connect (check `servers.failed`)
206
+ - `unknown`: No servers configured or initialization failed
207
+
208
+ ### /toolbox-status Slash Command
209
+
210
+ The plugin automatically creates and maintains a `/toolbox-status` slash command:
211
+
212
+ ```
213
+ ~/.config/opencode/command/toolbox-status.md
214
+ ```
215
+
216
+ - **Auto-created** on first plugin launch
217
+ - **Auto-updated** when plugin version changes (tracked via `toolbox_version` in frontmatter)
218
+
219
+ Use it in OpenCode by typing `/toolbox-status` to get a formatted status report.
220
+
138
221
  ## Search Modes
139
222
 
140
223
  ### BM25 (Natural Language)
@@ -172,13 +255,69 @@ bun test --coverage # Run with coverage
172
255
  bun run build
173
256
  ```
174
257
 
258
+ ## Observability
259
+
260
+ The toolbox plugin provides built-in logging and status monitoring to help you understand what's happening.
261
+
262
+ ### Logging
263
+
264
+ All plugin operations are logged **silently** to a dedicated log file (no screen output):
265
+
266
+ ```
267
+ ~/.local/share/opencode/toolbox.log
268
+ ```
269
+
270
+ Log entries include:
271
+ - Plugin initialization status
272
+ - MCP server connection status (connected/error)
273
+ - Tool search operations (BM25/regex queries + result counts)
274
+ - Tool execution results (success/failure with duration)
275
+ - Errors with details
276
+
277
+ **View logs:**
278
+ ```bash
279
+ # Watch logs in real-time
280
+ tail -f ~/.local/share/opencode/toolbox.log
281
+
282
+ # Check for errors only
283
+ grep "ERROR" ~/.local/share/opencode/toolbox.log
284
+
285
+ # Check for warnings
286
+ grep "WARN" ~/.local/share/opencode/toolbox.log
287
+ ```
288
+
289
+ **Log format:**
290
+ ```
291
+ 2026-01-08T12:34:56.789Z [INFO] Toolbox plugin loaded successfully {"configPath":"...","serverCount":6}
292
+ 2026-01-08T12:34:57.123Z [INFO] Initialization complete: 5/6 servers connected, 42 tools indexed
293
+ 2026-01-08T12:34:57.124Z [WARN] 1 server(s) failed to connect: weather
294
+ 2026-01-08T12:35:00.456Z [INFO] BM25 search completed: "web search" -> 3 results
295
+ ```
296
+
297
+ ### Status Tool
298
+
299
+ Use the `toolbox_status` command to check plugin health at any time:
300
+
301
+ ```
302
+ toolbox_status({})
303
+ ```
304
+
305
+ This shows:
306
+ - **Plugin Status**: Initialization, config path, uptime, search/execution counts
307
+ - **Server Status**: Connection ratio (e.g., "2/3"), details per server
308
+ - **Tools**: Total available tools, servers with tools
309
+ - **Health**: Overall health status (healthy/degraded/unknown)
310
+
311
+ **Connection Ratio**: Shows `success/total` for servers. If `success < total`, it indicates failed connections.
312
+
175
313
  ## Troubleshooting
176
314
 
177
315
  ### Plugin not loading
178
316
 
179
- 1. Check OpenCode logs for plugin errors
180
- 2. Verify `opencode-toolbox` is in the `plugin` array in `opencode.jsonc`
181
- 3. Ensure `toolbox.jsonc` exists and is valid JSON
317
+ 1. Run `toolbox_status({})` to check initialization status
318
+ 2. Check OpenCode logs at `~/.local/share/opencode/log/` for plugin errors
319
+ 3. Verify `opencode-toolbox` is in the `plugin` array in `opencode.jsonc`
320
+ 4. Ensure `toolbox.jsonc` exists and is valid JSON
182
321
 
183
322
  ### Search finds no tools
184
323
 
@@ -186,11 +325,21 @@ bun run build
186
325
  2. Check tool descriptions for relevant keywords
187
326
  3. Try broader search terms or regex patterns
188
327
 
328
+ ### MCP servers not connecting
329
+
330
+ 1. Run `toolbox_status({})` to see which servers failed
331
+ 2. Check logs for specific error messages from failed servers
332
+ 3. Verify server command works standalone: `npx -y @anthropic/mcp-time`
333
+ 4. For remote servers, verify URL is accessible
334
+ 5. Check environment variables are set correctly
335
+
189
336
  ### Execute fails
190
337
 
191
- 1. Verify the tool name format: `serverName_toolName`
192
- 2. Check `arguments` is valid JSON
193
- 3. Ensure underlying MCP server is running
338
+ 1. Run `toolbox_status({})` to check server health
339
+ 2. Verify tool name format: `serverName_toolName`
340
+ 3. Check `arguments` is valid JSON
341
+ 4. Ensure underlying MCP server is running and connected
342
+ 5. Check logs for detailed error messages
194
343
 
195
344
  ## License
196
345
 
package/dist/index.js CHANGED
@@ -19266,6 +19266,9 @@ function tool(input) {
19266
19266
  return input;
19267
19267
  }
19268
19268
  tool.schema = exports_external;
19269
+ // src/plugin.ts
19270
+ import { appendFile, mkdir, writeFile, readFile } from "fs/promises";
19271
+
19269
19272
  // node_modules/zod/v4/classic/external.js
19270
19273
  var exports_external2 = {};
19271
19274
  __export(exports_external2, {
@@ -37908,8 +37911,69 @@ function generateSignature(tool3) {
37908
37911
  }).join(", ");
37909
37912
  return `${tool3.id.name}(${argList})`;
37910
37913
  }
37914
+ // package.json
37915
+ var package_default = {
37916
+ name: "opencode-toolbox",
37917
+ version: "0.5.0",
37918
+ description: "Tool Search Tool Plugin for OpenCode - search and execute tools from MCP servers on-demand",
37919
+ main: "dist/index.js",
37920
+ module: "dist/index.js",
37921
+ type: "module",
37922
+ exports: {
37923
+ ".": {
37924
+ import: "./dist/index.js",
37925
+ types: "./dist/index.d.ts"
37926
+ }
37927
+ },
37928
+ scripts: {
37929
+ build: "bun build src/index.ts --outdir dist --target bun",
37930
+ dev: "bun --hot src/index.ts",
37931
+ test: "bun test",
37932
+ "test:coverage": "bun test --coverage",
37933
+ typecheck: "tsc --noEmit"
37934
+ },
37935
+ files: [
37936
+ "dist",
37937
+ "README.md"
37938
+ ],
37939
+ keywords: [
37940
+ "opencode",
37941
+ "plugin",
37942
+ "tool-search",
37943
+ "mcp"
37944
+ ],
37945
+ license: "MIT",
37946
+ devDependencies: {
37947
+ "@types/bun": "latest",
37948
+ typescript: "^5.9.3"
37949
+ },
37950
+ peerDependencies: {
37951
+ typescript: "^5"
37952
+ },
37953
+ dependencies: {
37954
+ "@modelcontextprotocol/sdk": "^1.25.1",
37955
+ "@opencode-ai/plugin": "latest",
37956
+ "jsonc-parser": "^3.3.1",
37957
+ zod: "^4.3.5"
37958
+ }
37959
+ };
37960
+
37911
37961
  // src/plugin.ts
37962
+ var PLUGIN_VERSION = package_default.version;
37912
37963
  var DEFAULT_CONFIG_PATH = `${process.env.HOME}/.config/opencode/toolbox.jsonc`;
37964
+ var LOG_FILE_PATH = `${process.env.HOME}/.local/share/opencode/toolbox.log`;
37965
+ var LOG_DIR = `${process.env.HOME}/.local/share/opencode`;
37966
+ var COMMAND_DIR = `${process.env.HOME}/.config/opencode/command`;
37967
+ var COMMAND_FILE_PATH = `${COMMAND_DIR}/toolbox-status.md`;
37968
+ function getCommandContent() {
37969
+ return `---
37970
+ description: Check toolbox plugin status and server health
37971
+ toolbox_version: ${PLUGIN_VERSION}
37972
+ ---
37973
+ Run toolbox_status({}) and show me the results in a readable format.
37974
+ Highlight any failed servers or issues.
37975
+ `;
37976
+ }
37913
37977
  function parseToolName(fullName) {
37914
37978
  const underscoreIndex = fullName.indexOf("_");
37915
37979
  if (underscoreIndex === -1) {
@@ -37948,12 +38012,38 @@ Returns tools with schemas. Use toolbox_execute() to run them.`;
37948
38012
  var EXECUTE_DESC = `Execute a tool discovered via toolbox_search_bm25 or toolbox_search_regex.
37949
38013
 
37950
38014
  Pass arguments as JSON string matching the tool's schema.`;
38015
+ var STATUS_DESC = `Get toolbox status including plugin initialization, MCP server connections, and tool counts.
38016
+
38017
+ Shows success/total metrics to highlight failures. Use to check if toolbox is working correctly.`;
38018
+ function log(level, message, extra) {
38019
+ const timestamp = new Date().toISOString();
38020
+ const extraStr = extra ? ` ${JSON.stringify(extra)}` : "";
38021
+ const line = `${timestamp} [${level.toUpperCase()}] ${message}${extraStr}
38022
+ `;
38023
+ mkdir(LOG_DIR, { recursive: true }).then(() => appendFile(LOG_FILE_PATH, line)).catch(() => {});
38024
+ }
38025
+ function ensureCommandFile() {
38026
+ const content = getCommandContent();
38027
+ readFile(COMMAND_FILE_PATH, "utf-8").then((existing) => {
38028
+ const versionMatch = existing.match(/toolbox_version:\s*([^\n]+)/);
38029
+ const existingVersion = versionMatch?.[1]?.trim() ?? null;
38030
+ if (existingVersion !== PLUGIN_VERSION) {
38031
+ return writeFile(COMMAND_FILE_PATH, content).then(() => {
38032
+ log("info", `Updated /toolbox-status command (${existingVersion} -> ${PLUGIN_VERSION})`);
38033
+ });
38034
+ }
38035
+ }).catch(() => {
38036
+ mkdir(COMMAND_DIR, { recursive: true }).then(() => writeFile(COMMAND_FILE_PATH, content)).then(() => log("info", `Created /toolbox-status command (v${PLUGIN_VERSION})`)).catch(() => {});
38037
+ });
38038
+ }
37951
38039
  var SYSTEM_PROMPT_BASE = `# Extended Toolbox
37952
38040
 
37953
38041
  You have access to an extended toolbox with additional capabilities (web search, time utilities, code search, etc.).
37954
38042
 
37955
38043
  ## Rule
37956
38044
  ALWAYS search before saying "I cannot do that" or "I don't have access to."
38045
+ DO NOT try to execute a tool without having the tool's exact tool schema. If you don't have the tool's schema
38046
+ then run toolbox_search_* to get them.
37957
38047
 
37958
38048
  ## Workflow
37959
38049
  1. Search: toolbox_search_bm25({ text: "what you need" }) or toolbox_search_regex({ pattern: "prefix_.*" })
@@ -37981,26 +38071,60 @@ ${JSON.stringify(toolboxSchema, null, 2)}
37981
38071
  \`\`\``;
37982
38072
  }
37983
38073
  var ToolboxPlugin = async (ctx) => {
38074
+ const { client } = ctx;
37984
38075
  const configPath = process.env.OPENCODE_TOOLBOX_CONFIG || DEFAULT_CONFIG_PATH;
37985
38076
  const configResult = await loadConfig(configPath);
37986
38077
  if (!configResult.success) {
37987
- console.error("[Toolbox] Failed to load config:", configResult.error.issues);
38078
+ const errorMsg = `Failed to load config from ${configPath}: ${configResult.error.issues.map((i) => i.message).join(", ")}`;
38079
+ log("error", errorMsg);
37988
38080
  return {};
37989
38081
  }
37990
38082
  const config3 = configResult.data;
37991
38083
  const mcpManager = new MCPManager;
37992
38084
  const bm25Index = new BM25Index;
37993
38085
  let initialized = false;
38086
+ let searchCount = 0;
38087
+ let executionCount = 0;
38088
+ let executionSuccessCount = 0;
38089
+ ensureCommandFile();
38090
+ const serverNames = Object.keys(config3.mcp);
38091
+ log("info", `Toolbox plugin loaded successfully`, {
38092
+ configPath,
38093
+ serverCount: serverNames.length,
38094
+ servers: serverNames
38095
+ });
37994
38096
  async function ensureInitialized() {
37995
38097
  if (initialized)
37996
38098
  return;
37997
38099
  try {
38100
+ log("info", "Initializing MCP servers...");
37998
38101
  await mcpManager.initialize(config3.mcp);
37999
38102
  const allTools = mcpManager.getAllCatalogTools();
38000
38103
  bm25Index.indexTools(allTools);
38001
38104
  initialized = true;
38105
+ const servers = mcpManager.getAllServers();
38106
+ const connectedServers = servers.filter((s) => s.status === "connected");
38107
+ const failedServers = servers.filter((s) => s.status === "error");
38108
+ const initMsg = `Initialization complete: ${connectedServers.length}/${servers.length} servers connected, ${allTools.length} tools indexed`;
38109
+ log("info", initMsg, {
38110
+ totalServers: servers.length,
38111
+ connectedServers: connectedServers.length,
38112
+ failedServers: failedServers.length,
38113
+ totalTools: allTools.length,
38114
+ servers: servers.map((s) => ({
38115
+ name: s.name,
38116
+ status: s.status,
38117
+ toolCount: s.tools.length,
38118
+ error: s.error || null
38119
+ }))
38120
+ });
38121
+ if (failedServers.length > 0) {
38122
+ const warnMsg = `${failedServers.length} server(s) failed to connect: ${failedServers.map((s) => s.name).join(", ")}`;
38123
+ log("warn", warnMsg);
38124
+ }
38002
38125
  } catch (error92) {
38003
- console.error("[Toolbox] Failed to initialize MCP servers:", error92);
38126
+ const errorMsg = `Failed to initialize MCP servers: ${error92 instanceof Error ? error92.message : String(error92)}`;
38127
+ log("error", errorMsg);
38004
38128
  throw error92;
38005
38129
  }
38006
38130
  }
@@ -38021,9 +38145,16 @@ var ToolboxPlugin = async (ctx) => {
38021
38145
  error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
38022
38146
  });
38023
38147
  }
38148
+ searchCount++;
38024
38149
  const searchLimit = args.limit || config3.settings?.defaultLimit || 5;
38025
38150
  const allTools = mcpManager.getAllCatalogTools();
38026
38151
  const results = bm25Index.search(args.text, searchLimit);
38152
+ log("info", `BM25 search completed: "${args.text}" -> ${results.length} results`, {
38153
+ searchType: "bm25",
38154
+ query: args.text,
38155
+ resultsCount: results.length,
38156
+ limit: searchLimit
38157
+ });
38027
38158
  return formatSearchResults(results, allTools);
38028
38159
  }
38029
38160
  }),
@@ -38042,15 +38173,23 @@ var ToolboxPlugin = async (ctx) => {
38042
38173
  error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
38043
38174
  });
38044
38175
  }
38176
+ searchCount++;
38045
38177
  const searchLimit = args.limit || config3.settings?.defaultLimit || 5;
38046
38178
  const allTools = mcpManager.getAllCatalogTools();
38047
38179
  const result = searchWithRegex(allTools, args.pattern, searchLimit);
38048
38180
  if ("error" in result) {
38181
+ log("warn", `Regex search failed: "${args.pattern}" -> ${result.error}`);
38049
38182
  return JSON.stringify({
38050
38183
  success: false,
38051
38184
  error: result.error
38052
38185
  });
38053
38186
  }
38187
+ log("info", `Regex search completed: "${args.pattern}" -> ${result.length} results`, {
38188
+ searchType: "regex",
38189
+ pattern: args.pattern,
38190
+ resultsCount: result.length,
38191
+ limit: searchLimit
38192
+ });
38054
38193
  return formatSearchResults(result, allTools);
38055
38194
  }
38056
38195
  }),
@@ -38071,6 +38210,9 @@ var ToolboxPlugin = async (ctx) => {
38071
38210
  }
38072
38211
  const parsed = parseToolName(args.name);
38073
38212
  if (!parsed) {
38213
+ log("warn", `Invalid tool name format: ${args.name}`, {
38214
+ toolName: args.name
38215
+ });
38074
38216
  return JSON.stringify({
38075
38217
  success: false,
38076
38218
  error: `Invalid tool name format: ${args.name}. Expected format: serverName_toolName (e.g., 'time_get_current_time')`
@@ -38081,25 +38223,106 @@ var ToolboxPlugin = async (ctx) => {
38081
38223
  try {
38082
38224
  toolArgs = JSON.parse(args.arguments);
38083
38225
  } catch (error92) {
38226
+ log("warn", `Failed to parse arguments as JSON for ${args.name}`, {
38227
+ toolName: args.name,
38228
+ arguments: args.arguments
38229
+ });
38084
38230
  return JSON.stringify({
38085
38231
  success: false,
38086
38232
  error: `Failed to parse arguments as JSON: ${error92 instanceof Error ? error92.message : String(error92)}`
38087
38233
  });
38088
38234
  }
38089
38235
  }
38236
+ executionCount++;
38090
38237
  try {
38238
+ const startTime = Date.now();
38091
38239
  const result = await mcpManager.callTool(parsed.serverName, parsed.toolName, toolArgs);
38240
+ const duration5 = Date.now() - startTime;
38241
+ executionSuccessCount++;
38242
+ log("info", `Tool executed successfully: ${args.name}`, {
38243
+ server: parsed.serverName,
38244
+ tool: parsed.toolName,
38245
+ durationMs: duration5
38246
+ });
38092
38247
  return JSON.stringify({
38093
38248
  success: true,
38094
38249
  result
38095
38250
  });
38096
38251
  } catch (error92) {
38252
+ const errorMsg = `Tool execution failed: ${error92 instanceof Error ? error92.message : String(error92)}`;
38253
+ log("error", errorMsg, {
38254
+ server: parsed.serverName,
38255
+ tool: parsed.toolName,
38256
+ error: errorMsg
38257
+ });
38097
38258
  return JSON.stringify({
38098
38259
  success: false,
38099
- error: `Tool execution failed: ${error92 instanceof Error ? error92.message : String(error92)}`
38260
+ error: errorMsg
38100
38261
  });
38101
38262
  }
38102
38263
  }
38264
+ }),
38265
+ toolbox_status: tool({
38266
+ description: STATUS_DESC,
38267
+ args: {},
38268
+ async execute() {
38269
+ if (!initialized) {
38270
+ try {
38271
+ await ensureInitialized();
38272
+ } catch (error92) {
38273
+ return JSON.stringify({
38274
+ status: "error",
38275
+ message: "Failed to initialize toolbox",
38276
+ error: error92 instanceof Error ? error92.message : String(error92)
38277
+ });
38278
+ }
38279
+ }
38280
+ const servers = mcpManager.getAllServers();
38281
+ const connectedServers = servers.filter((s) => s.status === "connected");
38282
+ const failedServers = servers.filter((s) => s.status === "error");
38283
+ const connectingServers = servers.filter((s) => s.status === "connecting");
38284
+ const totalTools = mcpManager.getAllCatalogTools().length;
38285
+ const status = {
38286
+ plugin: {
38287
+ initialized: true,
38288
+ configPath,
38289
+ uptime: process.uptime(),
38290
+ searches: searchCount,
38291
+ executions: executionCount,
38292
+ successRate: executionCount > 0 ? `${Math.round(executionSuccessCount / executionCount * 100)}%` : "N/A"
38293
+ },
38294
+ servers: {
38295
+ total: servers.length,
38296
+ connected: connectedServers.length,
38297
+ failed: failedServers.length,
38298
+ connecting: connectingServers.length,
38299
+ connectionRatio: `${connectedServers.length}/${servers.length}`,
38300
+ details: servers.map((server) => ({
38301
+ name: server.name,
38302
+ status: server.status,
38303
+ type: server.config.type,
38304
+ toolCount: server.tools.length,
38305
+ error: server.error || null,
38306
+ healthy: server.status === "connected"
38307
+ }))
38308
+ },
38309
+ tools: {
38310
+ total: totalTools,
38311
+ available: totalTools,
38312
+ serversWithTools: servers.filter((s) => s.tools.length > 0).length
38313
+ },
38314
+ health: {
38315
+ status: failedServers.length === 0 && servers.length > 0 ? "healthy" : failedServers.length > 0 ? "degraded" : "unknown",
38316
+ message: servers.length === 0 ? "No servers configured" : failedServers.length === 0 ? "All servers connected" : `${failedServers.length} server(s) failed to connect`
38317
+ }
38318
+ };
38319
+ log("info", `Status requested: ${connectedServers.length}/${servers.length} servers connected`, {
38320
+ connectedServers: connectedServers.length,
38321
+ totalServers: servers.length,
38322
+ totalTools
38323
+ });
38324
+ return JSON.stringify(status, null, 2);
38325
+ }
38103
38326
  })
38104
38327
  },
38105
38328
  "experimental.chat.system.transform": async (_input, output) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-toolbox",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Tool Search Tool Plugin for OpenCode - search and execute tools from MCP servers on-demand",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",