@bahulam/code 0.1.21 → 0.1.23
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/package.json +1 -1
- package/src/agents/loader.mjs +1 -0
- package/src/agents/parser.mjs +1 -0
- package/src/agents/registry.mjs +5 -1
- package/src/commands/mcp.mjs +296 -0
- package/src/commands/plugin-manage.mjs +31 -20
- package/src/commands/plugin.mjs +3 -6
- package/src/core/headless.mjs +9 -0
- package/src/core/paths.mjs +24 -0
- package/src/core/stream-client.mjs +48 -12
- package/src/core/tool-executor.mjs +133 -11
- package/src/local-service/server.mjs +1 -4
- package/src/mcp/client.mjs +54 -5
- package/src/mcp/loader.mjs +98 -0
- package/src/mcp/transport-shttp.mjs +2 -1
- package/src/plugins/manifest.mjs +207 -1
- package/src/plugins/pi-compat/requirements.mjs +67 -1
- package/src/plugins/pi-compat/scaffold.mjs +209 -4
- package/src/plugins/preflight.mjs +28 -7
- package/src/plugins/registry.mjs +12 -9
- package/src/plugins/state-tools.mjs +86 -0
- package/src/plugins/state.mjs +168 -16
- package/src/terminal/main.mjs +9 -0
- package/src/terminal/repl.mjs +40 -0
- package/src/tools/registry.mjs +7 -1
- package/src/ui/commands.mjs +35 -10
|
@@ -31,6 +31,7 @@ import { backgroundTasks } from './background-tasks.mjs';
|
|
|
31
31
|
import { normalizeLintOutput, resolveLintCommand } from './lint-resolver.mjs';
|
|
32
32
|
import { PluginRegistry } from '../plugins/registry.mjs';
|
|
33
33
|
import { loadPluginTool } from '../plugins/executor.mjs';
|
|
34
|
+
import { makePluginState } from '../plugins/state.mjs';
|
|
34
35
|
import * as fs from 'node:fs';
|
|
35
36
|
import * as os from 'node:os';
|
|
36
37
|
import * as path from 'node:path';
|
|
@@ -60,9 +61,9 @@ export function createToolExecutor({
|
|
|
60
61
|
// no reactive pulse.
|
|
61
62
|
stateEmit = null,
|
|
62
63
|
delegateRunner = null,
|
|
63
|
-
// Execution channel. 'main' (REPL/headless/CLI): plugin agents
|
|
64
|
-
//
|
|
65
|
-
//
|
|
64
|
+
// Execution channel. 'main' (REPL/headless/CLI): plugin entry agents and
|
|
65
|
+
// allowlisted plugin agents are listed in the agent-context envelope.
|
|
66
|
+
// Other plugin helpers stay workspace-scoped.
|
|
66
67
|
// 'workspace' (plugin workspace sessions via agent-relay): the
|
|
67
68
|
// session plugin's agents are fully available.
|
|
68
69
|
channel = 'main',
|
|
@@ -632,20 +633,82 @@ export function createToolExecutor({
|
|
|
632
633
|
return true;
|
|
633
634
|
}
|
|
634
635
|
|
|
635
|
-
// Per-plugin state handles are opened lazily on first
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
//
|
|
636
|
+
// Per-plugin state handles are opened lazily on first call and cached
|
|
637
|
+
// process-wide. `makePluginState` itself dedupes on plugin name, so
|
|
638
|
+
// this Map only exists to avoid re-attaching stateEmit on every
|
|
639
|
+
// registered tool — and to keep the agent-context summary reading the
|
|
640
|
+
// same connection a plugin tool writes through.
|
|
639
641
|
const _pluginStateHandles = new Map(); // pluginName -> state proxy
|
|
640
|
-
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* The normalized `config.state` block a plugin declared, or null when
|
|
645
|
+
* it declared none. Declared tables are created and additively
|
|
646
|
+
* migrated when the DB opens, so this is also the schema the plugin's
|
|
647
|
+
* own handlers query via `state.query()`.
|
|
648
|
+
*/
|
|
649
|
+
function pluginStateDecl(pluginName) {
|
|
650
|
+
if (!pluginName || typeof pluginRegistry?.list !== 'function') return null;
|
|
651
|
+
const plugin = pluginRegistry.list().find(p => p.metadata?.name === pluginName);
|
|
652
|
+
return plugin?.config?.state || null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function _pluginStateFor(pluginName) {
|
|
641
656
|
if (!pluginName) return null;
|
|
642
657
|
if (_pluginStateHandles.has(pluginName)) return _pluginStateHandles.get(pluginName);
|
|
643
|
-
const
|
|
644
|
-
const state = makePluginState(pluginName, {
|
|
658
|
+
const decl = pluginStateDecl(pluginName);
|
|
659
|
+
const state = makePluginState(pluginName, {
|
|
660
|
+
emit: stateEmit,
|
|
661
|
+
tables: decl?.tables || [],
|
|
662
|
+
});
|
|
645
663
|
_pluginStateHandles.set(pluginName, state);
|
|
646
664
|
return state;
|
|
647
665
|
}
|
|
648
666
|
|
|
667
|
+
// ── Declared plugin state → agent context ────────────────────
|
|
668
|
+
// Tier 1 of the state-visibility contract. Plugins that declare
|
|
669
|
+
// `config.state.context_always` get exactly those keys and streams
|
|
670
|
+
// injected into the agent context each turn, so a fresh session opens
|
|
671
|
+
// already knowing what the previous one left behind — instead of
|
|
672
|
+
// spending a tool call to rediscover it, or silently redoing work.
|
|
673
|
+
//
|
|
674
|
+
// Opt-in by construction: a plugin that declares nothing contributes
|
|
675
|
+
// nothing, and an opted-in plugin that has recorded nothing yet is
|
|
676
|
+
// skipped rather than shipping an empty block.
|
|
677
|
+
//
|
|
678
|
+
// Reads are keyed on the state DB's size+mtime so the payload stays
|
|
679
|
+
// byte-identical between turns when nothing wrote. Same reasoning as
|
|
680
|
+
// the memory digest above: a context block that churns on every turn
|
|
681
|
+
// invalidates the backend's prompt cache on every ExecuteRequest.
|
|
682
|
+
const _pluginStateCache = new Map(); // pluginName -> { key, entry }
|
|
683
|
+
|
|
684
|
+
function pluginStateContext() {
|
|
685
|
+
if (typeof pluginRegistry?.list !== 'function') return [];
|
|
686
|
+
const out = [];
|
|
687
|
+
for (const plugin of pluginRegistry.list()) {
|
|
688
|
+
const name = plugin.metadata?.name;
|
|
689
|
+
const decl = plugin.config?.state;
|
|
690
|
+
if (!name || !decl?.context_always?.length) continue;
|
|
691
|
+
try {
|
|
692
|
+
const state = _pluginStateFor(name);
|
|
693
|
+
const stat = fs.existsSync(state.path) ? fs.statSync(state.path) : null;
|
|
694
|
+
const key = stat ? `${stat.size}:${Math.round(stat.mtimeMs)}` : 'missing';
|
|
695
|
+
const cached = _pluginStateCache.get(name);
|
|
696
|
+
if (cached && cached.key === key) {
|
|
697
|
+
out.push(cached.entry);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const summary = state.summary(decl.context_always);
|
|
701
|
+
const hasContent = Object.keys(summary.kv).length > 0
|
|
702
|
+
|| Object.values(summary.streams).some(rows => rows.length > 0);
|
|
703
|
+
if (!hasContent) continue;
|
|
704
|
+
const entry = { plugin: name, ...summary };
|
|
705
|
+
_pluginStateCache.set(name, { key, entry });
|
|
706
|
+
out.push(entry);
|
|
707
|
+
} catch { /* one broken plugin must never break the session */ }
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
}
|
|
711
|
+
|
|
649
712
|
/**
|
|
650
713
|
* Register one MCP tool under `<serverName>.<toolName>` (namespaced
|
|
651
714
|
* to prevent collisions between plugins that ship servers with the
|
|
@@ -706,6 +769,56 @@ export function createToolExecutor({
|
|
|
706
769
|
const name = String(toolDef.name || '').trim();
|
|
707
770
|
if (!name || toolMap[name]) continue;
|
|
708
771
|
const pluginName = toolDef._plugin_name || toolDef.plugin_name || null;
|
|
772
|
+
if (toolDef._state_tool) {
|
|
773
|
+
// Manifest-declared state query tool (config.state.context_tools).
|
|
774
|
+
// There is no module to import — the author declared a table
|
|
775
|
+
// and an optional WHERE clause; the CLI supplies the handler.
|
|
776
|
+
// `readTable` refuses undeclared tables, so author-supplied
|
|
777
|
+
// SQL can't be steered into arbitrary table access.
|
|
778
|
+
const spec = toolDef._state_tool;
|
|
779
|
+
registerPluginTool(name, async (args) => {
|
|
780
|
+
try {
|
|
781
|
+
const state = _pluginStateFor(spec.plugin || pluginName);
|
|
782
|
+
if (!state) {
|
|
783
|
+
return {
|
|
784
|
+
success: false,
|
|
785
|
+
output: `Plugin state unavailable for '${name}'.`,
|
|
786
|
+
_tool: name,
|
|
787
|
+
_plugin: pluginName,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const bound = (spec.params || []).map(key => args?.[key] ?? null);
|
|
791
|
+
// An unfiltered call should list everything, not match
|
|
792
|
+
// nothing — `topic = NULL` is never true in SQL. Apply
|
|
793
|
+
// the WHERE only once a declared param was supplied.
|
|
794
|
+
const useWhere = Boolean(spec.where) && bound.some(v => v !== null);
|
|
795
|
+
const rows = state.readTable(spec.table, {
|
|
796
|
+
where: useWhere ? spec.where : '',
|
|
797
|
+
params: useWhere ? bound : [],
|
|
798
|
+
limit: args?.limit ?? spec.limit,
|
|
799
|
+
});
|
|
800
|
+
return {
|
|
801
|
+
success: true,
|
|
802
|
+
output: rows.length
|
|
803
|
+
? JSON.stringify(rows, null, 2)
|
|
804
|
+
: `No rows in ${spec.table}${useWhere ? ' matching those filters' : ''}.`,
|
|
805
|
+
rows,
|
|
806
|
+
count: rows.length,
|
|
807
|
+
_tool: name,
|
|
808
|
+
_plugin: pluginName,
|
|
809
|
+
_state_tool: true,
|
|
810
|
+
};
|
|
811
|
+
} catch (err) {
|
|
812
|
+
return {
|
|
813
|
+
success: false,
|
|
814
|
+
output: `Plugin state tool error (${name}): ${err.message}`,
|
|
815
|
+
_tool: name,
|
|
816
|
+
_plugin: pluginName,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
}, { pluginName, source: 'state', stateTool: spec });
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
709
822
|
if (toolDef._composed?.kind === 'pi') {
|
|
710
823
|
// Composed pi tools resolve at invocation time: look up the
|
|
711
824
|
// installed pi package's directory, load the specific handler
|
|
@@ -2281,7 +2394,7 @@ export function createToolExecutor({
|
|
|
2281
2394
|
const agents = filterLocalAgents(args).map(compactAgentMetadata);
|
|
2282
2395
|
const payload = { agents, count: agents.length };
|
|
2283
2396
|
if (agents.some(agent => agent.runnable === false)) {
|
|
2284
|
-
payload.note = 'Agents with runnable:false are workspace-scoped plugin
|
|
2397
|
+
payload.note = 'Agents with runnable:false are workspace-scoped plugin helpers; declare an entry_agent or add their slug to settings plugins.agent_allowlist to invoke them from the main loop.';
|
|
2285
2398
|
}
|
|
2286
2399
|
return {
|
|
2287
2400
|
success: true,
|
|
@@ -2777,6 +2890,7 @@ export function createToolExecutor({
|
|
|
2777
2890
|
getAgentContext() {
|
|
2778
2891
|
const global = projectRegistry.getGlobalContext();
|
|
2779
2892
|
const mem = _readMemorySnapshot();
|
|
2893
|
+
const pluginState = pluginStateContext();
|
|
2780
2894
|
return {
|
|
2781
2895
|
identity: global.identity,
|
|
2782
2896
|
preferences: global.preferences,
|
|
@@ -2804,6 +2918,14 @@ export function createToolExecutor({
|
|
|
2804
2918
|
spec: agent.spec,
|
|
2805
2919
|
})),
|
|
2806
2920
|
sub_agent_observability: agentRegistry.observability(),
|
|
2921
|
+
// Cross-session plugin state. Only plugins that opted in via
|
|
2922
|
+
// config.state.context_always appear here, and only the keys
|
|
2923
|
+
// and streams they named — this is how a plugin's local app
|
|
2924
|
+
// state survives a session boundary without the agent having
|
|
2925
|
+
// to know to go looking for it.
|
|
2926
|
+
...(pluginState.length ? {
|
|
2927
|
+
plugin_state: pluginState,
|
|
2928
|
+
} : {}),
|
|
2807
2929
|
// Background jobs the model should know about. Stable fields
|
|
2808
2930
|
// only (no durations) so the entry — and the prompt cache —
|
|
2809
2931
|
// changes on status transitions, not every turn.
|
|
@@ -640,10 +640,7 @@ function scanPlugins(session) {
|
|
|
640
640
|
const dirs = new Map();
|
|
641
641
|
try {
|
|
642
642
|
const registry = new PluginRegistry({
|
|
643
|
-
pluginDirs: [
|
|
644
|
-
path.join(session.root_path || process.cwd(), '.bahulam', 'plugins'),
|
|
645
|
-
path.join(os.homedir(), '.bahulam', 'plugins'),
|
|
646
|
-
],
|
|
643
|
+
pluginDirs: [path.join(os.homedir(), '.bahulam', 'plugins')],
|
|
647
644
|
}).scan();
|
|
648
645
|
for (const manifest of registry.list()) {
|
|
649
646
|
const pluginName = manifest.metadata?.name || '';
|
package/src/mcp/client.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { spawn } from 'child_process';
|
|
14
14
|
|
|
15
15
|
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
16
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
16
17
|
|
|
17
18
|
export class McpClient {
|
|
18
19
|
/**
|
|
@@ -102,7 +103,9 @@ export class McpClient {
|
|
|
102
103
|
}
|
|
103
104
|
});
|
|
104
105
|
this.connected = true;
|
|
105
|
-
|
|
106
|
+
await this._initRemote();
|
|
107
|
+
this._notifyRemote('notifications/initialized', {});
|
|
108
|
+
return this.serverInfo;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
async _connectWebSocket() {
|
|
@@ -117,6 +120,7 @@ export class McpClient {
|
|
|
117
120
|
capabilities: {},
|
|
118
121
|
clientInfo: { name: 'bahulam-code', version: '2.0.0' },
|
|
119
122
|
});
|
|
123
|
+
this._notifyRemote('notifications/initialized', {});
|
|
120
124
|
return this.serverInfo;
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -132,6 +136,7 @@ export class McpClient {
|
|
|
132
136
|
capabilities: {},
|
|
133
137
|
clientInfo: { name: 'bahulam-code', version: '2.0.0' },
|
|
134
138
|
});
|
|
139
|
+
this._notifyRemote('notifications/initialized', {});
|
|
135
140
|
return this.serverInfo;
|
|
136
141
|
}
|
|
137
142
|
|
|
@@ -145,11 +150,39 @@ export class McpClient {
|
|
|
145
150
|
return result;
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
_notifyRemote(method, params) {
|
|
154
|
+
if (this.transport?.request) {
|
|
155
|
+
// WebSocket / sHTTP transports have native request() — use send()
|
|
156
|
+
// for notifications (no id, no response expected).
|
|
157
|
+
if (this.transport.send) {
|
|
158
|
+
this.transport.send({ jsonrpc: '2.0', method, params }).catch(() => {});
|
|
159
|
+
}
|
|
160
|
+
} else if (this.transport) {
|
|
161
|
+
// SSE transport — fire-and-forget via send()
|
|
162
|
+
this.transport.send({ jsonrpc: '2.0', method, params }).catch(() => {});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
148
166
|
async _transportRequest(method, params) {
|
|
149
167
|
return new Promise((resolve, reject) => {
|
|
150
168
|
const id = ++this.requestId;
|
|
151
|
-
|
|
152
|
-
|
|
169
|
+
const timeout = setTimeout(() => {
|
|
170
|
+
if (this.pending.has(id)) {
|
|
171
|
+
this.pending.delete(id);
|
|
172
|
+
reject(new Error(`MCP request timeout: ${method} (${DEFAULT_REQUEST_TIMEOUT_MS}ms)`));
|
|
173
|
+
}
|
|
174
|
+
}, DEFAULT_REQUEST_TIMEOUT_MS);
|
|
175
|
+
this.pending.set(id, {
|
|
176
|
+
resolve: (val) => { clearTimeout(timeout); resolve(val); },
|
|
177
|
+
reject: (err) => { clearTimeout(timeout); reject(err); },
|
|
178
|
+
});
|
|
179
|
+
this.transport.send({ jsonrpc: '2.0', id, method, params }).catch(err => {
|
|
180
|
+
if (this.pending.has(id)) {
|
|
181
|
+
this.pending.delete(id);
|
|
182
|
+
clearTimeout(timeout);
|
|
183
|
+
reject(err);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
153
186
|
});
|
|
154
187
|
}
|
|
155
188
|
|
|
@@ -177,7 +210,14 @@ export class McpClient {
|
|
|
177
210
|
result = await this._request('tools/call', params);
|
|
178
211
|
}
|
|
179
212
|
if (result?.content && Array.isArray(result.content)) {
|
|
180
|
-
|
|
213
|
+
const textParts = result.content.filter(c => c.type === 'text').map(c => c.text);
|
|
214
|
+
const imageParts = result.content.filter(c => c.type === 'image');
|
|
215
|
+
if (imageParts.length > 0 && textParts.length === 0) {
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
if (textParts.length > 0) {
|
|
219
|
+
return textParts.join('\n');
|
|
220
|
+
}
|
|
181
221
|
}
|
|
182
222
|
return result;
|
|
183
223
|
}
|
|
@@ -222,7 +262,16 @@ export class McpClient {
|
|
|
222
262
|
_request(method, params) {
|
|
223
263
|
return new Promise((resolve, reject) => {
|
|
224
264
|
const id = ++this.requestId;
|
|
225
|
-
|
|
265
|
+
const timeout = setTimeout(() => {
|
|
266
|
+
if (this.pending.has(id)) {
|
|
267
|
+
this.pending.delete(id);
|
|
268
|
+
reject(new Error(`MCP request timeout: ${method} (${DEFAULT_REQUEST_TIMEOUT_MS}ms)`));
|
|
269
|
+
}
|
|
270
|
+
}, DEFAULT_REQUEST_TIMEOUT_MS);
|
|
271
|
+
this.pending.set(id, {
|
|
272
|
+
resolve: (val) => { clearTimeout(timeout); resolve(val); },
|
|
273
|
+
reject: (err) => { clearTimeout(timeout); reject(err); },
|
|
274
|
+
});
|
|
226
275
|
const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params });
|
|
227
276
|
this.process.stdin.write(msg + '\n');
|
|
228
277
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Settings Loader — bridges settings-based mcpServers to the tool executor.
|
|
3
|
+
*
|
|
4
|
+
* The agent-relay path already spawns MCP servers from plugin manifests.
|
|
5
|
+
* This module provides the same capability for CLI/REPL/headless mode by
|
|
6
|
+
* reading mcpServers from the settings chain (~/.claude/settings.json etc.)
|
|
7
|
+
* and optionally from ~/.bahulam/config.json.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { loadMcpServers } from '../mcp/loader.mjs';
|
|
11
|
+
* const mcpClients = await loadMcpServers(toolExecutor, settings);
|
|
12
|
+
* // ... later ...
|
|
13
|
+
* await mcpClients.disconnectAll();
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { McpClient } from './client.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Recursively expand ${VAR} patterns in a config object using process.env.
|
|
20
|
+
* Matches the behavior in agent-relay.mjs _expandEnvInMcpConfig.
|
|
21
|
+
* @param {*} value
|
|
22
|
+
* @returns {*}
|
|
23
|
+
*/
|
|
24
|
+
function expandEnv(value) {
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
return value.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? '');
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) return value.map(expandEnv);
|
|
29
|
+
if (value && typeof value === 'object') {
|
|
30
|
+
return Object.fromEntries(
|
|
31
|
+
Object.entries(value).map(([k, v]) => [k, expandEnv(v)]),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load and connect MCP servers from settings, registering their tools
|
|
39
|
+
* with the tool executor.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} toolExecutor - the createToolExecutor() instance
|
|
42
|
+
* @param {object} settings - loaded settings (from loadSettings())
|
|
43
|
+
* @param {object} [options]
|
|
44
|
+
* @param {string} [options.pluginName='settings'] - namespace for tool registration
|
|
45
|
+
* @returns {Promise<{clients: Array, disconnectAll: Function}>}
|
|
46
|
+
*/
|
|
47
|
+
export async function loadMcpServers(toolExecutor, settings, options = {}) {
|
|
48
|
+
const pluginName = options.pluginName || 'settings';
|
|
49
|
+
const servers = settings?.mcpServers || {};
|
|
50
|
+
const clients = [];
|
|
51
|
+
|
|
52
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
53
|
+
if (!config || typeof config !== 'object') continue;
|
|
54
|
+
if (!config.command && !config.url) continue;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Merge the config's env block into process.env BEFORE expanding
|
|
58
|
+
// ${VAR} patterns in args/env. This matches the Claude Desktop
|
|
59
|
+
// convention: the env block sets vars for the spawned process
|
|
60
|
+
// AND for ${VAR} expansion in the same config.
|
|
61
|
+
if (config.env && typeof config.env === 'object') {
|
|
62
|
+
for (const [k, v] of Object.entries(config.env)) {
|
|
63
|
+
if (typeof v === 'string' && !process.env[k]) {
|
|
64
|
+
process.env[k] = v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const expanded = expandEnv(config);
|
|
69
|
+
const client = new McpClient(expanded);
|
|
70
|
+
await client.connect();
|
|
71
|
+
const tools = await client.listTools();
|
|
72
|
+
for (const tool of tools) {
|
|
73
|
+
if (toolExecutor.registerMcpTool) {
|
|
74
|
+
toolExecutor.registerMcpTool(pluginName, name, tool.name, client, tool.inputSchema || {});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
clients.push({ name, client });
|
|
78
|
+
if (process.env.MCP_DEBUG) {
|
|
79
|
+
process.stderr.write(`[mcp:settings] ${name}: ${tools.length} tools registered\n`);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// One server failure must never block the session.
|
|
83
|
+
if (process.env.MCP_DEBUG) {
|
|
84
|
+
process.stderr.write(`[mcp:settings] ${name} failed: ${err.message}\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
clients,
|
|
91
|
+
async disconnectAll() {
|
|
92
|
+
for (const { name, client } of clients) {
|
|
93
|
+
try { toolExecutor?.unregisterMcpServer?.(pluginName, name); } catch {}
|
|
94
|
+
try { await client.disconnect(); } catch {}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -16,6 +16,7 @@ export class StreamableHttpTransport {
|
|
|
16
16
|
this.timeout = options.timeout || 30000;
|
|
17
17
|
this.sessionId = options.sessionId || null;
|
|
18
18
|
this.connected = false;
|
|
19
|
+
this.requestId = 0;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
async connect() {
|
|
@@ -42,7 +43,7 @@ export class StreamableHttpTransport {
|
|
|
42
43
|
* Collects all events and returns the final result.
|
|
43
44
|
*/
|
|
44
45
|
async request(method, params) {
|
|
45
|
-
const id =
|
|
46
|
+
const id = ++this.requestId;
|
|
46
47
|
const body = { jsonrpc: '2.0', id, method, params };
|
|
47
48
|
|
|
48
49
|
const headers = {
|