opencode-toolbox 0.7.0 → 0.8.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 +22 -6
  2. package/dist/index.js +514 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # opencode-toolbox (Tool Search Tool)
2
2
 
3
+ [![npm version](https://badge.fury.io/js/opencode-toolbox.svg)](https://www.npmjs.com/package/opencode-toolbox)
4
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-toolbox)](https://www.npmjs.com/package/opencode-toolbox)
5
+ [![license](https://img.shields.io/npm/l/opencode-toolbox)](LICENSE)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](https://www.typescriptlang.org/)
7
+ [![Build Status](https://github.com/assagman/opencode-toolbox/actions/workflows/ci.yml/badge.svg)](https://github.com/assagman/opencode-toolbox/actions/workflows/ci.yml)
8
+ [![codecov](https://codecov.io/gh/assagman/opencode-toolbox/branch/main/graph/badge.svg)](https://codecov.io/gh/assagman/opencode-toolbox)
9
+
3
10
  An OpenCode plugin that implements a **tool search tool** pattern, allowing users to keep only a tiny set of tools in LLM context while accessing a larger MCP catalog on-demand.
4
11
 
5
12
  ## Motivation
@@ -20,10 +27,19 @@ Add `opencode-toolbox` to your `~/.config/opencode/opencode.jsonc`:
20
27
 
21
28
  ```jsonc
22
29
  {
23
- "plugin": ["opencode-toolbox"]
30
+ "plugin": ["opencode-toolbox@x.y.z"]
24
31
  }
25
32
  ```
26
33
 
34
+ > **⚡ Performance Tip:** Always pin to a specific version for instant startup.
35
+ >
36
+ > | Configuration | Startup Time | Why |
37
+ > |---------------|--------------|-----|
38
+ > | ✅ `opencode-toolbox@x.y.z` | ~0ms | Cached version matches, skips npm check |
39
+ > | ❌ `opencode-toolbox` | ~1000ms | Resolves "latest" via npm on every startup |
40
+ >
41
+ > OpenCode caches plugins by exact version. When you omit the version, it defaults to `"latest"` which doesn't match the cached resolved version, triggering a fresh `bun add` on every startup. Check [npm](https://www.npmjs.com/package/opencode-toolbox) for the latest version.
42
+
27
43
  ### 2. Configure Toolbox
28
44
 
29
45
  Create `~/.config/opencode/toolbox.jsonc`:
@@ -105,16 +121,16 @@ Both search tools return tool schemas so the LLM knows exact parameters:
105
121
  }
106
122
  }
107
123
  ],
108
- "usage": "Use toolbox_execute({ name: '<tool_name>', arguments: '<json>' }) to run a discovered tool"
124
+ "usage": "Use toolbox_execute({ toolId: '<toolId>', arguments: '<json>' }) to run a discovered tool"
109
125
  }
110
126
  ```
111
127
 
112
128
  ### toolbox_execute
113
129
 
114
- Execute a discovered tool with JSON-encoded arguments:
130
+ Execute a discovered tool with JSON-encoded arguments. The `toolId` format is `{serverName}_{toolName}`:
115
131
 
116
132
  ```
117
- toolbox_execute({ name: "time_get_current_time", arguments: '{"timezone": "Asia/Tokyo"}' })
133
+ toolbox_execute({ toolId: "time_get_current_time", arguments: '{"timezone": "Asia/Tokyo"}' })
118
134
  ```
119
135
 
120
136
  ## Example Flow
@@ -128,7 +144,7 @@ LLM: I need to find a time-related tool.
128
144
  Toolbox: Returns time_get_current_time with its schema
129
145
 
130
146
  LLM: Now I know the parameters. Let me call it.
131
- toolbox_execute({ name: "time_get_current_time", arguments: '{"timezone":"Asia/Tokyo"}' })
147
+ toolbox_execute({ toolId: "time_get_current_time", arguments: '{"timezone":"Asia/Tokyo"}' })
132
148
 
133
149
  Toolbox: { "datetime": "2026-01-07T02:15:00+09:00", "timezone": "Asia/Tokyo" }
134
150
 
@@ -368,7 +384,7 @@ This shows:
368
384
  ### Execute fails
369
385
 
370
386
  1. Run `toolbox_status({})` to check server health
371
- 2. Verify tool name format: `serverName_toolName`
387
+ 2. Verify `toolId` format: `{serverName}_{toolName}`
372
388
  3. Check `arguments` is valid JSON
373
389
  4. Ensure underlying MCP server is running and connected
374
390
  5. Check logs for detailed error messages
package/dist/index.js CHANGED
@@ -32809,7 +32809,7 @@ var LocalServerConfigSchema = exports_external2.object({
32809
32809
  });
32810
32810
  var RemoteServerConfigSchema = exports_external2.object({
32811
32811
  type: exports_external2.literal("remote"),
32812
- url: exports_external2.string().url().describe("SSE endpoint URL"),
32812
+ url: exports_external2.string().url().describe("Remote MCP endpoint URL"),
32813
32813
  headers: exports_external2.record(exports_external2.string(), exports_external2.string()).optional().describe("HTTP headers for authentication")
32814
32814
  });
32815
32815
  var ServerConfigSchema = exports_external2.discriminatedUnion("type", [
@@ -33953,6 +33953,7 @@ var InitializedNotificationSchema = NotificationSchema.extend({
33953
33953
  method: literal2("notifications/initialized"),
33954
33954
  params: NotificationsParamsSchema.optional()
33955
33955
  });
33956
+ var isInitializedNotification = (value) => InitializedNotificationSchema.safeParse(value).success;
33956
33957
  var PingRequestSchema = RequestSchema.extend({
33957
33958
  method: literal2("ping"),
33958
33959
  params: BaseRequestParamsSchema.optional()
@@ -37584,6 +37585,395 @@ class SSEClientTransport {
37584
37585
  }
37585
37586
  }
37586
37587
 
37588
+ // node_modules/eventsource-parser/dist/stream.js
37589
+ class EventSourceParserStream extends TransformStream {
37590
+ constructor({ onError, onRetry, onComment } = {}) {
37591
+ let parser;
37592
+ super({
37593
+ start(controller) {
37594
+ parser = createParser({
37595
+ onEvent: (event) => {
37596
+ controller.enqueue(event);
37597
+ },
37598
+ onError(error92) {
37599
+ onError === "terminate" ? controller.error(error92) : typeof onError == "function" && onError(error92);
37600
+ },
37601
+ onRetry,
37602
+ onComment
37603
+ });
37604
+ },
37605
+ transform(chunk) {
37606
+ parser.feed(chunk);
37607
+ }
37608
+ });
37609
+ }
37610
+ }
37611
+
37612
+ // node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js
37613
+ var DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS = {
37614
+ initialReconnectionDelay: 1000,
37615
+ maxReconnectionDelay: 30000,
37616
+ reconnectionDelayGrowFactor: 1.5,
37617
+ maxRetries: 2
37618
+ };
37619
+
37620
+ class StreamableHTTPError extends Error {
37621
+ constructor(code, message) {
37622
+ super(`Streamable HTTP error: ${message}`);
37623
+ this.code = code;
37624
+ }
37625
+ }
37626
+
37627
+ class StreamableHTTPClientTransport {
37628
+ constructor(url3, opts) {
37629
+ this._hasCompletedAuthFlow = false;
37630
+ this._url = url3;
37631
+ this._resourceMetadataUrl = undefined;
37632
+ this._scope = undefined;
37633
+ this._requestInit = opts?.requestInit;
37634
+ this._authProvider = opts?.authProvider;
37635
+ this._fetch = opts?.fetch;
37636
+ this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
37637
+ this._sessionId = opts?.sessionId;
37638
+ this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
37639
+ }
37640
+ async _authThenStart() {
37641
+ if (!this._authProvider) {
37642
+ throw new UnauthorizedError("No auth provider");
37643
+ }
37644
+ let result;
37645
+ try {
37646
+ result = await auth(this._authProvider, {
37647
+ serverUrl: this._url,
37648
+ resourceMetadataUrl: this._resourceMetadataUrl,
37649
+ scope: this._scope,
37650
+ fetchFn: this._fetchWithInit
37651
+ });
37652
+ } catch (error92) {
37653
+ this.onerror?.(error92);
37654
+ throw error92;
37655
+ }
37656
+ if (result !== "AUTHORIZED") {
37657
+ throw new UnauthorizedError;
37658
+ }
37659
+ return await this._startOrAuthSse({ resumptionToken: undefined });
37660
+ }
37661
+ async _commonHeaders() {
37662
+ const headers = {};
37663
+ if (this._authProvider) {
37664
+ const tokens = await this._authProvider.tokens();
37665
+ if (tokens) {
37666
+ headers["Authorization"] = `Bearer ${tokens.access_token}`;
37667
+ }
37668
+ }
37669
+ if (this._sessionId) {
37670
+ headers["mcp-session-id"] = this._sessionId;
37671
+ }
37672
+ if (this._protocolVersion) {
37673
+ headers["mcp-protocol-version"] = this._protocolVersion;
37674
+ }
37675
+ const extraHeaders = normalizeHeaders(this._requestInit?.headers);
37676
+ return new Headers({
37677
+ ...headers,
37678
+ ...extraHeaders
37679
+ });
37680
+ }
37681
+ async _startOrAuthSse(options) {
37682
+ const { resumptionToken } = options;
37683
+ try {
37684
+ const headers = await this._commonHeaders();
37685
+ headers.set("Accept", "text/event-stream");
37686
+ if (resumptionToken) {
37687
+ headers.set("last-event-id", resumptionToken);
37688
+ }
37689
+ const response = await (this._fetch ?? fetch)(this._url, {
37690
+ method: "GET",
37691
+ headers,
37692
+ signal: this._abortController?.signal
37693
+ });
37694
+ if (!response.ok) {
37695
+ await response.body?.cancel();
37696
+ if (response.status === 401 && this._authProvider) {
37697
+ return await this._authThenStart();
37698
+ }
37699
+ if (response.status === 405) {
37700
+ return;
37701
+ }
37702
+ throw new StreamableHTTPError(response.status, `Failed to open SSE stream: ${response.statusText}`);
37703
+ }
37704
+ this._handleSseStream(response.body, options, true);
37705
+ } catch (error92) {
37706
+ this.onerror?.(error92);
37707
+ throw error92;
37708
+ }
37709
+ }
37710
+ _getNextReconnectionDelay(attempt) {
37711
+ if (this._serverRetryMs !== undefined) {
37712
+ return this._serverRetryMs;
37713
+ }
37714
+ const initialDelay = this._reconnectionOptions.initialReconnectionDelay;
37715
+ const growFactor = this._reconnectionOptions.reconnectionDelayGrowFactor;
37716
+ const maxDelay = this._reconnectionOptions.maxReconnectionDelay;
37717
+ return Math.min(initialDelay * Math.pow(growFactor, attempt), maxDelay);
37718
+ }
37719
+ _scheduleReconnection(options, attemptCount = 0) {
37720
+ const maxRetries = this._reconnectionOptions.maxRetries;
37721
+ if (attemptCount >= maxRetries) {
37722
+ this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
37723
+ return;
37724
+ }
37725
+ const delay = this._getNextReconnectionDelay(attemptCount);
37726
+ this._reconnectionTimeout = setTimeout(() => {
37727
+ this._startOrAuthSse(options).catch((error92) => {
37728
+ this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error92 instanceof Error ? error92.message : String(error92)}`));
37729
+ this._scheduleReconnection(options, attemptCount + 1);
37730
+ });
37731
+ }, delay);
37732
+ }
37733
+ _handleSseStream(stream, options, isReconnectable) {
37734
+ if (!stream) {
37735
+ return;
37736
+ }
37737
+ const { onresumptiontoken, replayMessageId } = options;
37738
+ let lastEventId;
37739
+ let hasPrimingEvent = false;
37740
+ let receivedResponse = false;
37741
+ const processStream = async () => {
37742
+ try {
37743
+ const reader = stream.pipeThrough(new TextDecoderStream).pipeThrough(new EventSourceParserStream({
37744
+ onRetry: (retryMs) => {
37745
+ this._serverRetryMs = retryMs;
37746
+ }
37747
+ })).getReader();
37748
+ while (true) {
37749
+ const { value: event, done } = await reader.read();
37750
+ if (done) {
37751
+ break;
37752
+ }
37753
+ if (event.id) {
37754
+ lastEventId = event.id;
37755
+ hasPrimingEvent = true;
37756
+ onresumptiontoken?.(event.id);
37757
+ }
37758
+ if (!event.data) {
37759
+ continue;
37760
+ }
37761
+ if (!event.event || event.event === "message") {
37762
+ try {
37763
+ const message = JSONRPCMessageSchema.parse(JSON.parse(event.data));
37764
+ if (isJSONRPCResultResponse(message)) {
37765
+ receivedResponse = true;
37766
+ if (replayMessageId !== undefined) {
37767
+ message.id = replayMessageId;
37768
+ }
37769
+ }
37770
+ this.onmessage?.(message);
37771
+ } catch (error92) {
37772
+ this.onerror?.(error92);
37773
+ }
37774
+ }
37775
+ }
37776
+ const canResume = isReconnectable || hasPrimingEvent;
37777
+ const needsReconnect = canResume && !receivedResponse;
37778
+ if (needsReconnect && this._abortController && !this._abortController.signal.aborted) {
37779
+ this._scheduleReconnection({
37780
+ resumptionToken: lastEventId,
37781
+ onresumptiontoken,
37782
+ replayMessageId
37783
+ }, 0);
37784
+ }
37785
+ } catch (error92) {
37786
+ this.onerror?.(new Error(`SSE stream disconnected: ${error92}`));
37787
+ const canResume = isReconnectable || hasPrimingEvent;
37788
+ const needsReconnect = canResume && !receivedResponse;
37789
+ if (needsReconnect && this._abortController && !this._abortController.signal.aborted) {
37790
+ try {
37791
+ this._scheduleReconnection({
37792
+ resumptionToken: lastEventId,
37793
+ onresumptiontoken,
37794
+ replayMessageId
37795
+ }, 0);
37796
+ } catch (error93) {
37797
+ this.onerror?.(new Error(`Failed to reconnect: ${error93 instanceof Error ? error93.message : String(error93)}`));
37798
+ }
37799
+ }
37800
+ }
37801
+ };
37802
+ processStream();
37803
+ }
37804
+ async start() {
37805
+ if (this._abortController) {
37806
+ throw new Error("StreamableHTTPClientTransport already started! If using Client class, note that connect() calls start() automatically.");
37807
+ }
37808
+ this._abortController = new AbortController;
37809
+ }
37810
+ async finishAuth(authorizationCode) {
37811
+ if (!this._authProvider) {
37812
+ throw new UnauthorizedError("No auth provider");
37813
+ }
37814
+ const result = await auth(this._authProvider, {
37815
+ serverUrl: this._url,
37816
+ authorizationCode,
37817
+ resourceMetadataUrl: this._resourceMetadataUrl,
37818
+ scope: this._scope,
37819
+ fetchFn: this._fetchWithInit
37820
+ });
37821
+ if (result !== "AUTHORIZED") {
37822
+ throw new UnauthorizedError("Failed to authorize");
37823
+ }
37824
+ }
37825
+ async close() {
37826
+ if (this._reconnectionTimeout) {
37827
+ clearTimeout(this._reconnectionTimeout);
37828
+ this._reconnectionTimeout = undefined;
37829
+ }
37830
+ this._abortController?.abort();
37831
+ this.onclose?.();
37832
+ }
37833
+ async send(message, options) {
37834
+ try {
37835
+ const { resumptionToken, onresumptiontoken } = options || {};
37836
+ if (resumptionToken) {
37837
+ this._startOrAuthSse({ resumptionToken, replayMessageId: isJSONRPCRequest(message) ? message.id : undefined }).catch((err) => this.onerror?.(err));
37838
+ return;
37839
+ }
37840
+ const headers = await this._commonHeaders();
37841
+ headers.set("content-type", "application/json");
37842
+ headers.set("accept", "application/json, text/event-stream");
37843
+ const init = {
37844
+ ...this._requestInit,
37845
+ method: "POST",
37846
+ headers,
37847
+ body: JSON.stringify(message),
37848
+ signal: this._abortController?.signal
37849
+ };
37850
+ const response = await (this._fetch ?? fetch)(this._url, init);
37851
+ const sessionId = response.headers.get("mcp-session-id");
37852
+ if (sessionId) {
37853
+ this._sessionId = sessionId;
37854
+ }
37855
+ if (!response.ok) {
37856
+ const text = await response.text().catch(() => null);
37857
+ if (response.status === 401 && this._authProvider) {
37858
+ if (this._hasCompletedAuthFlow) {
37859
+ throw new StreamableHTTPError(401, "Server returned 401 after successful authentication");
37860
+ }
37861
+ const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
37862
+ this._resourceMetadataUrl = resourceMetadataUrl;
37863
+ this._scope = scope;
37864
+ const result = await auth(this._authProvider, {
37865
+ serverUrl: this._url,
37866
+ resourceMetadataUrl: this._resourceMetadataUrl,
37867
+ scope: this._scope,
37868
+ fetchFn: this._fetchWithInit
37869
+ });
37870
+ if (result !== "AUTHORIZED") {
37871
+ throw new UnauthorizedError;
37872
+ }
37873
+ this._hasCompletedAuthFlow = true;
37874
+ return this.send(message);
37875
+ }
37876
+ if (response.status === 403 && this._authProvider) {
37877
+ const { resourceMetadataUrl, scope, error: error92 } = extractWWWAuthenticateParams(response);
37878
+ if (error92 === "insufficient_scope") {
37879
+ const wwwAuthHeader = response.headers.get("WWW-Authenticate");
37880
+ if (this._lastUpscopingHeader === wwwAuthHeader) {
37881
+ throw new StreamableHTTPError(403, "Server returned 403 after trying upscoping");
37882
+ }
37883
+ if (scope) {
37884
+ this._scope = scope;
37885
+ }
37886
+ if (resourceMetadataUrl) {
37887
+ this._resourceMetadataUrl = resourceMetadataUrl;
37888
+ }
37889
+ this._lastUpscopingHeader = wwwAuthHeader ?? undefined;
37890
+ const result = await auth(this._authProvider, {
37891
+ serverUrl: this._url,
37892
+ resourceMetadataUrl: this._resourceMetadataUrl,
37893
+ scope: this._scope,
37894
+ fetchFn: this._fetch
37895
+ });
37896
+ if (result !== "AUTHORIZED") {
37897
+ throw new UnauthorizedError;
37898
+ }
37899
+ return this.send(message);
37900
+ }
37901
+ }
37902
+ throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
37903
+ }
37904
+ this._hasCompletedAuthFlow = false;
37905
+ this._lastUpscopingHeader = undefined;
37906
+ if (response.status === 202) {
37907
+ await response.body?.cancel();
37908
+ if (isInitializedNotification(message)) {
37909
+ this._startOrAuthSse({ resumptionToken: undefined }).catch((err) => this.onerror?.(err));
37910
+ }
37911
+ return;
37912
+ }
37913
+ const messages = Array.isArray(message) ? message : [message];
37914
+ const hasRequests = messages.filter((msg) => ("method" in msg) && ("id" in msg) && msg.id !== undefined).length > 0;
37915
+ const contentType = response.headers.get("content-type");
37916
+ if (hasRequests) {
37917
+ if (contentType?.includes("text/event-stream")) {
37918
+ this._handleSseStream(response.body, { onresumptiontoken }, false);
37919
+ } else if (contentType?.includes("application/json")) {
37920
+ const data = await response.json();
37921
+ const responseMessages = Array.isArray(data) ? data.map((msg) => JSONRPCMessageSchema.parse(msg)) : [JSONRPCMessageSchema.parse(data)];
37922
+ for (const msg of responseMessages) {
37923
+ this.onmessage?.(msg);
37924
+ }
37925
+ } else {
37926
+ await response.body?.cancel();
37927
+ throw new StreamableHTTPError(-1, `Unexpected content type: ${contentType}`);
37928
+ }
37929
+ } else {
37930
+ await response.body?.cancel();
37931
+ }
37932
+ } catch (error92) {
37933
+ this.onerror?.(error92);
37934
+ throw error92;
37935
+ }
37936
+ }
37937
+ get sessionId() {
37938
+ return this._sessionId;
37939
+ }
37940
+ async terminateSession() {
37941
+ if (!this._sessionId) {
37942
+ return;
37943
+ }
37944
+ try {
37945
+ const headers = await this._commonHeaders();
37946
+ const init = {
37947
+ ...this._requestInit,
37948
+ method: "DELETE",
37949
+ headers,
37950
+ signal: this._abortController?.signal
37951
+ };
37952
+ const response = await (this._fetch ?? fetch)(this._url, init);
37953
+ await response.body?.cancel();
37954
+ if (!response.ok && response.status !== 405) {
37955
+ throw new StreamableHTTPError(response.status, `Failed to terminate session: ${response.statusText}`);
37956
+ }
37957
+ this._sessionId = undefined;
37958
+ } catch (error92) {
37959
+ this.onerror?.(error92);
37960
+ throw error92;
37961
+ }
37962
+ }
37963
+ setProtocolVersion(version3) {
37964
+ this._protocolVersion = version3;
37965
+ }
37966
+ get protocolVersion() {
37967
+ return this._protocolVersion;
37968
+ }
37969
+ async resumeStream(lastEventId, options) {
37970
+ await this._startOrAuthSse({
37971
+ resumptionToken: lastEventId,
37972
+ onresumptiontoken: options?.onresumptiontoken
37973
+ });
37974
+ }
37975
+ }
37976
+
37587
37977
  // src/mcp-client/remote.ts
37588
37978
  class RemoteMCPClient {
37589
37979
  client;
@@ -37591,12 +37981,17 @@ class RemoteMCPClient {
37591
37981
  toolsCache;
37592
37982
  name;
37593
37983
  config;
37984
+ transportType;
37594
37985
  constructor(config3) {
37595
37986
  this.transport = null;
37596
37987
  this.toolsCache = null;
37597
37988
  this.name = config3.name;
37598
37989
  this.config = config3;
37599
- this.client = new Client({
37990
+ this.transportType = null;
37991
+ this.client = this.createClient();
37992
+ }
37993
+ createClient() {
37994
+ return new Client({
37600
37995
  name: `opencode-toolbox-client-${this.name}`,
37601
37996
  version: "0.1.0"
37602
37997
  }, {});
@@ -37606,12 +38001,46 @@ class RemoteMCPClient {
37606
38001
  throw new Error(`Remote MCP server ${this.name} has no URL`);
37607
38002
  }
37608
38003
  const url3 = new URL(this.config.url);
37609
- this.transport = new SSEClientTransport(url3, {
37610
- requestInit: {
37611
- headers: this.config.headers
38004
+ this.transportType = null;
38005
+ let streamableTransport = null;
38006
+ try {
38007
+ streamableTransport = new StreamableHTTPClientTransport(url3, {
38008
+ requestInit: {
38009
+ headers: this.config.headers
38010
+ }
38011
+ });
38012
+ await this.client.connect(streamableTransport);
38013
+ this.transport = streamableTransport;
38014
+ this.transportType = "streamable-http";
38015
+ return;
38016
+ } catch (error92) {
38017
+ if (streamableTransport) {
38018
+ await streamableTransport.close().catch(() => {});
37612
38019
  }
37613
- });
37614
- await this.client.connect(this.transport);
38020
+ this.client = this.createClient();
38021
+ }
38022
+ let sseTransport = null;
38023
+ try {
38024
+ const sseHeaders = {
38025
+ Accept: "text/event-stream",
38026
+ ...this.config.headers
38027
+ };
38028
+ sseTransport = new SSEClientTransport(url3, {
38029
+ requestInit: {
38030
+ headers: sseHeaders
38031
+ }
38032
+ });
38033
+ await this.client.connect(sseTransport);
38034
+ this.transport = sseTransport;
38035
+ this.transportType = "sse";
38036
+ } catch (error92) {
38037
+ if (sseTransport) {
38038
+ await sseTransport.close().catch(() => {});
38039
+ }
38040
+ this.transport = null;
38041
+ this.transportType = null;
38042
+ throw error92;
38043
+ }
37615
38044
  }
37616
38045
  async listTools() {
37617
38046
  const result = await this.client.listTools();
@@ -37630,10 +38059,14 @@ class RemoteMCPClient {
37630
38059
  this.transport = null;
37631
38060
  }
37632
38061
  this.toolsCache = null;
38062
+ this.transportType = null;
37633
38063
  }
37634
38064
  getCachedTools() {
37635
38065
  return this.toolsCache;
37636
38066
  }
38067
+ getTransportType() {
38068
+ return this.transportType;
38069
+ }
37637
38070
  }
37638
38071
  // src/mcp-client/manager.ts
37639
38072
  import { EventEmitter } from "events";
@@ -38294,7 +38727,7 @@ function formatSearchResults(results, allTools) {
38294
38727
  schema: catalogTool?.inputSchema || null
38295
38728
  };
38296
38729
  }),
38297
- usage: "Use toolbox_execute({ name: '<tool_name>', arguments: '<json>' }) to run a discovered tool"
38730
+ usage: "Use toolbox_execute({ toolId: '<toolId>', arguments: '<json>' }) to run a discovered tool"
38298
38731
  };
38299
38732
  return JSON.stringify(output, null, 2);
38300
38733
  }
@@ -38308,7 +38741,8 @@ Use when you know part of a tool name or server prefix (e.g., "time_.*", "exa_.*
38308
38741
  Returns tools with schemas. Use toolbox_execute() to run them.`;
38309
38742
  var EXECUTE_DESC = `Execute a tool discovered via toolbox_search_bm25 or toolbox_search_regex.
38310
38743
 
38311
- Pass arguments as JSON string matching the tool's schema.`;
38744
+ Pass arguments as JSON string matching the tool's schema.
38745
+ toolId format: {serverName}_{toolName}`;
38312
38746
  var STATUS_DESC = `Get toolbox status including plugin initialization, MCP server connections, and tool counts.
38313
38747
 
38314
38748
  Shows success/total metrics to highlight failures. Use to check if toolbox is working correctly.`;
@@ -38320,7 +38754,11 @@ var TEST_DESC = `Test all toolbox tools with minimal predefined prompts.
38320
38754
  Executes every registered tool with super simple inputs to verify they work. Returns pass/fail for each tool.`;
38321
38755
  var TEST_PROMPTS = {
38322
38756
  time_get_current_time: {},
38323
- time_convert_time: { source_timezone: "UTC", time: "12:00", target_timezone: "America/New_York" },
38757
+ time_convert_time: {
38758
+ source_timezone: "UTC",
38759
+ time: "12:00",
38760
+ target_timezone: "America/New_York"
38761
+ },
38324
38762
  brave_brave_web_search: { query: "test", count: 1 },
38325
38763
  brave_brave_local_search: { query: "coffee", count: 1 },
38326
38764
  brave_brave_video_search: { query: "test", count: 1 },
@@ -38328,14 +38766,20 @@ var TEST_PROMPTS = {
38328
38766
  brave_brave_news_search: { query: "test", count: 1 },
38329
38767
  brave_brave_summarizer: { key: "test" },
38330
38768
  brightdata_search_engine: { query: "hello", engine: "google", count: 1 },
38331
- brightdata_search_engine_batch: { queries: [{ query: "test", engine: "google", count: 1 }] },
38769
+ brightdata_search_engine_batch: {
38770
+ queries: [{ query: "test", engine: "google", count: 1 }]
38771
+ },
38332
38772
  brightdata_scrape_as_markdown: { url: "https://example.com" },
38333
38773
  brightdata_scrape_as_html: { url: "https://example.com" },
38334
38774
  brightdata_scrape_batch: { urls: ["https://example.com"] },
38335
38775
  brightdata_extract: { url: "https://example.com" },
38336
38776
  brightdata_session_stats: {},
38337
- brightdata_web_data_reuter_news: { url: "https://www.reuters.com/technology/" },
38338
- brightdata_web_data_github_repository_file: { url: "https://github.com/octocat/Hello-World/blob/master/README" },
38777
+ brightdata_web_data_reuter_news: {
38778
+ url: "https://www.reuters.com/technology/"
38779
+ },
38780
+ brightdata_web_data_github_repository_file: {
38781
+ url: "https://github.com/octocat/Hello-World/blob/master/README"
38782
+ },
38339
38783
  "tavily_tavily-search": { query: "test", maxResults: 1 },
38340
38784
  "tavily_tavily-extract": { urls: ["https://example.com"] },
38341
38785
  "tavily_tavily-map": { url: "https://example.com" },
@@ -38398,26 +38842,56 @@ function ensureCommandFile() {
38398
38842
  mkdir(COMMAND_DIR, { recursive: true }).then(() => writeFile(COMMAND_FILE_PATH, COMMAND_CONTENT)).then(() => log("info", "Created /toolbox-status command file")).catch(() => {});
38399
38843
  });
38400
38844
  }
38401
- var SYSTEM_PROMPT_BASE = `# Extended Toolbox
38402
-
38403
- You have access to an extended toolbox with additional capabilities (web search, time utilities, code search, etc.).
38404
-
38405
- ## Rules
38406
- 1. ALWAYS toolbox_search_* before saying "I cannot do that" or "I don't have access to."
38407
- 2. ALWAYS toolbox_search_* if you think that user wants you to use some tools
38408
- 3. ALWAYS toolbox_search_* if you think that user may refer specific tool name which is not exist in the context
38409
-
38410
- ## Workflow
38411
- 1. Search: toolbox_search_bm25({ text: "what you need" }) or toolbox_search_regex({ pattern: "prefix_.*" })
38412
- 2. Execute: toolbox_execute({ name: "tool_name", arguments: '{"key": "value"}' })`;
38413
38845
  function generateSystemPrompt(configuredServers) {
38414
- if (configuredServers.length === 0) {
38415
- return SYSTEM_PROMPT_BASE;
38416
- }
38417
- return `${SYSTEM_PROMPT_BASE}
38418
-
38419
- ## Registered MCP Servers
38420
- - ${configuredServers.join(", ")}`;
38846
+ const registry3 = configuredServers.length > 0 ? configuredServers.map((s) => `${s}_*`).join(`
38847
+ `) : "(no servers configured)";
38848
+ return `
38849
+ <MCPTools>
38850
+ <Rules>
38851
+ ALWAYS toolbox_search_* before saying "I cannot do that" or "I don't have access to"
38852
+ ALWAYS toolbox_search_* if user wants to use tools or refers to unknown tool names
38853
+ </Rules>
38854
+ <MCPServers>
38855
+ <Registry>
38856
+ ${registry3}
38857
+ </Registry>
38858
+ <NamingConvention>
38859
+ serverName: MCP server name
38860
+ toolName: tool name provided by MCP server
38861
+ toolId: {serverName}_{toolName}
38862
+ </NamingConvention>
38863
+ <Patterns>
38864
+ ALL: ".*"
38865
+ SERVER: "{serverName}_.*"
38866
+ TOOL: "{serverName}_{toolName}"
38867
+ </Patterns>
38868
+ <Discovery>
38869
+ <ListAllTools>
38870
+ toolbox_search_regex({ pattern: ".*" })
38871
+ </ListAllTools>
38872
+ <ServerTools>
38873
+ toolbox_search_regex({ pattern: "serverName_.*" })
38874
+ </ServerTools>
38875
+ <FreeSearch>
38876
+ toolbox_search_bm25({ text: "description keywords" })
38877
+ </FreeSearch>
38878
+ </Discovery>
38879
+ <Execute>
38880
+ toolbox_execute({ toolId: "toolId", arguments: '{}' })
38881
+ </Execute>
38882
+ <When>
38883
+ regex: know server name or partial tool name
38884
+ bm25: know what you want to do, not tool name
38885
+ </When>
38886
+ <Fallback>
38887
+ toolbox_search_regex \u2192 toolbox_search_bm25 \u2192 toolbox_status \u2192 ask user
38888
+ </Fallback>
38889
+ <Troubleshoot>
38890
+ If tool not found: check server prefix, try bm25 with descriptive text
38891
+ Check server health: toolbox_status()
38892
+ </Troubleshoot>
38893
+ </MCPServers>
38894
+ </MCPTools>`;
38421
38895
  }
38422
38896
  var ToolboxPlugin = async (ctx) => {
38423
38897
  const pluginLoadStart = performance.now();
@@ -38565,7 +39039,7 @@ var ToolboxPlugin = async (ctx) => {
38565
39039
  toolbox_execute: tool({
38566
39040
  description: EXECUTE_DESC,
38567
39041
  args: {
38568
- name: tool.schema.string().describe("Full tool name from search results (e.g., 'time_get_current_time', 'exa_web_search')"),
39042
+ toolId: tool.schema.string().describe("Tool ID from search results. Format: {serverName}_{toolName} (e.g., 'time_get_current_time', 'brave_web_search')"),
38569
39043
  arguments: tool.schema.string().optional().describe("JSON-encoded arguments for the tool, matching its schema. Use '{}' or omit for tools with no required arguments.")
38570
39044
  },
38571
39045
  async execute(args) {
@@ -38579,15 +39053,15 @@ var ToolboxPlugin = async (ctx) => {
38579
39053
  error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
38580
39054
  });
38581
39055
  }
38582
- const parsed = parseToolName(args.name);
39056
+ const parsed = parseToolName(args.toolId);
38583
39057
  if (!parsed) {
38584
39058
  timer();
38585
- log("warn", `Invalid tool name format: ${args.name}`, {
38586
- toolName: args.name
39059
+ log("warn", `Invalid toolId format: ${args.toolId}`, {
39060
+ toolId: args.toolId
38587
39061
  });
38588
39062
  return JSON.stringify({
38589
39063
  success: false,
38590
- error: `Invalid tool name format: ${args.name}. Expected format: serverName_toolName (e.g., 'time_get_current_time')`
39064
+ error: `Invalid toolId format: ${args.toolId}. Expected format: {serverName}_{toolName} (e.g., 'time_get_current_time')`
38591
39065
  });
38592
39066
  }
38593
39067
  let toolArgs = {};
@@ -38596,8 +39070,8 @@ var ToolboxPlugin = async (ctx) => {
38596
39070
  toolArgs = JSON.parse(args.arguments);
38597
39071
  } catch (error92) {
38598
39072
  timer();
38599
- log("warn", `Failed to parse arguments as JSON for ${args.name}`, {
38600
- toolName: args.name,
39073
+ log("warn", `Failed to parse arguments as JSON for ${args.toolId}`, {
39074
+ toolId: args.toolId,
38601
39075
  arguments: args.arguments
38602
39076
  });
38603
39077
  return JSON.stringify({
@@ -38611,7 +39085,7 @@ var ToolboxPlugin = async (ctx) => {
38611
39085
  const result = await mcpManager.callTool(parsed.serverName, parsed.toolName, toolArgs);
38612
39086
  const duration5 = timer();
38613
39087
  executionSuccessCount++;
38614
- log("info", `Tool executed successfully: ${args.name} in ${duration5.toFixed(2)}ms`, {
39088
+ log("info", `Tool executed successfully: ${args.toolId} in ${duration5.toFixed(2)}ms`, {
38615
39089
  server: parsed.serverName,
38616
39090
  tool: parsed.toolName,
38617
39091
  durationMs: duration5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-toolbox",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",