opencode-toolbox 0.6.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.
- package/README.md +22 -6
- package/dist/index.js +705 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# opencode-toolbox (Tool Search Tool)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-toolbox)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-toolbox)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://github.com/assagman/opencode-toolbox/actions/workflows/ci.yml)
|
|
8
|
+
[](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({
|
|
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({
|
|
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({
|
|
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
|
|
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("
|
|
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.
|
|
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.
|
|
37610
|
-
|
|
37611
|
-
|
|
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
|
-
|
|
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({
|
|
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,13 +38741,90 @@ 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.`;
|
|
38315
38749
|
var PERF_DESC = `Get detailed performance metrics for the toolbox plugin.
|
|
38316
38750
|
|
|
38317
38751
|
Shows initialization times, search latencies, execution stats, and per-server metrics.`;
|
|
38752
|
+
var TEST_DESC = `Test all toolbox tools with minimal predefined prompts.
|
|
38753
|
+
|
|
38754
|
+
Executes every registered tool with super simple inputs to verify they work. Returns pass/fail for each tool.`;
|
|
38755
|
+
var TEST_PROMPTS = {
|
|
38756
|
+
time_get_current_time: {},
|
|
38757
|
+
time_convert_time: {
|
|
38758
|
+
source_timezone: "UTC",
|
|
38759
|
+
time: "12:00",
|
|
38760
|
+
target_timezone: "America/New_York"
|
|
38761
|
+
},
|
|
38762
|
+
brave_brave_web_search: { query: "test", count: 1 },
|
|
38763
|
+
brave_brave_local_search: { query: "coffee", count: 1 },
|
|
38764
|
+
brave_brave_video_search: { query: "test", count: 1 },
|
|
38765
|
+
brave_brave_image_search: { query: "test", count: 1 },
|
|
38766
|
+
brave_brave_news_search: { query: "test", count: 1 },
|
|
38767
|
+
brave_brave_summarizer: { key: "test" },
|
|
38768
|
+
brightdata_search_engine: { query: "hello", engine: "google", count: 1 },
|
|
38769
|
+
brightdata_search_engine_batch: {
|
|
38770
|
+
queries: [{ query: "test", engine: "google", count: 1 }]
|
|
38771
|
+
},
|
|
38772
|
+
brightdata_scrape_as_markdown: { url: "https://example.com" },
|
|
38773
|
+
brightdata_scrape_as_html: { url: "https://example.com" },
|
|
38774
|
+
brightdata_scrape_batch: { urls: ["https://example.com"] },
|
|
38775
|
+
brightdata_extract: { url: "https://example.com" },
|
|
38776
|
+
brightdata_session_stats: {},
|
|
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
|
+
},
|
|
38783
|
+
"tavily_tavily-search": { query: "test", maxResults: 1 },
|
|
38784
|
+
"tavily_tavily-extract": { urls: ["https://example.com"] },
|
|
38785
|
+
"tavily_tavily-map": { url: "https://example.com" },
|
|
38786
|
+
"context7_resolve-library-id": { libraryName: "react" },
|
|
38787
|
+
octocode_githubSearchRepositories: { query: "test", maxResults: 1 },
|
|
38788
|
+
octocode_githubSearchCode: { query: "function test", maxResults: 1 },
|
|
38789
|
+
octocode_githubViewRepoStructure: { owner: "octocat", repo: "Hello-World" },
|
|
38790
|
+
perplexity_perplexity_ask: { query: "What is 1+1?" },
|
|
38791
|
+
perplexity_perplexity_search: { query: "test", maxResults: 1 }
|
|
38792
|
+
};
|
|
38793
|
+
function generateMinimalArgs(schema2) {
|
|
38794
|
+
const args = {};
|
|
38795
|
+
if (schema2.type !== "object" || !schema2.properties) {
|
|
38796
|
+
return args;
|
|
38797
|
+
}
|
|
38798
|
+
const properties = schema2.properties;
|
|
38799
|
+
const required3 = schema2.required || [];
|
|
38800
|
+
for (const propName of required3) {
|
|
38801
|
+
const prop = properties[propName];
|
|
38802
|
+
if (!prop)
|
|
38803
|
+
continue;
|
|
38804
|
+
const enumValues = prop.enum;
|
|
38805
|
+
switch (prop.type) {
|
|
38806
|
+
case "string":
|
|
38807
|
+
args[propName] = prop.default ?? enumValues?.[0] ?? "test";
|
|
38808
|
+
break;
|
|
38809
|
+
case "number":
|
|
38810
|
+
case "integer":
|
|
38811
|
+
args[propName] = prop.default ?? prop.minimum ?? 1;
|
|
38812
|
+
break;
|
|
38813
|
+
case "boolean":
|
|
38814
|
+
args[propName] = prop.default ?? false;
|
|
38815
|
+
break;
|
|
38816
|
+
case "array":
|
|
38817
|
+
args[propName] = prop.default ?? [];
|
|
38818
|
+
break;
|
|
38819
|
+
case "object":
|
|
38820
|
+
args[propName] = prop.default ?? {};
|
|
38821
|
+
break;
|
|
38822
|
+
default:
|
|
38823
|
+
args[propName] = prop.default ?? null;
|
|
38824
|
+
}
|
|
38825
|
+
}
|
|
38826
|
+
return args;
|
|
38827
|
+
}
|
|
38318
38828
|
var isTestEnv = !!process.env.BUN_TEST;
|
|
38319
38829
|
function log(level, message, extra) {
|
|
38320
38830
|
if (isTestEnv)
|
|
@@ -38332,37 +38842,56 @@ function ensureCommandFile() {
|
|
|
38332
38842
|
mkdir(COMMAND_DIR, { recursive: true }).then(() => writeFile(COMMAND_FILE_PATH, COMMAND_CONTENT)).then(() => log("info", "Created /toolbox-status command file")).catch(() => {});
|
|
38333
38843
|
});
|
|
38334
38844
|
}
|
|
38335
|
-
|
|
38336
|
-
|
|
38337
|
-
|
|
38338
|
-
|
|
38339
|
-
|
|
38340
|
-
|
|
38341
|
-
|
|
38342
|
-
|
|
38343
|
-
|
|
38344
|
-
|
|
38345
|
-
|
|
38346
|
-
|
|
38347
|
-
|
|
38348
|
-
|
|
38349
|
-
|
|
38350
|
-
|
|
38351
|
-
|
|
38352
|
-
|
|
38353
|
-
|
|
38354
|
-
|
|
38355
|
-
|
|
38356
|
-
|
|
38357
|
-
|
|
38358
|
-
|
|
38359
|
-
|
|
38360
|
-
|
|
38361
|
-
|
|
38362
|
-
|
|
38363
|
-
|
|
38364
|
-
|
|
38365
|
-
|
|
38845
|
+
function generateSystemPrompt(configuredServers) {
|
|
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>`;
|
|
38366
38895
|
}
|
|
38367
38896
|
var ToolboxPlugin = async (ctx) => {
|
|
38368
38897
|
const pluginLoadStart = performance.now();
|
|
@@ -38510,7 +39039,7 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38510
39039
|
toolbox_execute: tool({
|
|
38511
39040
|
description: EXECUTE_DESC,
|
|
38512
39041
|
args: {
|
|
38513
|
-
|
|
39042
|
+
toolId: tool.schema.string().describe("Tool ID from search results. Format: {serverName}_{toolName} (e.g., 'time_get_current_time', 'brave_web_search')"),
|
|
38514
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.")
|
|
38515
39044
|
},
|
|
38516
39045
|
async execute(args) {
|
|
@@ -38524,15 +39053,15 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38524
39053
|
error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
|
|
38525
39054
|
});
|
|
38526
39055
|
}
|
|
38527
|
-
const parsed = parseToolName(args.
|
|
39056
|
+
const parsed = parseToolName(args.toolId);
|
|
38528
39057
|
if (!parsed) {
|
|
38529
39058
|
timer();
|
|
38530
|
-
log("warn", `Invalid
|
|
38531
|
-
|
|
39059
|
+
log("warn", `Invalid toolId format: ${args.toolId}`, {
|
|
39060
|
+
toolId: args.toolId
|
|
38532
39061
|
});
|
|
38533
39062
|
return JSON.stringify({
|
|
38534
39063
|
success: false,
|
|
38535
|
-
error: `Invalid
|
|
39064
|
+
error: `Invalid toolId format: ${args.toolId}. Expected format: {serverName}_{toolName} (e.g., 'time_get_current_time')`
|
|
38536
39065
|
});
|
|
38537
39066
|
}
|
|
38538
39067
|
let toolArgs = {};
|
|
@@ -38541,8 +39070,8 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38541
39070
|
toolArgs = JSON.parse(args.arguments);
|
|
38542
39071
|
} catch (error92) {
|
|
38543
39072
|
timer();
|
|
38544
|
-
log("warn", `Failed to parse arguments as JSON for ${args.
|
|
38545
|
-
|
|
39073
|
+
log("warn", `Failed to parse arguments as JSON for ${args.toolId}`, {
|
|
39074
|
+
toolId: args.toolId,
|
|
38546
39075
|
arguments: args.arguments
|
|
38547
39076
|
});
|
|
38548
39077
|
return JSON.stringify({
|
|
@@ -38556,7 +39085,7 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38556
39085
|
const result = await mcpManager.callTool(parsed.serverName, parsed.toolName, toolArgs);
|
|
38557
39086
|
const duration5 = timer();
|
|
38558
39087
|
executionSuccessCount++;
|
|
38559
|
-
log("info", `Tool executed successfully: ${args.
|
|
39088
|
+
log("info", `Tool executed successfully: ${args.toolId} in ${duration5.toFixed(2)}ms`, {
|
|
38560
39089
|
server: parsed.serverName,
|
|
38561
39090
|
tool: parsed.toolName,
|
|
38562
39091
|
durationMs: duration5
|
|
@@ -38663,15 +39192,137 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38663
39192
|
}
|
|
38664
39193
|
}, null, 2);
|
|
38665
39194
|
}
|
|
39195
|
+
}),
|
|
39196
|
+
toolbox_test: tool({
|
|
39197
|
+
description: TEST_DESC,
|
|
39198
|
+
args: {
|
|
39199
|
+
timeout: tool.schema.number().optional().describe("Timeout per tool in ms (default: 10000)")
|
|
39200
|
+
},
|
|
39201
|
+
async execute(args) {
|
|
39202
|
+
const startTime = performance.now();
|
|
39203
|
+
const output = [];
|
|
39204
|
+
output.push("=".repeat(80));
|
|
39205
|
+
output.push("TOOLBOX TEST - Full Execution Log");
|
|
39206
|
+
output.push("=".repeat(80));
|
|
39207
|
+
output.push("");
|
|
39208
|
+
try {
|
|
39209
|
+
await ensureInitialized();
|
|
39210
|
+
} catch (error92) {
|
|
39211
|
+
output.push(`[FATAL] Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`);
|
|
39212
|
+
return output.join(`
|
|
39213
|
+
`);
|
|
39214
|
+
}
|
|
39215
|
+
const allTools = mcpManager.getAllCatalogTools();
|
|
39216
|
+
const timeout = args.timeout || 1e4;
|
|
39217
|
+
output.push(`[INFO] Found ${allTools.length} tools to test`);
|
|
39218
|
+
output.push(`[INFO] Timeout per tool: ${timeout}ms`);
|
|
39219
|
+
output.push(`[INFO] Started at: ${new Date().toISOString()}`);
|
|
39220
|
+
output.push("");
|
|
39221
|
+
let passed = 0;
|
|
39222
|
+
let failed = 0;
|
|
39223
|
+
let timedOut = 0;
|
|
39224
|
+
let skipped = 0;
|
|
39225
|
+
for (let i = 0;i < allTools.length; i++) {
|
|
39226
|
+
const catalogTool = allTools[i];
|
|
39227
|
+
const toolId = catalogTool.idString;
|
|
39228
|
+
const testNum = i + 1;
|
|
39229
|
+
output.push("-".repeat(80));
|
|
39230
|
+
output.push(`[TEST ${testNum}/${allTools.length}] ${toolId}`);
|
|
39231
|
+
output.push("-".repeat(80));
|
|
39232
|
+
const parsed = parseToolName(toolId);
|
|
39233
|
+
if (!parsed) {
|
|
39234
|
+
output.push(`[SKIP] Invalid tool name format`);
|
|
39235
|
+
output.push("");
|
|
39236
|
+
skipped++;
|
|
39237
|
+
continue;
|
|
39238
|
+
}
|
|
39239
|
+
output.push(`[INFO] Server: ${parsed.serverName}`);
|
|
39240
|
+
output.push(`[INFO] Tool: ${parsed.toolName}`);
|
|
39241
|
+
output.push(`[INFO] Description: ${catalogTool.description || "(no description)"}`);
|
|
39242
|
+
output.push("");
|
|
39243
|
+
let testArgs;
|
|
39244
|
+
let argsSource;
|
|
39245
|
+
const predefinedArgs = TEST_PROMPTS[toolId];
|
|
39246
|
+
if (predefinedArgs !== undefined) {
|
|
39247
|
+
testArgs = predefinedArgs;
|
|
39248
|
+
argsSource = "PREDEFINED";
|
|
39249
|
+
} else {
|
|
39250
|
+
testArgs = generateMinimalArgs(catalogTool.inputSchema);
|
|
39251
|
+
argsSource = Object.keys(testArgs).length > 0 ? "GENERATED" : "EMPTY";
|
|
39252
|
+
}
|
|
39253
|
+
output.push(`[INPUT] Arguments source: ${argsSource}`);
|
|
39254
|
+
output.push(`[INPUT] Request payload:`);
|
|
39255
|
+
output.push(JSON.stringify(testArgs, null, 2).split(`
|
|
39256
|
+
`).map((line) => " " + line).join(`
|
|
39257
|
+
`));
|
|
39258
|
+
output.push("");
|
|
39259
|
+
const toolStart = performance.now();
|
|
39260
|
+
try {
|
|
39261
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
39262
|
+
setTimeout(() => reject(new Error("TIMEOUT")), timeout);
|
|
39263
|
+
});
|
|
39264
|
+
const execPromise = mcpManager.callTool(parsed.serverName, parsed.toolName, testArgs);
|
|
39265
|
+
const result = await Promise.race([execPromise, timeoutPromise]);
|
|
39266
|
+
const duration5 = Math.round(performance.now() - toolStart);
|
|
39267
|
+
output.push(`[OUTPUT] Response received in ${duration5}ms:`);
|
|
39268
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
39269
|
+
output.push(resultStr.split(`
|
|
39270
|
+
`).map((line) => " " + line).join(`
|
|
39271
|
+
`));
|
|
39272
|
+
output.push("");
|
|
39273
|
+
output.push(`[PASS] \u2713 Test passed in ${duration5}ms`);
|
|
39274
|
+
passed++;
|
|
39275
|
+
} catch (error92) {
|
|
39276
|
+
const duration5 = Math.round(performance.now() - toolStart);
|
|
39277
|
+
const errorMsg = error92 instanceof Error ? error92.message : String(error92);
|
|
39278
|
+
if (errorMsg === "TIMEOUT") {
|
|
39279
|
+
output.push(`[OUTPUT] No response - timed out after ${timeout}ms`);
|
|
39280
|
+
output.push("");
|
|
39281
|
+
output.push(`[TIMEOUT] \u2717 Test timed out after ${duration5}ms`);
|
|
39282
|
+
timedOut++;
|
|
39283
|
+
} else {
|
|
39284
|
+
output.push(`[OUTPUT] Error response:`);
|
|
39285
|
+
output.push(` ${errorMsg}`);
|
|
39286
|
+
output.push("");
|
|
39287
|
+
output.push(`[FAIL] \u2717 Test failed in ${duration5}ms`);
|
|
39288
|
+
output.push(`[FAIL] Error: ${errorMsg}`);
|
|
39289
|
+
failed++;
|
|
39290
|
+
}
|
|
39291
|
+
}
|
|
39292
|
+
output.push("");
|
|
39293
|
+
}
|
|
39294
|
+
const totalDuration = Math.round(performance.now() - startTime);
|
|
39295
|
+
const total = allTools.length;
|
|
39296
|
+
const successRate = total > 0 ? Math.round(passed / total * 100) : 0;
|
|
39297
|
+
output.push("=".repeat(80));
|
|
39298
|
+
output.push("TEST SUMMARY");
|
|
39299
|
+
output.push("=".repeat(80));
|
|
39300
|
+
output.push("");
|
|
39301
|
+
output.push(`Total tests: ${total}`);
|
|
39302
|
+
output.push(`Passed: ${passed} \u2713`);
|
|
39303
|
+
output.push(`Failed: ${failed} \u2717`);
|
|
39304
|
+
output.push(`Timed out: ${timedOut} \u23F1`);
|
|
39305
|
+
output.push(`Skipped: ${skipped} \u2298`);
|
|
39306
|
+
output.push("");
|
|
39307
|
+
output.push(`Success rate: ${successRate}%`);
|
|
39308
|
+
output.push(`Total duration: ${totalDuration}ms`);
|
|
39309
|
+
output.push(`Finished at: ${new Date().toISOString()}`);
|
|
39310
|
+
output.push("");
|
|
39311
|
+
output.push("=".repeat(80));
|
|
39312
|
+
log("info", `Toolbox test completed: ${passed}/${total} passed in ${totalDuration}ms`, {
|
|
39313
|
+
passed,
|
|
39314
|
+
failed,
|
|
39315
|
+
timedOut,
|
|
39316
|
+
skipped,
|
|
39317
|
+
total
|
|
39318
|
+
});
|
|
39319
|
+
return output.join(`
|
|
39320
|
+
`);
|
|
39321
|
+
}
|
|
38666
39322
|
})
|
|
38667
39323
|
},
|
|
38668
39324
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
38669
|
-
|
|
38670
|
-
try {
|
|
38671
|
-
await mcpManager.waitForPartial();
|
|
38672
|
-
} catch {}
|
|
38673
|
-
}
|
|
38674
|
-
output.system.push(generateSystemPrompt(mcpManager));
|
|
39325
|
+
output.system.push(generateSystemPrompt(serverNames));
|
|
38675
39326
|
}
|
|
38676
39327
|
};
|
|
38677
39328
|
};
|
package/package.json
CHANGED