@causyne/agent 1.10.13 → 1.10.17
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/docs/ARCHITECTURE.md +101 -17
- package/docs/SECURITY_REVIEW.md +13 -1
- package/docs/THIRD_PARTY_NOTICES.md +43 -10
- package/package.json +10 -4
- package/src/agentLoop.js +25 -3
- package/src/agentProfiles.js +135 -0
- package/src/agentProfiles.ts +143 -0
- package/src/approvalGrants.js +17 -35
- package/src/approvalGrants.ts +54 -0
- package/src/auditLog.d.ts +3 -0
- package/src/browser/camofoxRuntime.js +93 -0
- package/src/browser/camofoxRuntime.ts +112 -0
- package/src/browser/causyneBrowser.js +495 -0
- package/src/browser/causyneBrowser.ts +594 -0
- package/src/browser/searchProviders.js +156 -0
- package/src/browser/searchProviders.ts +156 -0
- package/src/browser/snapshotWindow.js +28 -0
- package/src/browser/snapshotWindow.ts +40 -0
- package/src/browser/urlPolicy.js +89 -0
- package/src/browser/urlPolicy.ts +89 -0
- package/src/checkpoints.js +19 -6
- package/src/commands.js +37 -8
- package/src/config.d.ts +2 -0
- package/src/hooks.js +121 -107
- package/src/hooks.ts +193 -0
- package/src/mcp/mcpClient.js +520 -132
- package/src/mcp/mcpClient.ts +584 -0
- package/src/mcp/mcpRegistry.js +399 -185
- package/src/mcp/mcpRegistry.ts +451 -0
- package/src/planCache.js +473 -450
- package/src/planCache.ts +840 -0
- package/src/plugins/pluginManager.js +40 -5
- package/src/plugins/pluginSkills.js +84 -0
- package/src/plugins/pluginSkills.ts +106 -0
- package/src/provisioning/capabilityProvisioner.d.ts +10 -0
- package/src/provisioning/capabilityProvisioner.js +5 -0
- package/src/provisioning/capabilityTools.js +2 -1
- package/src/sandbox.js +32 -2
- package/src/sessionJournal.js +471 -0
- package/src/sessionJournal.ts +496 -0
- package/src/sessionStore.js +3 -279
- package/src/skillsManager.js +18 -0
- package/src/subagentJournal.js +215 -0
- package/src/subagentJournal.ts +254 -0
- package/src/taskShape.js +179 -198
- package/src/taskShape.ts +259 -0
- package/src/tools/browserAutomator.js +45 -241
- package/src/tools/browserAutomator.ts +57 -0
- package/src/tools/manageLocalAiDependencies.js +1 -1
- package/src/tools/subagentOrchestrator.js +73 -12
- package/src/tools/webFetch.js +183 -68
- package/src/tools/webFetch.ts +185 -0
- package/src/tools/webSearch.js +50 -75
- package/src/tools/webSearch.ts +56 -0
- package/src/types/contracts.js +8 -0
- package/src/types/contracts.ts +263 -0
- package/src/utils/paths.d.ts +3 -0
- package/src/version.d.ts +1 -0
- package/src/mcp/jsonRpc.js +0 -165
package/src/mcp/mcpRegistry.js
CHANGED
|
@@ -1,207 +1,421 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// Discovered tools are namespaced `mcp__<server>__<tool>`, which is what keeps
|
|
4
|
-
// them from ever colliding with a native tool name — and makes it obvious in a
|
|
5
|
-
// transcript, an audit log and an approval prompt that the call is leaving
|
|
6
|
-
// Causyne's own code.
|
|
7
|
-
|
|
8
|
-
import { McpClient } from './mcpClient.js';
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { McpClient, McpConnectionError } from './mcpClient.js';
|
|
9
3
|
import { loadConfig, saveConfig } from '../config.js';
|
|
10
4
|
import { auditLog } from '../auditLog.js';
|
|
11
|
-
|
|
12
5
|
const NAMESPACE = 'mcp__';
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
const RESOURCE_LIST_TOOL = '_causyne_resources_list';
|
|
7
|
+
const RESOURCE_READ_TOOL = '_causyne_resource_read';
|
|
8
|
+
const PROMPT_LIST_TOOL = '_causyne_prompts_list';
|
|
9
|
+
const PROMPT_GET_TOOL = '_causyne_prompt_get';
|
|
10
|
+
const connected = new Map();
|
|
11
|
+
const connecting = new Map();
|
|
12
|
+
const statuses = new Map();
|
|
13
|
+
const connectionEpochs = new Map();
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function cloneStringRecord(value) {
|
|
18
|
+
if (!isRecord(value))
|
|
19
|
+
return undefined;
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const [key, item] of Object.entries(value)) {
|
|
22
|
+
if (typeof item === 'string')
|
|
23
|
+
result[key] = item;
|
|
24
|
+
}
|
|
25
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
26
|
+
}
|
|
27
|
+
function cloneStringArray(value) {
|
|
28
|
+
if (!Array.isArray(value))
|
|
29
|
+
return undefined;
|
|
30
|
+
return value.filter((item) => typeof item === 'string');
|
|
31
|
+
}
|
|
32
|
+
function normalizeDefinition(value) {
|
|
33
|
+
if (!isRecord(value) || typeof value.name !== 'string')
|
|
34
|
+
return null;
|
|
35
|
+
const definition = { name: value.name };
|
|
36
|
+
if (typeof value.command === 'string')
|
|
37
|
+
definition.command = value.command;
|
|
38
|
+
if (typeof value.url === 'string')
|
|
39
|
+
definition.url = value.url;
|
|
40
|
+
const args = cloneStringArray(value.args);
|
|
41
|
+
const env = cloneStringRecord(value.env);
|
|
42
|
+
const headers = cloneStringRecord(value.headers);
|
|
43
|
+
if (args)
|
|
44
|
+
definition.args = args;
|
|
45
|
+
if (env)
|
|
46
|
+
definition.env = env;
|
|
47
|
+
if (headers)
|
|
48
|
+
definition.headers = headers;
|
|
49
|
+
if (typeof value.cwd === 'string')
|
|
50
|
+
definition.cwd = value.cwd;
|
|
51
|
+
if (typeof value.enabled === 'boolean')
|
|
52
|
+
definition.enabled = value.enabled;
|
|
53
|
+
if (typeof value.timeoutMs === 'number')
|
|
54
|
+
definition.timeoutMs = value.timeoutMs;
|
|
55
|
+
if (value.protocol === 'legacy' || value.protocol === 'auto')
|
|
56
|
+
definition.protocol = value.protocol;
|
|
57
|
+
if (isRecord(value.tools)) {
|
|
58
|
+
const include = cloneStringArray(value.tools.include);
|
|
59
|
+
const exclude = cloneStringArray(value.tools.exclude);
|
|
60
|
+
if (include || exclude) {
|
|
61
|
+
definition.tools = {};
|
|
62
|
+
if (include)
|
|
63
|
+
definition.tools.include = include;
|
|
64
|
+
if (exclude)
|
|
65
|
+
definition.tools.exclude = exclude;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return McpClient.validateDefinition(definition) === null ? definition : null;
|
|
69
|
+
}
|
|
70
|
+
function canonicalize(value) {
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return value.map(canonicalize);
|
|
73
|
+
if (!isRecord(value))
|
|
74
|
+
return value;
|
|
75
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonicalize(value[key])]));
|
|
76
|
+
}
|
|
77
|
+
export function definitionDigest(definition) {
|
|
78
|
+
return createHash('sha256').update(JSON.stringify(canonicalize(definition))).digest('hex');
|
|
79
|
+
}
|
|
80
|
+
function consentRecord(value) {
|
|
81
|
+
if (!isRecord(value) || typeof value.digest !== 'string' || typeof value.grantedAt !== 'number')
|
|
82
|
+
return null;
|
|
83
|
+
return { digest: value.digest, grantedAt: value.grantedAt };
|
|
84
|
+
}
|
|
85
|
+
function globMatches(name, pattern) {
|
|
86
|
+
const source = pattern
|
|
87
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
88
|
+
.replaceAll('*', '.*')
|
|
89
|
+
.replaceAll('?', '.');
|
|
90
|
+
return new RegExp(`^${source}$`, 'u').test(name);
|
|
91
|
+
}
|
|
92
|
+
function toolAllowed(server, toolName) {
|
|
93
|
+
const include = server.tools?.include;
|
|
94
|
+
const exclude = server.tools?.exclude;
|
|
95
|
+
if (include && !include.some(pattern => globMatches(toolName, pattern)))
|
|
96
|
+
return false;
|
|
97
|
+
if (exclude?.some(pattern => globMatches(toolName, pattern)))
|
|
98
|
+
return false;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function utilityDefinitions(server, client) {
|
|
102
|
+
const definitions = [];
|
|
103
|
+
if (client.supportsResources()) {
|
|
104
|
+
definitions.push({
|
|
105
|
+
name: toolName(server.name, RESOURCE_LIST_TOOL),
|
|
106
|
+
description: `[MCP: ${server.name}] List the resources and resource templates exposed by this server.`,
|
|
107
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false }
|
|
108
|
+
}, {
|
|
109
|
+
name: toolName(server.name, RESOURCE_READ_TOOL),
|
|
110
|
+
description: `[MCP: ${server.name}] Read one resource by its exact URI.`,
|
|
111
|
+
parameters: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: { uri: { type: 'string', description: 'Exact MCP resource URI.' } },
|
|
114
|
+
required: ['uri'],
|
|
115
|
+
additionalProperties: false
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (client.supportsPrompts()) {
|
|
120
|
+
definitions.push({
|
|
121
|
+
name: toolName(server.name, PROMPT_LIST_TOOL),
|
|
122
|
+
description: `[MCP: ${server.name}] List the prompts exposed by this server.`,
|
|
123
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false }
|
|
124
|
+
}, {
|
|
125
|
+
name: toolName(server.name, PROMPT_GET_TOOL),
|
|
126
|
+
description: `[MCP: ${server.name}] Get a prompt by name with optional string arguments.`,
|
|
127
|
+
parameters: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
name: { type: 'string', description: 'Exact MCP prompt name.' },
|
|
131
|
+
arguments: { type: 'object', additionalProperties: { type: 'string' } }
|
|
132
|
+
},
|
|
133
|
+
required: ['name'],
|
|
134
|
+
additionalProperties: false
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return definitions.filter(definition => toolAllowed(server, parseToolName(definition.name)?.tool ?? ''));
|
|
139
|
+
}
|
|
140
|
+
function config() {
|
|
141
|
+
return loadConfig();
|
|
142
|
+
}
|
|
143
|
+
function connectionEpoch(name) {
|
|
144
|
+
return connectionEpochs.get(name) ?? 0;
|
|
145
|
+
}
|
|
146
|
+
function invalidateConnection(name) {
|
|
147
|
+
connectionEpochs.set(name, connectionEpoch(name) + 1);
|
|
148
|
+
}
|
|
149
|
+
/** Server definitions the user configured. Model output never reaches this source. */
|
|
16
150
|
export function configuredServers() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
stored.command = definition.command;
|
|
42
|
-
if (Array.isArray(definition.args)) stored.args = definition.args;
|
|
43
|
-
if (definition.env && typeof definition.env === 'object') stored.env = definition.env;
|
|
44
|
-
if (typeof definition.cwd === 'string') stored.cwd = definition.cwd;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
config.mcpServers = [...servers, stored];
|
|
48
|
-
// Registering is not consenting: leave it ungranted.
|
|
49
|
-
config.mcpConsent = { ...(config.mcpConsent || {}), [definition.name]: false };
|
|
50
|
-
saveConfig(config);
|
|
51
|
-
|
|
52
|
-
auditLog.log({ event: 'mcp_server_added', server: definition.name, transport: definition.url ? 'http' : 'stdio' });
|
|
53
|
-
return { ok: true };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Remove a server, drop its consent, and close any live connection. */
|
|
151
|
+
const servers = config().mcpServers;
|
|
152
|
+
if (!Array.isArray(servers))
|
|
153
|
+
return [];
|
|
154
|
+
return servers.map(normalizeDefinition).filter((server) => server !== null);
|
|
155
|
+
}
|
|
156
|
+
export function addServer(value) {
|
|
157
|
+
const problem = McpClient.validateDefinition(value);
|
|
158
|
+
if (problem)
|
|
159
|
+
return { ok: false, error: problem };
|
|
160
|
+
const definition = normalizeDefinition(value);
|
|
161
|
+
if (!definition)
|
|
162
|
+
return { ok: false, error: 'invalid server definition' };
|
|
163
|
+
const current = config();
|
|
164
|
+
const servers = Array.isArray(current.mcpServers) ? [...current.mcpServers] : [];
|
|
165
|
+
if (servers.some(server => isRecord(server) && server.name === definition.name)) {
|
|
166
|
+
return { ok: false, error: `a server named "${definition.name}" is already configured` };
|
|
167
|
+
}
|
|
168
|
+
current.mcpServers = [...servers, definition];
|
|
169
|
+
current.mcpConsent = { ...(current.mcpConsent ?? {}), [definition.name]: false };
|
|
170
|
+
saveConfig(current);
|
|
171
|
+
statuses.set(definition.name, definition.enabled === false ? { state: 'disabled' } : { state: 'disconnected' });
|
|
172
|
+
auditLog.log({ event: 'mcp_server_added', server: definition.name, transport: definition.url ? 'http' : 'stdio' });
|
|
173
|
+
return { ok: true };
|
|
174
|
+
}
|
|
57
175
|
export function removeServer(name) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
auditLog.log({ event: 'mcp_server_removed', server: name });
|
|
74
|
-
return { ok: true };
|
|
75
|
-
}
|
|
76
|
-
|
|
176
|
+
const current = config();
|
|
177
|
+
const servers = Array.isArray(current.mcpServers) ? current.mcpServers : [];
|
|
178
|
+
if (!servers.some(server => isRecord(server) && server.name === name)) {
|
|
179
|
+
return { ok: false, error: `no server named "${name}" is configured` };
|
|
180
|
+
}
|
|
181
|
+
current.mcpServers = servers.filter(server => !isRecord(server) || server.name !== name);
|
|
182
|
+
const consent = { ...(current.mcpConsent ?? {}) };
|
|
183
|
+
delete consent[name];
|
|
184
|
+
current.mcpConsent = consent;
|
|
185
|
+
saveConfig(current);
|
|
186
|
+
void disconnectServer(name);
|
|
187
|
+
statuses.delete(name);
|
|
188
|
+
auditLog.log({ event: 'mcp_server_removed', server: name });
|
|
189
|
+
return { ok: true };
|
|
190
|
+
}
|
|
77
191
|
export function isServerGranted(name) {
|
|
78
|
-
|
|
79
|
-
|
|
192
|
+
const definition = configuredServers().find(server => server.name === name);
|
|
193
|
+
if (!definition || definition.enabled === false)
|
|
194
|
+
return false;
|
|
195
|
+
const consent = consentRecord(config().mcpConsent?.[name]);
|
|
196
|
+
return consent?.digest === definitionDigest(definition);
|
|
80
197
|
}
|
|
81
|
-
|
|
82
198
|
export function grantServer(name) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
199
|
+
const definition = configuredServers().find(server => server.name === name);
|
|
200
|
+
if (!definition)
|
|
201
|
+
return { ok: false, error: `no server named "${name}" is configured` };
|
|
202
|
+
const current = config();
|
|
203
|
+
const digest = definitionDigest(definition);
|
|
204
|
+
current.mcpConsent = {
|
|
205
|
+
...(current.mcpConsent ?? {}),
|
|
206
|
+
[name]: { digest, grantedAt: Date.now() }
|
|
207
|
+
};
|
|
208
|
+
saveConfig(current);
|
|
209
|
+
statuses.set(name, definition.enabled === false ? { state: 'disabled' } : { state: 'disconnected' });
|
|
210
|
+
auditLog.log({ event: 'mcp_server_granted', server: name, definitionDigest: digest.slice(0, 12) });
|
|
211
|
+
return { ok: true };
|
|
88
212
|
}
|
|
89
|
-
|
|
90
213
|
export function revokeServer(name) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
214
|
+
const current = config();
|
|
215
|
+
current.mcpConsent = { ...(current.mcpConsent ?? {}), [name]: false };
|
|
216
|
+
saveConfig(current);
|
|
217
|
+
void disconnectServer(name);
|
|
218
|
+
statuses.set(name, { state: 'disconnected' });
|
|
219
|
+
auditLog.log({ event: 'mcp_server_revoked', server: name });
|
|
220
|
+
}
|
|
99
221
|
export function toolName(serverName, tool) {
|
|
100
|
-
|
|
222
|
+
return `${NAMESPACE}${serverName}__${tool}`;
|
|
101
223
|
}
|
|
102
|
-
|
|
103
224
|
export function isMcpToolName(name) {
|
|
104
|
-
|
|
225
|
+
return typeof name === 'string' && name.startsWith(NAMESPACE);
|
|
105
226
|
}
|
|
106
|
-
|
|
107
|
-
/** Split `mcp__server__tool` back into its parts. Tool names may contain `__`. */
|
|
108
227
|
export function parseToolName(name) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
* Connect to a granted server, or return the live client.
|
|
118
|
-
* Connecting is never automatic: the caller has already checked consent.
|
|
119
|
-
*/
|
|
228
|
+
if (!isMcpToolName(name))
|
|
229
|
+
return null;
|
|
230
|
+
const rest = name.slice(NAMESPACE.length);
|
|
231
|
+
const separator = rest.indexOf('__');
|
|
232
|
+
if (separator <= 0 || separator === rest.length - 2)
|
|
233
|
+
return null;
|
|
234
|
+
return { server: rest.slice(0, separator), tool: rest.slice(separator + 2) };
|
|
235
|
+
}
|
|
120
236
|
export async function connectServer(name) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Tool schemas for every granted, reachable server.
|
|
131
|
-
*
|
|
132
|
-
* A server that fails to start is skipped silently rather than failing the
|
|
133
|
-
* whole catalogue — one broken third-party server must not take the agent down.
|
|
134
|
-
*/
|
|
135
|
-
export async function discoverTools() {
|
|
136
|
-
const definitions = [];
|
|
137
|
-
for (const server of configuredServers()) {
|
|
138
|
-
if (!isServerGranted(server.name)) continue;
|
|
139
|
-
let client;
|
|
140
|
-
try {
|
|
141
|
-
client = await connectServer(server.name);
|
|
142
|
-
} catch (e) {
|
|
143
|
-
console.warn(`[MCP] ${server.name} unavailable: ${e.message}`);
|
|
144
|
-
continue;
|
|
237
|
+
const definition = configuredServers().find(server => server.name === name);
|
|
238
|
+
if (!definition)
|
|
239
|
+
throw new Error(`no MCP server named "${name}" is configured`);
|
|
240
|
+
if (definition.enabled === false) {
|
|
241
|
+
statuses.set(name, { state: 'disabled' });
|
|
242
|
+
throw new Error(`MCP server "${name}" is disabled`);
|
|
145
243
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
});
|
|
244
|
+
if (!isServerGranted(name))
|
|
245
|
+
throw new Error(`MCP server "${name}" has not been granted for its current configuration`);
|
|
246
|
+
const live = connected.get(name);
|
|
247
|
+
if (live && live.status.state === 'connected' && definitionDigest(live.definition) === definitionDigest(definition)) {
|
|
248
|
+
return live;
|
|
152
249
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
250
|
+
if (live)
|
|
251
|
+
await disconnectServer(name);
|
|
252
|
+
const pending = connecting.get(name);
|
|
253
|
+
if (pending)
|
|
254
|
+
return pending;
|
|
255
|
+
const startedAtEpoch = connectionEpoch(name);
|
|
256
|
+
let promise;
|
|
257
|
+
promise = (async () => {
|
|
258
|
+
const client = new McpClient(definition, {
|
|
259
|
+
workspace: process.cwd(),
|
|
260
|
+
onStatus: status => {
|
|
261
|
+
if (connectionEpoch(name) !== startedAtEpoch)
|
|
262
|
+
return;
|
|
263
|
+
statuses.set(name, status);
|
|
264
|
+
if (status.state === 'failed' || status.state === 'needs_auth')
|
|
265
|
+
connected.delete(name);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
statuses.set(name, { state: 'connecting' });
|
|
269
|
+
try {
|
|
270
|
+
await client.connect();
|
|
271
|
+
const current = configuredServers().find(server => server.name === name);
|
|
272
|
+
const connectionIsCurrent = connectionEpoch(name) === startedAtEpoch
|
|
273
|
+
&& current !== undefined
|
|
274
|
+
&& definitionDigest(current) === definitionDigest(definition)
|
|
275
|
+
&& isServerGranted(name);
|
|
276
|
+
if (!connectionIsCurrent) {
|
|
277
|
+
await client.close();
|
|
278
|
+
throw new Error(`MCP server "${name}" connection was cancelled`);
|
|
279
|
+
}
|
|
280
|
+
connected.set(name, client);
|
|
281
|
+
statuses.set(name, client.status);
|
|
282
|
+
return client;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
const status = error instanceof McpConnectionError
|
|
286
|
+
? { state: error.state, error: error.message }
|
|
287
|
+
: { state: 'failed', error: error instanceof Error ? error.message : String(error) };
|
|
288
|
+
if (connectionEpoch(name) === startedAtEpoch)
|
|
289
|
+
statuses.set(name, status);
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
if (connecting.get(name) === promise)
|
|
294
|
+
connecting.delete(name);
|
|
295
|
+
}
|
|
296
|
+
})();
|
|
297
|
+
connecting.set(name, promise);
|
|
298
|
+
return promise;
|
|
299
|
+
}
|
|
300
|
+
export async function disconnectServer(name) {
|
|
301
|
+
invalidateConnection(name);
|
|
302
|
+
const client = connected.get(name);
|
|
303
|
+
connected.delete(name);
|
|
304
|
+
connecting.delete(name);
|
|
305
|
+
if (client)
|
|
306
|
+
await client.close();
|
|
307
|
+
if (configuredServers().some(server => server.name === name))
|
|
308
|
+
statuses.set(name, { state: 'disconnected' });
|
|
309
|
+
}
|
|
310
|
+
export async function refreshServer(name) {
|
|
311
|
+
const client = await connectServer(name);
|
|
312
|
+
return client.refreshTools();
|
|
313
|
+
}
|
|
314
|
+
export async function discoverTools() {
|
|
315
|
+
const granted = configuredServers().filter(server => isServerGranted(server.name));
|
|
316
|
+
const results = await Promise.all(granted.map(async (server) => {
|
|
317
|
+
try {
|
|
318
|
+
const client = await connectServer(server.name);
|
|
319
|
+
return client.tools
|
|
320
|
+
.filter(tool => toolAllowed(server, tool.name))
|
|
321
|
+
.map(tool => ({
|
|
322
|
+
name: toolName(server.name, tool.name),
|
|
323
|
+
description: `[MCP: ${server.name}] ${tool.description}`.trim(),
|
|
324
|
+
parameters: tool.inputSchema
|
|
325
|
+
}))
|
|
326
|
+
.concat(utilityDefinitions(server, client));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
330
|
+
console.warn(`[MCP] ${server.name} unavailable: ${message}`);
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
}));
|
|
334
|
+
return results.flat();
|
|
335
|
+
}
|
|
161
336
|
export function resolveMcpTool(name) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
337
|
+
const parsed = parseToolName(name);
|
|
338
|
+
if (!parsed)
|
|
339
|
+
return null;
|
|
340
|
+
const definition = configuredServers().find(server => server.name === parsed.server);
|
|
341
|
+
if (!definition)
|
|
342
|
+
return null;
|
|
343
|
+
return {
|
|
344
|
+
schema: {
|
|
345
|
+
name: name,
|
|
346
|
+
description: `MCP tool ${parsed.tool} on server ${parsed.server}`,
|
|
347
|
+
parameters: { type: 'object', properties: {} }
|
|
348
|
+
},
|
|
349
|
+
async execute(args, context = {}) {
|
|
350
|
+
if (!isServerGranted(parsed.server)) {
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
requiresConsent: true,
|
|
354
|
+
error: `The MCP server "${parsed.server}" is configured but not enabled for its current definition. ` +
|
|
355
|
+
`Request approval with manage_local_dependencies action "grant_mcp_server" and group "${parsed.server}".`
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const client = await connectServer(parsed.server);
|
|
360
|
+
if (parsed.tool === RESOURCE_LIST_TOOL)
|
|
361
|
+
return await client.listResources(context.signal);
|
|
362
|
+
if (parsed.tool === RESOURCE_READ_TOOL) {
|
|
363
|
+
return typeof args.uri === 'string'
|
|
364
|
+
? await client.readResource(args.uri, context.signal)
|
|
365
|
+
: { success: false, error: '`uri` must be a string' };
|
|
366
|
+
}
|
|
367
|
+
if (parsed.tool === PROMPT_LIST_TOOL)
|
|
368
|
+
return await client.listPrompts(context.signal);
|
|
369
|
+
if (parsed.tool === PROMPT_GET_TOOL) {
|
|
370
|
+
if (typeof args.name !== 'string')
|
|
371
|
+
return { success: false, error: '`name` must be a string' };
|
|
372
|
+
const promptArgs = {};
|
|
373
|
+
if (isRecord(args.arguments)) {
|
|
374
|
+
for (const [key, value] of Object.entries(args.arguments)) {
|
|
375
|
+
if (typeof value !== 'string')
|
|
376
|
+
return { success: false, error: '`arguments` values must be strings' };
|
|
377
|
+
promptArgs[key] = value;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return await client.getPrompt(args.name, promptArgs, context.signal);
|
|
381
|
+
}
|
|
382
|
+
const tool = client.tools.find(item => item.name === parsed.tool);
|
|
383
|
+
if (!tool || !toolAllowed(definition, parsed.tool)) {
|
|
384
|
+
return { success: false, error: `MCP server "${parsed.server}" does not expose permitted tool "${parsed.tool}"` };
|
|
385
|
+
}
|
|
386
|
+
return await client.callTool(parsed.tool, args ?? {}, context.signal);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
return { success: false, error: `MCP server "${parsed.server}": ${error instanceof Error ? error.message : String(error)}` };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
186
393
|
}
|
|
187
|
-
|
|
188
|
-
/** State for the user-facing listing. */
|
|
189
394
|
export function serverStatus() {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
395
|
+
return configuredServers().map(server => {
|
|
396
|
+
const live = connected.get(server.name);
|
|
397
|
+
const status = server.enabled === false
|
|
398
|
+
? { state: 'disabled' }
|
|
399
|
+
: statuses.get(server.name) ?? { state: 'disconnected' };
|
|
400
|
+
const result = {
|
|
401
|
+
name: server.name,
|
|
402
|
+
transport: status.transport ?? (server.url ? 'http' : 'stdio'),
|
|
403
|
+
target: server.url ?? [server.command, ...(server.args ?? [])].filter(Boolean).join(' '),
|
|
404
|
+
granted: isServerGranted(server.name),
|
|
405
|
+
connected: status.state === 'connected',
|
|
406
|
+
status: status.state,
|
|
407
|
+
tools: live?.tools.filter(tool => toolAllowed(server, tool.name)).map(tool => tool.name) ?? []
|
|
408
|
+
};
|
|
409
|
+
return status.error ? { ...result, error: status.error } : result;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
export async function closeAll() {
|
|
413
|
+
for (const name of new Set([...connected.keys(), ...connecting.keys()]))
|
|
414
|
+
invalidateConnection(name);
|
|
415
|
+
const clients = [...connected.values()];
|
|
416
|
+
connected.clear();
|
|
417
|
+
connecting.clear();
|
|
418
|
+
await Promise.allSettled(clients.map(client => client.close()));
|
|
419
|
+
statuses.clear();
|
|
420
|
+
connectionEpochs.clear();
|
|
207
421
|
}
|