opencode-toolbox 0.5.1 → 0.7.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 +36 -1
- package/dist/index.js +684 -107
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Create `~/.config/opencode/toolbox.jsonc`:
|
|
|
63
63
|
|
|
64
64
|
## Usage
|
|
65
65
|
|
|
66
|
-
The plugin exposes
|
|
66
|
+
The plugin exposes five tools:
|
|
67
67
|
|
|
68
68
|
### toolbox_search_bm25
|
|
69
69
|
|
|
@@ -215,6 +215,41 @@ The plugin automatically creates a `/toolbox-status` slash command on first laun
|
|
|
215
215
|
|
|
216
216
|
Use it in OpenCode by typing `/toolbox-status` to get a formatted status report.
|
|
217
217
|
|
|
218
|
+
### toolbox_perf
|
|
219
|
+
|
|
220
|
+
Get detailed performance metrics for the toolbox plugin:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
toolbox_perf({})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Returns performance data including initialization times, search latencies, and execution stats:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"init": {
|
|
231
|
+
"duration": 1234.56,
|
|
232
|
+
"serverCount": 6,
|
|
233
|
+
"toolCount": 42
|
|
234
|
+
},
|
|
235
|
+
"timers": {
|
|
236
|
+
"search.bm25": { "count": 15, "total": 45.2, "avg": 3.01, "min": 1.2, "max": 8.5 },
|
|
237
|
+
"search.regex": { "count": 5, "total": 12.1, "avg": 2.42, "min": 1.1, "max": 4.2 },
|
|
238
|
+
"tool.execute": { "count": 10, "total": 892.3, "avg": 89.23, "min": 12.5, "max": 245.8 }
|
|
239
|
+
},
|
|
240
|
+
"indexStats": {
|
|
241
|
+
"documentCount": 42,
|
|
242
|
+
"avgDocLength": 15.3
|
|
243
|
+
},
|
|
244
|
+
"config": {
|
|
245
|
+
"initMode": "eager",
|
|
246
|
+
"connectionTimeout": 5000,
|
|
247
|
+
"requestTimeout": 30000,
|
|
248
|
+
"retryAttempts": 2
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
218
253
|
## Search Modes
|
|
219
254
|
|
|
220
255
|
### BM25 (Natural Language)
|
package/dist/index.js
CHANGED
|
@@ -32816,8 +32816,16 @@ var ServerConfigSchema = exports_external2.discriminatedUnion("type", [
|
|
|
32816
32816
|
LocalServerConfigSchema,
|
|
32817
32817
|
RemoteServerConfigSchema
|
|
32818
32818
|
]);
|
|
32819
|
+
var ConnectionConfigSchema = exports_external2.object({
|
|
32820
|
+
connectTimeout: exports_external2.number().min(100).max(60000).default(5000),
|
|
32821
|
+
requestTimeout: exports_external2.number().min(100).max(300000).default(30000),
|
|
32822
|
+
retryAttempts: exports_external2.number().min(0).max(10).default(2),
|
|
32823
|
+
retryDelay: exports_external2.number().min(0).max(30000).default(1000)
|
|
32824
|
+
});
|
|
32819
32825
|
var SettingsConfigSchema = exports_external2.object({
|
|
32820
|
-
defaultLimit: exports_external2.number().min(1).max(20).default(5)
|
|
32826
|
+
defaultLimit: exports_external2.number().min(1).max(20).default(5),
|
|
32827
|
+
initMode: exports_external2.enum(["eager", "lazy"]).default("eager"),
|
|
32828
|
+
connection: ConnectionConfigSchema.optional()
|
|
32821
32829
|
});
|
|
32822
32830
|
var ConfigSchema = exports_external2.object({
|
|
32823
32831
|
mcp: exports_external2.record(exports_external2.string(), ServerConfigSchema),
|
|
@@ -37627,6 +37635,9 @@ class RemoteMCPClient {
|
|
|
37627
37635
|
return this.toolsCache;
|
|
37628
37636
|
}
|
|
37629
37637
|
}
|
|
37638
|
+
// src/mcp-client/manager.ts
|
|
37639
|
+
import { EventEmitter } from "events";
|
|
37640
|
+
|
|
37630
37641
|
// src/catalog/catalog.ts
|
|
37631
37642
|
function normalizeTool(serverName, tool3) {
|
|
37632
37643
|
const id = {
|
|
@@ -37674,15 +37685,178 @@ function buildSearchableText(serverName, tool3, args) {
|
|
|
37674
37685
|
function normalizeTools(serverName, tools) {
|
|
37675
37686
|
return tools.map((tool3) => normalizeTool(serverName, tool3));
|
|
37676
37687
|
}
|
|
37688
|
+
// src/profiler/profiler.ts
|
|
37689
|
+
function percentile(sorted, p) {
|
|
37690
|
+
if (sorted.length === 0)
|
|
37691
|
+
return 0;
|
|
37692
|
+
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
37693
|
+
return sorted[Math.max(0, index)];
|
|
37694
|
+
}
|
|
37695
|
+
function calculateStats(measurements) {
|
|
37696
|
+
if (measurements.length === 0)
|
|
37697
|
+
return null;
|
|
37698
|
+
const sorted = [...measurements].sort((a, b) => a - b);
|
|
37699
|
+
const total = sorted.reduce((sum, v) => sum + v, 0);
|
|
37700
|
+
return {
|
|
37701
|
+
count: sorted.length,
|
|
37702
|
+
min: sorted[0],
|
|
37703
|
+
max: sorted[sorted.length - 1],
|
|
37704
|
+
avg: total / sorted.length,
|
|
37705
|
+
p50: percentile(sorted, 50),
|
|
37706
|
+
p95: percentile(sorted, 95),
|
|
37707
|
+
p99: percentile(sorted, 99),
|
|
37708
|
+
total
|
|
37709
|
+
};
|
|
37710
|
+
}
|
|
37711
|
+
|
|
37712
|
+
class Profiler {
|
|
37713
|
+
marks = new Map;
|
|
37714
|
+
measures = new Map;
|
|
37715
|
+
serverMetrics = new Map;
|
|
37716
|
+
initStartTime = null;
|
|
37717
|
+
initEndTime = null;
|
|
37718
|
+
initState = "idle";
|
|
37719
|
+
indexBuildTime = null;
|
|
37720
|
+
toolCount = 0;
|
|
37721
|
+
incrementalUpdates = 0;
|
|
37722
|
+
startTime = performance.now();
|
|
37723
|
+
mark(name) {
|
|
37724
|
+
this.marks.set(name, performance.now());
|
|
37725
|
+
}
|
|
37726
|
+
measure(name, startMark) {
|
|
37727
|
+
const start = this.marks.get(startMark);
|
|
37728
|
+
if (start === undefined) {
|
|
37729
|
+
return -1;
|
|
37730
|
+
}
|
|
37731
|
+
const duration5 = performance.now() - start;
|
|
37732
|
+
const existing = this.measures.get(name) || [];
|
|
37733
|
+
existing.push(duration5);
|
|
37734
|
+
this.measures.set(name, existing);
|
|
37735
|
+
return duration5;
|
|
37736
|
+
}
|
|
37737
|
+
record(name, duration5) {
|
|
37738
|
+
const existing = this.measures.get(name) || [];
|
|
37739
|
+
existing.push(duration5);
|
|
37740
|
+
this.measures.set(name, existing);
|
|
37741
|
+
}
|
|
37742
|
+
getStats(name) {
|
|
37743
|
+
const measurements = this.measures.get(name);
|
|
37744
|
+
if (!measurements)
|
|
37745
|
+
return null;
|
|
37746
|
+
return calculateStats(measurements);
|
|
37747
|
+
}
|
|
37748
|
+
initStart() {
|
|
37749
|
+
this.initStartTime = performance.now();
|
|
37750
|
+
this.initState = "initializing";
|
|
37751
|
+
}
|
|
37752
|
+
initComplete(state) {
|
|
37753
|
+
this.initEndTime = performance.now();
|
|
37754
|
+
this.initState = state;
|
|
37755
|
+
}
|
|
37756
|
+
recordServerConnect(name, connectTime, toolCount, status, error92) {
|
|
37757
|
+
this.serverMetrics.set(name, {
|
|
37758
|
+
name,
|
|
37759
|
+
connectTime,
|
|
37760
|
+
toolCount,
|
|
37761
|
+
status,
|
|
37762
|
+
error: error92
|
|
37763
|
+
});
|
|
37764
|
+
}
|
|
37765
|
+
recordIndexBuild(duration5, toolCount) {
|
|
37766
|
+
this.indexBuildTime = duration5;
|
|
37767
|
+
this.toolCount = toolCount;
|
|
37768
|
+
}
|
|
37769
|
+
recordIncrementalUpdate(toolCount) {
|
|
37770
|
+
this.incrementalUpdates++;
|
|
37771
|
+
this.toolCount += toolCount;
|
|
37772
|
+
}
|
|
37773
|
+
getInitState() {
|
|
37774
|
+
return this.initState;
|
|
37775
|
+
}
|
|
37776
|
+
getInitDuration() {
|
|
37777
|
+
if (this.initStartTime === null)
|
|
37778
|
+
return null;
|
|
37779
|
+
if (this.initEndTime !== null) {
|
|
37780
|
+
return this.initEndTime - this.initStartTime;
|
|
37781
|
+
}
|
|
37782
|
+
return performance.now() - this.initStartTime;
|
|
37783
|
+
}
|
|
37784
|
+
export() {
|
|
37785
|
+
return {
|
|
37786
|
+
timestamp: new Date().toISOString(),
|
|
37787
|
+
uptime: performance.now() - this.startTime,
|
|
37788
|
+
initialization: {
|
|
37789
|
+
startTime: this.initStartTime || 0,
|
|
37790
|
+
endTime: this.initEndTime,
|
|
37791
|
+
duration: this.getInitDuration(),
|
|
37792
|
+
state: this.initState,
|
|
37793
|
+
servers: Array.from(this.serverMetrics.values())
|
|
37794
|
+
},
|
|
37795
|
+
indexing: {
|
|
37796
|
+
buildTime: this.indexBuildTime,
|
|
37797
|
+
toolCount: this.toolCount,
|
|
37798
|
+
incrementalUpdates: this.incrementalUpdates
|
|
37799
|
+
},
|
|
37800
|
+
searches: {
|
|
37801
|
+
bm25: this.getStats("search.bm25"),
|
|
37802
|
+
regex: this.getStats("search.regex")
|
|
37803
|
+
},
|
|
37804
|
+
executions: this.getStats("tool.execute")
|
|
37805
|
+
};
|
|
37806
|
+
}
|
|
37807
|
+
reset() {
|
|
37808
|
+
this.marks.clear();
|
|
37809
|
+
this.measures.clear();
|
|
37810
|
+
this.serverMetrics.clear();
|
|
37811
|
+
this.initStartTime = null;
|
|
37812
|
+
this.initEndTime = null;
|
|
37813
|
+
this.initState = "idle";
|
|
37814
|
+
this.indexBuildTime = null;
|
|
37815
|
+
this.toolCount = 0;
|
|
37816
|
+
this.incrementalUpdates = 0;
|
|
37817
|
+
}
|
|
37818
|
+
startTimer(name) {
|
|
37819
|
+
const start = performance.now();
|
|
37820
|
+
return () => {
|
|
37821
|
+
const duration5 = performance.now() - start;
|
|
37822
|
+
this.record(name, duration5);
|
|
37823
|
+
return duration5;
|
|
37824
|
+
};
|
|
37825
|
+
}
|
|
37826
|
+
}
|
|
37827
|
+
var globalProfiler = new Profiler;
|
|
37677
37828
|
// src/mcp-client/manager.ts
|
|
37678
|
-
|
|
37829
|
+
var DEFAULT_CONNECTION_CONFIG = {
|
|
37830
|
+
connectTimeout: 5000,
|
|
37831
|
+
requestTimeout: 30000,
|
|
37832
|
+
retryAttempts: 2,
|
|
37833
|
+
retryDelay: 1000
|
|
37834
|
+
};
|
|
37835
|
+
function sleep(ms) {
|
|
37836
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37837
|
+
}
|
|
37838
|
+
function withTimeout(promise4, ms, errorMessage) {
|
|
37839
|
+
return Promise.race([
|
|
37840
|
+
promise4,
|
|
37841
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(errorMessage)), ms))
|
|
37842
|
+
]);
|
|
37843
|
+
}
|
|
37844
|
+
|
|
37845
|
+
class MCPManager extends EventEmitter {
|
|
37679
37846
|
servers;
|
|
37680
37847
|
clients;
|
|
37681
37848
|
clientFactory;
|
|
37849
|
+
connectionConfig;
|
|
37850
|
+
initState = "idle";
|
|
37851
|
+
initPromise = null;
|
|
37852
|
+
serversPending = 0;
|
|
37853
|
+
serversCompleted = 0;
|
|
37682
37854
|
constructor(options) {
|
|
37855
|
+
super();
|
|
37683
37856
|
this.servers = new Map;
|
|
37684
37857
|
this.clients = new Map;
|
|
37685
37858
|
this.clientFactory = options?.clientFactory || this.defaultClientFactory.bind(this);
|
|
37859
|
+
this.connectionConfig = { ...DEFAULT_CONNECTION_CONFIG, ...options?.connectionConfig };
|
|
37686
37860
|
}
|
|
37687
37861
|
defaultClientFactory(name, config3) {
|
|
37688
37862
|
if (config3.type === "local") {
|
|
@@ -37694,40 +37868,135 @@ class MCPManager {
|
|
|
37694
37868
|
}
|
|
37695
37869
|
}
|
|
37696
37870
|
async initialize(servers) {
|
|
37871
|
+
if (this.initState !== "idle") {
|
|
37872
|
+
return this.initPromise || Promise.resolve();
|
|
37873
|
+
}
|
|
37874
|
+
globalProfiler.initStart();
|
|
37875
|
+
this.initState = "initializing";
|
|
37876
|
+
this.serversPending = Object.keys(servers).length;
|
|
37877
|
+
this.serversCompleted = 0;
|
|
37697
37878
|
const promises = Object.entries(servers).map(async ([name, config3]) => {
|
|
37698
|
-
return this.
|
|
37879
|
+
return this.connectServerWithRetry(name, config3);
|
|
37699
37880
|
});
|
|
37700
|
-
|
|
37881
|
+
this.initPromise = Promise.all(promises).then(() => {
|
|
37882
|
+
this.finalizeInit();
|
|
37883
|
+
});
|
|
37884
|
+
return this.initPromise;
|
|
37885
|
+
}
|
|
37886
|
+
initializeBackground(servers) {
|
|
37887
|
+
this.initialize(servers).catch(() => {});
|
|
37888
|
+
}
|
|
37889
|
+
finalizeInit() {
|
|
37890
|
+
const allServers = Array.from(this.servers.values());
|
|
37891
|
+
const connected = allServers.filter((s) => s.status === "connected");
|
|
37892
|
+
const failed = allServers.filter((s) => s.status === "error");
|
|
37893
|
+
if (connected.length === allServers.length) {
|
|
37894
|
+
this.initState = "ready";
|
|
37895
|
+
} else if (connected.length > 0) {
|
|
37896
|
+
this.initState = "degraded";
|
|
37897
|
+
} else {
|
|
37898
|
+
this.initState = "degraded";
|
|
37899
|
+
}
|
|
37900
|
+
globalProfiler.initComplete(this.initState);
|
|
37901
|
+
this.emit("init:complete", this.initState);
|
|
37902
|
+
}
|
|
37903
|
+
async connectServerWithRetry(name, config3) {
|
|
37904
|
+
const maxAttempts = this.connectionConfig.retryAttempts + 1;
|
|
37905
|
+
let lastError = null;
|
|
37906
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
37907
|
+
try {
|
|
37908
|
+
await this.connectServer(name, config3);
|
|
37909
|
+
return;
|
|
37910
|
+
} catch (error92) {
|
|
37911
|
+
lastError = error92 instanceof Error ? error92 : new Error(String(error92));
|
|
37912
|
+
if (attempt < maxAttempts) {
|
|
37913
|
+
await sleep(this.connectionConfig.retryDelay);
|
|
37914
|
+
}
|
|
37915
|
+
}
|
|
37916
|
+
}
|
|
37917
|
+
this.servers.set(name, {
|
|
37918
|
+
name,
|
|
37919
|
+
config: config3,
|
|
37920
|
+
tools: [],
|
|
37921
|
+
status: "error",
|
|
37922
|
+
error: lastError?.message || "Connection failed after retries"
|
|
37923
|
+
});
|
|
37924
|
+
globalProfiler.recordServerConnect(name, -1, 0, "error", lastError?.message);
|
|
37925
|
+
this.emit("server:error", name, lastError?.message || "Unknown error");
|
|
37926
|
+
this.checkPartialReady();
|
|
37701
37927
|
}
|
|
37702
37928
|
async connectServer(name, config3) {
|
|
37929
|
+
const startTime = performance.now();
|
|
37703
37930
|
this.servers.set(name, {
|
|
37704
37931
|
name,
|
|
37705
37932
|
config: config3,
|
|
37706
37933
|
tools: [],
|
|
37707
37934
|
status: "connecting"
|
|
37708
37935
|
});
|
|
37709
|
-
|
|
37710
|
-
|
|
37711
|
-
|
|
37712
|
-
|
|
37713
|
-
|
|
37714
|
-
|
|
37715
|
-
|
|
37716
|
-
|
|
37717
|
-
|
|
37718
|
-
|
|
37719
|
-
|
|
37720
|
-
|
|
37721
|
-
|
|
37722
|
-
|
|
37723
|
-
|
|
37724
|
-
|
|
37725
|
-
|
|
37726
|
-
|
|
37727
|
-
|
|
37728
|
-
|
|
37936
|
+
const client = this.clientFactory(name, config3);
|
|
37937
|
+
await withTimeout(client.connect(), this.connectionConfig.connectTimeout, `Connection to ${name} timed out after ${this.connectionConfig.connectTimeout}ms`);
|
|
37938
|
+
const tools = await withTimeout(client.listTools(), this.connectionConfig.requestTimeout, `Listing tools from ${name} timed out after ${this.connectionConfig.requestTimeout}ms`);
|
|
37939
|
+
const catalogTools = normalizeTools(name, tools);
|
|
37940
|
+
const connectTime = performance.now() - startTime;
|
|
37941
|
+
this.servers.set(name, {
|
|
37942
|
+
name,
|
|
37943
|
+
config: config3,
|
|
37944
|
+
tools: catalogTools,
|
|
37945
|
+
status: "connected"
|
|
37946
|
+
});
|
|
37947
|
+
this.clients.set(name, client);
|
|
37948
|
+
globalProfiler.recordServerConnect(name, connectTime, catalogTools.length, "connected");
|
|
37949
|
+
this.emit("server:connected", name, catalogTools);
|
|
37950
|
+
this.checkPartialReady();
|
|
37951
|
+
}
|
|
37952
|
+
checkPartialReady() {
|
|
37953
|
+
this.serversCompleted++;
|
|
37954
|
+
if (this.initState === "initializing") {
|
|
37955
|
+
const connected = Array.from(this.servers.values()).filter((s) => s.status === "connected");
|
|
37956
|
+
if (connected.length === 1 && this.serversCompleted < this.serversPending) {
|
|
37957
|
+
this.initState = "partial";
|
|
37958
|
+
this.emit("init:partial");
|
|
37959
|
+
}
|
|
37729
37960
|
}
|
|
37730
37961
|
}
|
|
37962
|
+
isReady() {
|
|
37963
|
+
return this.initState === "ready" || this.initState === "partial" || this.initState === "degraded";
|
|
37964
|
+
}
|
|
37965
|
+
isComplete() {
|
|
37966
|
+
return this.initState === "ready" || this.initState === "degraded";
|
|
37967
|
+
}
|
|
37968
|
+
getInitState() {
|
|
37969
|
+
return this.initState;
|
|
37970
|
+
}
|
|
37971
|
+
async waitForReady() {
|
|
37972
|
+
if (this.isComplete()) {
|
|
37973
|
+
return this.initState;
|
|
37974
|
+
}
|
|
37975
|
+
if (this.initPromise) {
|
|
37976
|
+
await this.initPromise;
|
|
37977
|
+
return this.initState;
|
|
37978
|
+
}
|
|
37979
|
+
return this.initState;
|
|
37980
|
+
}
|
|
37981
|
+
waitForPartial() {
|
|
37982
|
+
if (this.isReady()) {
|
|
37983
|
+
return Promise.resolve();
|
|
37984
|
+
}
|
|
37985
|
+
return new Promise((resolve) => {
|
|
37986
|
+
const onPartial = () => {
|
|
37987
|
+
this.off("init:partial", onPartial);
|
|
37988
|
+
this.off("init:complete", onComplete);
|
|
37989
|
+
resolve();
|
|
37990
|
+
};
|
|
37991
|
+
const onComplete = () => {
|
|
37992
|
+
this.off("init:partial", onPartial);
|
|
37993
|
+
this.off("init:complete", onComplete);
|
|
37994
|
+
resolve();
|
|
37995
|
+
};
|
|
37996
|
+
this.on("init:partial", onPartial);
|
|
37997
|
+
this.on("init:complete", onComplete);
|
|
37998
|
+
});
|
|
37999
|
+
}
|
|
37731
38000
|
getAllTools() {
|
|
37732
38001
|
const allTools = [];
|
|
37733
38002
|
for (const [name, server] of this.servers) {
|
|
@@ -37751,7 +38020,7 @@ class MCPManager {
|
|
|
37751
38020
|
if (!client) {
|
|
37752
38021
|
throw new Error(`MCP client not found for server: ${serverName}`);
|
|
37753
38022
|
}
|
|
37754
|
-
return client.callTool(toolName, args);
|
|
38023
|
+
return withTimeout(client.callTool(toolName, args), this.connectionConfig.requestTimeout, `Tool execution timed out after ${this.connectionConfig.requestTimeout}ms`);
|
|
37755
38024
|
}
|
|
37756
38025
|
getServer(name) {
|
|
37757
38026
|
return this.servers.get(name);
|
|
@@ -37764,12 +38033,17 @@ class MCPManager {
|
|
|
37764
38033
|
await Promise.all(promises);
|
|
37765
38034
|
this.servers.clear();
|
|
37766
38035
|
this.clients.clear();
|
|
38036
|
+
this.initState = "idle";
|
|
38037
|
+
this.initPromise = null;
|
|
37767
38038
|
}
|
|
37768
38039
|
}
|
|
37769
38040
|
// src/search/bm25.ts
|
|
37770
38041
|
function tokenize(text) {
|
|
37771
38042
|
return text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 0);
|
|
37772
38043
|
}
|
|
38044
|
+
function yieldToEventLoop() {
|
|
38045
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
38046
|
+
}
|
|
37773
38047
|
|
|
37774
38048
|
class BM25Index {
|
|
37775
38049
|
documents;
|
|
@@ -37777,6 +38051,7 @@ class BM25Index {
|
|
|
37777
38051
|
docLengths;
|
|
37778
38052
|
avgDocLength = 0;
|
|
37779
38053
|
totalDocs = 0;
|
|
38054
|
+
totalTokens = 0;
|
|
37780
38055
|
k1 = 1.2;
|
|
37781
38056
|
b = 0.75;
|
|
37782
38057
|
constructor() {
|
|
@@ -37785,20 +38060,82 @@ class BM25Index {
|
|
|
37785
38060
|
this.docLengths = new Map;
|
|
37786
38061
|
}
|
|
37787
38062
|
indexTools(tools) {
|
|
37788
|
-
this.
|
|
37789
|
-
this.
|
|
37790
|
-
|
|
38063
|
+
this.clear();
|
|
38064
|
+
this.addToolsBatch(tools);
|
|
38065
|
+
}
|
|
38066
|
+
async indexToolsAsync(tools, chunkSize = 50) {
|
|
38067
|
+
this.clear();
|
|
38068
|
+
await this.addToolsAsync(tools, chunkSize);
|
|
38069
|
+
}
|
|
38070
|
+
addToolsBatch(tools) {
|
|
37791
38071
|
for (const tool3 of tools) {
|
|
37792
|
-
|
|
37793
|
-
|
|
37794
|
-
|
|
37795
|
-
|
|
37796
|
-
|
|
37797
|
-
|
|
38072
|
+
this.addToolInternal(tool3);
|
|
38073
|
+
}
|
|
38074
|
+
this.recalculateAvgDocLength();
|
|
38075
|
+
}
|
|
38076
|
+
async addToolsAsync(tools, chunkSize = 50) {
|
|
38077
|
+
for (let i = 0;i < tools.length; i += chunkSize) {
|
|
38078
|
+
const chunk = tools.slice(i, i + chunkSize);
|
|
38079
|
+
for (const tool3 of chunk) {
|
|
38080
|
+
this.addToolInternal(tool3);
|
|
38081
|
+
}
|
|
38082
|
+
if (i + chunkSize < tools.length) {
|
|
38083
|
+
await yieldToEventLoop();
|
|
37798
38084
|
}
|
|
37799
38085
|
}
|
|
37800
|
-
this.
|
|
37801
|
-
|
|
38086
|
+
this.recalculateAvgDocLength();
|
|
38087
|
+
}
|
|
38088
|
+
addTool(tool3) {
|
|
38089
|
+
this.addToolInternal(tool3);
|
|
38090
|
+
this.recalculateAvgDocLengthIncremental();
|
|
38091
|
+
}
|
|
38092
|
+
addToolInternal(tool3) {
|
|
38093
|
+
if (this.documents.has(tool3.idString)) {
|
|
38094
|
+
return;
|
|
38095
|
+
}
|
|
38096
|
+
const tokens = tokenize(tool3.searchableText);
|
|
38097
|
+
this.documents.set(tool3.idString, { tokens, tool: tool3 });
|
|
38098
|
+
this.docLengths.set(tool3.idString, tokens.length);
|
|
38099
|
+
this.totalTokens += tokens.length;
|
|
38100
|
+
this.totalDocs++;
|
|
38101
|
+
const uniqueTokens = new Set(tokens);
|
|
38102
|
+
for (const token of uniqueTokens) {
|
|
38103
|
+
this.docFreqs.set(token, (this.docFreqs.get(token) || 0) + 1);
|
|
38104
|
+
}
|
|
38105
|
+
}
|
|
38106
|
+
removeTool(idString) {
|
|
38107
|
+
const doc3 = this.documents.get(idString);
|
|
38108
|
+
if (!doc3)
|
|
38109
|
+
return false;
|
|
38110
|
+
const uniqueTokens = new Set(doc3.tokens);
|
|
38111
|
+
for (const token of uniqueTokens) {
|
|
38112
|
+
const freq = this.docFreqs.get(token) || 0;
|
|
38113
|
+
if (freq <= 1) {
|
|
38114
|
+
this.docFreqs.delete(token);
|
|
38115
|
+
} else {
|
|
38116
|
+
this.docFreqs.set(token, freq - 1);
|
|
38117
|
+
}
|
|
38118
|
+
}
|
|
38119
|
+
this.totalTokens -= doc3.tokens.length;
|
|
38120
|
+
this.documents.delete(idString);
|
|
38121
|
+
this.docLengths.delete(idString);
|
|
38122
|
+
this.totalDocs--;
|
|
38123
|
+
this.recalculateAvgDocLengthIncremental();
|
|
38124
|
+
return true;
|
|
38125
|
+
}
|
|
38126
|
+
recalculateAvgDocLength() {
|
|
38127
|
+
if (this.totalDocs === 0) {
|
|
38128
|
+
this.avgDocLength = 0;
|
|
38129
|
+
return;
|
|
38130
|
+
}
|
|
38131
|
+
this.avgDocLength = this.totalTokens / this.totalDocs;
|
|
38132
|
+
}
|
|
38133
|
+
recalculateAvgDocLengthIncremental() {
|
|
38134
|
+
if (this.totalDocs === 0) {
|
|
38135
|
+
this.avgDocLength = 0;
|
|
38136
|
+
return;
|
|
38137
|
+
}
|
|
38138
|
+
this.avgDocLength = this.totalTokens / this.totalDocs;
|
|
37802
38139
|
}
|
|
37803
38140
|
search(query, limit = 5) {
|
|
37804
38141
|
const queryTokens = tokenize(query);
|
|
@@ -37854,10 +38191,21 @@ class BM25Index {
|
|
|
37854
38191
|
this.docLengths.clear();
|
|
37855
38192
|
this.avgDocLength = 0;
|
|
37856
38193
|
this.totalDocs = 0;
|
|
38194
|
+
this.totalTokens = 0;
|
|
37857
38195
|
}
|
|
37858
38196
|
get size() {
|
|
37859
38197
|
return this.totalDocs;
|
|
37860
38198
|
}
|
|
38199
|
+
has(idString) {
|
|
38200
|
+
return this.documents.has(idString);
|
|
38201
|
+
}
|
|
38202
|
+
getStats() {
|
|
38203
|
+
return {
|
|
38204
|
+
docCount: this.totalDocs,
|
|
38205
|
+
termCount: this.docFreqs.size,
|
|
38206
|
+
avgDocLength: this.avgDocLength
|
|
38207
|
+
};
|
|
38208
|
+
}
|
|
37861
38209
|
}
|
|
37862
38210
|
// src/search/regex.ts
|
|
37863
38211
|
var MAX_REGEX_LENGTH = 200;
|
|
@@ -37964,7 +38312,79 @@ Pass arguments as JSON string matching the tool's schema.`;
|
|
|
37964
38312
|
var STATUS_DESC = `Get toolbox status including plugin initialization, MCP server connections, and tool counts.
|
|
37965
38313
|
|
|
37966
38314
|
Shows success/total metrics to highlight failures. Use to check if toolbox is working correctly.`;
|
|
38315
|
+
var PERF_DESC = `Get detailed performance metrics for the toolbox plugin.
|
|
38316
|
+
|
|
38317
|
+
Shows initialization times, search latencies, execution stats, and per-server metrics.`;
|
|
38318
|
+
var TEST_DESC = `Test all toolbox tools with minimal predefined prompts.
|
|
38319
|
+
|
|
38320
|
+
Executes every registered tool with super simple inputs to verify they work. Returns pass/fail for each tool.`;
|
|
38321
|
+
var TEST_PROMPTS = {
|
|
38322
|
+
time_get_current_time: {},
|
|
38323
|
+
time_convert_time: { source_timezone: "UTC", time: "12:00", target_timezone: "America/New_York" },
|
|
38324
|
+
brave_brave_web_search: { query: "test", count: 1 },
|
|
38325
|
+
brave_brave_local_search: { query: "coffee", count: 1 },
|
|
38326
|
+
brave_brave_video_search: { query: "test", count: 1 },
|
|
38327
|
+
brave_brave_image_search: { query: "test", count: 1 },
|
|
38328
|
+
brave_brave_news_search: { query: "test", count: 1 },
|
|
38329
|
+
brave_brave_summarizer: { key: "test" },
|
|
38330
|
+
brightdata_search_engine: { query: "hello", engine: "google", count: 1 },
|
|
38331
|
+
brightdata_search_engine_batch: { queries: [{ query: "test", engine: "google", count: 1 }] },
|
|
38332
|
+
brightdata_scrape_as_markdown: { url: "https://example.com" },
|
|
38333
|
+
brightdata_scrape_as_html: { url: "https://example.com" },
|
|
38334
|
+
brightdata_scrape_batch: { urls: ["https://example.com"] },
|
|
38335
|
+
brightdata_extract: { url: "https://example.com" },
|
|
38336
|
+
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" },
|
|
38339
|
+
"tavily_tavily-search": { query: "test", maxResults: 1 },
|
|
38340
|
+
"tavily_tavily-extract": { urls: ["https://example.com"] },
|
|
38341
|
+
"tavily_tavily-map": { url: "https://example.com" },
|
|
38342
|
+
"context7_resolve-library-id": { libraryName: "react" },
|
|
38343
|
+
octocode_githubSearchRepositories: { query: "test", maxResults: 1 },
|
|
38344
|
+
octocode_githubSearchCode: { query: "function test", maxResults: 1 },
|
|
38345
|
+
octocode_githubViewRepoStructure: { owner: "octocat", repo: "Hello-World" },
|
|
38346
|
+
perplexity_perplexity_ask: { query: "What is 1+1?" },
|
|
38347
|
+
perplexity_perplexity_search: { query: "test", maxResults: 1 }
|
|
38348
|
+
};
|
|
38349
|
+
function generateMinimalArgs(schema2) {
|
|
38350
|
+
const args = {};
|
|
38351
|
+
if (schema2.type !== "object" || !schema2.properties) {
|
|
38352
|
+
return args;
|
|
38353
|
+
}
|
|
38354
|
+
const properties = schema2.properties;
|
|
38355
|
+
const required3 = schema2.required || [];
|
|
38356
|
+
for (const propName of required3) {
|
|
38357
|
+
const prop = properties[propName];
|
|
38358
|
+
if (!prop)
|
|
38359
|
+
continue;
|
|
38360
|
+
const enumValues = prop.enum;
|
|
38361
|
+
switch (prop.type) {
|
|
38362
|
+
case "string":
|
|
38363
|
+
args[propName] = prop.default ?? enumValues?.[0] ?? "test";
|
|
38364
|
+
break;
|
|
38365
|
+
case "number":
|
|
38366
|
+
case "integer":
|
|
38367
|
+
args[propName] = prop.default ?? prop.minimum ?? 1;
|
|
38368
|
+
break;
|
|
38369
|
+
case "boolean":
|
|
38370
|
+
args[propName] = prop.default ?? false;
|
|
38371
|
+
break;
|
|
38372
|
+
case "array":
|
|
38373
|
+
args[propName] = prop.default ?? [];
|
|
38374
|
+
break;
|
|
38375
|
+
case "object":
|
|
38376
|
+
args[propName] = prop.default ?? {};
|
|
38377
|
+
break;
|
|
38378
|
+
default:
|
|
38379
|
+
args[propName] = prop.default ?? null;
|
|
38380
|
+
}
|
|
38381
|
+
}
|
|
38382
|
+
return args;
|
|
38383
|
+
}
|
|
38384
|
+
var isTestEnv = !!process.env.BUN_TEST;
|
|
37967
38385
|
function log(level, message, extra) {
|
|
38386
|
+
if (isTestEnv)
|
|
38387
|
+
return;
|
|
37968
38388
|
const timestamp = new Date().toISOString();
|
|
37969
38389
|
const extraStr = extra ? ` ${JSON.stringify(extra)}` : "";
|
|
37970
38390
|
const line = `${timestamp} [${level.toUpperCase()}] ${message}${extraStr}
|
|
@@ -37972,6 +38392,8 @@ function log(level, message, extra) {
|
|
|
37972
38392
|
mkdir(LOG_DIR, { recursive: true }).then(() => appendFile(LOG_FILE_PATH, line)).catch(() => {});
|
|
37973
38393
|
}
|
|
37974
38394
|
function ensureCommandFile() {
|
|
38395
|
+
if (isTestEnv)
|
|
38396
|
+
return;
|
|
37975
38397
|
access(COMMAND_FILE_PATH).catch(() => {
|
|
37976
38398
|
mkdir(COMMAND_DIR, { recursive: true }).then(() => writeFile(COMMAND_FILE_PATH, COMMAND_CONTENT)).then(() => log("info", "Created /toolbox-status command file")).catch(() => {});
|
|
37977
38399
|
});
|
|
@@ -37980,37 +38402,25 @@ var SYSTEM_PROMPT_BASE = `# Extended Toolbox
|
|
|
37980
38402
|
|
|
37981
38403
|
You have access to an extended toolbox with additional capabilities (web search, time utilities, code search, etc.).
|
|
37982
38404
|
|
|
37983
|
-
##
|
|
37984
|
-
ALWAYS
|
|
37985
|
-
|
|
37986
|
-
|
|
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
|
|
37987
38409
|
|
|
37988
38410
|
## Workflow
|
|
37989
38411
|
1. Search: toolbox_search_bm25({ text: "what you need" }) or toolbox_search_regex({ pattern: "prefix_.*" })
|
|
37990
38412
|
2. Execute: toolbox_execute({ name: "tool_name", arguments: '{"key": "value"}' })`;
|
|
37991
|
-
function generateSystemPrompt(
|
|
37992
|
-
|
|
37993
|
-
if (servers.length === 0) {
|
|
37994
|
-
return SYSTEM_PROMPT_BASE;
|
|
37995
|
-
}
|
|
37996
|
-
const toolboxSchema = {};
|
|
37997
|
-
for (const server of servers) {
|
|
37998
|
-
if (server.status === "connected" && server.tools.length > 0) {
|
|
37999
|
-
toolboxSchema[server.name] = server.tools.map((t) => t.idString);
|
|
38000
|
-
}
|
|
38001
|
-
}
|
|
38002
|
-
if (Object.keys(toolboxSchema).length === 0) {
|
|
38413
|
+
function generateSystemPrompt(configuredServers) {
|
|
38414
|
+
if (configuredServers.length === 0) {
|
|
38003
38415
|
return SYSTEM_PROMPT_BASE;
|
|
38004
38416
|
}
|
|
38005
38417
|
return `${SYSTEM_PROMPT_BASE}
|
|
38006
38418
|
|
|
38007
|
-
##
|
|
38008
|
-
|
|
38009
|
-
\`\`\`json
|
|
38010
|
-
${JSON.stringify(toolboxSchema, null, 2)}
|
|
38011
|
-
\`\`\``;
|
|
38419
|
+
## Registered MCP Servers
|
|
38420
|
+
- ${configuredServers.join(", ")}`;
|
|
38012
38421
|
}
|
|
38013
38422
|
var ToolboxPlugin = async (ctx) => {
|
|
38423
|
+
const pluginLoadStart = performance.now();
|
|
38014
38424
|
const { client } = ctx;
|
|
38015
38425
|
const configPath = process.env.OPENCODE_TOOLBOX_CONFIG || DEFAULT_CONFIG_PATH;
|
|
38016
38426
|
const configResult = await loadConfig(configPath);
|
|
@@ -38020,53 +38430,64 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38020
38430
|
return {};
|
|
38021
38431
|
}
|
|
38022
38432
|
const config3 = configResult.data;
|
|
38023
|
-
const
|
|
38433
|
+
const initMode = config3.settings?.initMode || "eager";
|
|
38434
|
+
const connectionConfig = {
|
|
38435
|
+
connectTimeout: config3.settings?.connection?.connectTimeout || 5000,
|
|
38436
|
+
requestTimeout: config3.settings?.connection?.requestTimeout || 30000,
|
|
38437
|
+
retryAttempts: config3.settings?.connection?.retryAttempts || 2,
|
|
38438
|
+
retryDelay: config3.settings?.connection?.retryDelay || 1000
|
|
38439
|
+
};
|
|
38440
|
+
const mcpManager = new MCPManager({ connectionConfig });
|
|
38024
38441
|
const bm25Index = new BM25Index;
|
|
38025
|
-
let initialized = false;
|
|
38026
38442
|
let searchCount = 0;
|
|
38027
38443
|
let executionCount = 0;
|
|
38028
38444
|
let executionSuccessCount = 0;
|
|
38029
38445
|
ensureCommandFile();
|
|
38030
38446
|
const serverNames = Object.keys(config3.mcp);
|
|
38447
|
+
const pluginLoadDuration = performance.now() - pluginLoadStart;
|
|
38031
38448
|
log("info", `Toolbox plugin loaded successfully`, {
|
|
38032
38449
|
configPath,
|
|
38450
|
+
logPath: LOG_FILE_PATH,
|
|
38033
38451
|
serverCount: serverNames.length,
|
|
38034
|
-
servers: serverNames
|
|
38452
|
+
servers: serverNames,
|
|
38453
|
+
initMode,
|
|
38454
|
+
loadDurationMs: Math.round(pluginLoadDuration * 100) / 100
|
|
38455
|
+
});
|
|
38456
|
+
mcpManager.on("server:connected", (serverName, tools) => {
|
|
38457
|
+
const startTime = performance.now();
|
|
38458
|
+
bm25Index.addToolsBatch(tools);
|
|
38459
|
+
const indexTime = performance.now() - startTime;
|
|
38460
|
+
globalProfiler.recordIncrementalUpdate(tools.length);
|
|
38461
|
+
log("info", `Server ${serverName} connected, indexed ${tools.length} tools in ${indexTime.toFixed(2)}ms`);
|
|
38462
|
+
});
|
|
38463
|
+
mcpManager.on("server:error", (serverName, error92) => {
|
|
38464
|
+
log("warn", `Server ${serverName} failed: ${error92}`);
|
|
38465
|
+
});
|
|
38466
|
+
mcpManager.on("init:complete", (state) => {
|
|
38467
|
+
const duration5 = globalProfiler.getInitDuration();
|
|
38468
|
+
const servers = mcpManager.getAllServers();
|
|
38469
|
+
const connectedServers = servers.filter((s) => s.status === "connected");
|
|
38470
|
+
log("info", `Initialization complete in ${duration5?.toFixed(2)}ms: ${connectedServers.length}/${servers.length} servers, ${bm25Index.size} tools indexed`, {
|
|
38471
|
+
state,
|
|
38472
|
+
totalServers: servers.length,
|
|
38473
|
+
connectedServers: connectedServers.length,
|
|
38474
|
+
totalTools: bm25Index.size
|
|
38475
|
+
});
|
|
38035
38476
|
});
|
|
38477
|
+
if (initMode === "eager") {
|
|
38478
|
+
mcpManager.initializeBackground(config3.mcp);
|
|
38479
|
+
log("info", "Started eager background initialization");
|
|
38480
|
+
}
|
|
38036
38481
|
async function ensureInitialized() {
|
|
38037
|
-
if (
|
|
38482
|
+
if (mcpManager.isReady()) {
|
|
38038
38483
|
return;
|
|
38039
|
-
|
|
38040
|
-
|
|
38484
|
+
}
|
|
38485
|
+
if (initMode === "lazy" && mcpManager.getInitState() === "idle") {
|
|
38486
|
+
log("info", "Starting lazy initialization on first use");
|
|
38041
38487
|
await mcpManager.initialize(config3.mcp);
|
|
38042
|
-
|
|
38043
|
-
bm25Index.indexTools(allTools);
|
|
38044
|
-
initialized = true;
|
|
38045
|
-
const servers = mcpManager.getAllServers();
|
|
38046
|
-
const connectedServers = servers.filter((s) => s.status === "connected");
|
|
38047
|
-
const failedServers = servers.filter((s) => s.status === "error");
|
|
38048
|
-
const initMsg = `Initialization complete: ${connectedServers.length}/${servers.length} servers connected, ${allTools.length} tools indexed`;
|
|
38049
|
-
log("info", initMsg, {
|
|
38050
|
-
totalServers: servers.length,
|
|
38051
|
-
connectedServers: connectedServers.length,
|
|
38052
|
-
failedServers: failedServers.length,
|
|
38053
|
-
totalTools: allTools.length,
|
|
38054
|
-
servers: servers.map((s) => ({
|
|
38055
|
-
name: s.name,
|
|
38056
|
-
status: s.status,
|
|
38057
|
-
toolCount: s.tools.length,
|
|
38058
|
-
error: s.error || null
|
|
38059
|
-
}))
|
|
38060
|
-
});
|
|
38061
|
-
if (failedServers.length > 0) {
|
|
38062
|
-
const warnMsg = `${failedServers.length} server(s) failed to connect: ${failedServers.map((s) => s.name).join(", ")}`;
|
|
38063
|
-
log("warn", warnMsg);
|
|
38064
|
-
}
|
|
38065
|
-
} catch (error92) {
|
|
38066
|
-
const errorMsg = `Failed to initialize MCP servers: ${error92 instanceof Error ? error92.message : String(error92)}`;
|
|
38067
|
-
log("error", errorMsg);
|
|
38068
|
-
throw error92;
|
|
38488
|
+
return;
|
|
38069
38489
|
}
|
|
38490
|
+
await mcpManager.waitForPartial();
|
|
38070
38491
|
}
|
|
38071
38492
|
return {
|
|
38072
38493
|
tool: {
|
|
@@ -38077,9 +38498,11 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38077
38498
|
limit: tool.schema.number().optional().describe("Maximum number of results to return (default: 5)")
|
|
38078
38499
|
},
|
|
38079
38500
|
async execute(args) {
|
|
38501
|
+
const timer = globalProfiler.startTimer("search.bm25");
|
|
38080
38502
|
try {
|
|
38081
38503
|
await ensureInitialized();
|
|
38082
38504
|
} catch (error92) {
|
|
38505
|
+
timer();
|
|
38083
38506
|
return JSON.stringify({
|
|
38084
38507
|
success: false,
|
|
38085
38508
|
error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
|
|
@@ -38089,11 +38512,13 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38089
38512
|
const searchLimit = args.limit || config3.settings?.defaultLimit || 5;
|
|
38090
38513
|
const allTools = mcpManager.getAllCatalogTools();
|
|
38091
38514
|
const results = bm25Index.search(args.text, searchLimit);
|
|
38092
|
-
|
|
38515
|
+
const duration5 = timer();
|
|
38516
|
+
log("info", `BM25 search completed: "${args.text}" -> ${results.length} results in ${duration5.toFixed(2)}ms`, {
|
|
38093
38517
|
searchType: "bm25",
|
|
38094
38518
|
query: args.text,
|
|
38095
38519
|
resultsCount: results.length,
|
|
38096
|
-
limit: searchLimit
|
|
38520
|
+
limit: searchLimit,
|
|
38521
|
+
durationMs: duration5
|
|
38097
38522
|
});
|
|
38098
38523
|
return formatSearchResults(results, allTools);
|
|
38099
38524
|
}
|
|
@@ -38105,9 +38530,11 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38105
38530
|
limit: tool.schema.number().optional().describe("Maximum number of results to return (default: 5)")
|
|
38106
38531
|
},
|
|
38107
38532
|
async execute(args) {
|
|
38533
|
+
const timer = globalProfiler.startTimer("search.regex");
|
|
38108
38534
|
try {
|
|
38109
38535
|
await ensureInitialized();
|
|
38110
38536
|
} catch (error92) {
|
|
38537
|
+
timer();
|
|
38111
38538
|
return JSON.stringify({
|
|
38112
38539
|
success: false,
|
|
38113
38540
|
error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
|
|
@@ -38117,6 +38544,7 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38117
38544
|
const searchLimit = args.limit || config3.settings?.defaultLimit || 5;
|
|
38118
38545
|
const allTools = mcpManager.getAllCatalogTools();
|
|
38119
38546
|
const result = searchWithRegex(allTools, args.pattern, searchLimit);
|
|
38547
|
+
const duration5 = timer();
|
|
38120
38548
|
if ("error" in result) {
|
|
38121
38549
|
log("warn", `Regex search failed: "${args.pattern}" -> ${result.error}`);
|
|
38122
38550
|
return JSON.stringify({
|
|
@@ -38124,11 +38552,12 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38124
38552
|
error: result.error
|
|
38125
38553
|
});
|
|
38126
38554
|
}
|
|
38127
|
-
log("info", `Regex search completed: "${args.pattern}" -> ${result.length} results`, {
|
|
38555
|
+
log("info", `Regex search completed: "${args.pattern}" -> ${result.length} results in ${duration5.toFixed(2)}ms`, {
|
|
38128
38556
|
searchType: "regex",
|
|
38129
38557
|
pattern: args.pattern,
|
|
38130
38558
|
resultsCount: result.length,
|
|
38131
|
-
limit: searchLimit
|
|
38559
|
+
limit: searchLimit,
|
|
38560
|
+
durationMs: duration5
|
|
38132
38561
|
});
|
|
38133
38562
|
return formatSearchResults(result, allTools);
|
|
38134
38563
|
}
|
|
@@ -38140,9 +38569,11 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38140
38569
|
arguments: tool.schema.string().optional().describe("JSON-encoded arguments for the tool, matching its schema. Use '{}' or omit for tools with no required arguments.")
|
|
38141
38570
|
},
|
|
38142
38571
|
async execute(args) {
|
|
38572
|
+
const timer = globalProfiler.startTimer("tool.execute");
|
|
38143
38573
|
try {
|
|
38144
38574
|
await ensureInitialized();
|
|
38145
38575
|
} catch (error92) {
|
|
38576
|
+
timer();
|
|
38146
38577
|
return JSON.stringify({
|
|
38147
38578
|
success: false,
|
|
38148
38579
|
error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
|
|
@@ -38150,6 +38581,7 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38150
38581
|
}
|
|
38151
38582
|
const parsed = parseToolName(args.name);
|
|
38152
38583
|
if (!parsed) {
|
|
38584
|
+
timer();
|
|
38153
38585
|
log("warn", `Invalid tool name format: ${args.name}`, {
|
|
38154
38586
|
toolName: args.name
|
|
38155
38587
|
});
|
|
@@ -38163,6 +38595,7 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38163
38595
|
try {
|
|
38164
38596
|
toolArgs = JSON.parse(args.arguments);
|
|
38165
38597
|
} catch (error92) {
|
|
38598
|
+
timer();
|
|
38166
38599
|
log("warn", `Failed to parse arguments as JSON for ${args.name}`, {
|
|
38167
38600
|
toolName: args.name,
|
|
38168
38601
|
arguments: args.arguments
|
|
@@ -38175,11 +38608,10 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38175
38608
|
}
|
|
38176
38609
|
executionCount++;
|
|
38177
38610
|
try {
|
|
38178
|
-
const startTime = Date.now();
|
|
38179
38611
|
const result = await mcpManager.callTool(parsed.serverName, parsed.toolName, toolArgs);
|
|
38180
|
-
const duration5 =
|
|
38612
|
+
const duration5 = timer();
|
|
38181
38613
|
executionSuccessCount++;
|
|
38182
|
-
log("info", `Tool executed successfully: ${args.name}`, {
|
|
38614
|
+
log("info", `Tool executed successfully: ${args.name} in ${duration5.toFixed(2)}ms`, {
|
|
38183
38615
|
server: parsed.serverName,
|
|
38184
38616
|
tool: parsed.toolName,
|
|
38185
38617
|
durationMs: duration5
|
|
@@ -38189,11 +38621,13 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38189
38621
|
result
|
|
38190
38622
|
});
|
|
38191
38623
|
} catch (error92) {
|
|
38624
|
+
const duration5 = timer();
|
|
38192
38625
|
const errorMsg = `Tool execution failed: ${error92 instanceof Error ? error92.message : String(error92)}`;
|
|
38193
38626
|
log("error", errorMsg, {
|
|
38194
38627
|
server: parsed.serverName,
|
|
38195
38628
|
tool: parsed.toolName,
|
|
38196
|
-
error: errorMsg
|
|
38629
|
+
error: errorMsg,
|
|
38630
|
+
durationMs: duration5
|
|
38197
38631
|
});
|
|
38198
38632
|
return JSON.stringify({
|
|
38199
38633
|
success: false,
|
|
@@ -38206,7 +38640,7 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38206
38640
|
description: STATUS_DESC,
|
|
38207
38641
|
args: {},
|
|
38208
38642
|
async execute() {
|
|
38209
|
-
if (!
|
|
38643
|
+
if (!mcpManager.isReady()) {
|
|
38210
38644
|
try {
|
|
38211
38645
|
await ensureInitialized();
|
|
38212
38646
|
} catch (error92) {
|
|
@@ -38222,9 +38656,13 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38222
38656
|
const failedServers = servers.filter((s) => s.status === "error");
|
|
38223
38657
|
const connectingServers = servers.filter((s) => s.status === "connecting");
|
|
38224
38658
|
const totalTools = mcpManager.getAllCatalogTools().length;
|
|
38659
|
+
const initDuration = globalProfiler.getInitDuration();
|
|
38225
38660
|
const status = {
|
|
38226
38661
|
plugin: {
|
|
38227
|
-
initialized:
|
|
38662
|
+
initialized: mcpManager.isComplete(),
|
|
38663
|
+
initState: mcpManager.getInitState(),
|
|
38664
|
+
initMode,
|
|
38665
|
+
initDurationMs: initDuration ? Math.round(initDuration) : null,
|
|
38228
38666
|
configPath,
|
|
38229
38667
|
uptime: process.uptime(),
|
|
38230
38668
|
searches: searchCount,
|
|
@@ -38248,7 +38686,7 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38248
38686
|
},
|
|
38249
38687
|
tools: {
|
|
38250
38688
|
total: totalTools,
|
|
38251
|
-
|
|
38689
|
+
indexed: bm25Index.size,
|
|
38252
38690
|
serversWithTools: servers.filter((s) => s.tools.length > 0).length
|
|
38253
38691
|
},
|
|
38254
38692
|
health: {
|
|
@@ -38263,15 +38701,154 @@ var ToolboxPlugin = async (ctx) => {
|
|
|
38263
38701
|
});
|
|
38264
38702
|
return JSON.stringify(status, null, 2);
|
|
38265
38703
|
}
|
|
38704
|
+
}),
|
|
38705
|
+
toolbox_perf: tool({
|
|
38706
|
+
description: PERF_DESC,
|
|
38707
|
+
args: {},
|
|
38708
|
+
async execute() {
|
|
38709
|
+
const report = globalProfiler.export();
|
|
38710
|
+
return JSON.stringify({
|
|
38711
|
+
...report,
|
|
38712
|
+
indexStats: bm25Index.getStats(),
|
|
38713
|
+
config: {
|
|
38714
|
+
initMode,
|
|
38715
|
+
connectionTimeout: connectionConfig.connectTimeout,
|
|
38716
|
+
requestTimeout: connectionConfig.requestTimeout,
|
|
38717
|
+
retryAttempts: connectionConfig.retryAttempts
|
|
38718
|
+
}
|
|
38719
|
+
}, null, 2);
|
|
38720
|
+
}
|
|
38721
|
+
}),
|
|
38722
|
+
toolbox_test: tool({
|
|
38723
|
+
description: TEST_DESC,
|
|
38724
|
+
args: {
|
|
38725
|
+
timeout: tool.schema.number().optional().describe("Timeout per tool in ms (default: 10000)")
|
|
38726
|
+
},
|
|
38727
|
+
async execute(args) {
|
|
38728
|
+
const startTime = performance.now();
|
|
38729
|
+
const output = [];
|
|
38730
|
+
output.push("=".repeat(80));
|
|
38731
|
+
output.push("TOOLBOX TEST - Full Execution Log");
|
|
38732
|
+
output.push("=".repeat(80));
|
|
38733
|
+
output.push("");
|
|
38734
|
+
try {
|
|
38735
|
+
await ensureInitialized();
|
|
38736
|
+
} catch (error92) {
|
|
38737
|
+
output.push(`[FATAL] Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`);
|
|
38738
|
+
return output.join(`
|
|
38739
|
+
`);
|
|
38740
|
+
}
|
|
38741
|
+
const allTools = mcpManager.getAllCatalogTools();
|
|
38742
|
+
const timeout = args.timeout || 1e4;
|
|
38743
|
+
output.push(`[INFO] Found ${allTools.length} tools to test`);
|
|
38744
|
+
output.push(`[INFO] Timeout per tool: ${timeout}ms`);
|
|
38745
|
+
output.push(`[INFO] Started at: ${new Date().toISOString()}`);
|
|
38746
|
+
output.push("");
|
|
38747
|
+
let passed = 0;
|
|
38748
|
+
let failed = 0;
|
|
38749
|
+
let timedOut = 0;
|
|
38750
|
+
let skipped = 0;
|
|
38751
|
+
for (let i = 0;i < allTools.length; i++) {
|
|
38752
|
+
const catalogTool = allTools[i];
|
|
38753
|
+
const toolId = catalogTool.idString;
|
|
38754
|
+
const testNum = i + 1;
|
|
38755
|
+
output.push("-".repeat(80));
|
|
38756
|
+
output.push(`[TEST ${testNum}/${allTools.length}] ${toolId}`);
|
|
38757
|
+
output.push("-".repeat(80));
|
|
38758
|
+
const parsed = parseToolName(toolId);
|
|
38759
|
+
if (!parsed) {
|
|
38760
|
+
output.push(`[SKIP] Invalid tool name format`);
|
|
38761
|
+
output.push("");
|
|
38762
|
+
skipped++;
|
|
38763
|
+
continue;
|
|
38764
|
+
}
|
|
38765
|
+
output.push(`[INFO] Server: ${parsed.serverName}`);
|
|
38766
|
+
output.push(`[INFO] Tool: ${parsed.toolName}`);
|
|
38767
|
+
output.push(`[INFO] Description: ${catalogTool.description || "(no description)"}`);
|
|
38768
|
+
output.push("");
|
|
38769
|
+
let testArgs;
|
|
38770
|
+
let argsSource;
|
|
38771
|
+
const predefinedArgs = TEST_PROMPTS[toolId];
|
|
38772
|
+
if (predefinedArgs !== undefined) {
|
|
38773
|
+
testArgs = predefinedArgs;
|
|
38774
|
+
argsSource = "PREDEFINED";
|
|
38775
|
+
} else {
|
|
38776
|
+
testArgs = generateMinimalArgs(catalogTool.inputSchema);
|
|
38777
|
+
argsSource = Object.keys(testArgs).length > 0 ? "GENERATED" : "EMPTY";
|
|
38778
|
+
}
|
|
38779
|
+
output.push(`[INPUT] Arguments source: ${argsSource}`);
|
|
38780
|
+
output.push(`[INPUT] Request payload:`);
|
|
38781
|
+
output.push(JSON.stringify(testArgs, null, 2).split(`
|
|
38782
|
+
`).map((line) => " " + line).join(`
|
|
38783
|
+
`));
|
|
38784
|
+
output.push("");
|
|
38785
|
+
const toolStart = performance.now();
|
|
38786
|
+
try {
|
|
38787
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
38788
|
+
setTimeout(() => reject(new Error("TIMEOUT")), timeout);
|
|
38789
|
+
});
|
|
38790
|
+
const execPromise = mcpManager.callTool(parsed.serverName, parsed.toolName, testArgs);
|
|
38791
|
+
const result = await Promise.race([execPromise, timeoutPromise]);
|
|
38792
|
+
const duration5 = Math.round(performance.now() - toolStart);
|
|
38793
|
+
output.push(`[OUTPUT] Response received in ${duration5}ms:`);
|
|
38794
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
38795
|
+
output.push(resultStr.split(`
|
|
38796
|
+
`).map((line) => " " + line).join(`
|
|
38797
|
+
`));
|
|
38798
|
+
output.push("");
|
|
38799
|
+
output.push(`[PASS] \u2713 Test passed in ${duration5}ms`);
|
|
38800
|
+
passed++;
|
|
38801
|
+
} catch (error92) {
|
|
38802
|
+
const duration5 = Math.round(performance.now() - toolStart);
|
|
38803
|
+
const errorMsg = error92 instanceof Error ? error92.message : String(error92);
|
|
38804
|
+
if (errorMsg === "TIMEOUT") {
|
|
38805
|
+
output.push(`[OUTPUT] No response - timed out after ${timeout}ms`);
|
|
38806
|
+
output.push("");
|
|
38807
|
+
output.push(`[TIMEOUT] \u2717 Test timed out after ${duration5}ms`);
|
|
38808
|
+
timedOut++;
|
|
38809
|
+
} else {
|
|
38810
|
+
output.push(`[OUTPUT] Error response:`);
|
|
38811
|
+
output.push(` ${errorMsg}`);
|
|
38812
|
+
output.push("");
|
|
38813
|
+
output.push(`[FAIL] \u2717 Test failed in ${duration5}ms`);
|
|
38814
|
+
output.push(`[FAIL] Error: ${errorMsg}`);
|
|
38815
|
+
failed++;
|
|
38816
|
+
}
|
|
38817
|
+
}
|
|
38818
|
+
output.push("");
|
|
38819
|
+
}
|
|
38820
|
+
const totalDuration = Math.round(performance.now() - startTime);
|
|
38821
|
+
const total = allTools.length;
|
|
38822
|
+
const successRate = total > 0 ? Math.round(passed / total * 100) : 0;
|
|
38823
|
+
output.push("=".repeat(80));
|
|
38824
|
+
output.push("TEST SUMMARY");
|
|
38825
|
+
output.push("=".repeat(80));
|
|
38826
|
+
output.push("");
|
|
38827
|
+
output.push(`Total tests: ${total}`);
|
|
38828
|
+
output.push(`Passed: ${passed} \u2713`);
|
|
38829
|
+
output.push(`Failed: ${failed} \u2717`);
|
|
38830
|
+
output.push(`Timed out: ${timedOut} \u23F1`);
|
|
38831
|
+
output.push(`Skipped: ${skipped} \u2298`);
|
|
38832
|
+
output.push("");
|
|
38833
|
+
output.push(`Success rate: ${successRate}%`);
|
|
38834
|
+
output.push(`Total duration: ${totalDuration}ms`);
|
|
38835
|
+
output.push(`Finished at: ${new Date().toISOString()}`);
|
|
38836
|
+
output.push("");
|
|
38837
|
+
output.push("=".repeat(80));
|
|
38838
|
+
log("info", `Toolbox test completed: ${passed}/${total} passed in ${totalDuration}ms`, {
|
|
38839
|
+
passed,
|
|
38840
|
+
failed,
|
|
38841
|
+
timedOut,
|
|
38842
|
+
skipped,
|
|
38843
|
+
total
|
|
38844
|
+
});
|
|
38845
|
+
return output.join(`
|
|
38846
|
+
`);
|
|
38847
|
+
}
|
|
38266
38848
|
})
|
|
38267
38849
|
},
|
|
38268
38850
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
38269
|
-
|
|
38270
|
-
await ensureInitialized();
|
|
38271
|
-
output.system.push(generateSystemPrompt(mcpManager));
|
|
38272
|
-
} catch {
|
|
38273
|
-
output.system.push(SYSTEM_PROMPT_BASE);
|
|
38274
|
-
}
|
|
38851
|
+
output.system.push(generateSystemPrompt(serverNames));
|
|
38275
38852
|
}
|
|
38276
38853
|
};
|
|
38277
38854
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-toolbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
"dev": "bun --hot src/index.ts",
|
|
17
17
|
"test": "bun test",
|
|
18
18
|
"test:coverage": "bun test --coverage",
|
|
19
|
-
"typecheck": "tsc --noEmit"
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"bench": "bun run bench:search && bun run bench:init && bun run bench:concurrent",
|
|
21
|
+
"bench:search": "bun run bench/search.bench.ts",
|
|
22
|
+
"bench:init": "bun run bench/init.bench.ts",
|
|
23
|
+
"bench:concurrent": "bun run bench/concurrent.bench.ts"
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
22
26
|
"dist",
|