@ai-ecoverse/slicc-agent 3.27.1 → 3.29.0
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/README.md +46 -4
- package/dist/codemode/index.js +10 -4
- package/dist/codemode/index.js.map +2 -2
- package/dist/control.js +33 -30
- package/dist/control.js.map +2 -2
- package/dist/host.js +1 -0
- package/dist/host.js.map +2 -2
- package/dist/main.js +21 -2
- package/dist/main.js.map +2 -2
- package/dist/mcp/allow.js +39 -0
- package/dist/mcp/allow.js.map +7 -0
- package/dist/mcp/catalog.js +182 -0
- package/dist/mcp/catalog.js.map +7 -0
- package/dist/mcp/config.js +201 -0
- package/dist/mcp/config.js.map +7 -0
- package/dist/mcp/connection.js +296 -0
- package/dist/mcp/connection.js.map +7 -0
- package/dist/mcp/content.js +139 -0
- package/dist/mcp/content.js.map +7 -0
- package/dist/mcp/credentials.js +45 -0
- package/dist/mcp/credentials.js.map +7 -0
- package/dist/mcp/index.js +54 -0
- package/dist/mcp/index.js.map +7 -0
- package/dist/mcp/runtime.js +209 -0
- package/dist/mcp/runtime.js.map +7 -0
- package/dist/mcp/section.js +35 -0
- package/dist/mcp/section.js.map +7 -0
- package/dist/mcp/sse.js +112 -0
- package/dist/mcp/sse.js.map +7 -0
- package/dist/mcp/tools.js +413 -0
- package/dist/mcp/tools.js.map +7 -0
- package/dist/patches.js +2 -0
- package/dist/patches.js.map +2 -2
- package/dist/roles/roles.js +10 -1
- package/dist/roles/roles.js.map +2 -2
- package/dist/scoops/index.js +2 -1
- package/dist/scoops/index.js.map +2 -2
- package/dist/scoops/service.js +21 -6
- package/dist/scoops/service.js.map +2 -2
- package/dist/services.js.map +2 -2
- package/dist/spectrum/agent-port.js +5 -3
- package/dist/spectrum/agent-port.js.map +2 -2
- package/dist/types/codemode/index.d.ts +8 -3
- package/dist/types/host.d.ts +2 -0
- package/dist/types/mcp/allow.d.ts +12 -0
- package/dist/types/mcp/catalog.d.ts +29 -0
- package/dist/types/mcp/config.d.ts +35 -0
- package/dist/types/mcp/connection.d.ts +57 -0
- package/dist/types/mcp/content.d.ts +27 -0
- package/dist/types/mcp/credentials.d.ts +15 -0
- package/dist/types/mcp/index.d.ts +16 -0
- package/dist/types/mcp/runtime.d.ts +42 -0
- package/dist/types/mcp/section.d.ts +7 -0
- package/dist/types/mcp/sse.d.ts +16 -0
- package/dist/types/mcp/tools.d.ts +44 -0
- package/dist/types/patches.d.ts +1 -0
- package/dist/types/scoops/index.d.ts +1 -0
- package/dist/types/scoops/service.d.ts +1 -0
- package/dist/types/services.d.ts +2 -0
- package/dist/types/spectrum/agent-port.d.ts +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSON_RPC_ERROR_CODES,
|
|
3
|
+
McpAuthRequiredError,
|
|
4
|
+
McpClient,
|
|
5
|
+
McpError,
|
|
6
|
+
McpHttpError,
|
|
7
|
+
McpSessionExpiredError
|
|
8
|
+
} from "@earendil-works/pi-mcp";
|
|
9
|
+
import { DEFAULT_TIMEOUT_SECONDS } from "./config.js";
|
|
10
|
+
const CONNECT_RETRY_DELAYS_MS = [250, 1e3];
|
|
11
|
+
const CLIENT_NAME = "slicc";
|
|
12
|
+
function errorMessage(error) {
|
|
13
|
+
return error instanceof Error ? error.message : String(error);
|
|
14
|
+
}
|
|
15
|
+
function isTransient(error) {
|
|
16
|
+
if (error instanceof McpHttpError)
|
|
17
|
+
return error.status === 408 || error.status === 429 || error.status >= 500 && error.status !== 501;
|
|
18
|
+
return error instanceof TypeError;
|
|
19
|
+
}
|
|
20
|
+
function isMcpApp(item) {
|
|
21
|
+
const uri = item.uri ?? item.uriTemplate ?? "";
|
|
22
|
+
return uri.startsWith("ui://") || /;\s*profile\s*=\s*"?mcp-app"?/i.test(item.mimeType ?? "");
|
|
23
|
+
}
|
|
24
|
+
async function withoutTemplates(list, empty) {
|
|
25
|
+
try {
|
|
26
|
+
return await list();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof McpError && error.code === JSON_RPC_ERROR_CODES.methodNotFound)
|
|
29
|
+
return empty;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const sleep = (ms, signal) => new Promise((resolve) => {
|
|
34
|
+
const timer = setTimeout(resolve, ms);
|
|
35
|
+
signal.addEventListener(
|
|
36
|
+
"abort",
|
|
37
|
+
() => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
resolve();
|
|
40
|
+
},
|
|
41
|
+
{ once: true }
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
class McpServerConnection {
|
|
45
|
+
entry;
|
|
46
|
+
state = "connecting";
|
|
47
|
+
error;
|
|
48
|
+
missing = [];
|
|
49
|
+
tools = [];
|
|
50
|
+
hasResources = false;
|
|
51
|
+
resources = [];
|
|
52
|
+
resourceTemplates = [];
|
|
53
|
+
instructions;
|
|
54
|
+
secrets = [];
|
|
55
|
+
#client;
|
|
56
|
+
#opening;
|
|
57
|
+
#shutdown = new AbortController();
|
|
58
|
+
#busy = /* @__PURE__ */ new Map();
|
|
59
|
+
#seen = false;
|
|
60
|
+
#expired = /* @__PURE__ */ new Set();
|
|
61
|
+
#options;
|
|
62
|
+
constructor(options) {
|
|
63
|
+
this.entry = options.entry;
|
|
64
|
+
this.#options = options;
|
|
65
|
+
}
|
|
66
|
+
get name() {
|
|
67
|
+
return this.entry.name;
|
|
68
|
+
}
|
|
69
|
+
get timeoutMs() {
|
|
70
|
+
return (this.entry.config.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
|
|
71
|
+
}
|
|
72
|
+
get live() {
|
|
73
|
+
return this.state === "connected" || this.state === "connecting" && this.#seen;
|
|
74
|
+
}
|
|
75
|
+
get closed() {
|
|
76
|
+
return this.#shutdown.signal.aborted;
|
|
77
|
+
}
|
|
78
|
+
getClient() {
|
|
79
|
+
if (this.closed) return Promise.reject(new Error(`MCP server "${this.name}" is shut down`));
|
|
80
|
+
if (this.#client?.connectionState === "connected") return Promise.resolve(this.#client);
|
|
81
|
+
this.#opening ??= this.#open().finally(() => {
|
|
82
|
+
this.#opening = void 0;
|
|
83
|
+
});
|
|
84
|
+
return this.#opening;
|
|
85
|
+
}
|
|
86
|
+
callTool(name, args, options) {
|
|
87
|
+
return this.#with((client) => client.callTool(name, args, options));
|
|
88
|
+
}
|
|
89
|
+
readResource(uri, options) {
|
|
90
|
+
return this.#with((client) => client.readResource(uri, options), true);
|
|
91
|
+
}
|
|
92
|
+
resourcesPage(cursor, options) {
|
|
93
|
+
return this.#with((client) => client.listResourcesPage(cursor, options), true);
|
|
94
|
+
}
|
|
95
|
+
resourceTemplatesPage(cursor, options) {
|
|
96
|
+
return this.#with(
|
|
97
|
+
(client) => withoutTemplates(() => client.listResourceTemplatesPage(cursor, options), {
|
|
98
|
+
resourceTemplates: []
|
|
99
|
+
}),
|
|
100
|
+
true
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
allResources(options) {
|
|
104
|
+
return this.#with((client) => client.listResources(options), true);
|
|
105
|
+
}
|
|
106
|
+
allResourceTemplates(options) {
|
|
107
|
+
return this.#with(
|
|
108
|
+
(client) => withoutTemplates(() => client.listResourceTemplates(options), []),
|
|
109
|
+
true
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
async #with(run, readOnly = false) {
|
|
113
|
+
for (let attempt = 1; ; attempt++) {
|
|
114
|
+
const client = await this.getClient();
|
|
115
|
+
this.#busy.set(client, (this.#busy.get(client) ?? 0) + 1);
|
|
116
|
+
try {
|
|
117
|
+
return await run(client);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (readOnly && attempt === 1 && error instanceof McpHttpError && isTransient(error)) {
|
|
120
|
+
await sleep(CONNECT_RETRY_DELAYS_MS[0], this.#shutdown.signal);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (error instanceof McpSessionExpiredError && attempt === 1) {
|
|
124
|
+
if (this.#client === client) {
|
|
125
|
+
this.#client = void 0;
|
|
126
|
+
this.#expired.add(client);
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!(error instanceof McpAuthRequiredError)) throw error;
|
|
131
|
+
await this.#drop(client);
|
|
132
|
+
this.#mark("needs-auth");
|
|
133
|
+
throw new Error(this.signInMessage());
|
|
134
|
+
} finally {
|
|
135
|
+
this.#release(client);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
#release(client) {
|
|
140
|
+
const left = this.#busy.get(client) - 1;
|
|
141
|
+
this.#busy.set(client, left);
|
|
142
|
+
if (left > 0 || !this.#expired.delete(client)) return;
|
|
143
|
+
this.#busy.delete(client);
|
|
144
|
+
void this.#drop(client);
|
|
145
|
+
}
|
|
146
|
+
signInMessage() {
|
|
147
|
+
return `MCP server "${this.name}" rejected the request as unauthorized (401). Check its token in Settings \u203A Accounts.`;
|
|
148
|
+
}
|
|
149
|
+
#mark(state, error) {
|
|
150
|
+
this.state = state;
|
|
151
|
+
this.error = error;
|
|
152
|
+
this.#options.onChange?.(this);
|
|
153
|
+
}
|
|
154
|
+
async #drop(client) {
|
|
155
|
+
if (this.#client === client) this.#client = void 0;
|
|
156
|
+
await client.close().catch(() => void 0);
|
|
157
|
+
}
|
|
158
|
+
async #open() {
|
|
159
|
+
this.#mark("connecting");
|
|
160
|
+
for (let attempt = 0; ; attempt++) {
|
|
161
|
+
try {
|
|
162
|
+
return await this.#connectOnce();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const delay = CONNECT_RETRY_DELAYS_MS[attempt];
|
|
165
|
+
if (this.closed || delay === void 0 || !isTransient(error)) throw this.#failed(error);
|
|
166
|
+
await sleep(delay, this.#shutdown.signal);
|
|
167
|
+
if (this.closed) throw this.#failed(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async #connectOnce() {
|
|
172
|
+
const made = await this.#options.transport(this.entry);
|
|
173
|
+
if ("missing" in made) {
|
|
174
|
+
this.missing = made.missing;
|
|
175
|
+
throw new MissingSecret(made.missing);
|
|
176
|
+
}
|
|
177
|
+
this.secrets = made.secrets;
|
|
178
|
+
const client = new McpClient({
|
|
179
|
+
name: CLIENT_NAME,
|
|
180
|
+
version: this.#options.version,
|
|
181
|
+
requestTimeoutMs: this.timeoutMs
|
|
182
|
+
});
|
|
183
|
+
const log = this.#options.onLog;
|
|
184
|
+
if (log) client.onNotification("notifications/message", (params) => log(this, params));
|
|
185
|
+
let closing;
|
|
186
|
+
const closeClient = () => {
|
|
187
|
+
closing ??= client.close().catch(() => void 0);
|
|
188
|
+
return closing;
|
|
189
|
+
};
|
|
190
|
+
this.#shutdown.signal.addEventListener("abort", closeClient, { once: true });
|
|
191
|
+
try {
|
|
192
|
+
await client.connect(made.transport);
|
|
193
|
+
client.onNotification("notifications/tools/list_changed", () => {
|
|
194
|
+
void this.#refreshTools(client);
|
|
195
|
+
});
|
|
196
|
+
client.onNotification("notifications/resources/list_changed", () => {
|
|
197
|
+
void this.#refreshResources(client);
|
|
198
|
+
});
|
|
199
|
+
const hasResources = client.serverCapabilities?.resources !== void 0;
|
|
200
|
+
const [tools, resources] = await Promise.all([
|
|
201
|
+
client.serverCapabilities?.tools ? client.listTools() : [],
|
|
202
|
+
hasResources ? this.#fetchResources(client) : { resources: [], resourceTemplates: [] }
|
|
203
|
+
]);
|
|
204
|
+
if (this.closed) throw new Error("shut down while connecting");
|
|
205
|
+
if (client.connectionState !== "connected") throw new Error("connection closed during setup");
|
|
206
|
+
this.#client = client;
|
|
207
|
+
this.tools = tools;
|
|
208
|
+
this.hasResources = hasResources;
|
|
209
|
+
this.resources = resources.resources;
|
|
210
|
+
this.resourceTemplates = resources.resourceTemplates;
|
|
211
|
+
this.instructions = client.instructions?.trim() || void 0;
|
|
212
|
+
this.missing = [];
|
|
213
|
+
this.#seen = true;
|
|
214
|
+
this.state = "connected";
|
|
215
|
+
this.error = void 0;
|
|
216
|
+
this.#options.onTools(this);
|
|
217
|
+
this.#options.onChange?.(this);
|
|
218
|
+
return client;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
await closeClient();
|
|
221
|
+
throw error;
|
|
222
|
+
} finally {
|
|
223
|
+
this.#shutdown.signal.removeEventListener("abort", closeClient);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async #fetchResources(client) {
|
|
227
|
+
const [resources, resourceTemplates] = await Promise.all([
|
|
228
|
+
client.listResources().catch(() => []),
|
|
229
|
+
withoutTemplates(() => client.listResourceTemplates(), []).catch(
|
|
230
|
+
() => []
|
|
231
|
+
)
|
|
232
|
+
]);
|
|
233
|
+
return {
|
|
234
|
+
resources: resources.filter((resource) => !isMcpApp(resource)),
|
|
235
|
+
resourceTemplates: resourceTemplates.filter((template) => !isMcpApp(template))
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
#failed(error) {
|
|
239
|
+
if (error instanceof MissingSecret) {
|
|
240
|
+
this.#mark("needs-secret");
|
|
241
|
+
return new Error(error.message);
|
|
242
|
+
}
|
|
243
|
+
if (error instanceof McpAuthRequiredError && !this.closed) {
|
|
244
|
+
this.#mark("needs-auth");
|
|
245
|
+
return new Error(this.signInMessage());
|
|
246
|
+
}
|
|
247
|
+
this.#mark(this.closed ? "closed" : "failed", errorMessage(error));
|
|
248
|
+
return new Error(`MCP server "${this.name}" failed to connect: ${this.error}`);
|
|
249
|
+
}
|
|
250
|
+
async #refreshTools(client) {
|
|
251
|
+
try {
|
|
252
|
+
const tools = await client.listTools();
|
|
253
|
+
if (this.#client !== client || this.closed) return;
|
|
254
|
+
this.tools = tools;
|
|
255
|
+
this.#options.onTools(this);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.error = `Failed to refresh tools: ${errorMessage(error)}`;
|
|
258
|
+
}
|
|
259
|
+
this.#options.onChange?.(this);
|
|
260
|
+
}
|
|
261
|
+
async #refreshResources(client) {
|
|
262
|
+
const { resources, resourceTemplates } = await this.#fetchResources(client);
|
|
263
|
+
if (this.#client !== client || this.closed) return;
|
|
264
|
+
this.resources = resources;
|
|
265
|
+
this.resourceTemplates = resourceTemplates;
|
|
266
|
+
this.#options.onTools(this);
|
|
267
|
+
this.#options.onChange?.(this);
|
|
268
|
+
}
|
|
269
|
+
async close() {
|
|
270
|
+
this.#shutdown.abort();
|
|
271
|
+
this.state = "closed";
|
|
272
|
+
this.#options.onChange?.(this);
|
|
273
|
+
const client = this.#client;
|
|
274
|
+
this.#client = void 0;
|
|
275
|
+
await Promise.allSettled([client?.close(), this.#opening]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
class MissingSecret extends Error {
|
|
279
|
+
names;
|
|
280
|
+
constructor(names) {
|
|
281
|
+
super(
|
|
282
|
+
`needs ${names.map((name) => `\${${name}}`).join(", ")}: add the value in Settings \u203A Accounts`
|
|
283
|
+
);
|
|
284
|
+
this.names = names;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export {
|
|
288
|
+
CLIENT_NAME,
|
|
289
|
+
CONNECT_RETRY_DELAYS_MS,
|
|
290
|
+
McpServerConnection,
|
|
291
|
+
MissingSecret,
|
|
292
|
+
errorMessage,
|
|
293
|
+
isMcpApp,
|
|
294
|
+
isTransient
|
|
295
|
+
};
|
|
296
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/connection.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n type CallToolResult,\n JSON_RPC_ERROR_CODES,\n type ListResourcesResult,\n type ListResourceTemplatesResult,\n McpAuthRequiredError,\n McpClient,\n McpError,\n McpHttpError,\n type McpRequestOptions,\n McpSessionExpiredError,\n type Tool as McpTool,\n type McpTransport,\n type ReadResourceResult,\n type Resource,\n type ResourceTemplate,\n} from '@earendil-works/pi-mcp';\nimport { DEFAULT_TIMEOUT_SECONDS, type McpServerEntry } from './config.ts';\n\nexport type ServerState =\n | 'connecting'\n | 'connected'\n | 'needs-auth'\n | 'needs-secret'\n | 'failed'\n | 'closed';\n\nexport type TransportFactory = (\n entry: McpServerEntry\n) => Promise<{ transport: McpTransport; secrets: string[] } | { missing: string[] }>;\n\nexport const CONNECT_RETRY_DELAYS_MS = [250, 1_000];\nexport const CLIENT_NAME = 'slicc';\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function isTransient(error: unknown): boolean {\n if (error instanceof McpHttpError)\n return (\n error.status === 408 || error.status === 429 || (error.status >= 500 && error.status !== 501)\n );\n return error instanceof TypeError;\n}\n\nexport function isMcpApp(item: { uri?: string; uriTemplate?: string; mimeType?: string }): boolean {\n const uri = item.uri ?? item.uriTemplate ?? '';\n return uri.startsWith('ui://') || /;\\s*profile\\s*=\\s*\"?mcp-app\"?/i.test(item.mimeType ?? '');\n}\n\nasync function withoutTemplates<T>(list: () => Promise<T>, empty: T): Promise<T> {\n try {\n return await list();\n } catch (error) {\n if (error instanceof McpError && error.code === JSON_RPC_ERROR_CODES.methodNotFound)\n return empty;\n throw error;\n }\n}\n\nconst sleep = (ms: number, signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n\nexport interface ConnectionOptions {\n entry: McpServerEntry;\n version: string;\n transport: TransportFactory;\n onTools: (connection: McpServerConnection) => void;\n onChange?: (connection: McpServerConnection) => void;\n onLog?: (connection: McpServerConnection, params: unknown) => void;\n}\n\nexport class McpServerConnection {\n readonly entry: McpServerEntry;\n state: ServerState = 'connecting';\n error: string | undefined;\n missing: string[] = [];\n tools: McpTool[] = [];\n hasResources = false;\n resources: Resource[] = [];\n resourceTemplates: ResourceTemplate[] = [];\n instructions: string | undefined;\n secrets: string[] = [];\n #client: McpClient | undefined;\n #opening: Promise<McpClient> | undefined;\n readonly #shutdown = new AbortController();\n readonly #busy = new Map<McpClient, number>();\n #seen = false;\n readonly #expired = new Set<McpClient>();\n readonly #options: ConnectionOptions;\n\n constructor(options: ConnectionOptions) {\n this.entry = options.entry;\n this.#options = options;\n }\n\n get name(): string {\n return this.entry.name;\n }\n\n get timeoutMs(): number {\n return (this.entry.config.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1000;\n }\n\n get live(): boolean {\n return this.state === 'connected' || (this.state === 'connecting' && this.#seen);\n }\n\n get closed(): boolean {\n return this.#shutdown.signal.aborted;\n }\n\n getClient(): Promise<McpClient> {\n if (this.closed) return Promise.reject(new Error(`MCP server \"${this.name}\" is shut down`));\n if (this.#client?.connectionState === 'connected') return Promise.resolve(this.#client);\n this.#opening ??= this.#open().finally(() => {\n this.#opening = undefined;\n });\n return this.#opening;\n }\n\n callTool(\n name: string,\n args: Record<string, unknown>,\n options: McpRequestOptions\n ): Promise<CallToolResult> {\n return this.#with((client) => client.callTool(name, args, options));\n }\n\n readResource(uri: string, options: McpRequestOptions): Promise<ReadResourceResult> {\n return this.#with((client) => client.readResource(uri, options), true);\n }\n\n resourcesPage(\n cursor: string | undefined,\n options: McpRequestOptions\n ): Promise<ListResourcesResult> {\n return this.#with((client) => client.listResourcesPage(cursor, options), true);\n }\n\n resourceTemplatesPage(\n cursor: string | undefined,\n options: McpRequestOptions\n ): Promise<ListResourceTemplatesResult> {\n return this.#with(\n (client) =>\n withoutTemplates(() => client.listResourceTemplatesPage(cursor, options), {\n resourceTemplates: [],\n }),\n true\n );\n }\n\n allResources(options: McpRequestOptions): Promise<Resource[]> {\n return this.#with((client) => client.listResources(options), true);\n }\n\n allResourceTemplates(options: McpRequestOptions): Promise<ResourceTemplate[]> {\n return this.#with(\n (client) => withoutTemplates(() => client.listResourceTemplates(options), []),\n true\n );\n }\n\n async #with<T>(run: (client: McpClient) => Promise<T>, readOnly = false): Promise<T> {\n for (let attempt = 1; ; attempt++) {\n const client = await this.getClient();\n this.#busy.set(client, (this.#busy.get(client) ?? 0) + 1);\n try {\n return await run(client);\n } catch (error) {\n if (readOnly && attempt === 1 && error instanceof McpHttpError && isTransient(error)) {\n await sleep(CONNECT_RETRY_DELAYS_MS[0] as number, this.#shutdown.signal);\n continue;\n }\n if (error instanceof McpSessionExpiredError && attempt === 1) {\n if (this.#client === client) {\n this.#client = undefined;\n this.#expired.add(client);\n }\n continue;\n }\n if (!(error instanceof McpAuthRequiredError)) throw error;\n await this.#drop(client);\n this.#mark('needs-auth');\n throw new Error(this.signInMessage());\n } finally {\n this.#release(client);\n }\n }\n }\n\n #release(client: McpClient): void {\n const left = (this.#busy.get(client) as number) - 1;\n this.#busy.set(client, left);\n if (left > 0 || !this.#expired.delete(client)) return;\n this.#busy.delete(client);\n void this.#drop(client);\n }\n\n signInMessage(): string {\n return `MCP server \"${this.name}\" rejected the request as unauthorized (401). Check its token in Settings \u203A Accounts.`;\n }\n\n #mark(state: ServerState, error?: string): void {\n this.state = state;\n this.error = error;\n this.#options.onChange?.(this);\n }\n\n async #drop(client: McpClient): Promise<void> {\n if (this.#client === client) this.#client = undefined;\n await client.close().catch(() => undefined);\n }\n\n async #open(): Promise<McpClient> {\n this.#mark('connecting');\n for (let attempt = 0; ; attempt++) {\n try {\n return await this.#connectOnce();\n } catch (error) {\n const delay = CONNECT_RETRY_DELAYS_MS[attempt];\n if (this.closed || delay === undefined || !isTransient(error)) throw this.#failed(error);\n await sleep(delay, this.#shutdown.signal);\n if (this.closed) throw this.#failed(error);\n }\n }\n }\n\n async #connectOnce(): Promise<McpClient> {\n const made = await this.#options.transport(this.entry);\n if ('missing' in made) {\n this.missing = made.missing;\n throw new MissingSecret(made.missing);\n }\n this.secrets = made.secrets;\n const client = new McpClient({\n name: CLIENT_NAME,\n version: this.#options.version,\n requestTimeoutMs: this.timeoutMs,\n });\n const log = this.#options.onLog;\n if (log) client.onNotification('notifications/message', (params) => log(this, params));\n let closing: Promise<void> | undefined;\n const closeClient = () => {\n closing ??= client.close().catch(() => undefined);\n return closing;\n };\n this.#shutdown.signal.addEventListener('abort', closeClient, { once: true });\n try {\n await client.connect(made.transport);\n client.onNotification('notifications/tools/list_changed', () => {\n void this.#refreshTools(client);\n });\n client.onNotification('notifications/resources/list_changed', () => {\n void this.#refreshResources(client);\n });\n const hasResources = client.serverCapabilities?.resources !== undefined;\n const [tools, resources] = await Promise.all([\n client.serverCapabilities?.tools ? client.listTools() : [],\n hasResources ? this.#fetchResources(client) : { resources: [], resourceTemplates: [] },\n ]);\n if (this.closed) throw new Error('shut down while connecting');\n if (client.connectionState !== 'connected') throw new Error('connection closed during setup');\n this.#client = client;\n this.tools = tools;\n this.hasResources = hasResources;\n this.resources = resources.resources;\n this.resourceTemplates = resources.resourceTemplates;\n this.instructions = client.instructions?.trim() || undefined;\n this.missing = [];\n this.#seen = true;\n this.state = 'connected';\n this.error = undefined;\n this.#options.onTools(this);\n this.#options.onChange?.(this);\n return client;\n } catch (error) {\n await closeClient();\n throw error;\n } finally {\n this.#shutdown.signal.removeEventListener('abort', closeClient);\n }\n }\n\n async #fetchResources(client: McpClient) {\n const [resources, resourceTemplates] = await Promise.all([\n client.listResources().catch(() => [] as Resource[]),\n withoutTemplates(() => client.listResourceTemplates(), [] as ResourceTemplate[]).catch(\n () => [] as ResourceTemplate[]\n ),\n ]);\n return {\n resources: resources.filter((resource) => !isMcpApp(resource)),\n resourceTemplates: resourceTemplates.filter((template) => !isMcpApp(template)),\n };\n }\n\n #failed(error: unknown): Error {\n if (error instanceof MissingSecret) {\n this.#mark('needs-secret');\n return new Error(error.message);\n }\n if (error instanceof McpAuthRequiredError && !this.closed) {\n this.#mark('needs-auth');\n return new Error(this.signInMessage());\n }\n this.#mark(this.closed ? 'closed' : 'failed', errorMessage(error));\n return new Error(`MCP server \"${this.name}\" failed to connect: ${this.error}`);\n }\n\n async #refreshTools(client: McpClient): Promise<void> {\n try {\n const tools = await client.listTools();\n if (this.#client !== client || this.closed) return;\n this.tools = tools;\n this.#options.onTools(this);\n } catch (error) {\n this.error = `Failed to refresh tools: ${errorMessage(error)}`;\n }\n this.#options.onChange?.(this);\n }\n\n async #refreshResources(client: McpClient): Promise<void> {\n const { resources, resourceTemplates } = await this.#fetchResources(client);\n if (this.#client !== client || this.closed) return;\n this.resources = resources;\n this.resourceTemplates = resourceTemplates;\n this.#options.onTools(this);\n this.#options.onChange?.(this);\n }\n\n async close(): Promise<void> {\n this.#shutdown.abort();\n this.state = 'closed';\n this.#options.onChange?.(this);\n const client = this.#client;\n this.#client = undefined;\n await Promise.allSettled([client?.close(), this.#opening]);\n }\n}\n\nexport class MissingSecret extends Error {\n readonly names: string[];\n\n constructor(names: string[]) {\n super(\n `needs ${names.map((name) => `\\${${name}}`).join(', ')}: add the value in Settings \u203A Accounts`\n );\n this.names = names;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { toLlmContent } from "@earendil-works/pi-mcp";
|
|
2
|
+
const MCP_OUTPUT_MAX_BYTES = 20 * 1024;
|
|
3
|
+
const READ_MCP_RESOURCE = "read_mcp_resource";
|
|
4
|
+
function formatSize(bytes) {
|
|
5
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
6
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
7
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
8
|
+
}
|
|
9
|
+
function truncateMiddle(content, maxBytes) {
|
|
10
|
+
const bytes = new TextEncoder().encode(content);
|
|
11
|
+
const totalLines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
|
|
12
|
+
if (bytes.length <= maxBytes)
|
|
13
|
+
return { content, truncated: false, totalBytes: bytes.length, totalLines };
|
|
14
|
+
const boundary = (index) => index >= bytes.length || (bytes[index] & 192) !== 128;
|
|
15
|
+
let headEnd = Math.floor(maxBytes / 2);
|
|
16
|
+
while (headEnd > 0 && !boundary(headEnd)) headEnd--;
|
|
17
|
+
let tailStart = bytes.length - (maxBytes - Math.floor(maxBytes / 2));
|
|
18
|
+
while (tailStart < bytes.length && !boundary(tailStart)) tailStart++;
|
|
19
|
+
const decoder = new TextDecoder();
|
|
20
|
+
const head = decoder.decode(bytes.subarray(0, headEnd));
|
|
21
|
+
const tail = decoder.decode(bytes.subarray(tailStart));
|
|
22
|
+
const removed = Array.from(decoder.decode(bytes.subarray(headEnd, tailStart))).length;
|
|
23
|
+
return {
|
|
24
|
+
content: `${head}\u2026${removed} chars truncated\u2026${tail}`,
|
|
25
|
+
truncated: true,
|
|
26
|
+
totalBytes: bytes.length,
|
|
27
|
+
totalLines
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function textOf(content) {
|
|
31
|
+
return content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
32
|
+
}
|
|
33
|
+
async function limitContent(content, save) {
|
|
34
|
+
const combined = textOf(content);
|
|
35
|
+
const cut = truncateMiddle(combined, MCP_OUTPUT_MAX_BYTES);
|
|
36
|
+
if (!cut.truncated) return { content };
|
|
37
|
+
let fullOutputPath;
|
|
38
|
+
let where;
|
|
39
|
+
try {
|
|
40
|
+
fullOutputPath = await save(combined, ".txt");
|
|
41
|
+
where = `[Full output: ${fullOutputPath} (read it with offset/limit)]`;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
where = `[Could not save the full output: ${error instanceof Error ? error.message : String(error)}]`;
|
|
44
|
+
}
|
|
45
|
+
const tokens = Math.ceil(cut.totalBytes / 4);
|
|
46
|
+
const text = `Warning: truncated output (original token count: ${tokens})
|
|
47
|
+
Total output lines: ${cut.totalLines}
|
|
48
|
+
|
|
49
|
+
${cut.content}
|
|
50
|
+
|
|
51
|
+
${where}`;
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text }, ...content.filter((block) => block.type === "image")],
|
|
54
|
+
...fullOutputPath ? { fullOutputPath } : {}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function extensionOf(uri) {
|
|
58
|
+
const path = URL.canParse(uri) ? new URL(uri).pathname : uri;
|
|
59
|
+
return /\.[A-Za-z0-9]{1,8}$/.exec(path)?.[0] ?? ".bin";
|
|
60
|
+
}
|
|
61
|
+
function isTextMime(mimeType) {
|
|
62
|
+
if (!mimeType) return false;
|
|
63
|
+
const type = mimeType.split(";", 1)[0].trim().toLowerCase();
|
|
64
|
+
return type.startsWith("text/") || type === "application/json" || type.endsWith("+json") || type.endsWith("+xml");
|
|
65
|
+
}
|
|
66
|
+
function decode(base64) {
|
|
67
|
+
const binary = atob(base64);
|
|
68
|
+
const out = new Uint8Array(binary.length);
|
|
69
|
+
for (let at = 0; at < binary.length; at++) out[at] = binary.charCodeAt(at);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
async function blockContent(server, block, save, readable) {
|
|
73
|
+
if (block.type === "resource_link") {
|
|
74
|
+
const details = [
|
|
75
|
+
block.mimeType,
|
|
76
|
+
block.size === void 0 ? void 0 : formatSize(block.size)
|
|
77
|
+
].filter(Boolean);
|
|
78
|
+
const read = readable ? `. Read it with ${READ_MCP_RESOURCE} (server "${server}")` : "";
|
|
79
|
+
const description = block.description ? `: ${block.description}` : "";
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `[Resource ${block.uri} "${block.title ?? block.name}"${details.length ? ` (${details.join(", ")})` : ""}${description}${read}]`
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
if (block.type === "resource" && "blob" in block.resource && !block.resource.mimeType?.startsWith("image/")) {
|
|
88
|
+
const { uri, mimeType, blob } = block.resource;
|
|
89
|
+
const data = decode(blob);
|
|
90
|
+
if (isTextMime(mimeType)) return [{ type: "text", text: new TextDecoder().decode(data) }];
|
|
91
|
+
const kind = `${mimeType ?? "unknown type"}, ${formatSize(data.length)}`;
|
|
92
|
+
try {
|
|
93
|
+
const path = await save(data, extensionOf(uri));
|
|
94
|
+
return [{ type: "text", text: `[Binary resource ${uri} (${kind}) saved to ${path}]` }];
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
97
|
+
return [
|
|
98
|
+
{ type: "text", text: `[Binary resource ${uri} (${kind}) could not be saved: ${reason}]` }
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return toLlmContent({ content: [block] });
|
|
103
|
+
}
|
|
104
|
+
async function modelContent(server, blocks, save, readable = false) {
|
|
105
|
+
const parts = await Promise.all(
|
|
106
|
+
blocks.map((block) => blockContent(server, block, save, readable))
|
|
107
|
+
);
|
|
108
|
+
return parts.flat();
|
|
109
|
+
}
|
|
110
|
+
async function convertResult(server, tool, result, save, readable = false) {
|
|
111
|
+
const converted = result.content.length > 0 ? await modelContent(server, result.content, save, readable) : toLlmContent(result);
|
|
112
|
+
if (result.isError && textOf(converted) === "")
|
|
113
|
+
converted.push({ type: "text", text: `MCP tool ${server}/${tool} returned an error` });
|
|
114
|
+
const limited = await limitContent(converted, save);
|
|
115
|
+
return { ...limited, isError: result.isError === true };
|
|
116
|
+
}
|
|
117
|
+
function scriptResult(result) {
|
|
118
|
+
const { _meta: _ignored, ...rest } = result;
|
|
119
|
+
return rest;
|
|
120
|
+
}
|
|
121
|
+
function spiller(write, id, context) {
|
|
122
|
+
return async (data, extension) => {
|
|
123
|
+
const path = `/tmp/slicc-mcp-${id().replace(/[^\w-]/g, "_")}${extension}`;
|
|
124
|
+
if (!await write(path, data, context)) throw new Error(`could not write ${path}`);
|
|
125
|
+
return path;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
MCP_OUTPUT_MAX_BYTES,
|
|
130
|
+
READ_MCP_RESOURCE,
|
|
131
|
+
convertResult,
|
|
132
|
+
formatSize,
|
|
133
|
+
limitContent,
|
|
134
|
+
modelContent,
|
|
135
|
+
scriptResult,
|
|
136
|
+
spiller,
|
|
137
|
+
truncateMiddle
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/content.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ImageContent, TextContent } from '@earendil-works/pi-ai';\nimport { type CallToolResult, type ContentBlock, toLlmContent } from '@earendil-works/pi-mcp';\n\nexport const MCP_OUTPUT_MAX_BYTES = 20 * 1024;\nexport const READ_MCP_RESOURCE = 'read_mcp_resource';\n\nexport type Content = TextContent | ImageContent;\n\nexport type Saver = (data: string | Uint8Array, extension: string) => Promise<string>;\n\nexport interface Truncation {\n content: string;\n truncated: boolean;\n totalBytes: number;\n totalLines: number;\n}\n\nexport function formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes}B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n}\n\nexport function truncateMiddle(content: string, maxBytes: number): Truncation {\n const bytes = new TextEncoder().encode(content);\n const totalLines = content === '' ? 0 : content.replace(/\\n$/, '').split('\\n').length;\n if (bytes.length <= maxBytes)\n return { content, truncated: false, totalBytes: bytes.length, totalLines };\n const boundary = (index: number) =>\n index >= bytes.length || ((bytes[index] as number) & 0xc0) !== 0x80;\n let headEnd = Math.floor(maxBytes / 2);\n while (headEnd > 0 && !boundary(headEnd)) headEnd--;\n let tailStart = bytes.length - (maxBytes - Math.floor(maxBytes / 2));\n while (tailStart < bytes.length && !boundary(tailStart)) tailStart++;\n const decoder = new TextDecoder();\n const head = decoder.decode(bytes.subarray(0, headEnd));\n const tail = decoder.decode(bytes.subarray(tailStart));\n const removed = Array.from(decoder.decode(bytes.subarray(headEnd, tailStart))).length;\n return {\n content: `${head}\u2026${removed} chars truncated\u2026${tail}`,\n truncated: true,\n totalBytes: bytes.length,\n totalLines,\n };\n}\n\nfunction textOf(content: readonly Content[]): string {\n return content\n .filter((block): block is TextContent => block.type === 'text')\n .map((block) => block.text)\n .join('\\n');\n}\n\nexport async function limitContent(\n content: Content[],\n save: Saver\n): Promise<{ content: Content[]; fullOutputPath?: string }> {\n const combined = textOf(content);\n const cut = truncateMiddle(combined, MCP_OUTPUT_MAX_BYTES);\n if (!cut.truncated) return { content };\n let fullOutputPath: string | undefined;\n let where: string;\n try {\n fullOutputPath = await save(combined, '.txt');\n where = `[Full output: ${fullOutputPath} (read it with offset/limit)]`;\n } catch (error) {\n where = `[Could not save the full output: ${error instanceof Error ? error.message : String(error)}]`;\n }\n const tokens = Math.ceil(cut.totalBytes / 4);\n const text = `Warning: truncated output (original token count: ${tokens})\\nTotal output lines: ${cut.totalLines}\\n\\n${cut.content}\\n\\n${where}`;\n return {\n content: [{ type: 'text', text }, ...content.filter((block) => block.type === 'image')],\n ...(fullOutputPath ? { fullOutputPath } : {}),\n };\n}\n\nfunction extensionOf(uri: string): string {\n const path = URL.canParse(uri) ? new URL(uri).pathname : uri;\n return /\\.[A-Za-z0-9]{1,8}$/.exec(path)?.[0] ?? '.bin';\n}\n\nfunction isTextMime(mimeType: string | undefined): boolean {\n if (!mimeType) return false;\n const type = (mimeType.split(';', 1)[0] as string).trim().toLowerCase();\n return (\n type.startsWith('text/') ||\n type === 'application/json' ||\n type.endsWith('+json') ||\n type.endsWith('+xml')\n );\n}\n\nfunction decode(base64: string): Uint8Array {\n const binary = atob(base64);\n const out = new Uint8Array(binary.length);\n for (let at = 0; at < binary.length; at++) out[at] = binary.charCodeAt(at);\n return out;\n}\n\nasync function blockContent(\n server: string,\n block: ContentBlock,\n save: Saver,\n readable: boolean\n): Promise<Content[]> {\n if (block.type === 'resource_link') {\n const details = [\n block.mimeType,\n block.size === undefined ? undefined : formatSize(block.size),\n ].filter(Boolean);\n const read = readable ? `. Read it with ${READ_MCP_RESOURCE} (server \"${server}\")` : '';\n const description = block.description ? `: ${block.description}` : '';\n return [\n {\n type: 'text',\n text: `[Resource ${block.uri} \"${block.title ?? block.name}\"${details.length ? ` (${details.join(', ')})` : ''}${description}${read}]`,\n },\n ];\n }\n if (\n block.type === 'resource' &&\n 'blob' in block.resource &&\n !block.resource.mimeType?.startsWith('image/')\n ) {\n const { uri, mimeType, blob } = block.resource;\n const data = decode(blob);\n if (isTextMime(mimeType)) return [{ type: 'text', text: new TextDecoder().decode(data) }];\n const kind = `${mimeType ?? 'unknown type'}, ${formatSize(data.length)}`;\n try {\n const path = await save(data, extensionOf(uri));\n return [{ type: 'text', text: `[Binary resource ${uri} (${kind}) saved to ${path}]` }];\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n return [\n { type: 'text', text: `[Binary resource ${uri} (${kind}) could not be saved: ${reason}]` },\n ];\n }\n }\n return toLlmContent({ content: [block] }) as Content[];\n}\n\nexport async function modelContent(\n server: string,\n blocks: readonly ContentBlock[],\n save: Saver,\n readable = false\n): Promise<Content[]> {\n const parts = await Promise.all(\n blocks.map((block) => blockContent(server, block, save, readable))\n );\n return parts.flat();\n}\n\nexport async function convertResult(\n server: string,\n tool: string,\n result: CallToolResult,\n save: Saver,\n readable = false\n): Promise<{ content: Content[]; isError: boolean; fullOutputPath?: string }> {\n const converted =\n result.content.length > 0\n ? await modelContent(server, result.content, save, readable)\n : (toLlmContent(result) as Content[]);\n if (result.isError && textOf(converted) === '')\n converted.push({ type: 'text', text: `MCP tool ${server}/${tool} returned an error` });\n const limited = await limitContent(converted, save);\n return { ...limited, isError: result.isError === true };\n}\n\nexport function scriptResult(result: CallToolResult): Record<string, unknown> {\n const { _meta: _ignored, ...rest } = result as CallToolResult & { _meta?: unknown };\n return rest;\n}\n\nexport function spiller(\n write: (path: string, data: string | Uint8Array, context: Context) => Promise<boolean>,\n id: () => string,\n context: Context\n): Saver {\n return async (data, extension) => {\n const path = `/tmp/slicc-mcp-${id().replace(/[^\\w-]/g, '_')}${extension}`;\n if (!(await write(path, data, context))) throw new Error(`could not write ${path}`);\n return path;\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { placeholders } from "./config.js";
|
|
2
|
+
const MCP_CREDENTIAL_PREFIX = "mcp:";
|
|
3
|
+
async function digest(text) {
|
|
4
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
5
|
+
return [...new Uint8Array(bytes).slice(0, 12)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
}
|
|
7
|
+
async function credentialKey(server, url) {
|
|
8
|
+
return `${MCP_CREDENTIAL_PREFIX}${server}|${await digest(new URL(url).href)}`;
|
|
9
|
+
}
|
|
10
|
+
function secretNames(config) {
|
|
11
|
+
const names = /* @__PURE__ */ new Set();
|
|
12
|
+
for (const value of Object.values(config.headers ?? {}))
|
|
13
|
+
for (const name of placeholders(value)) names.add(name);
|
|
14
|
+
return [...names];
|
|
15
|
+
}
|
|
16
|
+
async function resolveHeaders(server, config, credentials) {
|
|
17
|
+
const names = secretNames(config);
|
|
18
|
+
if (!names.length) return { ok: true, headers: { ...config.headers }, secrets: [] };
|
|
19
|
+
const stored = await credentials.read(await credentialKey(server, config.url));
|
|
20
|
+
const env = stored?.type === "api_key" ? stored.env ?? {} : {};
|
|
21
|
+
const key = stored?.type === "api_key" ? stored.key : void 0;
|
|
22
|
+
const value = (name) => env[name] ?? (names.length === 1 ? key : void 0);
|
|
23
|
+
const missing = names.filter((name) => !value(name));
|
|
24
|
+
if (missing.length) return { ok: false, missing };
|
|
25
|
+
const headers = {};
|
|
26
|
+
for (const [header, text] of Object.entries(config.headers ?? {}))
|
|
27
|
+
headers[header] = text.replace(
|
|
28
|
+
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
29
|
+
(_, name) => String(value(name))
|
|
30
|
+
);
|
|
31
|
+
return { ok: true, headers, secrets: names.map((name) => String(value(name))) };
|
|
32
|
+
}
|
|
33
|
+
function scrub(text, secrets) {
|
|
34
|
+
let out = text;
|
|
35
|
+
for (const secret of secrets) if (secret.length >= 4) out = out.split(secret).join("[redacted]");
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
MCP_CREDENTIAL_PREFIX,
|
|
40
|
+
credentialKey,
|
|
41
|
+
resolveHeaders,
|
|
42
|
+
scrub,
|
|
43
|
+
secretNames
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/credentials.ts"],
|
|
4
|
+
"sourcesContent": ["import type { CredentialStore } from '@earendil-works/pi-ai';\nimport { type McpServerConfig, placeholders } from './config.ts';\n\nexport const MCP_CREDENTIAL_PREFIX = 'mcp:';\n\nasync function digest(text: string): Promise<string> {\n const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));\n return [...new Uint8Array(bytes).slice(0, 12)]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n}\n\nexport async function credentialKey(server: string, url: string): Promise<string> {\n return `${MCP_CREDENTIAL_PREFIX}${server}|${await digest(new URL(url).href)}`;\n}\n\nexport function secretNames(config: McpServerConfig): string[] {\n const names = new Set<string>();\n for (const value of Object.values(config.headers ?? {}))\n for (const name of placeholders(value)) names.add(name);\n return [...names];\n}\n\nexport type Resolved =\n | { ok: true; headers: Record<string, string>; secrets: string[] }\n | { ok: false; missing: string[] };\n\nexport async function resolveHeaders(\n server: string,\n config: McpServerConfig,\n credentials: CredentialStore\n): Promise<Resolved> {\n const names = secretNames(config);\n if (!names.length) return { ok: true, headers: { ...config.headers }, secrets: [] };\n const stored = await credentials.read(await credentialKey(server, config.url));\n const env = stored?.type === 'api_key' ? (stored.env ?? {}) : {};\n const key = stored?.type === 'api_key' ? stored.key : undefined;\n const value = (name: string) => env[name] ?? (names.length === 1 ? key : undefined);\n const missing = names.filter((name) => !value(name));\n if (missing.length) return { ok: false, missing };\n const headers: Record<string, string> = {};\n for (const [header, text] of Object.entries(config.headers ?? {}))\n headers[header] = text.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_, name: string) =>\n String(value(name))\n );\n return { ok: true, headers, secrets: names.map((name) => String(value(name))) };\n}\n\nexport function scrub(text: string, secrets: readonly string[]): string {\n let out = text;\n for (const secret of secrets) if (secret.length >= 4) out = out.split(secret).join('[redacted]');\n return out;\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAA+B,oBAAoB;AAE5C,MAAM,wBAAwB;AAErC,eAAe,OAAO,MAA+B;AACnD,QAAM,QAAQ,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,IAAI,CAAC;AAClF,SAAO,CAAC,GAAG,IAAI,WAAW,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC,EAC1C,IAAI,CAAC,SAAS,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAChD,KAAK,EAAE;AACZ;AAEA,eAAsB,cAAc,QAAgB,KAA8B;AAChF,SAAO,GAAG,qBAAqB,GAAG,MAAM,IAAI,MAAM,OAAO,IAAI,IAAI,GAAG,EAAE,IAAI,CAAC;AAC7E;AAEO,SAAS,YAAY,QAAmC;AAC7D,QAAM,QAAQ,oBAAI,IAAY;AAC9B,aAAW,SAAS,OAAO,OAAO,OAAO,WAAW,CAAC,CAAC;AACpD,eAAW,QAAQ,aAAa,KAAK,EAAG,OAAM,IAAI,IAAI;AACxD,SAAO,CAAC,GAAG,KAAK;AAClB;AAMA,eAAsB,eACpB,QACA,QACA,aACmB;AACnB,QAAM,QAAQ,YAAY,MAAM;AAChC,MAAI,CAAC,MAAM,OAAQ,QAAO,EAAE,IAAI,MAAM,SAAS,EAAE,GAAG,OAAO,QAAQ,GAAG,SAAS,CAAC,EAAE;AAClF,QAAM,SAAS,MAAM,YAAY,KAAK,MAAM,cAAc,QAAQ,OAAO,GAAG,CAAC;AAC7E,QAAM,MAAM,QAAQ,SAAS,YAAa,OAAO,OAAO,CAAC,IAAK,CAAC;AAC/D,QAAM,MAAM,QAAQ,SAAS,YAAY,OAAO,MAAM;AACtD,QAAM,QAAQ,CAAC,SAAiB,IAAI,IAAI,MAAM,MAAM,WAAW,IAAI,MAAM;AACzE,QAAM,UAAU,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC;AACnD,MAAI,QAAQ,OAAQ,QAAO,EAAE,IAAI,OAAO,QAAQ;AAChD,QAAM,UAAkC,CAAC;AACzC,aAAW,CAAC,QAAQ,IAAI,KAAK,OAAO,QAAQ,OAAO,WAAW,CAAC,CAAC;AAC9D,YAAQ,MAAM,IAAI,KAAK;AAAA,MAAQ;AAAA,MAAmC,CAAC,GAAG,SACpE,OAAO,MAAM,IAAI,CAAC;AAAA,IACpB;AACF,SAAO,EAAE,IAAI,MAAM,SAAS,SAAS,MAAM,IAAI,CAAC,SAAS,OAAO,MAAM,IAAI,CAAC,CAAC,EAAE;AAChF;AAEO,SAAS,MAAM,MAAc,SAAoC;AACtE,MAAI,MAAM;AACV,aAAW,UAAU,QAAS,KAAI,OAAO,UAAU,EAAG,OAAM,IAAI,MAAM,MAAM,EAAE,KAAK,YAAY;AAC/F,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseMcpConfig } from "./config.js";
|
|
2
|
+
import { allows, McpAllowDoc, mcpEntries, serverAllowed } from "./allow.js";
|
|
3
|
+
const loadRuntime = () => import("./runtime.js");
|
|
4
|
+
const idle = {
|
|
5
|
+
status: () => [],
|
|
6
|
+
reload: async () => void 0,
|
|
7
|
+
close: async () => void 0
|
|
8
|
+
};
|
|
9
|
+
async function unavailable(options, error, context) {
|
|
10
|
+
const file = `${options.home}/.pi/agent/mcp.json`;
|
|
11
|
+
const read = await options.env.readTextFile(file, context);
|
|
12
|
+
if (!parseMcpConfig(read.ok ? read.value : void 0).servers.length) return;
|
|
13
|
+
await options.licks.deliver(
|
|
14
|
+
{
|
|
15
|
+
channel: "fswatch",
|
|
16
|
+
source: "mcp:~/.pi/agent/mcp.json",
|
|
17
|
+
title: "MCP servers can't start in this SLICC",
|
|
18
|
+
text: error instanceof Error ? error.message : String(error),
|
|
19
|
+
body: "The MCP client didn't load, so the servers in ~/.pi/agent/mcp.json stay off until SLICC is updated.",
|
|
20
|
+
target: "cone",
|
|
21
|
+
severity: "warn",
|
|
22
|
+
eventId: "mcp\nunavailable"
|
|
23
|
+
},
|
|
24
|
+
context
|
|
25
|
+
).catch(() => void 0);
|
|
26
|
+
}
|
|
27
|
+
function setupMcp(registry, load = loadRuntime) {
|
|
28
|
+
let catalog;
|
|
29
|
+
return {
|
|
30
|
+
catalog: () => catalog,
|
|
31
|
+
direct: (names) => catalog?.direct(names) ?? [],
|
|
32
|
+
codemode: async (api, agent, context) => catalog ? catalog.codemode(api, agent, context) : { tools: [], globals: [] },
|
|
33
|
+
async attach(options, context) {
|
|
34
|
+
let started;
|
|
35
|
+
try {
|
|
36
|
+
started = await (await load()).startMcp(registry, options, context);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
await unavailable(options, error, context);
|
|
39
|
+
return idle;
|
|
40
|
+
}
|
|
41
|
+
catalog = started.catalog;
|
|
42
|
+
return started.runtime;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
McpAllowDoc,
|
|
48
|
+
allows,
|
|
49
|
+
loadRuntime,
|
|
50
|
+
mcpEntries,
|
|
51
|
+
serverAllowed,
|
|
52
|
+
setupMcp
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type {\n Agent,\n Registry,\n ToolExecutionApi,\n ToolRegistration,\n} from '@earendil-works/pi-durable';\nimport type { CodemodeExtra } from './allow.ts';\nimport type { McpCatalog } from './catalog.ts';\nimport { parseMcpConfig } from './config.ts';\nimport type { McpAttach, McpRuntime } from './runtime.ts';\n\nexport { allows, type CodemodeExtra, McpAllowDoc, mcpEntries, serverAllowed } from './allow.ts';\nexport type { McpAttach, McpRuntime, ServerStatus } from './runtime.ts';\n\nexport type McpLoader = () => Promise<typeof import('./runtime.ts')>;\n\nexport const loadRuntime: McpLoader = () => import('./runtime.ts');\n\nexport interface McpSetup {\n catalog(): McpCatalog | undefined;\n direct(names: readonly string[]): ToolRegistration[];\n codemode(api: ToolExecutionApi, agent: Agent, context: Context): Promise<CodemodeExtra>;\n attach(options: McpAttach, context: Context): Promise<McpRuntime>;\n}\n\nconst idle: McpRuntime = {\n status: () => [],\n reload: async () => undefined,\n close: async () => undefined,\n};\n\nasync function unavailable(options: McpAttach, error: unknown, context: Context) {\n const file = `${options.home}/.pi/agent/mcp.json`;\n const read = await options.env.readTextFile(file, context);\n if (!parseMcpConfig(read.ok ? read.value : undefined).servers.length) return;\n await options.licks\n .deliver(\n {\n channel: 'fswatch',\n source: 'mcp:~/.pi/agent/mcp.json',\n title: \"MCP servers can't start in this SLICC\",\n text: error instanceof Error ? error.message : String(error),\n body: \"The MCP client didn't load, so the servers in ~/.pi/agent/mcp.json stay off until SLICC is updated.\",\n target: 'cone',\n severity: 'warn',\n eventId: 'mcp\\nunavailable',\n },\n context\n )\n .catch(() => undefined);\n}\n\nexport function setupMcp(registry: Registry, load: McpLoader = loadRuntime): McpSetup {\n let catalog: McpCatalog | undefined;\n return {\n catalog: () => catalog,\n direct: (names) => catalog?.direct(names) ?? [],\n codemode: async (api, agent, context) =>\n catalog ? catalog.codemode(api, agent, context) : { tools: [], globals: [] },\n async attach(options, context) {\n let started: Awaited<ReturnType<Awaited<ReturnType<McpLoader>>['startMcp']>>;\n try {\n started = await (await load()).startMcp(registry, options, context);\n } catch (error) {\n await unavailable(options, error, context);\n return idle;\n }\n catalog = started.catalog;\n return started.runtime;\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AASA,SAAS,sBAAsB;AAG/B,SAAS,QAA4B,aAAa,YAAY,qBAAqB;AAK5E,MAAM,cAAyB,MAAM,OAAO,cAAc;AASjE,MAAM,OAAmB;AAAA,EACvB,QAAQ,MAAM,CAAC;AAAA,EACf,QAAQ,YAAY;AAAA,EACpB,OAAO,YAAY;AACrB;AAEA,eAAe,YAAY,SAAoB,OAAgB,SAAkB;AAC/E,QAAM,OAAO,GAAG,QAAQ,IAAI;AAC5B,QAAM,OAAO,MAAM,QAAQ,IAAI,aAAa,MAAM,OAAO;AACzD,MAAI,CAAC,eAAe,KAAK,KAAK,KAAK,QAAQ,MAAS,EAAE,QAAQ,OAAQ;AACtE,QAAM,QAAQ,MACX;AAAA,IACC;AAAA,MACE,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC3D,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS;AAAA,IACX;AAAA,IACA;AAAA,EACF,EACC,MAAM,MAAM,MAAS;AAC1B;AAEO,SAAS,SAAS,UAAoB,OAAkB,aAAuB;AACpF,MAAI;AACJ,SAAO;AAAA,IACL,SAAS,MAAM;AAAA,IACf,QAAQ,CAAC,UAAU,SAAS,OAAO,KAAK,KAAK,CAAC;AAAA,IAC9C,UAAU,OAAO,KAAK,OAAO,YAC3B,UAAU,QAAQ,SAAS,KAAK,OAAO,OAAO,IAAI,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,EAAE;AAAA,IAC7E,MAAM,OAAO,SAAS,SAAS;AAC7B,UAAI;AACJ,UAAI;AACF,kBAAU,OAAO,MAAM,KAAK,GAAG,SAAS,UAAU,SAAS,OAAO;AAAA,MACpE,SAAS,OAAO;AACd,cAAM,YAAY,SAAS,OAAO,OAAO;AACzC,eAAO;AAAA,MACT;AACA,gBAAU,QAAQ;AAClB,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|