salmon-loop 0.2.16 → 0.3.1

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.
Files changed (98) hide show
  1. package/dist/cli/authorization/non-interactive.js +7 -21
  2. package/dist/cli/commands/chat.js +1 -1
  3. package/dist/cli/commands/parallel.js +46 -41
  4. package/dist/cli/commands/run/assistant-message.js +3 -0
  5. package/dist/cli/commands/run/handler.js +2 -1
  6. package/dist/cli/commands/serve.js +112 -156
  7. package/dist/cli/headless/json-protocol.js +1 -1
  8. package/dist/cli/headless/stream-json-protocol.js +3 -2
  9. package/dist/cli/program-bootstrap.js +2 -2
  10. package/dist/cli/slash/runtime.js +5 -1
  11. package/dist/core/adapters/fs/node-fs.js +1 -0
  12. package/dist/core/backends/salmon-loop/task-executor.js +1 -0
  13. package/dist/core/benchmark/patch-artifact.js +1 -1
  14. package/dist/core/context/service.js +5 -2
  15. package/dist/core/extensions/index.js +2 -35
  16. package/dist/core/extensions/merge.js +14 -0
  17. package/dist/core/extensions/redact.js +9 -3
  18. package/dist/core/extensions/schemas.js +2 -51
  19. package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
  20. package/dist/core/facades/cli-program-bootstrap.js +1 -0
  21. package/dist/core/facades/cli-serve.js +2 -1
  22. package/dist/core/grizzco/dsl/strategies.js +1 -3
  23. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
  24. package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
  25. package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
  26. package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
  27. package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
  28. package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
  29. package/dist/core/grizzco/steps/apply.js +0 -7
  30. package/dist/core/grizzco/steps/autopilot.js +108 -6
  31. package/dist/core/grizzco/steps/preflight.js +10 -0
  32. package/dist/core/grizzco/steps/tool-runtime.js +1 -0
  33. package/dist/core/interaction/events/bus.js +14 -0
  34. package/dist/core/interaction/orchestration/facade.js +11 -1
  35. package/dist/core/llm/ai-sdk/request-params.js +40 -1
  36. package/dist/core/mcp/bridge/index.js +4 -0
  37. package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
  38. package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
  39. package/dist/core/mcp/bridge/tool-bridge.js +303 -0
  40. package/dist/core/mcp/cache/resource-cache.js +41 -0
  41. package/dist/core/mcp/catalog/discovery.js +51 -0
  42. package/dist/core/mcp/catalog/notification-router.js +28 -0
  43. package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
  44. package/dist/core/mcp/catalog/resource-catalog.js +7 -0
  45. package/dist/core/mcp/catalog/tool-catalog.js +4 -0
  46. package/dist/core/mcp/client/connection-manager.js +239 -0
  47. package/dist/core/mcp/client/lifecycle.js +13 -0
  48. package/dist/core/mcp/client/transport-factory.js +168 -0
  49. package/dist/core/mcp/config/index.js +32 -0
  50. package/dist/core/mcp/config/schema-v2.js +129 -0
  51. package/dist/core/mcp/host/elicitation-provider.js +209 -0
  52. package/dist/core/mcp/host/roots-provider.js +70 -0
  53. package/dist/core/mcp/host/sampling-provider.js +170 -0
  54. package/dist/core/mcp/index.js +4 -0
  55. package/dist/core/mcp/observability/events.js +19 -0
  56. package/dist/core/mcp/policy/approval-policy.js +2 -0
  57. package/dist/core/mcp/policy/classifier.js +172 -0
  58. package/dist/core/mcp/policy/grants.js +356 -0
  59. package/dist/core/mcp/policy/uri-policy.js +60 -0
  60. package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
  61. package/dist/core/mcp/types.js +2 -0
  62. package/dist/core/protocols/a2a/agent-card.js +38 -12
  63. package/dist/core/protocols/a2a/sdk/executor.js +105 -36
  64. package/dist/core/protocols/a2a/sdk/server.js +1311 -3
  65. package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
  66. package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
  67. package/dist/core/protocols/acp/acp-types.js +17 -0
  68. package/dist/core/protocols/acp/formal-agent.js +389 -502
  69. package/dist/core/protocols/acp/handlers.js +3 -0
  70. package/dist/core/protocols/acp/permission-provider.js +11 -39
  71. package/dist/core/protocols/acp/stdio-server.js +20 -1
  72. package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
  73. package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
  74. package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
  75. package/dist/core/public-capabilities/projections.js +1 -0
  76. package/dist/core/runtime/agent-server-runtime.js +2 -3
  77. package/dist/core/runtime/spawn-command.js +8 -2
  78. package/dist/core/runtime/spawn-interactive.js +26 -0
  79. package/dist/core/session/manager.js +48 -25
  80. package/dist/core/tools/builtin/index.js +6 -1
  81. package/dist/core/tools/builtin/proposal.js +0 -7
  82. package/dist/core/tools/builtin/workspace.js +76 -0
  83. package/dist/core/tools/dispatcher.js +1 -0
  84. package/dist/core/tools/loader.js +92 -46
  85. package/dist/core/verification/runner.js +60 -31
  86. package/dist/core/version.js +17 -0
  87. package/dist/core/workspace/capabilities.js +80 -0
  88. package/dist/locales/en.js +17 -3
  89. package/package.json +4 -2
  90. package/dist/core/protocols/a2a/mapper.js +0 -14
  91. package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
  92. package/dist/core/protocols/a2a/task-projection.js +0 -45
  93. package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
  94. package/dist/core/tools/mcp/client.js +0 -308
  95. package/dist/core/tools/mcp/loader.js +0 -110
  96. package/dist/core/tools/mcp/schema.js +0 -54
  97. package/dist/core/tools/mcp/streamable-http.js +0 -101
  98. package/dist/core/tools/mcp/types.js +0 -26
@@ -0,0 +1,303 @@
1
+ import { z } from 'zod';
2
+ import { LIMITS } from '../../config/limits.js';
3
+ import { Phase } from '../../types/runtime.js';
4
+ import { classifyMcpTool } from '../policy/classifier.js';
5
+ import { jsonSchemaToZod } from '../schema/json-schema-to-zod.js';
6
+ const MCP_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
7
+ const FALLBACK_SIDE_EFFECTS = ['process', 'network'];
8
+ const FALLBACK_ALLOWED_PHASES = [Phase.VERIFY];
9
+ const mcpContentItemSchema = z.object({}).catchall(z.unknown());
10
+ const mcpCallResultSchema = z
11
+ .object({
12
+ content: z.array(mcpContentItemSchema).default([]),
13
+ structuredContent: z.unknown().optional(),
14
+ isError: z.boolean().optional(),
15
+ _meta: z.record(z.string(), z.unknown()).optional(),
16
+ })
17
+ .passthrough();
18
+ const bridgedMcpToolOutputObjectSchema = z
19
+ .object({
20
+ content: z.array(mcpContentItemSchema),
21
+ structuredContent: z.unknown().optional(),
22
+ resourceLinks: z.array(mcpContentItemSchema),
23
+ raw: mcpCallResultSchema,
24
+ isError: z.boolean().optional(),
25
+ _meta: z.record(z.string(), z.unknown()).optional(),
26
+ })
27
+ .passthrough();
28
+ export const bridgedMcpToolOutputSchema = bridgedMcpToolOutputObjectSchema;
29
+ export function mcpToolDescriptorToToolSpec(input) {
30
+ assertMcpName(input.serverName, 'server');
31
+ assertMcpName(input.descriptor.name, 'tool');
32
+ const classification = normalizeClassifierInput(input.classification);
33
+ const sideEffects = resolveSideEffects(classification);
34
+ const riskLevel = input.grant.riskLevel ??
35
+ classification.riskLevel ??
36
+ inferRiskLevel(sideEffects, classification.kind);
37
+ const concurrency = input.grant.concurrency ??
38
+ classification.concurrency ??
39
+ inferConcurrency(sideEffects, riskLevel);
40
+ const allowedPhases = input.grant.allowedPhases.length > 0 ? input.grant.allowedPhases : FALLBACK_ALLOWED_PHASES;
41
+ const mcpOutputSchema = input.descriptor.outputSchema
42
+ ? jsonSchemaToZod(input.descriptor.outputSchema).optional()
43
+ : z.any().optional();
44
+ return {
45
+ name: toModelToolName(input.serverName, input.descriptor.name),
46
+ source: 'mcp',
47
+ intent: input.grant.intent ?? classification.intent ?? inferIntent(sideEffects),
48
+ description: input.descriptor.description ?? input.descriptor.title ?? `MCP tool ${input.descriptor.name}`,
49
+ riskLevel,
50
+ sideEffects,
51
+ concurrency,
52
+ allowedPhases,
53
+ defaultTimeoutMs: input.grant.defaultTimeoutMs ?? LIMITS.defaultToolTimeoutMs,
54
+ inputSchema: jsonSchemaToZod(input.descriptor.inputSchema),
55
+ outputSchema: bridgedMcpToolOutputObjectSchema.extend({
56
+ structuredContent: mcpOutputSchema,
57
+ }),
58
+ summarizeArgsForAuthorization: async (args) => summarizeMcpAuthorization({ ...input, classification }, args, riskLevel, sideEffects),
59
+ executor: async (rawInput, ctx) => {
60
+ const args = coerceRecord(rawInput);
61
+ const result = await input.manager.callTool(input.serverName, input.descriptor.name, args, {
62
+ signal: ctx.signal,
63
+ });
64
+ return wrapMcpToolResult(result);
65
+ },
66
+ };
67
+ }
68
+ export function mcpClassifierResultToBridgeClassification(classification) {
69
+ if ('sideEffects' in classification) {
70
+ return {
71
+ kind: 'classified',
72
+ sideEffects: classification.sideEffects,
73
+ riskLevel: classification.riskLevel,
74
+ concurrency: inferConcurrency(classification.sideEffects, classification.riskLevel),
75
+ intent: inferIntent(classification.sideEffects),
76
+ };
77
+ }
78
+ const sideEffects = sideEffectsFromFacetClassifier(classification.facets);
79
+ return {
80
+ kind: 'classified',
81
+ sideEffects,
82
+ riskLevel: classification.risk,
83
+ concurrency: inferConcurrency(sideEffects, classification.risk),
84
+ intent: inferIntent(sideEffects),
85
+ reason: classification.reasons.join('; '),
86
+ };
87
+ }
88
+ export function wrapMcpToolResult(result) {
89
+ const normalized = unwrapMcpCallResult(result);
90
+ const content = Array.isArray(normalized.content) ? normalized.content : [];
91
+ const directResourceLinks = Array.isArray(result.resourceLinks) ? result.resourceLinks : [];
92
+ const resourceLinks = [...directResourceLinks, ...content.filter(isResourceLink)];
93
+ const output = {
94
+ content,
95
+ resourceLinks,
96
+ raw: {
97
+ ...normalized,
98
+ content,
99
+ },
100
+ };
101
+ if (normalized.structuredContent !== undefined) {
102
+ output.structuredContent = normalized.structuredContent;
103
+ }
104
+ if (normalized.isError !== undefined) {
105
+ output.isError = normalized.isError;
106
+ }
107
+ if (normalized._meta !== undefined) {
108
+ output._meta = normalized._meta;
109
+ }
110
+ return output;
111
+ }
112
+ export function mcpToolToToolSpec(input) {
113
+ const override = findOverride(input.server.capabilities, input.tool.name);
114
+ const classification = classifyMcpTool({
115
+ tool: input.tool,
116
+ trust: input.server.trust,
117
+ override,
118
+ });
119
+ const phase = input.server.capabilities.tools.phases[0] ?? Phase.VERIFY;
120
+ const grantDecision = input.policy.decideTool({
121
+ server: input.server.name,
122
+ toolName: input.tool.name,
123
+ phase,
124
+ classification,
125
+ });
126
+ const toolGrant = grantDecision.grant?.kind === 'tool'
127
+ ? grantDecision.grant
128
+ : {
129
+ phases: input.server.capabilities.tools.phases,
130
+ approval: input.server.capabilities.tools.approval,
131
+ };
132
+ const grant = {
133
+ allowedPhases: toolGrant.phases,
134
+ riskLevel: classification.riskLevel,
135
+ metadata: {
136
+ approval: toolGrant.approval,
137
+ policy: grantDecision.grant,
138
+ },
139
+ };
140
+ const spec = mcpToolDescriptorToToolSpec({
141
+ serverName: input.server.name,
142
+ descriptor: input.tool,
143
+ grant,
144
+ classification,
145
+ manager: input.manager,
146
+ });
147
+ return {
148
+ ...spec,
149
+ executor: async (args, ctx) => {
150
+ const runtimePhase = ctx.phase ?? phase;
151
+ const decision = input.policy.decideTool({
152
+ server: input.server.name,
153
+ toolName: input.tool.name,
154
+ phase: runtimePhase,
155
+ classification,
156
+ });
157
+ if (decision.outcome === 'deny')
158
+ throw new Error(decision.denyReason ?? 'MCP_TOOL_DENIED');
159
+ return spec.executor(args, ctx);
160
+ },
161
+ };
162
+ }
163
+ export async function registerMcpV2Tools(input) {
164
+ await input.manager.startAll();
165
+ for (const server of input.servers) {
166
+ if (!server.enabled || !server.capabilities.tools.exposeToModel)
167
+ continue;
168
+ const catalog = input.manager.getCatalog(server.name);
169
+ if (!catalog)
170
+ continue;
171
+ for (const tool of catalog.tools) {
172
+ const classification = classifyMcpTool({ tool: tool, trust: server.trust });
173
+ const decision = input.policy.decideTool({
174
+ server: server.name,
175
+ toolName: tool.name,
176
+ phase: server.capabilities.tools.phases[0] ?? Phase.VERIFY,
177
+ classification,
178
+ });
179
+ if (decision.outcome === 'deny')
180
+ continue;
181
+ input.registry.register(mcpToolToToolSpec({
182
+ server,
183
+ tool,
184
+ manager: input.manager,
185
+ policy: input.policy,
186
+ }));
187
+ }
188
+ }
189
+ }
190
+ function normalizeClassifierInput(classification) {
191
+ if ('kind' in classification) {
192
+ return classification;
193
+ }
194
+ return mcpClassifierResultToBridgeClassification(classification);
195
+ }
196
+ function sideEffectsFromFacetClassifier(facets) {
197
+ const effects = [];
198
+ if (facets.read)
199
+ effects.push('fs_read');
200
+ if (facets.write)
201
+ effects.push('fs_write');
202
+ if (facets.process)
203
+ effects.push('process');
204
+ if (facets.network)
205
+ effects.push('network');
206
+ return effects.length > 0 ? effects : ['none'];
207
+ }
208
+ function assertMcpName(value, label) {
209
+ if (!MCP_NAME_PATTERN.test(value)) {
210
+ throw new Error(`Invalid MCP ${label} name: ${value}`);
211
+ }
212
+ }
213
+ function toModelToolName(serverName, toolName) {
214
+ return `mcp.${serverName}.${toolName}`;
215
+ }
216
+ function findOverride(capabilities, toolName) {
217
+ return capabilities.tools.sideEffectOverrides?.[toolName];
218
+ }
219
+ function resolveSideEffects(classification) {
220
+ const effects = classification.kind === 'fallback'
221
+ ? classification.sideEffects && classification.sideEffects.length > 0
222
+ ? classification.sideEffects
223
+ : FALLBACK_SIDE_EFFECTS
224
+ : classification.sideEffects;
225
+ return effects.length > 0 ? dedupeSideEffects(effects) : ['none'];
226
+ }
227
+ function dedupeSideEffects(sideEffects) {
228
+ return Array.from(new Set(sideEffects));
229
+ }
230
+ function inferRiskLevel(sideEffects, classificationKind) {
231
+ if (classificationKind === 'fallback')
232
+ return 'high';
233
+ if (sideEffects.some((effect) => ['fs_write', 'git_write', 'runtime_write', 'snapshot_mutate', 'process', 'network'].includes(effect))) {
234
+ return 'high';
235
+ }
236
+ return 'low';
237
+ }
238
+ function inferConcurrency(sideEffects, riskLevel) {
239
+ if (riskLevel === 'high')
240
+ return 'serial_only';
241
+ if (sideEffects.every((effect) => effect === 'none' || effect === 'fs_read' || effect === 'git_read')) {
242
+ return 'parallel_ok';
243
+ }
244
+ return 'serial_only';
245
+ }
246
+ function inferIntent(sideEffects) {
247
+ if (sideEffects.includes('fs_write') || sideEffects.includes('git_write'))
248
+ return 'WRITE';
249
+ if (sideEffects.includes('fs_read') || sideEffects.includes('git_read'))
250
+ return 'READ';
251
+ return 'INFRA';
252
+ }
253
+ function summarizeMcpAuthorization(input, args, riskLevel, sideEffects) {
254
+ const payload = {
255
+ server: input.serverName,
256
+ tool: input.descriptor.name,
257
+ riskLevel,
258
+ sideEffects,
259
+ grant: {
260
+ allowedPhases: input.grant.allowedPhases,
261
+ grantedBy: input.grant.grantedBy,
262
+ grantedAt: input.grant.grantedAt,
263
+ metadata: input.grant.metadata,
264
+ },
265
+ classification: input.classification.kind === 'fallback'
266
+ ? { kind: 'fallback', reason: input.classification.reason }
267
+ : { kind: 'classified', reason: input.classification.reason },
268
+ args,
269
+ };
270
+ return safeStringify(payload);
271
+ }
272
+ function safeStringify(value, maxLength = 1200) {
273
+ try {
274
+ const raw = JSON.stringify(value);
275
+ return raw.length <= maxLength ? raw : `${raw.slice(0, maxLength)}...`;
276
+ }
277
+ catch {
278
+ return '[Unserializable]';
279
+ }
280
+ }
281
+ function coerceRecord(input) {
282
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
283
+ return {};
284
+ }
285
+ return input;
286
+ }
287
+ function unwrapMcpCallResult(result) {
288
+ if (result.result && typeof result.result === 'object' && !Array.isArray(result.result)) {
289
+ return {
290
+ ...result.result,
291
+ structuredContent: result.structuredContent ?? result.result.structuredContent,
292
+ resourceLinks: result.resourceLinks,
293
+ };
294
+ }
295
+ return result;
296
+ }
297
+ function isResourceLink(item) {
298
+ return Boolean(item &&
299
+ typeof item === 'object' &&
300
+ item.type === 'resource_link' &&
301
+ typeof item.uri === 'string');
302
+ }
303
+ //# sourceMappingURL=tool-bridge.js.map
@@ -0,0 +1,41 @@
1
+ export class ResourceCache {
2
+ ttlMs;
3
+ maxEntries;
4
+ now;
5
+ entries = new Map();
6
+ constructor(options = {}) {
7
+ this.ttlMs = options.ttlMs ?? 30_000;
8
+ this.maxEntries = options.maxEntries ?? 256;
9
+ this.now = options.now ?? (() => Date.now());
10
+ }
11
+ get(key) {
12
+ const entry = this.entries.get(key);
13
+ if (!entry)
14
+ return undefined;
15
+ if (entry.expiresAt <= this.now()) {
16
+ this.entries.delete(key);
17
+ return undefined;
18
+ }
19
+ return entry.value;
20
+ }
21
+ set(key, value, ttlMs = this.ttlMs) {
22
+ if (this.entries.size >= this.maxEntries) {
23
+ const firstKey = this.entries.keys().next().value;
24
+ if (firstKey)
25
+ this.entries.delete(firstKey);
26
+ }
27
+ this.entries.set(key, { value, expiresAt: this.now() + ttlMs });
28
+ }
29
+ clear() {
30
+ this.entries.clear();
31
+ }
32
+ deleteMatching(predicate) {
33
+ for (const key of this.entries.keys()) {
34
+ if (predicate(key))
35
+ this.entries.delete(key);
36
+ }
37
+ }
38
+ }
39
+ export class McpResourceCache extends ResourceCache {
40
+ }
41
+ //# sourceMappingURL=resource-cache.js.map
@@ -0,0 +1,51 @@
1
+ import { withPromptServer } from './prompt-catalog.js';
2
+ import { withResourceServer, withResourceTemplateServer } from './resource-catalog.js';
3
+ import { withToolServer } from './tool-catalog.js';
4
+ async function safeList(fn, fallback) {
5
+ try {
6
+ return await fn();
7
+ }
8
+ catch {
9
+ return fallback;
10
+ }
11
+ }
12
+ export async function discoverMcpCatalog(params) {
13
+ const capabilities = params.client.getServerCapabilities();
14
+ const [toolsResult, resourcesResult, templatesResult, promptsResult] = await Promise.all([
15
+ capabilities?.tools
16
+ ? safeList(() => listAllPages((cursor) => params.client.listTools(cursor)), [])
17
+ : [],
18
+ capabilities?.resources
19
+ ? safeList(() => listAllPages((cursor) => params.client.listResources(cursor)), [])
20
+ : [],
21
+ capabilities?.resources
22
+ ? safeList(() => listAllPages((cursor) => params.client.listResourceTemplates(cursor)), [])
23
+ : [],
24
+ capabilities?.prompts
25
+ ? safeList(() => listAllPages((cursor) => params.client.listPrompts(cursor)), [])
26
+ : [],
27
+ ]);
28
+ return {
29
+ serverName: params.server.name,
30
+ capabilities,
31
+ tools: withToolServer(params.server.name, toolsResult),
32
+ resources: withResourceServer(params.server.name, resourcesResult),
33
+ resourceTemplates: withResourceTemplateServer(params.server.name, templatesResult),
34
+ prompts: withPromptServer(params.server.name, promptsResult),
35
+ refreshedAt: new Date().toISOString(),
36
+ stale: false,
37
+ };
38
+ }
39
+ async function listAllPages(fetchPage) {
40
+ const items = [];
41
+ let cursor;
42
+ do {
43
+ const page = await fetchPage(cursor ? { cursor } : undefined);
44
+ const values = page.tools ?? page.resources ?? page.resourceTemplates ?? page.prompts;
45
+ if (Array.isArray(values))
46
+ items.push(...values);
47
+ cursor = typeof page.nextCursor === 'string' ? page.nextCursor : undefined;
48
+ } while (cursor);
49
+ return items;
50
+ }
51
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1,28 @@
1
+ export class McpNotificationRouter {
2
+ handlers = [];
3
+ onInvalidate(handler) {
4
+ this.handlers.push(handler);
5
+ }
6
+ async invalidate(event) {
7
+ for (const handler of this.handlers) {
8
+ await handler(event);
9
+ }
10
+ }
11
+ async route(input) {
12
+ const kind = this.kindForMethod(input.method);
13
+ if (!kind)
14
+ return false;
15
+ await this.invalidate({ serverName: input.serverName, kind });
16
+ return true;
17
+ }
18
+ kindForMethod(method) {
19
+ if (method === 'notifications/tools/list_changed')
20
+ return 'tools';
21
+ if (method === 'notifications/resources/list_changed')
22
+ return 'resources';
23
+ if (method === 'notifications/prompts/list_changed')
24
+ return 'prompts';
25
+ return null;
26
+ }
27
+ }
28
+ //# sourceMappingURL=notification-router.js.map
@@ -0,0 +1,4 @@
1
+ export function withPromptServer(serverName, prompts) {
2
+ return prompts.map((prompt) => ({ ...prompt, serverName }));
3
+ }
4
+ //# sourceMappingURL=prompt-catalog.js.map
@@ -0,0 +1,7 @@
1
+ export function withResourceServer(serverName, resources) {
2
+ return resources.map((resource) => ({ ...resource, serverName }));
3
+ }
4
+ export function withResourceTemplateServer(serverName, templates) {
5
+ return templates.map((template) => ({ ...template, serverName }));
6
+ }
7
+ //# sourceMappingURL=resource-catalog.js.map
@@ -0,0 +1,4 @@
1
+ export function withToolServer(serverName, tools) {
2
+ return tools.map((tool) => ({ ...tool, serverName }));
3
+ }
4
+ //# sourceMappingURL=tool-catalog.js.map
@@ -0,0 +1,239 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { CallToolResultSchema, PromptListChangedNotificationSchema, ReadResourceResultSchema, ResourceListChangedNotificationSchema, ResourceUpdatedNotificationSchema, ToolListChangedNotificationSchema, GetPromptResultSchema, } from '@modelcontextprotocol/sdk/types.js';
3
+ import { LIMITS } from '../../config/limits.js';
4
+ import { getLogger } from '../../observability/logger.js';
5
+ import { PACKAGE_VERSION } from '../../version.js';
6
+ import { discoverMcpCatalog } from '../catalog/discovery.js';
7
+ import { McpNotificationRouter } from '../catalog/notification-router.js';
8
+ import { createMcpTransport } from './transport-factory.js';
9
+ function buildClientCapabilities(input) {
10
+ const capabilities = {
11
+ roots: input?.roots ? { listChanged: true } : undefined,
12
+ sampling: input?.sampling ? {} : undefined,
13
+ elicitation: input?.elicitation ? { form: {} } : undefined,
14
+ };
15
+ return capabilities;
16
+ }
17
+ export class McpConnectionManager {
18
+ servers;
19
+ clientCapabilities;
20
+ connections = new Map();
21
+ resourceUpdateHandlers = [];
22
+ notifications = new McpNotificationRouter();
23
+ constructor(servers, clientCapabilities) {
24
+ this.servers = servers;
25
+ this.clientCapabilities = clientCapabilities;
26
+ this.notifications.onInvalidate((event) => {
27
+ this.markStale(event.serverName, event.kind);
28
+ });
29
+ }
30
+ async startAll() {
31
+ for (const server of this.servers) {
32
+ if (!server.enabled)
33
+ continue;
34
+ await this.connect(server);
35
+ }
36
+ }
37
+ async connect(server) {
38
+ const existing = this.connections.get(server.name);
39
+ if (existing && existing.status === 'ready')
40
+ return this.view(existing);
41
+ const transport = createMcpTransport(server);
42
+ const client = new Client({ name: 'salmon-loop', version: PACKAGE_VERSION }, { capabilities: buildClientCapabilities(this.clientCapabilities) });
43
+ const entry = {
44
+ server,
45
+ client,
46
+ transport,
47
+ status: 'connecting',
48
+ staleKinds: new Set(),
49
+ subscribedResources: new Set(),
50
+ };
51
+ this.connections.set(server.name, entry);
52
+ client.onerror = (error) => {
53
+ entry.status = 'degraded';
54
+ entry.error = error.message;
55
+ this.markStale(server.name, 'tools');
56
+ this.markStale(server.name, 'resources');
57
+ this.markStale(server.name, 'prompts');
58
+ getLogger().warn(`MCP server ${server.name} degraded: ${error.message}`);
59
+ };
60
+ client.onclose = () => {
61
+ if (entry.status === 'closed')
62
+ return;
63
+ entry.status = 'degraded';
64
+ entry.error = `MCP server ${server.name} connection closed`;
65
+ this.markStale(server.name, 'tools');
66
+ this.markStale(server.name, 'resources');
67
+ this.markStale(server.name, 'prompts');
68
+ };
69
+ client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
70
+ await this.notifications.invalidate({ serverName: server.name, kind: 'tools' });
71
+ });
72
+ client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
73
+ await this.notifications.invalidate({ serverName: server.name, kind: 'resources' });
74
+ });
75
+ client.setNotificationHandler(ResourceUpdatedNotificationSchema, async (notification) => {
76
+ const uri = notification.params?.uri;
77
+ if (typeof uri !== 'string')
78
+ return;
79
+ await this.emitResourceUpdated({ serverName: server.name, uri });
80
+ });
81
+ client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
82
+ await this.notifications.invalidate({ serverName: server.name, kind: 'prompts' });
83
+ });
84
+ client.fallbackNotificationHandler = async (notification) => {
85
+ await this.notifications.route({ serverName: server.name, method: notification.method });
86
+ };
87
+ try {
88
+ await client.connect(transport, { timeout: LIMITS.defaultToolTimeoutMs });
89
+ entry.catalog = await discoverMcpCatalog({ server, client });
90
+ entry.status = 'ready';
91
+ entry.error = undefined;
92
+ entry.staleKinds.clear();
93
+ }
94
+ catch (error) {
95
+ entry.status = 'degraded';
96
+ entry.error = error instanceof Error ? error.message : String(error);
97
+ getLogger().warn(`Failed to connect MCP server ${server.name}: ${entry.error}`);
98
+ }
99
+ return this.view(entry);
100
+ }
101
+ getCatalog(serverName) {
102
+ return this.connections.get(serverName)?.catalog;
103
+ }
104
+ listCatalogs() {
105
+ return Array.from(this.connections.values())
106
+ .map((entry) => entry.catalog)
107
+ .filter((catalog) => Boolean(catalog));
108
+ }
109
+ async refreshCatalog(serverName) {
110
+ const entry = this.requireReady(serverName);
111
+ entry.catalog = await discoverMcpCatalog({ server: entry.server, client: entry.client });
112
+ entry.staleKinds.clear();
113
+ return entry.catalog;
114
+ }
115
+ async callTool(serverName, toolName, args, options) {
116
+ const entry = this.requireReady(serverName);
117
+ return entry.client.callTool({ name: toolName, arguments: args }, CallToolResultSchema, {
118
+ timeout: LIMITS.defaultToolTimeoutMs,
119
+ signal: options?.signal,
120
+ });
121
+ }
122
+ async readResource(serverName, uri) {
123
+ const entry = this.requireReady(serverName);
124
+ return entry.client
125
+ .readResource({ uri }, { timeout: LIMITS.defaultToolTimeoutMs })
126
+ .then((result) => {
127
+ const parsed = ReadResourceResultSchema.parse(result);
128
+ return this.ensureResourceSubscription(entry, uri).then(() => parsed);
129
+ });
130
+ }
131
+ async getPrompt(serverName, name, args) {
132
+ const entry = this.requireReady(serverName);
133
+ return entry.client
134
+ .getPrompt({ name, arguments: args }, { timeout: LIMITS.defaultToolTimeoutMs })
135
+ .then((result) => GetPromptResultSchema.parse(result));
136
+ }
137
+ async stopAll() {
138
+ await Promise.all(Array.from(this.connections.values()).map((entry) => this.stopEntry(entry)));
139
+ this.connections.clear();
140
+ }
141
+ async stop(serverName) {
142
+ const entry = this.connections.get(serverName);
143
+ if (!entry)
144
+ return;
145
+ await this.stopEntry(entry);
146
+ this.connections.delete(serverName);
147
+ }
148
+ views() {
149
+ return Array.from(this.connections.values()).map((entry) => this.view(entry));
150
+ }
151
+ onResourceUpdated(handler) {
152
+ this.resourceUpdateHandlers.push(handler);
153
+ return () => {
154
+ this.resourceUpdateHandlers = this.resourceUpdateHandlers.filter((entry) => entry !== handler);
155
+ };
156
+ }
157
+ markStale(serverName, kind) {
158
+ const entry = this.connections.get(serverName);
159
+ if (!entry)
160
+ return;
161
+ entry.staleKinds.add(kind);
162
+ if (kind === 'resources')
163
+ entry.staleKinds.add('resourceTemplates');
164
+ if (entry.catalog)
165
+ entry.catalog = { ...entry.catalog, stale: true };
166
+ }
167
+ requireReady(serverName) {
168
+ const entry = this.connections.get(serverName);
169
+ if (!entry || entry.status !== 'ready') {
170
+ throw new Error(`MCP server ${serverName} is not ready`);
171
+ }
172
+ return entry;
173
+ }
174
+ async ensureResourceSubscription(entry, uri) {
175
+ if (!entry.server.capabilities.resources.subscribe)
176
+ return;
177
+ if (!entry.client.getServerCapabilities()?.resources?.subscribe)
178
+ return;
179
+ if (entry.subscribedResources.has(uri))
180
+ return;
181
+ try {
182
+ await entry.client.subscribeResource({ uri }, { timeout: LIMITS.defaultToolTimeoutMs });
183
+ entry.subscribedResources.add(uri);
184
+ }
185
+ catch (error) {
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ getLogger().warn(`MCP server ${entry.server.name} resource subscription failed for ${uri}: ${message}`);
188
+ }
189
+ }
190
+ async emitResourceUpdated(event) {
191
+ for (const handler of this.resourceUpdateHandlers) {
192
+ await handler(event);
193
+ }
194
+ }
195
+ async unsubscribeResources(entry) {
196
+ if (entry.subscribedResources.size === 0)
197
+ return;
198
+ if (!entry.server.capabilities.resources.subscribe) {
199
+ entry.subscribedResources.clear();
200
+ return;
201
+ }
202
+ if (!entry.client.getServerCapabilities()?.resources?.subscribe) {
203
+ entry.subscribedResources.clear();
204
+ return;
205
+ }
206
+ for (const uri of entry.subscribedResources) {
207
+ try {
208
+ await entry.client.unsubscribeResource({ uri }, { timeout: LIMITS.defaultToolTimeoutMs });
209
+ }
210
+ catch {
211
+ // best-effort unsubscribe during shutdown
212
+ }
213
+ }
214
+ entry.subscribedResources.clear();
215
+ }
216
+ async stopEntry(entry) {
217
+ entry.status = 'closed';
218
+ try {
219
+ await this.unsubscribeResources(entry);
220
+ const maybeHttpTransport = entry.transport;
221
+ if (typeof maybeHttpTransport.terminateSession === 'function') {
222
+ await maybeHttpTransport.terminateSession().catch(() => undefined);
223
+ }
224
+ await entry.client.close();
225
+ }
226
+ catch {
227
+ // best-effort shutdown
228
+ }
229
+ }
230
+ view(entry) {
231
+ return {
232
+ serverName: entry.server.name,
233
+ status: entry.status,
234
+ capabilities: entry.client.getServerCapabilities(),
235
+ error: entry.error,
236
+ };
237
+ }
238
+ }
239
+ //# sourceMappingURL=connection-manager.js.map