dsh-agent-plugins-market 0.7.0 → 0.7.2
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 +31 -26
- package/README.zh.md +31 -26
- package/client/client.js +29164 -1136
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +8 -1
- package/lib/application/catalog.js +27 -2
- package/lib/application/details.js +13 -2
- package/lib/application/install-store.js +1 -1
- package/lib/application/mcp-service.js +191 -19
- package/lib/application/panel-resources.js +16 -4
- package/lib/application/project-agent-roles.js +7 -1
- package/lib/catalog/archive.js +16 -1
- package/lib/catalog/manifests.js +124 -13
- package/lib/catalog/paths.js +8 -0
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-hooks.js +14 -1
- package/lib/catalog/scan-resolvers.js +117 -12
- package/lib/catalog/skills-parse.js +47 -0
- package/lib/catalog/source-catalog.js +8 -3
- package/lib/catalog/suite-components.js +2 -2
- package/lib/catalog/surfaces.js +73 -27
- package/lib/catalog/validate.js +205 -54
- package/lib/contracts/market.js +3 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +32 -13
- package/lib/model/layouts.js +44 -1
- package/lib/model/preset-source.js +8 -0
- package/lib/routes.js +43 -2
- package/lib/runtime/agent-role-router.js +203 -56
- package/lib/runtime/commands-mounts.js +50 -9
- package/lib/runtime/deadline.js +32 -6
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +7 -3
- package/lib/runtime/host-locale.js +45 -15
- package/lib/runtime/lsp-mounts.js +23 -2
- package/lib/runtime/mcp-backend.js +14 -15
- package/lib/runtime/mcp-client/config.js +8 -0
- package/lib/runtime/mcp-client/connection.js +9 -8
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-client/oauth.js +82 -14
- package/lib/runtime/mcp-client/tools.js +2 -1
- package/lib/runtime/mcp-config.js +182 -35
- package/lib/runtime/mcp-direct-config.js +49 -6
- package/lib/runtime/mcp-mounts.js +15 -3
- package/lib/runtime/mcp-overrides.js +131 -1
- package/lib/runtime/mcp-redaction.js +8 -0
- package/lib/runtime/mcp-status.js +21 -5
- package/lib/runtime/project-runtime.js +23 -9
- package/lib/runtime/reconcile-scheduler.js +6 -8
- package/lib/runtime/reconciler.js +2 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/server-config.js +49 -8
- package/lib/runtime/settings-namespace.js +21 -13
- package/lib/runtime/skills-provider.js +33 -11
- package/lib/runtime/source-auto-update.js +10 -14
- package/lib/runtime/subagent-catalog.js +62 -22
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +24 -20
- package/lib/runtime/user-commands.js +5 -1
- package/lib/runtime/user-panels.js +110 -41
- package/lib/runtime/user-store.js +114 -58
- package/lib/types/application/catalog-context.d.ts +6 -0
- package/lib/types/application/catalog.d.ts +19 -1
- package/lib/types/application/install-store.d.ts +1 -1
- package/lib/types/application/mcp-service.d.ts +38 -3
- package/lib/types/application/ports.d.ts +1 -1
- package/lib/types/application/queries.d.ts +8 -1
- package/lib/types/catalog/manifests.d.ts +6 -2
- package/lib/types/catalog/paths.d.ts +6 -0
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/skills-parse.d.ts +13 -0
- package/lib/types/catalog/source-catalog.d.ts +7 -2
- package/lib/types/catalog/surfaces.d.ts +14 -4
- package/lib/types/catalog/validate.d.ts +72 -11
- package/lib/types/contracts/market.d.ts +77 -0
- package/lib/types/contracts/mcp-status.d.ts +11 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +30 -0
- package/lib/types/model/preset-source.d.ts +18 -0
- package/lib/types/model/types.d.ts +37 -1
- package/lib/types/runtime/agent-role-router.d.ts +96 -18
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +10 -11
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/host-locale.d.ts +20 -2
- package/lib/types/runtime/lsp-mounts.d.ts +7 -0
- package/lib/types/runtime/mcp-backend.d.ts +8 -15
- package/lib/types/runtime/mcp-client/config.d.ts +8 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
- package/lib/types/runtime/mcp-config.d.ts +75 -13
- package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
- package/lib/types/runtime/mcp-overrides.d.ts +44 -0
- package/lib/types/runtime/mcp-redaction.d.ts +6 -0
- package/lib/types/runtime/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/server-config.d.ts +26 -1
- package/lib/types/runtime/settings-namespace.d.ts +10 -10
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +3 -4
- package/lib/types/runtime/subagent-catalog.d.ts +14 -2
- package/lib/types/runtime/timer-seat.d.ts +51 -0
- package/lib/types/runtime/tool-registry-observer.d.ts +14 -6
- package/lib/types/runtime/user-panels.d.ts +65 -12
- package/lib/types/runtime/user-store.d.ts +65 -16
- package/package.json +23 -6
- package/schemas/1.0.0/mcp.schema.json +0 -22
- package/schemas/1.1.0/mcp.schema.json +120 -0
- package/schemas/1.1.0/plugin.schema.json +65 -0
- package/schemas/README.md +25 -19
- package/schemas/agent-plugins/spec.md +23 -9
- package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
- package/schemas/com.deepseek.harness/spec.md +52 -0
|
@@ -1,88 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* host change can be re-diffed against `~/workspace/deepseek-harness`.
|
|
10
|
-
*
|
|
11
|
-
* Anything larger than a handful of lines (the JSON Schema subset validator)
|
|
12
|
-
* lives in its own module.
|
|
13
|
-
*
|
|
14
|
-
* @module runtime/mcp-client/host-seams
|
|
15
|
-
*/
|
|
16
|
-
// ---- dsh-timeout: MAX_TIMER_DELAY_MS (packages/util/timeout/src/index.ts) ----
|
|
17
|
-
/**
|
|
18
|
-
* The largest delay `setTimeout` accepts without overflowing to a 1ms clamp
|
|
19
|
-
* (2^31 - 1). Shared guard for reconnect and callback timers.
|
|
20
|
-
*/
|
|
21
|
-
export const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
22
|
-
// ---- dsh-subprocess: scrubbedParentEnv (packages/subprocess/subprocess/src/index.ts) ----
|
|
23
|
-
/** `DSH_*` environment namespace owned by the harness. */
|
|
24
|
-
export const DSH_ENV_PREFIX = 'DSH_';
|
|
25
|
-
/**
|
|
26
|
-
* Credential-shaped environment names are NOT forwarded to MCP server child
|
|
27
|
-
* processes (the harness's own `DEEPSEEK_API_KEY`/secrets must not leak into a
|
|
28
|
-
* spawned server implicitly). Deliberately supplied env entries survive
|
|
29
|
-
* because the config's explicit env merges after the scrub.
|
|
30
|
-
*/
|
|
31
|
-
export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i;
|
|
32
|
-
/**
|
|
33
|
-
* The ambient parent environment minus credential-shaped names and minus all
|
|
34
|
-
* `DSH_*` names — the canonical base every harness child starts from. `PATH`,
|
|
35
|
-
* `HOME`, locale, and proxy variables survive, so child CLIs run normally;
|
|
36
|
-
* harness identity never leaks implicitly. Both scrubs match case-insensitively:
|
|
37
|
-
* Windows environment names are case-insensitive, so a parent `dsh_*` entry
|
|
38
|
-
* would otherwise survive and read back as `$env:DSH_*` in the child.
|
|
39
|
-
* @returns a fresh environment object safe to hand to a child spawn.
|
|
40
|
-
*/
|
|
41
|
-
export function scrubbedParentEnv() {
|
|
42
|
-
const env = {};
|
|
43
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
44
|
-
if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.toUpperCase().startsWith(DSH_ENV_PREFIX))
|
|
45
|
-
env[key] = value;
|
|
46
|
-
}
|
|
47
|
-
return env;
|
|
48
|
-
}
|
|
49
|
-
/** Key segments are lowercase hyphenated identifiers. */
|
|
50
|
-
const KEY_SEGMENT_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
51
|
-
/**
|
|
52
|
-
* Brand a scope and an id as a {@link CredentialKey}.
|
|
53
|
-
* @param scope - the owning plugin's registered name (here: the `mcp-auth` record scope).
|
|
54
|
-
* @param id - the plugin's own addressing unit (here: the folded serverName).
|
|
55
|
-
* @returns the branded key.
|
|
56
|
-
* @throws TypeError when either segment is not a lowercase hyphenated identifier.
|
|
57
|
-
*/
|
|
58
|
-
export function credentialKey(scope, id) {
|
|
59
|
-
for (const segment of [scope, id]) {
|
|
60
|
-
if (!KEY_SEGMENT_PATTERN.test(segment)) {
|
|
61
|
-
throw new TypeError(`credential key segment "${segment}" must match ${String(KEY_SEGMENT_PATTERN)}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return `${scope}/${id}`;
|
|
65
|
-
}
|
|
66
|
-
// ---- dsh-attachment: image admission error routing (packages/attachment/attachment/src/error.ts) ----
|
|
67
|
-
/** Caller-correctable attachment failure codes raised while admitting image input. */
|
|
68
|
-
const IMAGE_ADMISSION_ERROR_CODES = [
|
|
69
|
-
'TOO_MANY_IMAGES',
|
|
70
|
-
'IMAGES_TOO_LARGE',
|
|
71
|
-
'UNSUPPORTED_IMAGE_TYPE',
|
|
72
|
-
'INVALID_IMAGE_BASE64',
|
|
73
|
-
'INVALID_IMAGE',
|
|
74
|
-
'IMAGE_TYPE_MISMATCH',
|
|
75
|
-
'IMAGE_TOO_LARGE',
|
|
76
|
-
'IMAGE_TOO_MANY_PIXELS',
|
|
77
|
-
'IMAGE_DIMENSION_TOO_LARGE'
|
|
78
|
-
];
|
|
79
|
-
/** Runtime membership for structurally compatible errors crossing package boundaries. */
|
|
80
|
-
const IMAGE_ADMISSION_ERROR_CODE_SET = new Set(IMAGE_ADMISSION_ERROR_CODES);
|
|
81
|
-
/**
|
|
82
|
-
* Distinguish caller-correctable image admission failures from storage faults,
|
|
83
|
-
* so an admission refusal projects text diagnostics while a storage fault is
|
|
84
|
-
* reported as such.
|
|
85
|
-
*/
|
|
86
|
-
export function isImageAdmissionError(error) {
|
|
87
|
-
return error instanceof Error && 'code' in error && typeof error.code === 'string' && IMAGE_ADMISSION_ERROR_CODE_SET.has(error.code);
|
|
88
|
-
}
|
|
1
|
+
// ---- dsh-timeout: timer bounds ----
|
|
2
|
+
export { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
|
|
3
|
+
// ---- dsh-subprocess: the canonical child environment ----
|
|
4
|
+
export { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess';
|
|
5
|
+
// ---- dsh-credentials: key grammar ----
|
|
6
|
+
export { credentialKey } from '@deepseek-ai/dsh-credentials';
|
|
7
|
+
// ---- dsh-attachment: durable image vocabulary ----
|
|
8
|
+
export { isImageAdmissionError } from '@deepseek-ai/dsh-attachment';
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
* @module runtime/mcp-client/oauth
|
|
29
29
|
*/
|
|
30
30
|
import { createServer } from 'node:http';
|
|
31
|
+
import { randomBytes } from 'node:crypto';
|
|
32
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
31
33
|
import { credentialKey, scrubbedParentEnv } from './host-seams.js';
|
|
34
|
+
import { redactErrorMessage, redactUrl } from '../mcp-redaction.js';
|
|
32
35
|
import { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
|
|
33
36
|
/** OAuth client identity presented during dynamic registration. */
|
|
34
37
|
const CLIENT_NAME = `DeepSeek Harness (${PLUGIN_NAME})`;
|
|
@@ -65,6 +68,18 @@ function asMcpAuthRecord(record) {
|
|
|
65
68
|
...(payload.tokens === undefined ? {} : { tokens: payload.tokens })
|
|
66
69
|
};
|
|
67
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Merge one locally changed durable field without overwriting a concurrent
|
|
73
|
+
* process's update. If the value read before the mutation no longer matches
|
|
74
|
+
* the store's locked current value, the current value wins conservatively.
|
|
75
|
+
*/
|
|
76
|
+
function mergeDurableField(base, desired, current) {
|
|
77
|
+
if (isDeepStrictEqual(base, desired))
|
|
78
|
+
return current;
|
|
79
|
+
if (isDeepStrictEqual(base, current))
|
|
80
|
+
return desired;
|
|
81
|
+
return current;
|
|
82
|
+
}
|
|
68
83
|
/**
|
|
69
84
|
* Error thrown when the human leg fails or is abandoned: the browser never
|
|
70
85
|
* returned, the callback carried an OAuth error, or no browser could open.
|
|
@@ -89,7 +104,7 @@ export const platformBrowserOpener = async (authorizationUrl, serverName, log) =
|
|
|
89
104
|
detached: platform !== 'win32'
|
|
90
105
|
});
|
|
91
106
|
child.once('error', error => {
|
|
92
|
-
log(`${serverName}: could not open a browser automatically (${String(error)}); open this URL manually: ${authorizationUrl.toString()}`);
|
|
107
|
+
log(`${serverName}: could not open a browser automatically (${redactErrorMessage(String(error))}); open this URL manually: ${redactUrl(authorizationUrl.toString())}`);
|
|
93
108
|
});
|
|
94
109
|
child.once('spawn', () => {
|
|
95
110
|
child.unref();
|
|
@@ -113,6 +128,8 @@ export class LoopbackOAuthClientProvider {
|
|
|
113
128
|
providerState = {};
|
|
114
129
|
/** The transport generation whose `finishAuth` completes the browser leg. */
|
|
115
130
|
activeTransport;
|
|
131
|
+
/** One process-local OAuth state value for the current browser leg. */
|
|
132
|
+
expectedState;
|
|
116
133
|
/** Resolves `true` when the leg saved tokens, `false` when it failed; undefined while no leg is open. */
|
|
117
134
|
browserLegSettled;
|
|
118
135
|
callbackServer;
|
|
@@ -148,6 +165,12 @@ export class LoopbackOAuthClientProvider {
|
|
|
148
165
|
software_version: PLUGIN_VERSION
|
|
149
166
|
};
|
|
150
167
|
}
|
|
168
|
+
/** Issue one unpredictable state value for the SDK's authorization request. */
|
|
169
|
+
state() {
|
|
170
|
+
const state = randomBytes(32).toString('base64url');
|
|
171
|
+
this.expectedState = state;
|
|
172
|
+
return state;
|
|
173
|
+
}
|
|
151
174
|
async clientInformation() {
|
|
152
175
|
return (await this.load()).clientInformation;
|
|
153
176
|
}
|
|
@@ -177,6 +200,11 @@ export class LoopbackOAuthClientProvider {
|
|
|
177
200
|
if (transport === undefined) {
|
|
178
201
|
throw new AuthorizationAbortedError(`${this.serverName}: no active transport to complete authorization into`);
|
|
179
202
|
}
|
|
203
|
+
// The SDK normally calls state() before this hook. Keep a defensive path
|
|
204
|
+
// for direct callers too, and overwrite any caller-supplied value with the
|
|
205
|
+
// state issued by this provider.
|
|
206
|
+
const state = this.expectedState ?? this.state();
|
|
207
|
+
authorizationUrl.searchParams.set('state', state);
|
|
180
208
|
this.browserLegSettled = Promise.withResolvers();
|
|
181
209
|
let succeeded = false;
|
|
182
210
|
try {
|
|
@@ -222,6 +250,7 @@ export class LoopbackOAuthClientProvider {
|
|
|
222
250
|
this.callbackServer?.close(() => undefined);
|
|
223
251
|
this.callbackServer = undefined;
|
|
224
252
|
this.boundPort = undefined;
|
|
253
|
+
this.expectedState = undefined;
|
|
225
254
|
}
|
|
226
255
|
saveCodeVerifier(codeVerifier) {
|
|
227
256
|
// The verifier only needs to survive the browser leg; keeping it out of
|
|
@@ -287,17 +316,41 @@ export class LoopbackOAuthClientProvider {
|
|
|
287
316
|
async mutate(mutator) {
|
|
288
317
|
const run = this.writeChain.then(async () => {
|
|
289
318
|
await this.load();
|
|
319
|
+
const base = {
|
|
320
|
+
clientInformation: this.providerState.clientInformation,
|
|
321
|
+
tokens: this.providerState.tokens
|
|
322
|
+
};
|
|
290
323
|
mutator(this.providerState);
|
|
291
324
|
if (this.store === undefined)
|
|
292
325
|
return;
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
326
|
+
const desired = {
|
|
327
|
+
clientInformation: this.providerState.clientInformation,
|
|
328
|
+
tokens: this.providerState.tokens
|
|
329
|
+
};
|
|
330
|
+
const saved = await this.store.modifyRecord(this.recordKey, async (current) => {
|
|
331
|
+
const currentPayload = asMcpAuthRecord(current);
|
|
332
|
+
// Never replace a record owned by another credential provider just
|
|
333
|
+
// because this process raced it; the next read will observe it as
|
|
334
|
+
// absent and the caller can decide what to do.
|
|
335
|
+
if (current !== undefined && currentPayload === undefined)
|
|
336
|
+
return current;
|
|
337
|
+
const payload = currentPayload ?? {};
|
|
338
|
+
const clientInformation = mergeDurableField(base.clientInformation, desired.clientInformation, payload.clientInformation);
|
|
339
|
+
const tokens = mergeDurableField(base.tokens, desired.tokens, payload.tokens);
|
|
340
|
+
const merged = {
|
|
341
|
+
kind: 'grant',
|
|
342
|
+
payload: {
|
|
343
|
+
...(clientInformation === undefined ? {} : { clientInformation }),
|
|
344
|
+
...(tokens === undefined ? {} : { tokens })
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
return merged;
|
|
348
|
+
});
|
|
349
|
+
const savedPayload = asMcpAuthRecord(saved);
|
|
350
|
+
this.providerState = {
|
|
351
|
+
...(savedPayload ?? {}),
|
|
352
|
+
...(this.providerState.codeVerifier === undefined ? {} : { codeVerifier: this.providerState.codeVerifier })
|
|
299
353
|
};
|
|
300
|
-
await this.store.modifyRecord(this.recordKey, () => Promise.resolve(record));
|
|
301
354
|
});
|
|
302
355
|
// A failed write must not poison the chain; the caller's rejection is the
|
|
303
356
|
// signal, and the next write re-reads whatever is durable now.
|
|
@@ -317,7 +370,7 @@ export class LoopbackOAuthClientProvider {
|
|
|
317
370
|
});
|
|
318
371
|
server.on('error', error => {
|
|
319
372
|
this.clearCallbackTimer();
|
|
320
|
-
reject(new AuthorizationAbortedError(`${this.serverName}: loopback callback server failed: ${String(error)}`));
|
|
373
|
+
reject(new AuthorizationAbortedError(`${this.serverName}: loopback callback server failed: ${redactErrorMessage(String(error))}`));
|
|
321
374
|
});
|
|
322
375
|
server.listen(this.storage.callbackPort ?? 0, '127.0.0.1', () => {
|
|
323
376
|
const address = server.address();
|
|
@@ -343,19 +396,34 @@ export class LoopbackOAuthClientProvider {
|
|
|
343
396
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
344
397
|
const oauthError = url.searchParams.get('error');
|
|
345
398
|
const code = url.searchParams.get('code');
|
|
346
|
-
const respond = (status, message) => {
|
|
399
|
+
const respond = (status, message, closeServer = true) => {
|
|
347
400
|
// Drain the request, then answer with a plain page the user can close.
|
|
348
401
|
request.resume();
|
|
349
402
|
request.socket.end(`HTTP/1.1 ${status} ${status === 200 ? 'OK' : 'Bad Request'}\r\n` + 'content-type: text/plain; charset=utf-8\r\nconnection: close\r\n\r\n' + `${message}\n`);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
403
|
+
if (closeServer) {
|
|
404
|
+
request.on('close', () => {
|
|
405
|
+
server.close(() => undefined);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
353
408
|
};
|
|
409
|
+
// Keep the listener alive for malformed or forged callbacks so the real
|
|
410
|
+
// browser redirect can still complete the same authorization leg.
|
|
411
|
+
if (url.pathname !== '/callback') {
|
|
412
|
+
respond(404, 'Not an OAuth callback endpoint.', false);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const expectedState = this.expectedState;
|
|
416
|
+
const callbackState = url.searchParams.get('state');
|
|
417
|
+
if (expectedState === undefined || callbackState !== expectedState) {
|
|
418
|
+
respond(400, 'Authorization callback carried an invalid state.', false);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
354
421
|
this.clearCallbackTimer();
|
|
355
422
|
this.callbackServer = undefined;
|
|
356
423
|
this.boundPort = undefined;
|
|
424
|
+
this.expectedState = undefined;
|
|
357
425
|
if (oauthError !== null) {
|
|
358
|
-
const description = url.searchParams.get('error_description') ?? '';
|
|
426
|
+
const description = redactErrorMessage(url.searchParams.get('error_description') ?? '');
|
|
359
427
|
respond(400, `Authorization failed: ${oauthError}${description === '' ? '' : ` — ${description}`}`);
|
|
360
428
|
reject(new AuthorizationAbortedError(`${this.serverName}: the authorization server returned "${oauthError}"${description === '' ? '' : ` (${description})`}`));
|
|
361
429
|
return;
|
|
@@ -21,6 +21,7 @@ import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
21
21
|
import { z } from 'zod';
|
|
22
22
|
import { assertSupportedJsonSchema } from './json-schema-subset.js';
|
|
23
23
|
import { containsImage, extractText, prepareImageProjection } from './projection.js';
|
|
24
|
+
import { redactErrorMessage } from '../mcp-redaction.js';
|
|
24
25
|
/**
|
|
25
26
|
* DeepSeek function-name contract: at most 64 characters. Wire-protocol
|
|
26
27
|
* constant, not configuration.
|
|
@@ -122,7 +123,7 @@ export async function syncTools(client, host, opts, previous) {
|
|
|
122
123
|
// sees either the full generation or none of it — never a partial set.
|
|
123
124
|
for (const dispose of disposers.values())
|
|
124
125
|
dispose();
|
|
125
|
-
host.logger.error(`mcp-client(${opts.serverName}): tool registration failed, no tools registered: ${String(error)}`);
|
|
126
|
+
host.logger.error(`mcp-client(${opts.serverName}): tool registration failed, no tools registered: ${redactErrorMessage(String(error))}`);
|
|
126
127
|
if (opts.registrationFailure === 'throw')
|
|
127
128
|
throw error;
|
|
128
129
|
return new Map();
|
|
@@ -4,16 +4,25 @@
|
|
|
4
4
|
* The portable format is translated, not executed directly: stdio commands
|
|
5
5
|
* resolve against the suite root (spec §7.2.1), `${PLUGIN_ROOT}` /
|
|
6
6
|
* `${PLUGIN_DATA}` expand against the suite root and its data directory, and
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* the child environment carries both variables (§9.1) with the data directory
|
|
8
|
+
* created before launch. Legacy HTTP+SSE servers are supported by the
|
|
9
|
+
* market's own bridge (the host client had no such transport); unrecognized
|
|
10
|
+
* shapes are still skipped with a per-server reason.
|
|
11
|
+
*
|
|
12
|
+
* Placeholder discipline follows the package's origin. A suite mounted from
|
|
13
|
+
* the portable v1 format (`portable: true`) is package data: §9.2 forbids any
|
|
14
|
+
* expansion beyond the two built-in variables, and per-server client policy
|
|
15
|
+
* (OAuth, tool lists, timeouts) comes from the `com.deepseek.harness`
|
|
16
|
+
* namespace instead of the `mcp.json` body. Dialect-native files and
|
|
17
|
+
* user-owned config keep the credential seam: every `${NAME}` — including
|
|
18
|
+
* ones inside a streamable-http `url` — expands through the optional DSH
|
|
19
|
+
* credentials resolver at mount time, and missing references fail closed per
|
|
20
|
+
* server instead of becoming empty strings.
|
|
12
21
|
*/
|
|
13
22
|
import { createHash } from 'node:crypto';
|
|
14
|
-
import { DEFAULT_TOOL_CALL_TIMEOUT_MS } from './mcp-client/config.js';
|
|
23
|
+
import { DEFAULT_STARTUP_TIMEOUT_MS, DEFAULT_TOOL_CALL_TIMEOUT_MS } from './mcp-client/config.js';
|
|
15
24
|
import { resolveCwd } from '../catalog/validate.js';
|
|
16
|
-
import { qualifiedSuiteId } from '../catalog/paths.js';
|
|
25
|
+
import { qualifiedSuiteId, suiteDataDir } from '../catalog/paths.js';
|
|
17
26
|
import { applyOverride } from './mcp-overrides.js';
|
|
18
27
|
import { PLUGIN_ROOT_VARIABLES, PLUGIN_DATA_VARIABLES } from '../model/layouts.js';
|
|
19
28
|
/** The max length the bridge accepts for a serverName. */
|
|
@@ -21,26 +30,10 @@ const SERVER_NAME_MAX = 32;
|
|
|
21
30
|
const PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
22
31
|
const BUILTIN_PLACEHOLDERS = new Set([...PLUGIN_ROOT_VARIABLES, ...PLUGIN_DATA_VARIABLES]);
|
|
23
32
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* as enabled while mounting something else.
|
|
29
|
-
*/
|
|
30
|
-
export function effectiveMcpServers(suite, overrides = {}) {
|
|
31
|
-
const rows = [];
|
|
32
|
-
for (const [serverKey, source] of Object.entries(suite.mcp?.servers ?? {})) {
|
|
33
|
-
const override = overrides[serverKey];
|
|
34
|
-
const server = applyOverride(source, override);
|
|
35
|
-
rows.push({ serverKey, server, override, enabled: override?.enabled !== false, credentialRefs: credentialRefsInServer(server) });
|
|
36
|
-
}
|
|
37
|
-
return rows;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Build one mount request per supported mcp.json server, resolving every
|
|
41
|
-
* `${NAME}` through the credential resolver before a child process or HTTP
|
|
42
|
-
* request is created. Missing references fail closed per server instead of
|
|
43
|
-
* becoming empty strings.
|
|
33
|
+
* Build one mount request per supported mcp.json server, expanding built-in
|
|
34
|
+
* variables (and, for user-owned data, `${NAME}` credential references)
|
|
35
|
+
* before a child process or HTTP request is created. Missing references fail
|
|
36
|
+
* closed per server instead of becoming empty strings.
|
|
44
37
|
*
|
|
45
38
|
* @param overrides user-owned per-server overrides (url/headers/env/args
|
|
46
39
|
* replacement plus enable/disable); applied before mount. Disabled servers
|
|
@@ -74,6 +67,141 @@ export async function toMcpMounts(suite, pluginDataRoot, overrides = {}, resolve
|
|
|
74
67
|
}
|
|
75
68
|
return { mounts, failures, credentialRefs: [...credentialRefs] };
|
|
76
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a suite's MCP declarations are package data under the portable
|
|
72
|
+
* format. Installed v1 suites are; the user's own `~/.agents/mcp.json`,
|
|
73
|
+
* project-native config, and every dialect-native file are user-owned data
|
|
74
|
+
* where `${NAME}` credential references stay resolvable.
|
|
75
|
+
*/
|
|
76
|
+
export function isPortableMcp(suite) {
|
|
77
|
+
return suite.dimension !== 'user' && suite.manifest.layout === 'agent-plugin-v1';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The `com.deepseek.harness` policy declared for one portable server.
|
|
81
|
+
* Server keys in the namespace match the server's own name in `mcp.json`;
|
|
82
|
+
* `serverKey` is that name at discovery time.
|
|
83
|
+
*/
|
|
84
|
+
export function namespaceMcpPolicy(suite, serverKey) {
|
|
85
|
+
return suite.manifest.harness?.mcpServers?.[serverKey];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The effective view of one suite's `mcp.json` servers: the user's per-server
|
|
89
|
+
* overrides applied, every row flagged with whether it is used at all, and each
|
|
90
|
+
* row's credential references read off the effective shape. Mounting, status
|
|
91
|
+
* and detail rendering read this one projection, so a server cannot be listed
|
|
92
|
+
* as enabled while mounting something else.
|
|
93
|
+
*
|
|
94
|
+
* Portable v1 suites (`isPortableMcp`) carry package data: their credential
|
|
95
|
+
* reference list is empty by definition (§9.2 leaves unrecognized
|
|
96
|
+
* placeholder-like text literal), and client policy comes from the
|
|
97
|
+
* `com.deepseek.harness` namespace instead. Overrides applied by the user are
|
|
98
|
+
* user-owned data, so a reference a user typed into an override still
|
|
99
|
+
* resolves.
|
|
100
|
+
*/
|
|
101
|
+
export function effectiveMcpServers(suite, overrides = {}) {
|
|
102
|
+
const rows = [];
|
|
103
|
+
const portable = isPortableMcp(suite);
|
|
104
|
+
for (const [serverKey, source] of Object.entries(suite.mcp?.servers ?? {})) {
|
|
105
|
+
const override = overrides[serverKey];
|
|
106
|
+
const declared = namespaceMcpPolicy(suite, serverKey);
|
|
107
|
+
const policy = resolveMcpPolicy(declaredMcpPolicy(source, declared), override);
|
|
108
|
+
const connection = applyNamespaceAuth(applyOverride(source, override), declared);
|
|
109
|
+
const effective = applyMcpPolicy(connection, policy);
|
|
110
|
+
const credentialRefs = portable ? credentialRefsInServer(overrideValues(override)) : credentialRefsInServer(effective);
|
|
111
|
+
rows.push({ serverKey, server: effective, override, enabled: override?.enabled !== false, credentialRefs, policy });
|
|
112
|
+
}
|
|
113
|
+
return rows;
|
|
114
|
+
}
|
|
115
|
+
/** Credential references a user typed into their own override values. */
|
|
116
|
+
function overrideValues(override) {
|
|
117
|
+
if (override === undefined)
|
|
118
|
+
return { type: 'stdio', command: '' };
|
|
119
|
+
return {
|
|
120
|
+
type: 'stdio',
|
|
121
|
+
command: '',
|
|
122
|
+
...(override.args !== undefined ? { args: override.args } : {}),
|
|
123
|
+
...(override.env !== undefined ? { env: override.env } : {}),
|
|
124
|
+
...(override.headers !== undefined ? { headers: override.headers } : {}),
|
|
125
|
+
...(override.url !== undefined ? { url: override.url, type: 'streamable-http' } : {})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The suite's declared per-server policy. The `com.deepseek.harness`
|
|
130
|
+
* namespace seat carries the policy a portable `mcp.json` has no room for and
|
|
131
|
+
* wins over an inline declaration; dialect-native files declare it inline.
|
|
132
|
+
*/
|
|
133
|
+
export function declaredMcpPolicy(server, policy) {
|
|
134
|
+
const enabledTools = policy?.enabledTools ?? server.enabledTools;
|
|
135
|
+
const disabledTools = policy?.disabledTools ?? server.disabledTools;
|
|
136
|
+
const startupTimeoutMs = policy?.startupTimeoutMs ?? server.startupTimeoutMs;
|
|
137
|
+
const toolCallTimeoutMs = policy?.toolCallTimeoutMs ?? server.toolCallTimeoutMs;
|
|
138
|
+
return {
|
|
139
|
+
...(enabledTools === undefined ? {} : { enabledTools }),
|
|
140
|
+
...(disabledTools === undefined ? {} : { disabledTools }),
|
|
141
|
+
...(startupTimeoutMs === undefined ? {} : { startupTimeoutMs }),
|
|
142
|
+
...(toolCallTimeoutMs === undefined ? {} : { toolCallTimeoutMs })
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Bring the suite's declaration and the user's override together.
|
|
147
|
+
*
|
|
148
|
+
* A timeout is a plain override: the user's value wins, a declaration fills in
|
|
149
|
+
* when the user set none, and the built-in default stands behind both. Tool
|
|
150
|
+
* filtering only tightens: the effective deny list is the suite's entries
|
|
151
|
+
* unioned with the user's, and the suite's allow-list stands as declared, so a
|
|
152
|
+
* user cannot open a tool the suite left out.
|
|
153
|
+
*/
|
|
154
|
+
export function resolveMcpPolicy(declared, override) {
|
|
155
|
+
const enabledTools = dedupe(declared.enabledTools);
|
|
156
|
+
const suiteDisabledTools = dedupe(declared.disabledTools);
|
|
157
|
+
const disabledTools = dedupe([...(declared.disabledTools ?? []), ...(override?.disabledTools ?? [])]);
|
|
158
|
+
return {
|
|
159
|
+
...(enabledTools === undefined ? {} : { enabledTools }),
|
|
160
|
+
...(suiteDisabledTools === undefined ? {} : { suiteDisabledTools }),
|
|
161
|
+
...(disabledTools === undefined ? {} : { disabledTools }),
|
|
162
|
+
toolCallTimeout: timeoutResolution(override?.toolCallTimeoutMs, declared.toolCallTimeoutMs, DEFAULT_TOOL_CALL_TIMEOUT_MS),
|
|
163
|
+
startupTimeout: timeoutResolution(override?.startupTimeoutMs, declared.startupTimeoutMs, DEFAULT_STARTUP_TIMEOUT_MS)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function timeoutResolution(user, suite, fallback) {
|
|
167
|
+
const effective = user ?? suite ?? fallback;
|
|
168
|
+
return {
|
|
169
|
+
user: user ?? null,
|
|
170
|
+
suite: suite ?? null,
|
|
171
|
+
effective,
|
|
172
|
+
source: user !== undefined ? 'user' : suite !== undefined ? 'suite' : 'default'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Apply one resolved policy onto a server, in the shape the bridge reads. */
|
|
176
|
+
function applyMcpPolicy(server, policy) {
|
|
177
|
+
return {
|
|
178
|
+
...server,
|
|
179
|
+
toolCallTimeoutMs: policy.toolCallTimeout.effective,
|
|
180
|
+
// An undeclared startup timeout stays absent: the built-in bridge applies
|
|
181
|
+
// its own default at connect time, and host compatibility mode, which
|
|
182
|
+
// cannot enforce one, keeps accepting the server.
|
|
183
|
+
...(policy.startupTimeout.source === 'default' ? {} : { startupTimeoutMs: policy.startupTimeout.effective }),
|
|
184
|
+
...(policy.enabledTools === undefined ? {} : { enabledTools: policy.enabledTools }),
|
|
185
|
+
...(policy.disabledTools === undefined ? {} : { disabledTools: policy.disabledTools })
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Apply the namespace policy's OAuth seat onto one server. A remote server
|
|
190
|
+
* keeps its OAuth opt-out from the namespace; a policy on a portable server
|
|
191
|
+
* also supplies the credential seam the portable file may not declare.
|
|
192
|
+
*/
|
|
193
|
+
function applyNamespaceAuth(server, policy) {
|
|
194
|
+
if (policy?.auth === undefined || server.type === 'stdio')
|
|
195
|
+
return server;
|
|
196
|
+
return { ...server, auth: { enabled: policy.auth.enabled, ...(policy.auth.scope === undefined ? {} : { scope: policy.auth.scope }) } };
|
|
197
|
+
}
|
|
198
|
+
/** Deduplicate a tool-name list, preserving declaration order; an empty result is absent. */
|
|
199
|
+
function dedupe(values) {
|
|
200
|
+
if (values === undefined)
|
|
201
|
+
return undefined;
|
|
202
|
+
const names = [...new Set(values.filter(name => name !== ''))];
|
|
203
|
+
return names.length > 0 ? names : undefined;
|
|
204
|
+
}
|
|
77
205
|
/** Find external credential references used by one MCP server definition. */
|
|
78
206
|
export function credentialRefsInServer(server) {
|
|
79
207
|
const values = [];
|
|
@@ -96,9 +224,11 @@ export function credentialRefsInServer(server) {
|
|
|
96
224
|
return [...refs].sort();
|
|
97
225
|
}
|
|
98
226
|
async function toResolvedMount(suite, serverKey, server, pluginDataRoot, resolver) {
|
|
227
|
+
const portable = isPortableMcp(suite);
|
|
228
|
+
const dataDir = suiteDataDir(pluginDataRoot, suite.sourceId, suite.id);
|
|
99
229
|
if (server.type === 'sse') {
|
|
100
230
|
// The market bridge supports the legacy HTTP+SSE transport natively.
|
|
101
|
-
const expand = expander(suite,
|
|
231
|
+
const expand = expander(suite, dataDir, resolver, portable);
|
|
102
232
|
const url = await expand.one(server.url);
|
|
103
233
|
const headers = await expand.map(server.headers ?? {});
|
|
104
234
|
const missing = unique([...url.missing, ...headers.missing]);
|
|
@@ -115,7 +245,7 @@ async function toResolvedMount(suite, serverKey, server, pluginDataRoot, resolve
|
|
|
115
245
|
};
|
|
116
246
|
return { request: { suiteId: qualifiedSuiteId(suite.sourceId, suite.id), serverKey, config: sseConfig } };
|
|
117
247
|
}
|
|
118
|
-
const expand = expander(suite,
|
|
248
|
+
const expand = expander(suite, dataDir, resolver, portable);
|
|
119
249
|
const serverName = deriveServerName(suite.id, serverKey);
|
|
120
250
|
if (server.type === 'stdio') {
|
|
121
251
|
const args = await expand.all(server.args ?? []);
|
|
@@ -133,8 +263,10 @@ async function toResolvedMount(suite, serverKey, server, pluginDataRoot, resolve
|
|
|
133
263
|
serverName,
|
|
134
264
|
command: server.command.startsWith('./') ? joinInside(suite.root, server.command.slice(2)) : server.command,
|
|
135
265
|
args: args.values,
|
|
136
|
-
|
|
137
|
-
|
|
266
|
+
// §9.1: configured env overlays the base, then the client sets
|
|
267
|
+
// PLUGIN_ROOT and PLUGIN_DATA, replacing same-name entries.
|
|
268
|
+
env: { ...env.values, PLUGIN_ROOT: suite.root, PLUGIN_DATA: dataDir },
|
|
269
|
+
cwd: resolveCwd(cwd.value, suite.root, dataDir),
|
|
138
270
|
...bridgePolicy(server),
|
|
139
271
|
failOnStartupError: true
|
|
140
272
|
}
|
|
@@ -177,13 +309,24 @@ function mapAuth(auth) {
|
|
|
177
309
|
function bridgePolicy(server) {
|
|
178
310
|
return {
|
|
179
311
|
toolCallTimeoutMs: server.toolCallTimeoutMs ?? DEFAULT_TOOL_CALL_TIMEOUT_MS,
|
|
312
|
+
// Only a DECLARED startup timeout is a policy: host compatibility mode
|
|
313
|
+
// cannot enforce one, so an undeclared server stays enforceable there and
|
|
314
|
+
// the built-in bridge applies its own default at connect time.
|
|
180
315
|
...(server.startupTimeoutMs === undefined ? {} : { startupTimeoutMs: server.startupTimeoutMs }),
|
|
181
316
|
...(server.enabledTools === undefined ? {} : { enabledTools: server.enabledTools }),
|
|
182
317
|
...(server.disabledTools === undefined ? {} : { disabledTools: server.disabledTools })
|
|
183
318
|
};
|
|
184
319
|
}
|
|
185
|
-
/**
|
|
186
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Per-mount expansion context.
|
|
322
|
+
*
|
|
323
|
+
* Portable package data (`portable: true`) honors §9.2: only the two built-in
|
|
324
|
+
* variables expand, single pass, and unrecognized placeholder-like text stays
|
|
325
|
+
* literal — the credential resolver is never consulted. User-owned data
|
|
326
|
+
* keeps the `${NAME}` seam, with `${NAME:-default}` fallbacks and missing
|
|
327
|
+
* references reported.
|
|
328
|
+
*/
|
|
329
|
+
function expander(suite, pluginData, resolver, portable) {
|
|
187
330
|
// Promise-valued cache: concurrent expansions of the same reference share
|
|
188
331
|
// one in-flight lookup, so a config using a token twice resolves it once.
|
|
189
332
|
const inflight = new Map();
|
|
@@ -211,15 +354,19 @@ function expander(suite, pluginData, resolver) {
|
|
|
211
354
|
replacement = suite.root;
|
|
212
355
|
else if (PLUGIN_DATA_VARIABLES.has(name))
|
|
213
356
|
replacement = pluginData;
|
|
214
|
-
else
|
|
357
|
+
else if (!portable)
|
|
215
358
|
replacement = (await lookup(name))?.value;
|
|
216
359
|
if (replacement === undefined || replacement === '') {
|
|
217
360
|
if (fallback !== undefined && fallback !== '')
|
|
218
361
|
replacement = fallback;
|
|
219
|
-
else {
|
|
362
|
+
else if (!portable) {
|
|
220
363
|
missing.push(name);
|
|
221
364
|
replacement = '';
|
|
222
365
|
}
|
|
366
|
+
else {
|
|
367
|
+
// §9.2: unrecognized placeholder-like text stays literal.
|
|
368
|
+
replacement = match[0];
|
|
369
|
+
}
|
|
223
370
|
}
|
|
224
371
|
output += replacement;
|
|
225
372
|
cursor = index + match[0].length;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
4
|
-
import { MCP_SCHEMA_ID,
|
|
4
|
+
import { MCP_SCHEMA_ID, validateMcpJson } from '../catalog/validate.js';
|
|
5
5
|
import { effectiveSurfaces } from '../model/types.js';
|
|
6
6
|
export const USER_MCP_SOURCE = '@user-mcp';
|
|
7
7
|
export const USER_MCP_SUITE = 'user-mcp';
|
|
@@ -44,13 +44,16 @@ export async function loadUserMcpSuite(agentsRoot) {
|
|
|
44
44
|
errors
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The user's own declaration file is local data, not a distributable package:
|
|
49
|
+
* keys this client does not know ride along untouched rather than failing the
|
|
50
|
+
* whole file, and the closed-set rule the package schema applies to suites
|
|
51
|
+
* does not apply here.
|
|
52
|
+
*/
|
|
47
53
|
async function validateUserMcp(agentsRoot, raw) {
|
|
48
|
-
const
|
|
49
|
-
if (errors.length > 0)
|
|
50
|
-
throw new Error(`invalid MCP configuration: ${errors.join('; ')}`);
|
|
51
|
-
const result = await validateMcpJson(agentsRoot, raw);
|
|
54
|
+
const result = await validateMcpJson(agentsRoot, raw, { packageRules: false });
|
|
52
55
|
if (result.config === undefined || result.errors.length > 0)
|
|
53
|
-
throw new Error(result.errors.join('; '));
|
|
56
|
+
throw new Error(`invalid MCP configuration: ${result.errors.join('; ')}`);
|
|
54
57
|
return result.config;
|
|
55
58
|
}
|
|
56
59
|
/** Add one service atomically; reject collisions and malformed config before writing. */
|
|
@@ -71,3 +74,43 @@ export async function addUserMcpServer(agentsRoot, name, server) {
|
|
|
71
74
|
export function userMcpPath(agentsRoot) {
|
|
72
75
|
return join(agentsRoot, 'mcp.json');
|
|
73
76
|
}
|
|
77
|
+
const SERVER_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
78
|
+
/**
|
|
79
|
+
* Import several pasted servers in one write.
|
|
80
|
+
*
|
|
81
|
+
* Every entry is judged on its own so one bad definition cannot take the rest
|
|
82
|
+
* of the paste with it, and an existing name is kept unless the caller asked to
|
|
83
|
+
* overwrite. The file is written once, after every entry has been checked, so a
|
|
84
|
+
* rejected entry never lands half-written beside the accepted ones.
|
|
85
|
+
*/
|
|
86
|
+
export async function importUserMcpServers(agentsRoot, entries, overwrite = false) {
|
|
87
|
+
const suite = await loadUserMcpSuite(agentsRoot);
|
|
88
|
+
if (suite.errors.length > 0)
|
|
89
|
+
throw new Error(suite.errors.join('; '));
|
|
90
|
+
const servers = { ...suite.mcp.servers };
|
|
91
|
+
const imported = [];
|
|
92
|
+
const skipped = [];
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (!SERVER_NAME_PATTERN.test(entry.name)) {
|
|
95
|
+
skipped.push({ name: entry.name, reason: 'invalid name' });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (Object.hasOwn(servers, entry.name) && !overwrite) {
|
|
99
|
+
skipped.push({ name: entry.name, reason: 'already exists' });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await validateUserMcp(agentsRoot, { $schema: MCP_SCHEMA_ID, mcpServers: { ...servers, [entry.name]: entry.server } });
|
|
104
|
+
}
|
|
105
|
+
catch (reason) {
|
|
106
|
+
skipped.push({ name: entry.name, reason: reason instanceof Error ? reason.message : String(reason) });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
servers[entry.name] = entry.server;
|
|
110
|
+
imported.push(entry.name);
|
|
111
|
+
}
|
|
112
|
+
if (imported.length > 0) {
|
|
113
|
+
await writeFileAtomic(userMcpPath(agentsRoot), `${JSON.stringify({ $schema: MCP_SCHEMA_ID, mcpServers: servers }, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
|
|
114
|
+
}
|
|
115
|
+
return { imported, skipped };
|
|
116
|
+
}
|