@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/mcpClient.js
CHANGED
|
@@ -1,136 +1,524 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// `src/tools/deferredMcpSearch.js` carries "mcp" in its name and is not an MCP
|
|
6
|
-
// client — it searches Causyne's own tool registry. The only code in the tree
|
|
7
|
-
// that spoke JSON-RPC was `palmierAutomator.js`, against one hardcoded server.
|
|
8
|
-
// So the single largest extensibility lever every competing agent has was
|
|
9
|
-
// missing, while the name suggested it was present.
|
|
10
|
-
//
|
|
11
|
-
// SECURITY POSTURE — read before changing anything here
|
|
12
|
-
//
|
|
13
|
-
// A client that launches third-party servers is the highest-risk pattern in
|
|
14
|
-
// this codebase, so the boundary is drawn in exactly one place and stated
|
|
15
|
-
// plainly:
|
|
16
|
-
//
|
|
17
|
-
// * A server command NEVER comes from model output. Servers are read from
|
|
18
|
-
// `config.mcpServers`, which only the user writes. The model can ask for a
|
|
19
|
-
// configured server to be enabled; it cannot name a new one, cannot alter a
|
|
20
|
-
// command, and cannot pass arguments to a process launch.
|
|
21
|
-
// * Enabling a server requires the same human approval as any other
|
|
22
|
-
// consent-gated action (`sandbox.js` classifies `grant_mcp_server` with the
|
|
23
|
-
// download actions).
|
|
24
|
-
// * What the model DOES control is tool arguments, which is the entire point
|
|
25
|
-
// of the protocol — the same trust boundary as any other tool call.
|
|
26
|
-
//
|
|
27
|
-
// If a future change lets a server command originate anywhere other than user
|
|
28
|
-
// config, that is a remote-code-execution hole, not a feature.
|
|
29
|
-
|
|
30
|
-
import { StdioChannel, HttpChannel } from './jsonRpc.js';
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { Client, SSEClientTransport, SdkError, SdkErrorCode, SdkHttpError, StreamableHTTPClientTransport, UnauthorizedError } from '@modelcontextprotocol/client';
|
|
4
|
+
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/client/stdio';
|
|
31
5
|
import { getPackageVersion } from '../version.js';
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
const MAX_TIMEOUT_MS = 10 * 60_000;
|
|
8
|
+
const STDERR_LIMIT_BYTES = 8 * 1024;
|
|
9
|
+
const TOOL_OUTPUT_LIMIT_CHARS = 1_000_000;
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function isStringRecord(value) {
|
|
14
|
+
return isRecord(value) && Object.values(value).every(item => typeof item === 'string');
|
|
15
|
+
}
|
|
16
|
+
function errorMessage(error) {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
19
|
+
function isAuthenticationError(error) {
|
|
20
|
+
if (error instanceof UnauthorizedError)
|
|
21
|
+
return true;
|
|
22
|
+
if (error instanceof SdkHttpError)
|
|
23
|
+
return error.status === 401;
|
|
24
|
+
return error instanceof SdkError && error.code === SdkErrorCode.ClientHttpAuthentication;
|
|
25
|
+
}
|
|
26
|
+
function normalizedTimeout(value) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return DEFAULT_TIMEOUT_MS;
|
|
29
|
+
return Math.max(1_000, Math.min(MAX_TIMEOUT_MS, value));
|
|
30
|
+
}
|
|
31
|
+
function validateStringArray(value, label) {
|
|
32
|
+
if (!Array.isArray(value))
|
|
33
|
+
return `${label} must be an array`;
|
|
34
|
+
if (!value.every(item => typeof item === 'string' && !item.includes('\0'))) {
|
|
35
|
+
return `${label} must contain only strings without NUL characters`;
|
|
36
|
+
}
|
|
59
37
|
return null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
env: this.definition.env || {},
|
|
72
|
-
cwd: this.definition.cwd
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await this.channel.open();
|
|
76
|
-
|
|
77
|
-
const result = await this.channel.send('initialize', {
|
|
78
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
79
|
-
capabilities: { tools: {} },
|
|
80
|
-
clientInfo: CLIENT_INFO
|
|
81
|
-
});
|
|
82
|
-
this.serverInfo = result?.serverInfo || null;
|
|
83
|
-
|
|
84
|
-
// Required by the protocol: the server may not serve requests until it has
|
|
85
|
-
// seen this.
|
|
86
|
-
this.channel.notify('notifications/initialized');
|
|
87
|
-
|
|
88
|
-
await this.listTools();
|
|
89
|
-
return this;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async listTools() {
|
|
93
|
-
const result = await this.channel.send('tools/list', {});
|
|
94
|
-
const raw = Array.isArray(result?.tools) ? result.tools : [];
|
|
95
|
-
|
|
96
|
-
// Only keep entries that are actually callable. A malformed advertisement
|
|
97
|
-
// reaching the model produces a tool it can call and we cannot dispatch —
|
|
98
|
-
// the exact failure mode D-6 was about, arriving from someone else's code.
|
|
99
|
-
this.tools = raw
|
|
100
|
-
.filter(t => t && typeof t.name === 'string' && t.name.length > 0)
|
|
101
|
-
.map(t => ({
|
|
102
|
-
name: t.name,
|
|
103
|
-
description: typeof t.description === 'string' ? t.description : '',
|
|
104
|
-
inputSchema: (t.inputSchema && typeof t.inputSchema === 'object')
|
|
105
|
-
? t.inputSchema
|
|
106
|
-
: { type: 'object', properties: {} }
|
|
107
|
-
}));
|
|
108
|
-
return this.tools;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async callTool(toolName, args) {
|
|
112
|
-
const result = await this.channel.send('tools/call', { name: toolName, arguments: args ?? {} });
|
|
113
|
-
|
|
114
|
-
// MCP returns content blocks. Flatten text for the agent loop, and report an
|
|
115
|
-
// error result as an error rather than as successful output.
|
|
116
|
-
const blocks = Array.isArray(result?.content) ? result.content : [];
|
|
117
|
-
const text = blocks
|
|
118
|
-
.map(b => (b?.type === 'text' && typeof b.text === 'string') ? b.text : '')
|
|
119
|
-
.filter(Boolean)
|
|
120
|
-
.join('\n');
|
|
121
|
-
|
|
122
|
-
if (result?.isError) {
|
|
123
|
-
return { success: false, error: text || 'the MCP server reported an error' };
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
success: true,
|
|
127
|
-
output: text || JSON.stringify(result ?? {}, null, 2)
|
|
38
|
+
}
|
|
39
|
+
function normalizeTool(tool) {
|
|
40
|
+
if (typeof tool.name !== 'string' || tool.name.trim().length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
const inputSchema = isRecord(tool.inputSchema)
|
|
43
|
+
? tool.inputSchema
|
|
44
|
+
: { type: 'object', properties: {} };
|
|
45
|
+
const normalized = {
|
|
46
|
+
name: tool.name,
|
|
47
|
+
description: typeof tool.description === 'string' ? tool.description : '',
|
|
48
|
+
inputSchema
|
|
128
49
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
50
|
+
if (isRecord(tool.annotations))
|
|
51
|
+
normalized.annotations = tool.annotations;
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
function describeContentBlock(block) {
|
|
55
|
+
if (!isRecord(block) || typeof block.type !== 'string')
|
|
56
|
+
return '';
|
|
57
|
+
if (block.type === 'text' && typeof block.text === 'string')
|
|
58
|
+
return block.text;
|
|
59
|
+
if ((block.type === 'image' || block.type === 'audio') && typeof block.data === 'string') {
|
|
60
|
+
const mime = typeof block.mimeType === 'string' ? block.mimeType : 'application/octet-stream';
|
|
61
|
+
const estimatedBytes = Math.floor(block.data.length * 0.75);
|
|
62
|
+
return `[MCP ${block.type}: ${mime}, ${estimatedBytes} encoded bytes]`;
|
|
63
|
+
}
|
|
64
|
+
if (block.type === 'resource_link') {
|
|
65
|
+
const uri = typeof block.uri === 'string' ? block.uri : '(unknown URI)';
|
|
66
|
+
const name = typeof block.name === 'string' ? ` ${block.name}` : '';
|
|
67
|
+
return `[MCP resource link:${name} ${uri}]`;
|
|
68
|
+
}
|
|
69
|
+
if (block.type === 'resource' && isRecord(block.resource)) {
|
|
70
|
+
const resource = block.resource;
|
|
71
|
+
const uri = typeof resource.uri === 'string' ? resource.uri : '(unknown URI)';
|
|
72
|
+
if (typeof resource.text === 'string')
|
|
73
|
+
return `[MCP resource ${uri}]\n${resource.text}`;
|
|
74
|
+
if (typeof resource.blob === 'string') {
|
|
75
|
+
const mime = typeof resource.mimeType === 'string' ? resource.mimeType : 'application/octet-stream';
|
|
76
|
+
return `[MCP resource ${uri}: ${mime}, ${Math.floor(resource.blob.length * 0.75)} encoded bytes]`;
|
|
77
|
+
}
|
|
78
|
+
return `[MCP resource ${uri}]`;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return JSON.stringify(block);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return `[MCP ${block.type} content]`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function boundedOutput(output) {
|
|
88
|
+
return output.length > TOOL_OUTPUT_LIMIT_CHARS
|
|
89
|
+
? `${output.slice(0, TOOL_OUTPUT_LIMIT_CHARS)}\n\n[MCP output truncated at ${TOOL_OUTPUT_LIMIT_CHARS} characters]`
|
|
90
|
+
: output;
|
|
91
|
+
}
|
|
92
|
+
function formatToolResult(result) {
|
|
93
|
+
const content = Array.isArray(result.content)
|
|
94
|
+
? result.content.map(describeContentBlock).filter(Boolean).join('\n\n')
|
|
95
|
+
: '';
|
|
96
|
+
let output = content;
|
|
97
|
+
if (!output && result.structuredContent !== undefined) {
|
|
98
|
+
output = JSON.stringify(result.structuredContent, null, 2);
|
|
99
|
+
}
|
|
100
|
+
if (!output)
|
|
101
|
+
output = JSON.stringify(result, null, 2);
|
|
102
|
+
return boundedOutput(output);
|
|
103
|
+
}
|
|
104
|
+
function formatResourceContents(contents) {
|
|
105
|
+
const output = contents.map(item => {
|
|
106
|
+
if (!isRecord(item))
|
|
107
|
+
return '';
|
|
108
|
+
const uri = typeof item.uri === 'string' ? item.uri : '(unknown URI)';
|
|
109
|
+
if (typeof item.text === 'string')
|
|
110
|
+
return `[MCP resource ${uri}]\n${item.text}`;
|
|
111
|
+
if (typeof item.blob === 'string') {
|
|
112
|
+
const mime = typeof item.mimeType === 'string' ? item.mimeType : 'application/octet-stream';
|
|
113
|
+
return `[MCP resource ${uri}: ${mime}, ${Math.floor(item.blob.length * 0.75)} encoded bytes]`;
|
|
114
|
+
}
|
|
115
|
+
return `[MCP resource ${uri}]`;
|
|
116
|
+
}).filter(Boolean).join('\n\n');
|
|
117
|
+
return boundedOutput(output);
|
|
118
|
+
}
|
|
119
|
+
function formatPromptMessages(messages) {
|
|
120
|
+
const output = messages.map(message => {
|
|
121
|
+
if (!isRecord(message))
|
|
122
|
+
return '';
|
|
123
|
+
const role = typeof message.role === 'string' ? message.role : 'unknown';
|
|
124
|
+
return `[${role}] ${describeContentBlock(message.content)}`;
|
|
125
|
+
}).filter(Boolean).join('\n\n');
|
|
126
|
+
return boundedOutput(output);
|
|
127
|
+
}
|
|
128
|
+
export class McpConnectionError extends Error {
|
|
129
|
+
state;
|
|
130
|
+
constructor(state, message, options) {
|
|
131
|
+
super(message, options);
|
|
132
|
+
this.name = 'McpConnectionError';
|
|
133
|
+
this.state = state;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export class McpClient {
|
|
137
|
+
name;
|
|
138
|
+
definition;
|
|
139
|
+
workspace;
|
|
140
|
+
tools = [];
|
|
141
|
+
serverInfo = null;
|
|
142
|
+
instructions = null;
|
|
143
|
+
status = { state: 'disconnected' };
|
|
144
|
+
client = null;
|
|
145
|
+
transport = null;
|
|
146
|
+
stderrTail = '';
|
|
147
|
+
closing = false;
|
|
148
|
+
onStatus;
|
|
149
|
+
onToolsChanged;
|
|
150
|
+
constructor(definition, options = {}) {
|
|
151
|
+
this.name = definition.name;
|
|
152
|
+
this.definition = definition;
|
|
153
|
+
this.workspace = path.resolve(options.workspace ?? process.cwd());
|
|
154
|
+
this.onStatus = options.onStatus;
|
|
155
|
+
this.onToolsChanged = options.onToolsChanged;
|
|
156
|
+
}
|
|
157
|
+
static validateDefinition(definition) {
|
|
158
|
+
if (!isRecord(definition))
|
|
159
|
+
return 'not an object';
|
|
160
|
+
if (typeof definition.name !== 'string' || !/^[a-z0-9_-]{1,48}$/i.test(definition.name)) {
|
|
161
|
+
return 'name must be 1-48 characters of [A-Za-z0-9_-]';
|
|
162
|
+
}
|
|
163
|
+
const hasStdio = typeof definition.command === 'string' && definition.command.trim().length > 0;
|
|
164
|
+
const hasHttp = typeof definition.url === 'string' && definition.url.trim().length > 0;
|
|
165
|
+
if (hasStdio === hasHttp)
|
|
166
|
+
return 'exactly one of `command` (stdio) or `url` (HTTP) is required';
|
|
167
|
+
if (hasStdio) {
|
|
168
|
+
if (definition.command.includes('\0'))
|
|
169
|
+
return '`command` must not contain a NUL character';
|
|
170
|
+
if (definition.args !== undefined) {
|
|
171
|
+
const problem = validateStringArray(definition.args, '`args`');
|
|
172
|
+
if (problem)
|
|
173
|
+
return problem;
|
|
174
|
+
}
|
|
175
|
+
if (definition.env !== undefined && !isStringRecord(definition.env)) {
|
|
176
|
+
return '`env` must be an object containing only string values';
|
|
177
|
+
}
|
|
178
|
+
if (definition.cwd !== undefined && (typeof definition.cwd !== 'string' || definition.cwd.includes('\0'))) {
|
|
179
|
+
return '`cwd` must be a string without NUL characters';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (hasHttp) {
|
|
183
|
+
let parsed;
|
|
184
|
+
try {
|
|
185
|
+
parsed = new URL(definition.url);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return '`url` must be a valid URL';
|
|
189
|
+
}
|
|
190
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
191
|
+
return '`url` must use http or https';
|
|
192
|
+
}
|
|
193
|
+
if (parsed.username || parsed.password)
|
|
194
|
+
return '`url` must not embed credentials; use headers or OAuth';
|
|
195
|
+
if (definition.headers !== undefined && !isStringRecord(definition.headers)) {
|
|
196
|
+
return '`headers` must be an object containing only string values';
|
|
197
|
+
}
|
|
198
|
+
if (isStringRecord(definition.headers)) {
|
|
199
|
+
for (const [key, value] of Object.entries(definition.headers)) {
|
|
200
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(key) || /[\r\n]/.test(value)) {
|
|
201
|
+
return '`headers` contains an invalid name or value';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (definition.timeoutMs !== undefined &&
|
|
207
|
+
(typeof definition.timeoutMs !== 'number' || !Number.isFinite(definition.timeoutMs) || definition.timeoutMs < 1_000 || definition.timeoutMs > MAX_TIMEOUT_MS)) {
|
|
208
|
+
return `\`timeoutMs\` must be between 1000 and ${MAX_TIMEOUT_MS}`;
|
|
209
|
+
}
|
|
210
|
+
if (definition.protocol !== undefined && definition.protocol !== 'legacy' && definition.protocol !== 'auto') {
|
|
211
|
+
return '`protocol` must be "legacy" or "auto"';
|
|
212
|
+
}
|
|
213
|
+
if (definition.enabled !== undefined && typeof definition.enabled !== 'boolean') {
|
|
214
|
+
return '`enabled` must be a boolean';
|
|
215
|
+
}
|
|
216
|
+
if (definition.tools !== undefined) {
|
|
217
|
+
if (!isRecord(definition.tools))
|
|
218
|
+
return '`tools` must be an object';
|
|
219
|
+
for (const key of ['include', 'exclude']) {
|
|
220
|
+
if (definition.tools[key] !== undefined) {
|
|
221
|
+
const problem = validateStringArray(definition.tools[key], `\`tools.${key}\``);
|
|
222
|
+
if (problem)
|
|
223
|
+
return problem;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
async connect() {
|
|
230
|
+
const problem = McpClient.validateDefinition(this.definition);
|
|
231
|
+
if (problem)
|
|
232
|
+
throw new McpConnectionError('failed', `invalid server definition: ${problem}`);
|
|
233
|
+
if (this.definition.enabled === false) {
|
|
234
|
+
this.setStatus({ state: 'disabled' });
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
this.closing = false;
|
|
238
|
+
this.setStatus({ state: 'connecting' });
|
|
239
|
+
const timeout = normalizedTimeout(this.definition.timeoutMs);
|
|
240
|
+
try {
|
|
241
|
+
if (this.definition.url) {
|
|
242
|
+
await this.connectRemote(timeout);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
await this.connectStdio(timeout);
|
|
246
|
+
}
|
|
247
|
+
await this.refreshTools();
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
await this.dispose();
|
|
252
|
+
const state = isAuthenticationError(error) ? 'needs_auth' : 'failed';
|
|
253
|
+
const suffix = this.stderrTail.trim() ? `: ${this.stderrTail.trim().slice(-600)}` : '';
|
|
254
|
+
const message = `${errorMessage(error)}${suffix}`;
|
|
255
|
+
this.setStatus({ state, error: message });
|
|
256
|
+
throw new McpConnectionError(state, message, { cause: error });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async refreshTools() {
|
|
260
|
+
if (!this.client)
|
|
261
|
+
throw new Error('MCP client is not connected');
|
|
262
|
+
if (!this.client.getServerCapabilities()?.tools) {
|
|
263
|
+
this.replaceTools([]);
|
|
264
|
+
return this.tools;
|
|
265
|
+
}
|
|
266
|
+
const result = await this.client.listTools(undefined, {
|
|
267
|
+
timeout: normalizedTimeout(this.definition.timeoutMs),
|
|
268
|
+
cacheMode: 'refresh'
|
|
269
|
+
});
|
|
270
|
+
this.replaceTools(result.tools);
|
|
271
|
+
return this.tools;
|
|
272
|
+
}
|
|
273
|
+
async callTool(toolName, args, signal) {
|
|
274
|
+
if (!this.client || this.status.state !== 'connected') {
|
|
275
|
+
return { success: false, error: `MCP server "${this.name}" is not connected` };
|
|
276
|
+
}
|
|
277
|
+
const toolDefinition = this.tools.find(tool => tool.name === toolName);
|
|
278
|
+
if (!toolDefinition)
|
|
279
|
+
return { success: false, error: `MCP server "${this.name}" does not advertise tool "${toolName}"` };
|
|
280
|
+
try {
|
|
281
|
+
const options = {
|
|
282
|
+
timeout: normalizedTimeout(this.definition.timeoutMs),
|
|
283
|
+
resetTimeoutOnProgress: true,
|
|
284
|
+
maxTotalTimeout: MAX_TIMEOUT_MS,
|
|
285
|
+
onprogress: () => { }
|
|
286
|
+
};
|
|
287
|
+
if (signal)
|
|
288
|
+
options.signal = signal;
|
|
289
|
+
const result = await this.client.callTool({ name: toolName, arguments: args }, options);
|
|
290
|
+
const output = formatToolResult(result);
|
|
291
|
+
if (result.isError)
|
|
292
|
+
return { success: false, error: output || 'the MCP server reported an error' };
|
|
293
|
+
const response = { success: true, output };
|
|
294
|
+
if (result.structuredContent !== undefined)
|
|
295
|
+
response.structuredContent = result.structuredContent;
|
|
296
|
+
return response;
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
return { success: false, error: `MCP server "${this.name}": ${errorMessage(error)}` };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
supportsResources() {
|
|
303
|
+
return Boolean(this.client?.getServerCapabilities()?.resources);
|
|
304
|
+
}
|
|
305
|
+
supportsPrompts() {
|
|
306
|
+
return Boolean(this.client?.getServerCapabilities()?.prompts);
|
|
307
|
+
}
|
|
308
|
+
async listResources(signal) {
|
|
309
|
+
if (!this.client || !this.supportsResources()) {
|
|
310
|
+
return { success: false, error: `MCP server "${this.name}" does not advertise resources` };
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const options = {
|
|
314
|
+
timeout: normalizedTimeout(this.definition.timeoutMs),
|
|
315
|
+
cacheMode: 'refresh'
|
|
316
|
+
};
|
|
317
|
+
if (signal)
|
|
318
|
+
options.signal = signal;
|
|
319
|
+
const [resourcesResult, templatesResult] = await Promise.allSettled([
|
|
320
|
+
this.client.listResources(undefined, options),
|
|
321
|
+
this.client.listResourceTemplates(undefined, options)
|
|
322
|
+
]);
|
|
323
|
+
if (resourcesResult.status === 'rejected' && templatesResult.status === 'rejected') {
|
|
324
|
+
throw resourcesResult.reason;
|
|
325
|
+
}
|
|
326
|
+
const warnings = [];
|
|
327
|
+
if (resourcesResult.status === 'rejected')
|
|
328
|
+
warnings.push(`resources/list: ${errorMessage(resourcesResult.reason)}`);
|
|
329
|
+
if (templatesResult.status === 'rejected')
|
|
330
|
+
warnings.push(`resources/templates/list: ${errorMessage(templatesResult.reason)}`);
|
|
331
|
+
const payload = {
|
|
332
|
+
resources: resourcesResult.status === 'fulfilled' ? resourcesResult.value.resources : [],
|
|
333
|
+
resourceTemplates: templatesResult.status === 'fulfilled' ? templatesResult.value.resourceTemplates : []
|
|
334
|
+
};
|
|
335
|
+
if (warnings.length > 0)
|
|
336
|
+
payload.warnings = warnings;
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
output: boundedOutput(JSON.stringify(payload, null, 2))
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
return { success: false, error: `MCP server "${this.name}": ${errorMessage(error)}` };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async readResource(uri, signal) {
|
|
347
|
+
if (!this.client || !this.supportsResources()) {
|
|
348
|
+
return { success: false, error: `MCP server "${this.name}" does not advertise resources` };
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const options = {
|
|
352
|
+
timeout: normalizedTimeout(this.definition.timeoutMs),
|
|
353
|
+
cacheMode: 'refresh'
|
|
354
|
+
};
|
|
355
|
+
if (signal)
|
|
356
|
+
options.signal = signal;
|
|
357
|
+
const result = await this.client.readResource({ uri }, options);
|
|
358
|
+
return { success: true, output: formatResourceContents(result.contents) };
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
return { success: false, error: `MCP server "${this.name}": ${errorMessage(error)}` };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async listPrompts(signal) {
|
|
365
|
+
if (!this.client || !this.supportsPrompts()) {
|
|
366
|
+
return { success: false, error: `MCP server "${this.name}" does not advertise prompts` };
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const options = {
|
|
370
|
+
timeout: normalizedTimeout(this.definition.timeoutMs),
|
|
371
|
+
cacheMode: 'refresh'
|
|
372
|
+
};
|
|
373
|
+
if (signal)
|
|
374
|
+
options.signal = signal;
|
|
375
|
+
const result = await this.client.listPrompts(undefined, options);
|
|
376
|
+
return { success: true, output: boundedOutput(JSON.stringify(result.prompts, null, 2)) };
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
return { success: false, error: `MCP server "${this.name}": ${errorMessage(error)}` };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async getPrompt(name, args, signal) {
|
|
383
|
+
if (!this.client || !this.supportsPrompts()) {
|
|
384
|
+
return { success: false, error: `MCP server "${this.name}" does not advertise prompts` };
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const options = { timeout: normalizedTimeout(this.definition.timeoutMs) };
|
|
388
|
+
if (signal)
|
|
389
|
+
options.signal = signal;
|
|
390
|
+
const result = await this.client.getPrompt({ name, arguments: args }, options);
|
|
391
|
+
const text = formatPromptMessages(result.messages);
|
|
392
|
+
return { success: true, output: result.description ? `${result.description}\n\n${text}` : text };
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
return { success: false, error: `MCP server "${this.name}": ${errorMessage(error)}` };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async close() {
|
|
399
|
+
this.closing = true;
|
|
400
|
+
await this.dispose();
|
|
401
|
+
this.tools = [];
|
|
402
|
+
this.setStatus({ state: 'disconnected' });
|
|
403
|
+
}
|
|
404
|
+
makeClient() {
|
|
405
|
+
const client = new Client({ name: 'causyne-agent', version: getPackageVersion() }, {
|
|
406
|
+
capabilities: { roots: {} },
|
|
407
|
+
versionNegotiation: { mode: this.definition.protocol ?? 'legacy' },
|
|
408
|
+
listMaxPages: 64,
|
|
409
|
+
listChanged: {
|
|
410
|
+
tools: {
|
|
411
|
+
onChanged: (error, result) => {
|
|
412
|
+
if (error) {
|
|
413
|
+
this.setStatus({ state: 'failed', error: `tool refresh failed: ${error.message}` });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
this.replaceTools(result ?? []);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
client.setRequestHandler('roots/list', async () => ({
|
|
422
|
+
roots: [{ uri: pathToFileURL(this.workspace).href, name: path.basename(this.workspace) || this.workspace }]
|
|
423
|
+
}));
|
|
424
|
+
client.onclose = () => {
|
|
425
|
+
if (!this.closing)
|
|
426
|
+
this.setStatus({ state: 'failed', error: 'connection closed by MCP server' });
|
|
427
|
+
};
|
|
428
|
+
client.onerror = error => {
|
|
429
|
+
if (!this.closing)
|
|
430
|
+
this.setStatus({ state: 'failed', error: error.message });
|
|
431
|
+
};
|
|
432
|
+
return client;
|
|
433
|
+
}
|
|
434
|
+
async connectStdio(timeout) {
|
|
435
|
+
const definition = this.definition;
|
|
436
|
+
if (!definition.command)
|
|
437
|
+
throw new Error('stdio command is missing');
|
|
438
|
+
const cwd = definition.cwd
|
|
439
|
+
? path.resolve(this.workspace, definition.cwd)
|
|
440
|
+
: this.workspace;
|
|
441
|
+
const transport = new StdioClientTransport({
|
|
442
|
+
command: definition.command,
|
|
443
|
+
args: definition.args ?? [],
|
|
444
|
+
env: { ...getDefaultEnvironment(), ...(definition.env ?? {}) },
|
|
445
|
+
cwd,
|
|
446
|
+
stderr: 'pipe'
|
|
447
|
+
});
|
|
448
|
+
transport.stderr?.on('data', chunk => {
|
|
449
|
+
this.stderrTail = (this.stderrTail + String(chunk)).slice(-STDERR_LIMIT_BYTES);
|
|
450
|
+
});
|
|
451
|
+
const client = this.makeClient();
|
|
452
|
+
this.transport = transport;
|
|
453
|
+
this.client = client;
|
|
454
|
+
await client.connect(transport, { timeout });
|
|
455
|
+
this.captureServer(client, 'stdio');
|
|
456
|
+
}
|
|
457
|
+
async connectRemote(timeout) {
|
|
458
|
+
const definition = this.definition;
|
|
459
|
+
if (!definition.url)
|
|
460
|
+
throw new Error('HTTP URL is missing');
|
|
461
|
+
const url = new URL(definition.url);
|
|
462
|
+
const requestInit = definition.headers ? { headers: definition.headers } : undefined;
|
|
463
|
+
try {
|
|
464
|
+
const transport = new StreamableHTTPClientTransport(url, requestInit ? { requestInit } : undefined);
|
|
465
|
+
const client = this.makeClient();
|
|
466
|
+
this.transport = transport;
|
|
467
|
+
this.client = client;
|
|
468
|
+
await client.connect(transport, { timeout });
|
|
469
|
+
this.captureServer(client, 'streamable-http');
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
if (isAuthenticationError(error))
|
|
473
|
+
throw error;
|
|
474
|
+
await this.dispose();
|
|
475
|
+
const transport = new SSEClientTransport(url, requestInit ? { requestInit } : undefined);
|
|
476
|
+
const client = this.makeClient();
|
|
477
|
+
this.transport = transport;
|
|
478
|
+
this.client = client;
|
|
479
|
+
await client.connect(transport, { timeout });
|
|
480
|
+
this.captureServer(client, 'sse');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
captureServer(client, transport) {
|
|
484
|
+
const server = client.getServerVersion();
|
|
485
|
+
this.serverInfo = server ? { name: server.name, version: server.version } : null;
|
|
486
|
+
this.instructions = client.getInstructions()?.trim() || null;
|
|
487
|
+
const status = { state: 'connected', connectedAt: Date.now() };
|
|
488
|
+
if (transport)
|
|
489
|
+
status.transport = transport;
|
|
490
|
+
this.setStatus(status);
|
|
491
|
+
}
|
|
492
|
+
replaceTools(tools) {
|
|
493
|
+
this.tools = tools.map(normalizeTool).filter((tool) => tool !== null);
|
|
494
|
+
this.onToolsChanged?.([...this.tools]);
|
|
495
|
+
}
|
|
496
|
+
setStatus(status) {
|
|
497
|
+
this.status = status;
|
|
498
|
+
this.onStatus?.({ ...status });
|
|
499
|
+
}
|
|
500
|
+
async dispose() {
|
|
501
|
+
const client = this.client;
|
|
502
|
+
const transport = this.transport;
|
|
503
|
+
this.client = null;
|
|
504
|
+
this.transport = null;
|
|
505
|
+
if (transport instanceof StreamableHTTPClientTransport) {
|
|
506
|
+
try {
|
|
507
|
+
await transport.terminateSession();
|
|
508
|
+
}
|
|
509
|
+
catch { }
|
|
510
|
+
}
|
|
511
|
+
if (client) {
|
|
512
|
+
try {
|
|
513
|
+
await client.close();
|
|
514
|
+
}
|
|
515
|
+
catch { }
|
|
516
|
+
}
|
|
517
|
+
else if (transport) {
|
|
518
|
+
try {
|
|
519
|
+
await transport.close();
|
|
520
|
+
}
|
|
521
|
+
catch { }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
136
524
|
}
|