salmon-loop 0.3.0 → 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.
- package/dist/cli/authorization/non-interactive.js +7 -21
- package/dist/cli/commands/chat.js +1 -1
- package/dist/cli/commands/parallel.js +46 -41
- package/dist/cli/commands/run/assistant-message.js +3 -0
- package/dist/cli/commands/run/handler.js +2 -1
- package/dist/cli/commands/serve.js +109 -153
- package/dist/cli/headless/json-protocol.js +1 -1
- package/dist/cli/headless/stream-json-protocol.js +3 -2
- package/dist/cli/slash/runtime.js +5 -1
- package/dist/core/adapters/fs/node-fs.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +1 -1
- package/dist/core/context/service.js +5 -2
- package/dist/core/extensions/index.js +2 -35
- package/dist/core/extensions/redact.js +9 -3
- package/dist/core/extensions/schemas.js +2 -51
- package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
- package/dist/core/facades/cli-serve.js +0 -1
- package/dist/core/grizzco/dsl/strategies.js +1 -3
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
- package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
- package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
- package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
- package/dist/core/grizzco/steps/apply.js +0 -7
- package/dist/core/grizzco/steps/autopilot.js +108 -6
- package/dist/core/grizzco/steps/preflight.js +10 -0
- package/dist/core/grizzco/steps/tool-runtime.js +1 -0
- package/dist/core/interaction/events/bus.js +14 -0
- package/dist/core/interaction/orchestration/facade.js +10 -0
- package/dist/core/mcp/bridge/index.js +4 -0
- package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
- package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
- package/dist/core/mcp/bridge/tool-bridge.js +303 -0
- package/dist/core/mcp/cache/resource-cache.js +41 -0
- package/dist/core/mcp/catalog/discovery.js +51 -0
- package/dist/core/mcp/catalog/notification-router.js +28 -0
- package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
- package/dist/core/mcp/catalog/resource-catalog.js +7 -0
- package/dist/core/mcp/catalog/tool-catalog.js +4 -0
- package/dist/core/mcp/client/connection-manager.js +239 -0
- package/dist/core/mcp/client/lifecycle.js +13 -0
- package/dist/core/mcp/client/transport-factory.js +168 -0
- package/dist/core/mcp/config/index.js +32 -0
- package/dist/core/mcp/config/schema-v2.js +129 -0
- package/dist/core/mcp/host/elicitation-provider.js +209 -0
- package/dist/core/mcp/host/roots-provider.js +70 -0
- package/dist/core/mcp/host/sampling-provider.js +170 -0
- package/dist/core/mcp/index.js +4 -0
- package/dist/core/mcp/observability/events.js +19 -0
- package/dist/core/mcp/policy/approval-policy.js +2 -0
- package/dist/core/mcp/policy/classifier.js +172 -0
- package/dist/core/mcp/policy/grants.js +356 -0
- package/dist/core/mcp/policy/uri-policy.js +60 -0
- package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
- package/dist/core/mcp/types.js +2 -0
- package/dist/core/protocols/a2a/agent-card.js +36 -11
- package/dist/core/protocols/a2a/sdk/executor.js +105 -36
- package/dist/core/protocols/a2a/sdk/server.js +1311 -3
- package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
- package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
- package/dist/core/protocols/acp/acp-types.js +17 -0
- package/dist/core/protocols/acp/formal-agent.js +271 -603
- package/dist/core/protocols/acp/handlers.js +3 -0
- package/dist/core/protocols/acp/permission-provider.js +11 -39
- package/dist/core/protocols/acp/stdio-server.js +20 -1
- package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
- package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
- package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
- package/dist/core/public-capabilities/projections.js +1 -0
- package/dist/core/runtime/agent-server-runtime.js +2 -3
- package/dist/core/runtime/spawn-command.js +8 -2
- package/dist/core/runtime/spawn-interactive.js +26 -0
- package/dist/core/session/manager.js +48 -25
- package/dist/core/tools/builtin/index.js +6 -1
- package/dist/core/tools/builtin/proposal.js +0 -7
- package/dist/core/tools/builtin/workspace.js +76 -0
- package/dist/core/tools/dispatcher.js +1 -0
- package/dist/core/tools/loader.js +92 -46
- package/dist/core/verification/runner.js +60 -31
- package/dist/core/workspace/capabilities.js +80 -0
- package/dist/locales/en.js +17 -3
- package/package.json +4 -2
- package/dist/core/protocols/a2a/mapper.js +0 -14
- package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
- package/dist/core/protocols/a2a/task-projection.js +0 -45
- package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
- package/dist/core/tools/mcp/client.js +0 -309
- package/dist/core/tools/mcp/loader.js +0 -110
- package/dist/core/tools/mcp/schema.js +0 -54
- package/dist/core/tools/mcp/streamable-http.js +0 -101
- package/dist/core/tools/mcp/types.js +0 -26
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { jsonSchemaToZod } from '../schema/json-schema-to-zod.js';
|
|
3
|
+
const SAFE_TOKEN_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
4
|
+
function normalizeToken(value) {
|
|
5
|
+
return value
|
|
6
|
+
.trim()
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
9
|
+
.replace(/^-+|-+$/g, '')
|
|
10
|
+
.replace(/-+/g, '-');
|
|
11
|
+
}
|
|
12
|
+
function safeToken(value, fallback) {
|
|
13
|
+
const normalized = normalizeToken(value);
|
|
14
|
+
if (SAFE_TOKEN_PATTERN.test(normalized))
|
|
15
|
+
return normalized;
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
function isPromptOptions(value) {
|
|
19
|
+
return Boolean(value &&
|
|
20
|
+
typeof value === 'object' &&
|
|
21
|
+
'serverName' in value &&
|
|
22
|
+
'client' in value &&
|
|
23
|
+
typeof value.client?.listPrompts === 'function');
|
|
24
|
+
}
|
|
25
|
+
function buildFallbackSchemaFromArguments(args = []) {
|
|
26
|
+
const shape = {};
|
|
27
|
+
for (const arg of args) {
|
|
28
|
+
const field = z.string().describe(arg.description ?? '');
|
|
29
|
+
shape[arg.name] = arg.required ? field : field.optional();
|
|
30
|
+
}
|
|
31
|
+
return z.object(shape).strict();
|
|
32
|
+
}
|
|
33
|
+
function buildJsonSchemaFromArguments(args = []) {
|
|
34
|
+
return {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: Object.fromEntries(args.map((arg) => [
|
|
37
|
+
arg.name,
|
|
38
|
+
{
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: arg.description,
|
|
41
|
+
},
|
|
42
|
+
])),
|
|
43
|
+
required: args.filter((arg) => arg.required).map((arg) => arg.name),
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function parseSlashArgs(req) {
|
|
48
|
+
const raw = req.argsText.trim();
|
|
49
|
+
if (!raw)
|
|
50
|
+
return {};
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
54
|
+
throw new Error('Prompt arguments must be a JSON object.');
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
throw new Error(`Invalid MCP prompt arguments for ${req.command.name}: ${message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function coercePromptArgs(args) {
|
|
64
|
+
return Object.fromEntries(Object.entries(args).map(([key, value]) => {
|
|
65
|
+
if (typeof value === 'string')
|
|
66
|
+
return [key, value];
|
|
67
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
68
|
+
return [key, String(value)];
|
|
69
|
+
if (value === null || value === undefined)
|
|
70
|
+
return [key, ''];
|
|
71
|
+
return [key, JSON.stringify(value)];
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
function renderPromptResultAsForwardedInput(result) {
|
|
75
|
+
return result.messages
|
|
76
|
+
.map((message) => {
|
|
77
|
+
const content = message.content;
|
|
78
|
+
if (content.type === 'text' && typeof content.text === 'string') {
|
|
79
|
+
return content.text;
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify(message.content);
|
|
82
|
+
})
|
|
83
|
+
.filter((part) => part.length > 0)
|
|
84
|
+
.join('\n\n');
|
|
85
|
+
}
|
|
86
|
+
export class McpPromptCommandProvider {
|
|
87
|
+
manager;
|
|
88
|
+
policy;
|
|
89
|
+
serverName;
|
|
90
|
+
client;
|
|
91
|
+
commandPrefix;
|
|
92
|
+
order;
|
|
93
|
+
promptsByCommand = new Map();
|
|
94
|
+
promptsByName = new Map();
|
|
95
|
+
constructor(optionsOrManager, policy) {
|
|
96
|
+
if (isPromptOptions(optionsOrManager)) {
|
|
97
|
+
this.serverName = optionsOrManager.serverName;
|
|
98
|
+
this.client = optionsOrManager.client;
|
|
99
|
+
this.commandPrefix = safeToken(optionsOrManager.commandPrefix ?? 'mcp', 'mcp');
|
|
100
|
+
this.order = optionsOrManager.order ?? 230;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.manager = optionsOrManager;
|
|
104
|
+
this.policy = policy;
|
|
105
|
+
this.commandPrefix = 'mcp';
|
|
106
|
+
this.order = 230;
|
|
107
|
+
}
|
|
108
|
+
async load() {
|
|
109
|
+
if (!this.client || !this.serverName) {
|
|
110
|
+
this.promptsByCommand.clear();
|
|
111
|
+
this.promptsByName.clear();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const prompts = await this.client.listPrompts();
|
|
115
|
+
this.promptsByCommand.clear();
|
|
116
|
+
this.promptsByName.clear();
|
|
117
|
+
for (const prompt of prompts) {
|
|
118
|
+
const descriptor = { ...prompt, serverName: prompt.serverName ?? this.serverName };
|
|
119
|
+
const command = this.commandNameForPrompt(descriptor);
|
|
120
|
+
this.promptsByCommand.set(command, descriptor);
|
|
121
|
+
this.promptsByName.set(descriptor.name, descriptor);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
listCommands() {
|
|
125
|
+
if (!this.manager || !this.policy) {
|
|
126
|
+
return this.listRecipes().map((recipe) => ({
|
|
127
|
+
name: recipe.slashCommand,
|
|
128
|
+
server: recipe.serverName,
|
|
129
|
+
prompt: recipe.promptName,
|
|
130
|
+
description: recipe.description,
|
|
131
|
+
exposure: 'recipe',
|
|
132
|
+
arguments: this.promptsByName.get(recipe.promptName)?.arguments ?? [],
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
const commands = [];
|
|
136
|
+
for (const catalog of this.manager.listCatalogs()) {
|
|
137
|
+
for (const prompt of catalog.prompts) {
|
|
138
|
+
const decision = this.policy.decidePrompt({
|
|
139
|
+
server: catalog.serverName,
|
|
140
|
+
name: prompt.name,
|
|
141
|
+
});
|
|
142
|
+
if (!decision.allowed || decision.grant?.kind !== 'prompt')
|
|
143
|
+
continue;
|
|
144
|
+
commands.push(this.toCommand(prompt, decision.grant.exposeAs ?? 'slash'));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return commands;
|
|
148
|
+
}
|
|
149
|
+
listSlashCommands() {
|
|
150
|
+
return Array.from(this.promptsByCommand.entries()).map(([name, prompt]) => ({
|
|
151
|
+
name,
|
|
152
|
+
description: this.descriptionForPrompt(prompt),
|
|
153
|
+
order: this.order,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
listRecipes() {
|
|
157
|
+
return Array.from(this.promptsByCommand.entries()).map(([slashCommand, prompt]) => ({
|
|
158
|
+
id: `mcp.${safeToken(prompt.serverName ?? this.serverName ?? 'server', 'server')}.${safeToken(prompt.name, 'prompt')}`,
|
|
159
|
+
slashCommand,
|
|
160
|
+
title: prompt.title ?? prompt.name,
|
|
161
|
+
description: this.descriptionForPrompt(prompt),
|
|
162
|
+
promptName: prompt.name,
|
|
163
|
+
serverName: prompt.serverName ?? this.serverName ?? 'unknown',
|
|
164
|
+
inputSchema: this.jsonSchemaForPrompt(prompt),
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
getHandler(commandName) {
|
|
168
|
+
const prompt = this.promptsByCommand.get(commandName.toLowerCase());
|
|
169
|
+
if (!prompt)
|
|
170
|
+
return undefined;
|
|
171
|
+
return {
|
|
172
|
+
execute: async (req) => {
|
|
173
|
+
const args = parseSlashArgs(req);
|
|
174
|
+
const invocation = await this.invokePrompt(prompt.name, args);
|
|
175
|
+
const input = renderPromptResultAsForwardedInput(invocation.result);
|
|
176
|
+
const result = { kind: 'forward', input };
|
|
177
|
+
return result;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async invoke(input) {
|
|
182
|
+
if (!this.manager || !this.policy) {
|
|
183
|
+
return this.invokePrompt(input.prompt, input.args ?? {}).then((invocation) => invocation.result);
|
|
184
|
+
}
|
|
185
|
+
const decision = this.policy.decidePrompt({ server: input.server, name: input.prompt });
|
|
186
|
+
if (!decision.allowed)
|
|
187
|
+
throw new Error(decision.denyReason ?? 'MCP_PROMPT_DENIED');
|
|
188
|
+
const descriptor = this.manager
|
|
189
|
+
.getCatalog(input.server)
|
|
190
|
+
?.prompts.find((candidate) => candidate.name === input.prompt);
|
|
191
|
+
if (!descriptor)
|
|
192
|
+
throw new Error(`MCP prompt not found: ${input.server}.${input.prompt}`);
|
|
193
|
+
const schema = this.buildArgsSchema(descriptor);
|
|
194
|
+
const args = schema.parse(input.args ?? {});
|
|
195
|
+
return this.manager.getPrompt(input.server, input.prompt, args);
|
|
196
|
+
}
|
|
197
|
+
async invokePrompt(name, rawArgs = {}) {
|
|
198
|
+
if (!this.client) {
|
|
199
|
+
throw new Error('MCP prompt client is not configured for direct invocation.');
|
|
200
|
+
}
|
|
201
|
+
const prompt = this.promptsByName.get(name);
|
|
202
|
+
if (!prompt) {
|
|
203
|
+
throw new Error(`MCP prompt not found: ${this.serverName ?? 'unknown'}/${name}`);
|
|
204
|
+
}
|
|
205
|
+
const args = this.validatePromptArgs(prompt, rawArgs);
|
|
206
|
+
const result = await this.client.getPrompt(prompt.name, args);
|
|
207
|
+
return {
|
|
208
|
+
promptName: prompt.name,
|
|
209
|
+
serverName: prompt.serverName ?? this.serverName ?? 'unknown',
|
|
210
|
+
args,
|
|
211
|
+
result,
|
|
212
|
+
audit: {
|
|
213
|
+
event: 'mcp.prompt.invoke',
|
|
214
|
+
serverName: prompt.serverName ?? this.serverName ?? 'unknown',
|
|
215
|
+
promptName: prompt.name,
|
|
216
|
+
args,
|
|
217
|
+
messageCount: result.messages.length,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
toCommand(prompt, exposure) {
|
|
222
|
+
return {
|
|
223
|
+
name: `/mcp.${prompt.serverName}.${prompt.name}`,
|
|
224
|
+
server: prompt.serverName,
|
|
225
|
+
prompt: prompt.name,
|
|
226
|
+
description: prompt.description,
|
|
227
|
+
exposure: exposure === 'recipe' ? 'recipe' : 'slash',
|
|
228
|
+
arguments: prompt.arguments ?? [],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
buildArgsSchema(prompt) {
|
|
232
|
+
const shape = {};
|
|
233
|
+
for (const arg of prompt.arguments ?? []) {
|
|
234
|
+
shape[arg.name] = arg.required ? z.string() : z.string().optional();
|
|
235
|
+
}
|
|
236
|
+
return z.object(shape).strict();
|
|
237
|
+
}
|
|
238
|
+
commandNameForPrompt(prompt) {
|
|
239
|
+
const server = safeToken(prompt.serverName ?? this.serverName ?? 'server', 'server');
|
|
240
|
+
const promptName = safeToken(prompt.name, 'prompt');
|
|
241
|
+
return `/${this.commandPrefix}-${server}-${promptName}`;
|
|
242
|
+
}
|
|
243
|
+
descriptionForPrompt(prompt) {
|
|
244
|
+
return prompt.description ?? prompt.title ?? `MCP prompt ${prompt.name}`;
|
|
245
|
+
}
|
|
246
|
+
jsonSchemaForPrompt(prompt) {
|
|
247
|
+
return prompt.inputSchema ?? buildJsonSchemaFromArguments(prompt.arguments);
|
|
248
|
+
}
|
|
249
|
+
zodSchemaForPrompt(prompt) {
|
|
250
|
+
if (prompt.inputSchema) {
|
|
251
|
+
return jsonSchemaToZod(prompt.inputSchema);
|
|
252
|
+
}
|
|
253
|
+
return buildFallbackSchemaFromArguments(prompt.arguments);
|
|
254
|
+
}
|
|
255
|
+
validatePromptArgs(prompt, rawArgs) {
|
|
256
|
+
const schema = this.zodSchemaForPrompt(prompt);
|
|
257
|
+
const parsed = schema.parse(rawArgs);
|
|
258
|
+
return coercePromptArgs(parsed);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=prompt-command-provider.js.map
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { ResourceCache } from '../cache/resource-cache.js';
|
|
2
|
+
export class ResourceContextProviderError extends Error {
|
|
3
|
+
diagnostic;
|
|
4
|
+
constructor(message, diagnostic) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.diagnostic = diagnostic;
|
|
7
|
+
this.name = 'ResourceContextProviderError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class ResourceContextProvider {
|
|
11
|
+
deps;
|
|
12
|
+
cache;
|
|
13
|
+
options;
|
|
14
|
+
constructor(deps) {
|
|
15
|
+
this.deps = deps;
|
|
16
|
+
this.cache = deps.cache ?? new ResourceCache();
|
|
17
|
+
this.options = {
|
|
18
|
+
budgetChars: deps.options?.budgetChars ?? 24_000,
|
|
19
|
+
maxResourceChars: deps.options?.maxResourceChars ?? 8_000,
|
|
20
|
+
maxResources: deps.options?.maxResources ?? 20,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async provide(input) {
|
|
24
|
+
const result = {
|
|
25
|
+
blocks: [],
|
|
26
|
+
diagnostics: [],
|
|
27
|
+
meta: { usedChars: 0, truncated: false, cacheHits: 0, cacheMisses: 0 },
|
|
28
|
+
};
|
|
29
|
+
const selected = this.selectResources(input?.resources ?? []);
|
|
30
|
+
for (const resource of selected.slice(0, this.options.maxResources)) {
|
|
31
|
+
const optional = resource.includeIntent !== 'required';
|
|
32
|
+
const policy = this.deps.policy.checkUri({
|
|
33
|
+
serverName: resource.serverName,
|
|
34
|
+
uri: resource.uri,
|
|
35
|
+
intent: resource.includeIntent ?? 'manual',
|
|
36
|
+
});
|
|
37
|
+
const allowed = typeof policy === 'boolean' ? policy : policy.allowed;
|
|
38
|
+
if (!allowed) {
|
|
39
|
+
const diagnostic = this.diagnostic('POLICY_DENIED', typeof policy === 'boolean'
|
|
40
|
+
? 'MCP resource denied by policy'
|
|
41
|
+
: (policy.reason ?? 'MCP resource denied by policy'), resource, optional);
|
|
42
|
+
if (!optional)
|
|
43
|
+
throw new ResourceContextProviderError(diagnostic.message, diagnostic);
|
|
44
|
+
result.diagnostics.push(diagnostic);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let remaining = this.options.budgetChars - result.meta.usedChars;
|
|
48
|
+
if (remaining <= 0) {
|
|
49
|
+
result.meta.truncated = true;
|
|
50
|
+
result.blocks.push(this.linkBlock(resource, 'budget_exhausted'));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const cacheKey = `${resource.serverName}:${resource.uri}:${Math.min(this.options.maxResourceChars, remaining)}`;
|
|
54
|
+
const cached = this.cache.get(cacheKey);
|
|
55
|
+
if (cached) {
|
|
56
|
+
result.meta.cacheHits += 1;
|
|
57
|
+
result.blocks.push(...cached);
|
|
58
|
+
for (const block of cached) {
|
|
59
|
+
if (block.type === 'resource_text') {
|
|
60
|
+
result.meta.usedChars += block.includedChars;
|
|
61
|
+
result.meta.truncated = result.meta.truncated || block.truncated;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
result.meta.cacheMisses += 1;
|
|
67
|
+
try {
|
|
68
|
+
const connection = this.deps.connections[resource.serverName];
|
|
69
|
+
if (!connection)
|
|
70
|
+
throw new Error(`MCP connection not found: ${resource.serverName}`);
|
|
71
|
+
const read = await connection.readResource({ uri: resource.uri });
|
|
72
|
+
const contents = read.contents ?? [];
|
|
73
|
+
const resourceBlocks = [];
|
|
74
|
+
for (const content of contents) {
|
|
75
|
+
if (remaining <= 0)
|
|
76
|
+
break;
|
|
77
|
+
const block = this.blockFromRead(resource, content, remaining);
|
|
78
|
+
resourceBlocks.push(block);
|
|
79
|
+
result.blocks.push(block);
|
|
80
|
+
if (block.type === 'resource_text') {
|
|
81
|
+
result.meta.usedChars += block.includedChars;
|
|
82
|
+
result.meta.truncated = result.meta.truncated || block.truncated;
|
|
83
|
+
remaining -= block.includedChars;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (resourceBlocks.length > 0) {
|
|
87
|
+
this.cache.set(cacheKey, resourceBlocks);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const diagnostic = this.diagnostic('READ_FAILED', error instanceof Error ? error.message : String(error), resource, optional);
|
|
92
|
+
if (!optional)
|
|
93
|
+
throw new ResourceContextProviderError(diagnostic.message, diagnostic);
|
|
94
|
+
result.diagnostics.push(diagnostic);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
invalidate(serverName, uri) {
|
|
100
|
+
this.cache.deleteMatching((key) => {
|
|
101
|
+
const serverPrefix = `${serverName}:`;
|
|
102
|
+
if (!key.startsWith(serverPrefix))
|
|
103
|
+
return false;
|
|
104
|
+
const separatorIndex = key.lastIndexOf(':');
|
|
105
|
+
if (separatorIndex <= serverPrefix.length)
|
|
106
|
+
return false;
|
|
107
|
+
const cachedUri = key.slice(serverPrefix.length, separatorIndex);
|
|
108
|
+
return cachedUri === uri || uri.startsWith(cachedUri);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
selectResources(manual) {
|
|
112
|
+
const catalog = this.deps.catalog.listResources();
|
|
113
|
+
const resources = catalog.filter((resource) => resource.includeIntent === 'required' || resource.includeIntent === 'autoInclude');
|
|
114
|
+
for (const item of manual) {
|
|
115
|
+
if (resources.some((resource) => resource.serverName === item.serverName && resource.uri === item.uri)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const catalogResource = catalog.find((resource) => resource.serverName === item.serverName && resource.uri === item.uri);
|
|
119
|
+
resources.push({
|
|
120
|
+
...catalogResource,
|
|
121
|
+
serverName: item.serverName,
|
|
122
|
+
uri: item.uri,
|
|
123
|
+
includeIntent: item.intent,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return resources;
|
|
127
|
+
}
|
|
128
|
+
blockFromRead(resource, content, remainingBudget) {
|
|
129
|
+
if (!content)
|
|
130
|
+
return this.linkBlock(resource, 'non_text');
|
|
131
|
+
const contentUri = this.contentUri(resource, content);
|
|
132
|
+
const mimeType = typeof content.mimeType === 'string' ? content.mimeType : resource.mimeType;
|
|
133
|
+
if (typeof content.blob === 'string')
|
|
134
|
+
return this.linkBlock(resource, 'blob', contentUri);
|
|
135
|
+
if (typeof content.text !== 'string')
|
|
136
|
+
return this.linkBlock(resource, 'non_text', contentUri);
|
|
137
|
+
if (!isTextMime(mimeType))
|
|
138
|
+
return this.linkBlock(resource, 'non_text', contentUri);
|
|
139
|
+
const raw = normalizeText(content.text, mimeType);
|
|
140
|
+
const limit = Math.max(0, Math.min(this.options.maxResourceChars, remainingBudget));
|
|
141
|
+
const truncated = raw.length > limit;
|
|
142
|
+
const text = truncated ? raw.slice(0, limit) : raw;
|
|
143
|
+
return {
|
|
144
|
+
type: 'resource_text',
|
|
145
|
+
serverName: resource.serverName,
|
|
146
|
+
uri: contentUri,
|
|
147
|
+
name: resource.name,
|
|
148
|
+
mimeType,
|
|
149
|
+
content: { text, format: isJsonMime(mimeType) ? 'json' : 'text' },
|
|
150
|
+
includedChars: text.length,
|
|
151
|
+
truncated,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
linkBlock(resource, reason, uri = resource.uri) {
|
|
155
|
+
return {
|
|
156
|
+
type: 'resource_link',
|
|
157
|
+
serverName: resource.serverName,
|
|
158
|
+
uri,
|
|
159
|
+
name: resource.name,
|
|
160
|
+
reason,
|
|
161
|
+
metadata: {
|
|
162
|
+
name: resource.name,
|
|
163
|
+
description: resource.description,
|
|
164
|
+
mimeType: resource.mimeType,
|
|
165
|
+
size: resource.size,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
contentUri(resource, content) {
|
|
170
|
+
return typeof content.uri === 'string' ? content.uri : resource.uri;
|
|
171
|
+
}
|
|
172
|
+
diagnostic(code, message, resource, optional) {
|
|
173
|
+
return {
|
|
174
|
+
code,
|
|
175
|
+
message,
|
|
176
|
+
serverName: resource.serverName,
|
|
177
|
+
uri: resource.uri,
|
|
178
|
+
optional,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export class McpResourceContextProvider {
|
|
183
|
+
provider;
|
|
184
|
+
constructor(manager, policy) {
|
|
185
|
+
this.provider = new ResourceContextProvider({
|
|
186
|
+
catalog: {
|
|
187
|
+
listResources: () => manager.listCatalogs().flatMap((catalog) => catalog.resources.map((resource) => ({
|
|
188
|
+
serverName: catalog.serverName,
|
|
189
|
+
uri: resource.uri,
|
|
190
|
+
name: resource.name,
|
|
191
|
+
description: resource.description,
|
|
192
|
+
mimeType: resource.mimeType,
|
|
193
|
+
includeIntent: 'autoInclude',
|
|
194
|
+
}))),
|
|
195
|
+
},
|
|
196
|
+
connections: new Proxy({}, {
|
|
197
|
+
get: (_target, serverName) => ({
|
|
198
|
+
readResource: ({ uri }) => manager.readResource(serverName, uri),
|
|
199
|
+
}),
|
|
200
|
+
}),
|
|
201
|
+
policy: {
|
|
202
|
+
checkUri: ({ serverName, uri }) => {
|
|
203
|
+
const decision = policy.decideResource({ server: serverName, uri });
|
|
204
|
+
return { allowed: decision.allowed, reason: decision.denyReason ?? decision.reason };
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
manager.onResourceUpdated((event) => {
|
|
209
|
+
this.provider.invalidate(event.serverName, event.uri);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async read(input) {
|
|
213
|
+
const result = await this.provider.provide({
|
|
214
|
+
resources: [{ serverName: input.server, uri: input.uri, intent: input.intent }],
|
|
215
|
+
});
|
|
216
|
+
const block = result.blocks[0];
|
|
217
|
+
if (!block) {
|
|
218
|
+
return {
|
|
219
|
+
server: input.server,
|
|
220
|
+
uri: input.uri,
|
|
221
|
+
diagnostics: result.diagnostics[0]?.message,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (block.type === 'resource_text') {
|
|
225
|
+
return {
|
|
226
|
+
server: input.server,
|
|
227
|
+
uri: input.uri,
|
|
228
|
+
mimeType: block.mimeType,
|
|
229
|
+
text: block.content.text,
|
|
230
|
+
truncated: block.truncated,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
server: input.server,
|
|
235
|
+
uri: input.uri,
|
|
236
|
+
mimeType: typeof block.metadata.mimeType === 'string' ? block.metadata.mimeType : undefined,
|
|
237
|
+
linkOnly: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function isTextMime(mimeType) {
|
|
242
|
+
if (!mimeType)
|
|
243
|
+
return true;
|
|
244
|
+
return mimeType.startsWith('text/') || isJsonMime(mimeType);
|
|
245
|
+
}
|
|
246
|
+
function isJsonMime(mimeType) {
|
|
247
|
+
return Boolean(mimeType && (mimeType === 'application/json' || mimeType.endsWith('+json')));
|
|
248
|
+
}
|
|
249
|
+
function normalizeText(text, mimeType) {
|
|
250
|
+
if (!isJsonMime(mimeType))
|
|
251
|
+
return text;
|
|
252
|
+
try {
|
|
253
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=resource-context-provider.js.map
|