@1mcp/agent 0.36.0 → 0.37.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/build/admin/assets/{admin-OAuthServicesWorkspace-BCGj2hP4.js → admin-OAuthServicesWorkspace-BHyDoEos.js} +1 -1
- package/build/admin/assets/admin-console-DnlZpz5X.js +3 -0
- package/build/admin/assets/admin-vendor-62eL-qdH.js +1 -0
- package/build/admin/assets/admin-vendor-BbMrl7MK.js +3 -0
- package/build/admin/assets/admin-vendor-DpjVwd81.js +62 -0
- package/build/admin/index.html +4 -1
- package/build/auth/sdkOAuthServerProvider.d.ts.map +1 -1
- package/build/auth/sdkOAuthServerProvider.js +49 -48
- package/build/auth/sdkOAuthServerProvider.js.map +1 -1
- package/build/auth/storage/authCodeRepository.js +2 -2
- package/build/auth/storage/authCodeRepository.js.map +1 -1
- package/build/auth/storage/authRequestRepository.js +2 -2
- package/build/auth/storage/authRequestRepository.js.map +1 -1
- package/build/auth/storage/fileStorageService.d.ts +39 -0
- package/build/auth/storage/fileStorageService.d.ts.map +1 -1
- package/build/auth/storage/fileStorageService.js +171 -38
- package/build/auth/storage/fileStorageService.js.map +1 -1
- package/build/auth/storage/oauthStorageService.d.ts +5 -0
- package/build/auth/storage/oauthStorageService.d.ts.map +1 -1
- package/build/auth/storage/oauthStorageService.js +6 -0
- package/build/auth/storage/oauthStorageService.js.map +1 -1
- package/build/commands/mcp/install.d.ts +47 -3
- package/build/commands/mcp/install.d.ts.map +1 -1
- package/build/commands/mcp/install.js +22 -14
- package/build/commands/mcp/install.js.map +1 -1
- package/build/commands/registry/options.d.ts +3 -0
- package/build/commands/registry/options.d.ts.map +1 -1
- package/build/commands/registry/options.js +27 -0
- package/build/commands/registry/options.js.map +1 -1
- package/build/commands/registry/search.d.ts.map +1 -1
- package/build/commands/registry/search.js +2 -10
- package/build/commands/registry/search.js.map +1 -1
- package/build/commands/registry/show.d.ts.map +1 -1
- package/build/commands/registry/show.js +2 -10
- package/build/commands/registry/show.js.map +1 -1
- package/build/commands/registry/status.d.ts.map +1 -1
- package/build/commands/registry/status.js +2 -10
- package/build/commands/registry/status.js.map +1 -1
- package/build/commands/registry/versions.d.ts.map +1 -1
- package/build/commands/registry/versions.js +2 -10
- package/build/commands/registry/versions.js.map +1 -1
- package/build/commands/serve/asyncLoadingOptions.d.ts +16 -0
- package/build/commands/serve/asyncLoadingOptions.d.ts.map +1 -1
- package/build/commands/serve/asyncLoadingOptions.js +29 -0
- package/build/commands/serve/asyncLoadingOptions.js.map +1 -1
- package/build/commands/serve/index.d.ts +24 -0
- package/build/commands/serve/index.d.ts.map +1 -1
- package/build/commands/serve/index.js +24 -0
- package/build/commands/serve/index.js.map +1 -1
- package/build/commands/serve/serve.d.ts +6 -0
- package/build/commands/serve/serve.d.ts.map +1 -1
- package/build/commands/serve/serve.js +45 -2
- package/build/commands/serve/serve.js.map +1 -1
- package/build/commands/serve/serveBackground.d.ts +2 -1
- package/build/commands/serve/serveBackground.d.ts.map +1 -1
- package/build/commands/serve/serveBackground.js +6 -0
- package/build/commands/serve/serveBackground.js.map +1 -1
- package/build/commands/wait/wait.js +1 -1
- package/build/commands/wait/wait.js.map +1 -1
- package/build/config/configManager.d.ts.map +1 -1
- package/build/config/configManager.js +19 -6
- package/build/config/configManager.js.map +1 -1
- package/build/config/configuredServerTargets.d.ts +1 -0
- package/build/config/configuredServerTargets.d.ts.map +1 -1
- package/build/config/configuredServerTargets.js +6 -0
- package/build/config/configuredServerTargets.js.map +1 -1
- package/build/constants/mcp.d.ts +1 -1
- package/build/constants/mcp.js +1 -1
- package/build/core/loading/backendLoadingPolicy.d.ts +11 -0
- package/build/core/loading/backendLoadingPolicy.d.ts.map +1 -0
- package/build/core/loading/backendLoadingPolicy.js +10 -0
- package/build/core/loading/backendLoadingPolicy.js.map +1 -0
- package/build/core/loading/mcpLoadingManager.d.ts +8 -24
- package/build/core/loading/mcpLoadingManager.d.ts.map +1 -1
- package/build/core/loading/mcpLoadingManager.js +116 -70
- package/build/core/loading/mcpLoadingManager.js.map +1 -1
- package/build/core/server/agentConfig.d.ts +26 -0
- package/build/core/server/agentConfig.d.ts.map +1 -1
- package/build/core/server/agentConfig.js +11 -0
- package/build/core/server/agentConfig.js.map +1 -1
- package/build/core/server/clientInstancePool.d.ts +4 -6
- package/build/core/server/clientInstancePool.d.ts.map +1 -1
- package/build/core/server/clientInstancePool.js +63 -63
- package/build/core/server/clientInstancePool.js.map +1 -1
- package/build/core/server/clientInstancePoolTypes.d.ts +9 -2
- package/build/core/server/clientInstancePoolTypes.d.ts.map +1 -1
- package/build/core/server/clientInstancePoolTypes.js +8 -3
- package/build/core/server/clientInstancePoolTypes.js.map +1 -1
- package/build/core/server/requestContextPreparation.d.ts.map +1 -1
- package/build/core/server/requestContextPreparation.js +2 -1
- package/build/core/server/requestContextPreparation.js.map +1 -1
- package/build/core/server/serverManager.d.ts.map +1 -1
- package/build/core/server/serverManager.js +2 -1
- package/build/core/server/serverManager.js.map +1 -1
- package/build/core/server/templateServerManager.d.ts +14 -1
- package/build/core/server/templateServerManager.d.ts.map +1 -1
- package/build/core/server/templateServerManager.js +64 -9
- package/build/core/server/templateServerManager.js.map +1 -1
- package/build/core/tools/internal/schemas/index.d.ts +1 -1
- package/build/core/tools/internal/schemas/installation.d.ts +1 -1
- package/build/core/types/transport.d.ts +82 -2
- package/build/core/types/transport.d.ts.map +1 -1
- package/build/core/types/transport.js +72 -0
- package/build/core/types/transport.js.map +1 -1
- package/build/domains/admin/adminConfiguredServerService.d.ts +146 -2
- package/build/domains/admin/adminConfiguredServerService.d.ts.map +1 -1
- package/build/domains/admin/adminConfiguredServerService.js +729 -81
- package/build/domains/admin/adminConfiguredServerService.js.map +1 -1
- package/build/domains/admin/adminDomain.d.ts +5 -0
- package/build/domains/admin/adminDomain.d.ts.map +1 -1
- package/build/domains/admin/adminDomain.js +11 -0
- package/build/domains/admin/adminDomain.js.map +1 -1
- package/build/domains/admin/adminOperationService.d.ts +1 -0
- package/build/domains/admin/adminOperationService.d.ts.map +1 -1
- package/build/domains/admin/adminOperationService.js +6 -2
- package/build/domains/admin/adminOperationService.js.map +1 -1
- package/build/domains/admin/templateServerDefinitionAnalyzer.d.ts +26 -0
- package/build/domains/admin/templateServerDefinitionAnalyzer.d.ts.map +1 -0
- package/build/domains/admin/templateServerDefinitionAnalyzer.js +148 -0
- package/build/domains/admin/templateServerDefinitionAnalyzer.js.map +1 -0
- package/build/domains/config-change/configChange.d.ts +1 -1
- package/build/domains/config-change/configChange.d.ts.map +1 -1
- package/build/domains/config-change/configChange.js +168 -49
- package/build/domains/config-change/configChange.js.map +1 -1
- package/build/domains/config-change/types.d.ts +16 -2
- package/build/domains/config-change/types.d.ts.map +1 -1
- package/build/domains/registry/mcpRegistryClient.d.ts +3 -3
- package/build/domains/registry/mcpRegistryClient.d.ts.map +1 -1
- package/build/domains/registry/mcpRegistryClient.js +8 -7
- package/build/domains/registry/mcpRegistryClient.js.map +1 -1
- package/build/logger/secureLogger.d.ts.map +1 -1
- package/build/logger/secureLogger.js +160 -23
- package/build/logger/secureLogger.js.map +1 -1
- package/build/server.d.ts +2 -1
- package/build/server.d.ts.map +1 -1
- package/build/server.js +8 -7
- package/build/server.js.map +1 -1
- package/build/transport/http/middlewares/securityMiddleware.d.ts +6 -0
- package/build/transport/http/middlewares/securityMiddleware.d.ts.map +1 -1
- package/build/transport/http/middlewares/securityMiddleware.js +35 -29
- package/build/transport/http/middlewares/securityMiddleware.js.map +1 -1
- package/build/transport/http/routes/adminRoutes.d.ts +17 -1
- package/build/transport/http/routes/adminRoutes.d.ts.map +1 -1
- package/build/transport/http/routes/adminRoutes.js +281 -17
- package/build/transport/http/routes/adminRoutes.js.map +1 -1
- package/build/transport/http/routes/healthRoutes.d.ts +6 -1
- package/build/transport/http/routes/healthRoutes.d.ts.map +1 -1
- package/build/transport/http/routes/healthRoutes.js +7 -3
- package/build/transport/http/routes/healthRoutes.js.map +1 -1
- package/build/transport/http/server.d.ts.map +1 -1
- package/build/transport/http/server.js +21 -3
- package/build/transport/http/server.js.map +1 -1
- package/build/transport/http/storage/streamableSessionRepository.d.ts +1 -0
- package/build/transport/http/storage/streamableSessionRepository.d.ts.map +1 -1
- package/build/transport/http/storage/streamableSessionRepository.js +6 -3
- package/build/transport/http/storage/streamableSessionRepository.js.map +1 -1
- package/package.json +1 -1
- package/build/admin/assets/admin-console-xbgqUjbT.js +0 -3
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mergeGlobalAndServerConfig } from '../../config/mcpConfigMerge.js';
|
|
3
3
|
import { normalizeDisabledToolsForServer } from '../../core/server/disabledTools.js';
|
|
4
|
+
import { templateRuntimeHash } from '../../core/server/templateIdentity.js';
|
|
4
5
|
import { GLOBAL_TRANSPORT_CONFIG_KEYS, transportConfigSchema, } from '../../core/types/index.js';
|
|
5
6
|
import { fingerprintConfiguredServerConfigDocument, fingerprintConfiguredServerDefaults, fingerprintConfiguredServerSecretValue, fingerprintConfiguredServerTarget, isConfiguredServerTargetDisabled, } from '../../domains/config-change/configChange.js';
|
|
6
7
|
import { createServerInstallationWorkflow, } from '../../domains/installation/serverInstallationWorkflow.js';
|
|
8
|
+
import Handlebars from 'handlebars';
|
|
7
9
|
import { z } from 'zod';
|
|
10
|
+
import { analyzeTemplateServerDefinition, } from './templateServerDefinitionAnalyzer.js';
|
|
8
11
|
const configuredServerCreateSecretSchema = z
|
|
9
12
|
.object({
|
|
10
13
|
fieldPath: z.array(z.string()).min(2),
|
|
@@ -17,6 +20,7 @@ const configuredServerCreateSecretSchema = z
|
|
|
17
20
|
.strict();
|
|
18
21
|
const configuredServerCreateDraftSchema = z
|
|
19
22
|
.object({
|
|
23
|
+
source: z.enum(['mcpServers', 'mcpTemplates']).optional().default('mcpServers'),
|
|
20
24
|
name: z.string(),
|
|
21
25
|
enabled: z.boolean().optional(),
|
|
22
26
|
tags: z.array(z.string()).optional(),
|
|
@@ -35,6 +39,33 @@ const configuredServerCreateDraftSchema = z
|
|
|
35
39
|
restartOnExit: z.boolean().optional(),
|
|
36
40
|
maxRestarts: z.number().optional(),
|
|
37
41
|
restartDelay: z.number().optional(),
|
|
42
|
+
disabled: z.union([z.boolean(), z.string()]).optional(),
|
|
43
|
+
stderr: z.union([z.enum(['inherit', 'ignore', 'overlapped', 'pipe']), z.number().int().min(0)]).optional(),
|
|
44
|
+
inheritParentEnv: z.boolean().optional(),
|
|
45
|
+
envFilter: z.array(z.string()).optional(),
|
|
46
|
+
oauth: z
|
|
47
|
+
.object({
|
|
48
|
+
clientId: z.string().optional(),
|
|
49
|
+
clientSecret: z.string().optional(),
|
|
50
|
+
scopes: z.array(z.string()).optional(),
|
|
51
|
+
autoRegister: z.boolean().optional(),
|
|
52
|
+
redirectUrl: z.string().optional(),
|
|
53
|
+
})
|
|
54
|
+
.optional(),
|
|
55
|
+
template: z
|
|
56
|
+
.object({
|
|
57
|
+
shareable: z.boolean().optional(),
|
|
58
|
+
maxInstances: z.number().min(0).optional(),
|
|
59
|
+
idleTimeout: z.number().min(0).optional(),
|
|
60
|
+
perClient: z.boolean().optional(),
|
|
61
|
+
extractionOptions: z
|
|
62
|
+
.object({
|
|
63
|
+
includeOptional: z.boolean().optional(),
|
|
64
|
+
includeEnvironment: z.boolean().optional(),
|
|
65
|
+
})
|
|
66
|
+
.optional(),
|
|
67
|
+
})
|
|
68
|
+
.optional(),
|
|
38
69
|
})
|
|
39
70
|
.strict(),
|
|
40
71
|
secrets: z.array(configuredServerCreateSecretSchema).optional(),
|
|
@@ -54,6 +85,12 @@ const CONFIGURED_SERVER_CREATE_TRANSPORT_FIELD_KEYS = new Set([
|
|
|
54
85
|
'restartOnExit',
|
|
55
86
|
'maxRestarts',
|
|
56
87
|
'restartDelay',
|
|
88
|
+
'disabled',
|
|
89
|
+
'stderr',
|
|
90
|
+
'inheritParentEnv',
|
|
91
|
+
'envFilter',
|
|
92
|
+
'oauth',
|
|
93
|
+
'template',
|
|
57
94
|
]);
|
|
58
95
|
function normalizeConfiguredServerCreateDraft(value) {
|
|
59
96
|
const parsed = configuredServerCreateDraftSchema.safeParse(value);
|
|
@@ -70,6 +107,7 @@ function normalizeConfiguredServerCreateDraft(value) {
|
|
|
70
107
|
localName: typeof record.name === 'string' ? record.name : '',
|
|
71
108
|
transport: transportType,
|
|
72
109
|
},
|
|
110
|
+
targetSource: record.source === 'mcpTemplates' ? 'mcpTemplates' : 'mcpServers',
|
|
73
111
|
validation: {
|
|
74
112
|
status: 'invalid',
|
|
75
113
|
errors: parsed.error.issues.map((issue) => ({
|
|
@@ -82,6 +120,7 @@ function normalizeConfiguredServerCreateDraft(value) {
|
|
|
82
120
|
}
|
|
83
121
|
const { transport } = parsed.data;
|
|
84
122
|
const draft = {
|
|
123
|
+
source: parsed.data.source,
|
|
85
124
|
name: parsed.data.name,
|
|
86
125
|
enabled: parsed.data.enabled,
|
|
87
126
|
tags: parsed.data.tags,
|
|
@@ -109,7 +148,7 @@ function normalizeConfiguredServerCreateDraft(value) {
|
|
|
109
148
|
maxRestarts: transport.maxRestarts,
|
|
110
149
|
restartDelay: transport.restartDelay,
|
|
111
150
|
};
|
|
112
|
-
return { draft, source, validation: { status: 'valid', errors: [] } };
|
|
151
|
+
return { draft, source, targetSource: parsed.data.source, validation: { status: 'valid', errors: [] } };
|
|
113
152
|
}
|
|
114
153
|
function configuredServerCreateSecretValues(draft, container) {
|
|
115
154
|
const values = {};
|
|
@@ -127,7 +166,7 @@ function mergeConfiguredServerCreateRecord(base, additions) {
|
|
|
127
166
|
const merged = { ...base, ...additions };
|
|
128
167
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
129
168
|
}
|
|
130
|
-
function createWorkflowValidation(workflow, config) {
|
|
169
|
+
function createWorkflowValidation(workflow, config, source = 'mcpServers') {
|
|
131
170
|
const errors = [];
|
|
132
171
|
for (const [field, messages] of Object.entries(workflow.fieldErrors ?? {})) {
|
|
133
172
|
for (const message of messages) {
|
|
@@ -149,6 +188,12 @@ function createWorkflowValidation(workflow, config) {
|
|
|
149
188
|
const parsed = transportConfigSchema.safeParse(config);
|
|
150
189
|
if (!parsed.success) {
|
|
151
190
|
for (const issue of parsed.error.issues) {
|
|
191
|
+
if (source === 'mcpTemplates' &&
|
|
192
|
+
issue.path[0] === 'url' &&
|
|
193
|
+
typeof config.url === 'string' &&
|
|
194
|
+
config.url.includes('{{')) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
152
197
|
errors.push({
|
|
153
198
|
fieldPath: ['transport', ...issue.path.map(String)],
|
|
154
199
|
code: 'invalid_transport_config',
|
|
@@ -199,7 +244,7 @@ function validateConfiguredServerCreateSecrets(draft) {
|
|
|
199
244
|
return { status: errors.length === 0 ? 'valid' : 'invalid', errors };
|
|
200
245
|
}
|
|
201
246
|
function createConfiguredServerCreateContract() {
|
|
202
|
-
const transportFields = TRANSPORT_FIELD_DEFINITIONS.filter((definition) => CONFIGURED_SERVER_CREATE_TRANSPORT_FIELD_KEYS.has(definition.key)).map((definition) => ({
|
|
247
|
+
const transportFields = TRANSPORT_FIELD_DEFINITIONS.filter((definition) => definition.key !== 'template' && CONFIGURED_SERVER_CREATE_TRANSPORT_FIELD_KEYS.has(definition.key)).map((definition) => ({
|
|
203
248
|
fieldPath: ['transport', definition.key],
|
|
204
249
|
label: definition.label,
|
|
205
250
|
control: definition.control,
|
|
@@ -234,12 +279,28 @@ function createConfiguredServerCreateContract() {
|
|
|
234
279
|
id: 'identity',
|
|
235
280
|
label: 'Identity',
|
|
236
281
|
fields: [
|
|
282
|
+
{
|
|
283
|
+
fieldPath: ['source'],
|
|
284
|
+
label: 'Definition Type',
|
|
285
|
+
control: 'select',
|
|
286
|
+
value: 'mcpServers',
|
|
287
|
+
options: ['mcpServers', 'mcpTemplates'],
|
|
288
|
+
editable: true,
|
|
289
|
+
},
|
|
237
290
|
{ fieldPath: ['name'], label: 'Server Name', control: 'text', editable: true },
|
|
238
|
-
{
|
|
291
|
+
{
|
|
292
|
+
fieldPath: ['enabled'],
|
|
293
|
+
label: 'Enabled',
|
|
294
|
+
control: 'switch',
|
|
295
|
+
value: true,
|
|
296
|
+
editable: true,
|
|
297
|
+
applicableSources: ['mcpServers'],
|
|
298
|
+
},
|
|
239
299
|
{ fieldPath: ['tags'], label: 'Tags', control: 'tag-list', value: [], editable: true },
|
|
240
300
|
],
|
|
241
301
|
},
|
|
242
302
|
{ id: 'transport', label: 'Transport', fields: transportFields },
|
|
303
|
+
{ id: 'template', label: 'Template instances', fields: templateLifecycleFields() },
|
|
243
304
|
],
|
|
244
305
|
};
|
|
245
306
|
}
|
|
@@ -307,7 +368,7 @@ function connectivityFingerprintFacts(connectivityCheck) {
|
|
|
307
368
|
return connectivityCheck;
|
|
308
369
|
return { status: connectivityCheck.status, mode: connectivityCheck.mode };
|
|
309
370
|
}
|
|
310
|
-
function previewCreateConfigChange(targetName, workflowStatus, changed) {
|
|
371
|
+
function previewCreateConfigChange(targetName, workflowStatus, changed, source = 'mcpServers') {
|
|
311
372
|
const status = workflowStatus === 'template_conflict'
|
|
312
373
|
? 'template_conflict'
|
|
313
374
|
: workflowStatus === 'exists'
|
|
@@ -317,15 +378,17 @@ function previewCreateConfigChange(targetName, workflowStatus, changed) {
|
|
|
317
378
|
: 'failed';
|
|
318
379
|
return {
|
|
319
380
|
status,
|
|
320
|
-
operation: 'create_static',
|
|
381
|
+
operation: source === 'mcpTemplates' ? 'create_template' : 'create_static',
|
|
321
382
|
configPath: '[redacted]',
|
|
322
383
|
target: {
|
|
323
384
|
name: targetName,
|
|
324
|
-
...(
|
|
325
|
-
? { source
|
|
326
|
-
: workflowStatus === '
|
|
327
|
-
? { source: '
|
|
328
|
-
:
|
|
385
|
+
...(changed
|
|
386
|
+
? { source }
|
|
387
|
+
: workflowStatus === 'template_conflict'
|
|
388
|
+
? { source: 'mcpTemplates' }
|
|
389
|
+
: workflowStatus === 'exists'
|
|
390
|
+
? { source: 'mcpServers' }
|
|
391
|
+
: {}),
|
|
329
392
|
},
|
|
330
393
|
changed,
|
|
331
394
|
backup: { created: false },
|
|
@@ -340,6 +403,11 @@ function createDraftCandidateName(value) {
|
|
|
340
403
|
const name = value.name;
|
|
341
404
|
return typeof name === 'string' ? name : '';
|
|
342
405
|
}
|
|
406
|
+
function createDraftCandidateSource(value) {
|
|
407
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
408
|
+
return 'mcpServers';
|
|
409
|
+
return value.source === 'mcpTemplates' ? 'mcpTemplates' : 'mcpServers';
|
|
410
|
+
}
|
|
343
411
|
function sanitizeCreateConfirmationFacts(facts) {
|
|
344
412
|
if (!facts)
|
|
345
413
|
return undefined;
|
|
@@ -396,7 +464,7 @@ export class AdminConfiguredServerService {
|
|
|
396
464
|
const candidateName = createDraftCandidateName(input.draft);
|
|
397
465
|
const context = {
|
|
398
466
|
...input.context,
|
|
399
|
-
target: { type: 'configured_server', id: candidateName },
|
|
467
|
+
target: { type: 'configured_server', id: `${createDraftCandidateSource(input.draft)}/${candidateName}` },
|
|
400
468
|
confirmationFacts: sanitizeCreateConfirmationFacts(input.context.confirmationFacts),
|
|
401
469
|
};
|
|
402
470
|
return this.options.operationService.executeMutation({
|
|
@@ -415,7 +483,8 @@ export class AdminConfiguredServerService {
|
|
|
415
483
|
if (!prepared.config) {
|
|
416
484
|
throw new AdminConfiguredServerApplyError('configured_server_create_invalid');
|
|
417
485
|
}
|
|
418
|
-
if (prepared.
|
|
486
|
+
if (prepared.targetSource === 'mcpServers' &&
|
|
487
|
+
prepared.enabled &&
|
|
419
488
|
!wouldUseLocalStdioTransport(prepared.config) &&
|
|
420
489
|
preview.connectivityCheck.status !== 'passed' &&
|
|
421
490
|
!(preview.connectivityCheck.status === 'skipped' && preview.connectivityCheck.reason === 'checker_unavailable') &&
|
|
@@ -438,13 +507,21 @@ export class AdminConfiguredServerService {
|
|
|
438
507
|
run: async () => {
|
|
439
508
|
let configChange;
|
|
440
509
|
try {
|
|
441
|
-
configChange =
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
510
|
+
configChange =
|
|
511
|
+
prepared.targetSource === 'mcpTemplates'
|
|
512
|
+
? await this.options.configChangeService.createTemplateConfiguredServerTarget({
|
|
513
|
+
targetName: prepared.targetName,
|
|
514
|
+
serverConfig: prepared.config,
|
|
515
|
+
backup: 'skip',
|
|
516
|
+
expectedConfigFingerprint: configFingerprint,
|
|
517
|
+
})
|
|
518
|
+
: await this.options.configChangeService.createStaticConfiguredServerTarget({
|
|
519
|
+
targetName: prepared.targetName,
|
|
520
|
+
serverConfig: prepared.config,
|
|
521
|
+
operation: 'install',
|
|
522
|
+
backup: 'skip',
|
|
523
|
+
expectedConfigFingerprint: configFingerprint,
|
|
524
|
+
});
|
|
448
525
|
}
|
|
449
526
|
catch {
|
|
450
527
|
throw new AdminConfiguredServerApplyError('configured_server_create_failed');
|
|
@@ -452,8 +529,12 @@ export class AdminConfiguredServerService {
|
|
|
452
529
|
assertSuccessfulConfiguredServerCreate(configChange);
|
|
453
530
|
return {
|
|
454
531
|
targetName: prepared.targetName,
|
|
532
|
+
targetSource: prepared.targetSource,
|
|
455
533
|
previewFingerprint: preview.previewFingerprint,
|
|
456
534
|
configChange,
|
|
535
|
+
...(prepared.targetSource === 'mcpTemplates'
|
|
536
|
+
? { runtimeImpact: { activeInstanceCount: 0, createdInstance: false } }
|
|
537
|
+
: {}),
|
|
457
538
|
};
|
|
458
539
|
},
|
|
459
540
|
};
|
|
@@ -472,14 +553,14 @@ export class AdminConfiguredServerService {
|
|
|
472
553
|
const source = configuredState.source;
|
|
473
554
|
const context = {
|
|
474
555
|
...input.context,
|
|
475
|
-
target: { type: 'configured_server', id: `${source}
|
|
556
|
+
target: { type: 'configured_server', id: `${source}/${input.targetName}` },
|
|
476
557
|
};
|
|
477
558
|
return this.options.operationService.executeReadOnly({
|
|
478
559
|
context,
|
|
479
560
|
operationName: 'getConfiguredServerDetail',
|
|
480
561
|
run: async () => {
|
|
481
|
-
const { currentConfig, serverDefaults } = configuredState;
|
|
482
|
-
const server = createConfiguredServerReadModel(input.targetName, mergeGlobalAndServerConfig(serverDefaults, currentConfig), source, fingerprintConfiguredServerTarget(currentConfig), Object.hasOwn(currentConfig, 'instructionOverride') ? currentConfig.instructionOverride : undefined, Object.hasOwn(currentConfig, 'instructionOverride'));
|
|
562
|
+
const { currentConfig, serverDefaults, collision } = configuredState;
|
|
563
|
+
const server = createConfiguredServerReadModel(input.targetName, mergeGlobalAndServerConfig(serverDefaults, currentConfig), source, fingerprintConfiguredServerTarget(currentConfig), Object.hasOwn(currentConfig, 'instructionOverride') ? currentConfig.instructionOverride : undefined, Object.hasOwn(currentConfig, 'instructionOverride'), collision, source === 'mcpTemplates' ? (this.options.getTemplateActiveInstanceCount?.(input.targetName) ?? 0) : 0, this.options.mutationAvailability?.available ?? true);
|
|
483
564
|
return {
|
|
484
565
|
server,
|
|
485
566
|
editContract: createConfiguredServerEditContract(server, currentConfig, serverDefaults),
|
|
@@ -501,7 +582,7 @@ export class AdminConfiguredServerService {
|
|
|
501
582
|
const configuredState = this.readConfiguredServerState(input.targetName, input.targetSource);
|
|
502
583
|
const context = {
|
|
503
584
|
...input.context,
|
|
504
|
-
target: { type: 'configured_server', id: `${configuredState.source}
|
|
585
|
+
target: { type: 'configured_server', id: `${configuredState.source}/${input.targetName}` },
|
|
505
586
|
};
|
|
506
587
|
return this.options.operationService.executeReadOnly({
|
|
507
588
|
context,
|
|
@@ -533,7 +614,7 @@ export class AdminConfiguredServerService {
|
|
|
533
614
|
const source = configuredState.source;
|
|
534
615
|
const context = {
|
|
535
616
|
...input.context,
|
|
536
|
-
target: { type: 'configured_server', id: `${source}
|
|
617
|
+
target: { type: 'configured_server', id: `${source}/${input.targetName}` },
|
|
537
618
|
};
|
|
538
619
|
const { currentConfig, serverDefaults } = configuredState;
|
|
539
620
|
return this.options.operationService.executeDryRun({
|
|
@@ -546,7 +627,7 @@ export class AdminConfiguredServerService {
|
|
|
546
627
|
const source = this.resolveConfiguredServerSource(input.targetName, input.targetSource);
|
|
547
628
|
const context = {
|
|
548
629
|
...input.context,
|
|
549
|
-
target: { type: 'configured_server', id: `${source}
|
|
630
|
+
target: { type: 'configured_server', id: `${source}/${input.targetName}` },
|
|
550
631
|
confirmationFacts: sanitizeApplyConfirmationFacts(input.context.confirmationFacts),
|
|
551
632
|
};
|
|
552
633
|
return this.options.operationService.executeMutation({
|
|
@@ -571,7 +652,8 @@ export class AdminConfiguredServerService {
|
|
|
571
652
|
if (preview.previewFingerprint !== input.previewFingerprint) {
|
|
572
653
|
throw new AdminConfiguredServerApplyError('configured_server_stale_preview');
|
|
573
654
|
}
|
|
574
|
-
if (
|
|
655
|
+
if (source === 'mcpServers' &&
|
|
656
|
+
connectionCritical &&
|
|
575
657
|
!wouldUseLocalStdioTransport(proposedConfig) &&
|
|
576
658
|
proposedReadModel.enabled &&
|
|
577
659
|
preview.connectivityCheck.status !== 'passed' &&
|
|
@@ -595,6 +677,14 @@ export class AdminConfiguredServerService {
|
|
|
595
677
|
: []),
|
|
596
678
|
],
|
|
597
679
|
run: async () => {
|
|
680
|
+
const instanceIdentities = source === 'mcpTemplates'
|
|
681
|
+
? (this.options.getTemplateActiveInstanceIdentities?.(input.targetName) ?? [])
|
|
682
|
+
: [];
|
|
683
|
+
const activeInstancesBefore = instanceIdentities.length > 0
|
|
684
|
+
? instanceIdentities.length
|
|
685
|
+
: source === 'mcpTemplates'
|
|
686
|
+
? (this.options.getTemplateActiveInstanceCount?.(input.targetName) ?? 0)
|
|
687
|
+
: 0;
|
|
598
688
|
const configChange = await this.options.configChangeService.editConfiguredServerTarget({
|
|
599
689
|
sourceName: input.targetName,
|
|
600
690
|
targetSource: source,
|
|
@@ -604,11 +694,142 @@ export class AdminConfiguredServerService {
|
|
|
604
694
|
expectedGlobalConfigFingerprint: fingerprintConfiguredServerDefaults(serverDefaults),
|
|
605
695
|
});
|
|
606
696
|
assertSuccessfulConfiguredServerEdit(configChange);
|
|
697
|
+
const runtimeImpact = source === 'mcpTemplates'
|
|
698
|
+
? await this.observeTemplateRetirement(input.targetName, configChange, preview.runtimeImpact?.retirementRequired === true, activeInstancesBefore, instanceIdentities)
|
|
699
|
+
: undefined;
|
|
607
700
|
return {
|
|
608
701
|
originalTargetName: input.targetName,
|
|
609
702
|
targetName: preview.proposedTargetName,
|
|
610
703
|
previewFingerprint: preview.previewFingerprint,
|
|
611
704
|
configChange,
|
|
705
|
+
...(runtimeImpact ? { runtimeImpact } : {}),
|
|
706
|
+
};
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
async previewConfiguredServerDelete(input) {
|
|
713
|
+
const context = {
|
|
714
|
+
...input.context,
|
|
715
|
+
target: { type: 'configured_server', id: `${input.targetSource}/${input.targetName}` },
|
|
716
|
+
};
|
|
717
|
+
return this.options.operationService.executeDryRun({
|
|
718
|
+
context,
|
|
719
|
+
operationName: 'previewConfiguredServerDelete',
|
|
720
|
+
run: async () => this.previewConfiguredServerDeleteResult(input),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
async deleteConfiguredServer(input) {
|
|
724
|
+
const qualifiedId = `${input.targetSource}/${input.targetName}`;
|
|
725
|
+
const context = {
|
|
726
|
+
...input.context,
|
|
727
|
+
target: { type: 'configured_server', id: qualifiedId },
|
|
728
|
+
confirmationFacts: sanitizeConfiguredServerIdentityConfirmationFacts(input.context.confirmationFacts),
|
|
729
|
+
};
|
|
730
|
+
return this.options.operationService.executeMutation({
|
|
731
|
+
context,
|
|
732
|
+
operationName: 'deleteConfiguredServer',
|
|
733
|
+
prepare: async () => {
|
|
734
|
+
let preview;
|
|
735
|
+
try {
|
|
736
|
+
preview = this.previewConfiguredServerDeleteResult(input);
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
if (error instanceof AdminConfiguredServerNotFoundError) {
|
|
740
|
+
throw new AdminConfiguredServerApplyError('configured_server_already_removed');
|
|
741
|
+
}
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
744
|
+
if (preview.previewFingerprint !== input.previewFingerprint) {
|
|
745
|
+
throw new AdminConfiguredServerApplyError('configured_server_stale_preview');
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
confirmationRequirements: [
|
|
749
|
+
{ code: 'previewConfirmed', expected: input.previewFingerprint },
|
|
750
|
+
{ code: 'targetIdentityConfirmed', expected: qualifiedId },
|
|
751
|
+
],
|
|
752
|
+
run: async () => {
|
|
753
|
+
const instanceIdentities = input.targetSource === 'mcpTemplates'
|
|
754
|
+
? (this.options.getTemplateActiveInstanceIdentities?.(input.targetName) ?? [])
|
|
755
|
+
: [];
|
|
756
|
+
const activeInstancesBefore = instanceIdentities.length > 0
|
|
757
|
+
? instanceIdentities.length
|
|
758
|
+
: preview.runtimeImpact.kind === 'template'
|
|
759
|
+
? preview.runtimeImpact.activeInstanceCount
|
|
760
|
+
: 0;
|
|
761
|
+
const configChange = await this.options.configChangeService.deleteConfiguredServerTarget({
|
|
762
|
+
targetName: input.targetName,
|
|
763
|
+
targetSource: input.targetSource,
|
|
764
|
+
expectedTargetFingerprint: preview.targetFingerprint,
|
|
765
|
+
});
|
|
766
|
+
assertSuccessfulConfiguredServerDelete(configChange);
|
|
767
|
+
const runtimeImpact = input.targetSource === 'mcpTemplates'
|
|
768
|
+
? await this.observeTemplateRetirement(input.targetName, configChange, true, activeInstancesBefore, instanceIdentities)
|
|
769
|
+
: undefined;
|
|
770
|
+
return {
|
|
771
|
+
target: preview.target,
|
|
772
|
+
qualifiedId,
|
|
773
|
+
previewFingerprint: preview.previewFingerprint,
|
|
774
|
+
configChange: sanitizeConfiguredServerDeleteResult(configChange),
|
|
775
|
+
...(runtimeImpact ? { runtimeImpact } : {}),
|
|
776
|
+
};
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
async previewConfiguredServerLifecycle(input) {
|
|
783
|
+
const context = {
|
|
784
|
+
...input.context,
|
|
785
|
+
target: { type: 'configured_server', id: `${input.targetSource}/${input.targetName}` },
|
|
786
|
+
};
|
|
787
|
+
return this.options.operationService.executeDryRun({
|
|
788
|
+
context,
|
|
789
|
+
operationName: 'previewConfiguredServerLifecycle',
|
|
790
|
+
run: async () => this.previewConfiguredServerLifecycleResult(input),
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
async applyConfiguredServerLifecycle(input) {
|
|
794
|
+
const qualifiedId = `${input.targetSource}/${input.targetName}`;
|
|
795
|
+
const context = {
|
|
796
|
+
...input.context,
|
|
797
|
+
target: { type: 'configured_server', id: qualifiedId },
|
|
798
|
+
confirmationFacts: sanitizeConfiguredServerIdentityConfirmationFacts(input.context.confirmationFacts),
|
|
799
|
+
};
|
|
800
|
+
return this.options.operationService.executeMutation({
|
|
801
|
+
context,
|
|
802
|
+
operationName: 'applyConfiguredServerLifecycle',
|
|
803
|
+
prepare: async () => {
|
|
804
|
+
const preview = await this.previewConfiguredServerLifecycleResult(input);
|
|
805
|
+
if (preview.previewFingerprint !== input.previewFingerprint) {
|
|
806
|
+
throw new AdminConfiguredServerApplyError('configured_server_stale_preview');
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
confirmationRequirements: [
|
|
810
|
+
{ code: 'previewConfirmed', expected: input.previewFingerprint },
|
|
811
|
+
{ code: 'targetIdentityConfirmed', expected: qualifiedId },
|
|
812
|
+
],
|
|
813
|
+
run: async () => {
|
|
814
|
+
const instanceIdentities = this.options.getTemplateActiveInstanceIdentities?.(input.targetName) ?? [];
|
|
815
|
+
const activeInstancesBefore = instanceIdentities.length || this.options.getTemplateActiveInstanceCount?.(input.targetName) || 0;
|
|
816
|
+
const configChange = await this.options.configChangeService.setConfiguredServerTargetEnabledState({
|
|
817
|
+
targetName: input.targetName,
|
|
818
|
+
targetSource: input.targetSource,
|
|
819
|
+
enabled: input.enabled,
|
|
820
|
+
backup: 'required',
|
|
821
|
+
expectedTargetFingerprint: preview.targetFingerprint,
|
|
822
|
+
});
|
|
823
|
+
assertSuccessfulConfiguredServerLifecycle(configChange);
|
|
824
|
+
const runtimeImpact = await this.observeTemplateRetirement(input.targetName, configChange, !input.enabled && configChange.changed, activeInstancesBefore, instanceIdentities);
|
|
825
|
+
return {
|
|
826
|
+
target: preview.target,
|
|
827
|
+
qualifiedId,
|
|
828
|
+
previewFingerprint: preview.previewFingerprint,
|
|
829
|
+
enabled: input.enabled,
|
|
830
|
+
outcome: mutationOutcome(input.enabled, configChange.changed),
|
|
831
|
+
configChange: sanitizeConfiguredServerLifecycleResult(configChange),
|
|
832
|
+
runtimeImpact,
|
|
612
833
|
};
|
|
613
834
|
},
|
|
614
835
|
};
|
|
@@ -629,7 +850,7 @@ export class AdminConfiguredServerService {
|
|
|
629
850
|
const source = input.targetSource ?? 'mcpServers';
|
|
630
851
|
const context = {
|
|
631
852
|
...input.context,
|
|
632
|
-
target: { type: 'configured_server', id: `${source}
|
|
853
|
+
target: { type: 'configured_server', id: `${source}/${input.targetName}` },
|
|
633
854
|
};
|
|
634
855
|
if (input.dryRun) {
|
|
635
856
|
return this.options.operationService.executeDryRun({
|
|
@@ -687,17 +908,160 @@ export class AdminConfiguredServerService {
|
|
|
687
908
|
}
|
|
688
909
|
return {
|
|
689
910
|
configFingerprint: fingerprintConfiguredServerConfigDocument(parsed),
|
|
690
|
-
servers: ['mcpServers', 'mcpTemplates'].flatMap((source) => Object.entries(parsed[source] ?? {}).map(([name, serverConfig]) => createConfiguredServerReadModel(name, mergeGlobalAndServerConfig(parsed.serverDefaults, serverConfig), source, fingerprintConfiguredServerTarget(serverConfig), Object.hasOwn(serverConfig, 'instructionOverride') ? serverConfig.instructionOverride : undefined, Object.hasOwn(serverConfig, 'instructionOverride')))),
|
|
911
|
+
servers: ['mcpServers', 'mcpTemplates'].flatMap((source) => Object.entries(parsed[source] ?? {}).map(([name, serverConfig]) => createConfiguredServerReadModel(name, mergeGlobalAndServerConfig(parsed.serverDefaults, serverConfig), source, fingerprintConfiguredServerTarget(serverConfig), Object.hasOwn(serverConfig, 'instructionOverride') ? serverConfig.instructionOverride : undefined, Object.hasOwn(serverConfig, 'instructionOverride'), Boolean((source === 'mcpTemplates' ? parsed.mcpServers : parsed.mcpTemplates)?.[name]), source === 'mcpTemplates' ? (this.options.getTemplateActiveInstanceCount?.(name) ?? 0) : 0, this.options.mutationAvailability?.available ?? true))),
|
|
691
912
|
};
|
|
692
913
|
}
|
|
693
|
-
|
|
694
|
-
const
|
|
914
|
+
previewConfiguredServerDeleteResult(input) {
|
|
915
|
+
const document = this.options.readConfigDocument();
|
|
916
|
+
const otherSource = input.targetSource === 'mcpServers' ? 'mcpTemplates' : 'mcpServers';
|
|
917
|
+
if (!document?.[input.targetSource]?.[input.targetName] && document?.[otherSource]?.[input.targetName]) {
|
|
918
|
+
throw new AdminConfiguredServerApplyError('configured_server_source_changed');
|
|
919
|
+
}
|
|
920
|
+
const { currentConfig, serverDefaults, collision } = this.readConfiguredServerState(input.targetName, input.targetSource, document);
|
|
921
|
+
const targetFingerprint = fingerprintConfiguredServerTarget(currentConfig);
|
|
922
|
+
const activeInstanceCount = input.targetSource === 'mcpTemplates'
|
|
923
|
+
? (this.options.getTemplateActiveInstanceCount?.(input.targetName) ?? 0)
|
|
924
|
+
: 0;
|
|
925
|
+
const definition = createConfiguredServerReadModel(input.targetName, mergeGlobalAndServerConfig(serverDefaults, currentConfig), input.targetSource, targetFingerprint, Object.hasOwn(currentConfig, 'instructionOverride') ? currentConfig.instructionOverride : undefined, Object.hasOwn(currentConfig, 'instructionOverride'), collision, activeInstanceCount, this.options.mutationAvailability?.available ?? true);
|
|
926
|
+
const qualifiedId = definition.definition?.qualifiedId ?? `${input.targetSource}/${input.targetName}`;
|
|
927
|
+
const authority = definition.definition?.authority ?? 'sole';
|
|
928
|
+
const runtimeImpact = input.targetSource === 'mcpTemplates'
|
|
929
|
+
? { kind: 'template', activeInstanceCount, retirement: 'reload_scheduled' }
|
|
930
|
+
: { kind: 'static', configuredBackendRemoval: 'after_reload' };
|
|
931
|
+
const warnings = [
|
|
932
|
+
'Deletion removes only this source-qualified definition and does not cascade to presets, filters, credentials, registry state, or individually selected live instances.',
|
|
933
|
+
...(collision
|
|
934
|
+
? [`The same name in ${input.targetSource === 'mcpServers' ? 'mcpTemplates' : 'mcpServers'} is preserved.`]
|
|
935
|
+
: []),
|
|
936
|
+
...(input.targetSource === 'mcpTemplates' && activeInstanceCount > 0
|
|
937
|
+
? [
|
|
938
|
+
`Reload schedules retirement of ${activeInstanceCount} active Template Server instance${activeInstanceCount === 1 ? '' : 's'}.`,
|
|
939
|
+
]
|
|
940
|
+
: []),
|
|
941
|
+
];
|
|
942
|
+
const previewFingerprint = `delete_preview_${createHash('sha256')
|
|
943
|
+
.update(stableStringify({
|
|
944
|
+
schemaVersion: 1,
|
|
945
|
+
qualifiedId,
|
|
946
|
+
targetFingerprint,
|
|
947
|
+
authority,
|
|
948
|
+
collision,
|
|
949
|
+
runtimeImpact,
|
|
950
|
+
warnings,
|
|
951
|
+
}))
|
|
952
|
+
.digest('hex')}`;
|
|
953
|
+
return {
|
|
954
|
+
target: definition.target,
|
|
955
|
+
qualifiedId,
|
|
956
|
+
targetFingerprint,
|
|
957
|
+
previewFingerprint,
|
|
958
|
+
authority,
|
|
959
|
+
removal: { definition, preservesSameNamedOtherSource: collision, cascades: false },
|
|
960
|
+
configChange: previewDeleteConfigChange(input.targetName, input.targetSource),
|
|
961
|
+
expectedBackup: { policy: 'required', recoveryCopy: true },
|
|
962
|
+
expectedReload: {
|
|
963
|
+
policy: 'observe_after_write',
|
|
964
|
+
possibleStatuses: ['observed', 'runtime_not_running', 'reload_disabled', 'failed'],
|
|
965
|
+
},
|
|
966
|
+
runtimeImpact,
|
|
967
|
+
warnings,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async previewConfiguredServerLifecycleResult(input) {
|
|
971
|
+
const document = this.options.readConfigDocument();
|
|
972
|
+
const currentConfig = document?.mcpTemplates?.[input.targetName];
|
|
973
|
+
if (!currentConfig) {
|
|
974
|
+
if (document?.mcpServers?.[input.targetName]) {
|
|
975
|
+
throw new AdminConfiguredServerApplyError('configured_server_source_changed');
|
|
976
|
+
}
|
|
977
|
+
throw new AdminConfiguredServerNotFoundError(input.targetName);
|
|
978
|
+
}
|
|
979
|
+
const targetFingerprint = fingerprintConfiguredServerTarget(currentConfig);
|
|
980
|
+
const qualifiedId = `mcpTemplates/${input.targetName}`;
|
|
981
|
+
const disabled = currentConfig.disabled;
|
|
982
|
+
const currentEnabled = !isConfiguredServerTargetDisabled(disabled);
|
|
983
|
+
const disabledValueKind = disabled === undefined
|
|
984
|
+
? 'absent'
|
|
985
|
+
: typeof disabled === 'string' && disabled.includes('{{')
|
|
986
|
+
? 'context_expression'
|
|
987
|
+
: 'literal';
|
|
988
|
+
const expressionReplacement = {
|
|
989
|
+
occurs: disabledValueKind === 'context_expression' && !input.enabled,
|
|
990
|
+
replacement: input.enabled ? 'enabled_absent' : 'disabled_true',
|
|
991
|
+
};
|
|
992
|
+
const activeInstanceCount = this.options.getTemplateActiveInstanceCount?.(input.targetName) ?? 0;
|
|
993
|
+
const configChange = await this.options.configChangeService.previewConfiguredServerTargetEnabledState({
|
|
994
|
+
targetName: input.targetName,
|
|
995
|
+
targetSource: input.targetSource,
|
|
996
|
+
enabled: input.enabled,
|
|
997
|
+
backup: 'skip',
|
|
998
|
+
});
|
|
999
|
+
if (configChange.status === 'not_found' || configChange.status === 'source_conflict') {
|
|
1000
|
+
throw new AdminConfiguredServerApplyError('configured_server_stale_preview');
|
|
1001
|
+
}
|
|
1002
|
+
if (configChange.status === 'failed') {
|
|
1003
|
+
throw new AdminConfiguredServerApplyError('configured_server_apply_failed');
|
|
1004
|
+
}
|
|
1005
|
+
const warnings = [
|
|
1006
|
+
...(expressionReplacement.occurs
|
|
1007
|
+
? ['The context-rendered disabled expression will be replaced with literal true without evaluating Request Context.']
|
|
1008
|
+
: []),
|
|
1009
|
+
...(input.enabled
|
|
1010
|
+
? ['Re-enable restores eligibility only; instances and Request Sessions are created lazily by future matching requests.']
|
|
1011
|
+
: activeInstanceCount > 0
|
|
1012
|
+
? [`Successful reload retires ${activeInstanceCount} active Template Server instance${activeInstanceCount === 1 ? '' : 's'} and removes their Request Session memberships.`]
|
|
1013
|
+
: []),
|
|
1014
|
+
];
|
|
1015
|
+
const runtimeImpact = {
|
|
1016
|
+
activeInstanceCount,
|
|
1017
|
+
retirement: !input.enabled && configChange.changed ? 'after_successful_reload' : 'not_required',
|
|
1018
|
+
recreation: 'lazy_future_match_only',
|
|
1019
|
+
};
|
|
1020
|
+
const previewFingerprint = `lifecycle_preview_${createHash('sha256')
|
|
1021
|
+
.update(stableStringify({
|
|
1022
|
+
schemaVersion: 1,
|
|
1023
|
+
qualifiedId,
|
|
1024
|
+
targetFingerprint,
|
|
1025
|
+
currentEnabled,
|
|
1026
|
+
proposedEnabled: input.enabled,
|
|
1027
|
+
disabledValueKind,
|
|
1028
|
+
expressionReplacement,
|
|
1029
|
+
changed: configChange.changed,
|
|
1030
|
+
runtimeImpact,
|
|
1031
|
+
warnings,
|
|
1032
|
+
}))
|
|
1033
|
+
.digest('hex')}`;
|
|
1034
|
+
return {
|
|
1035
|
+
target: { type: 'configured_server', id: input.targetName, source: input.targetSource },
|
|
1036
|
+
qualifiedId,
|
|
1037
|
+
targetFingerprint,
|
|
1038
|
+
previewFingerprint,
|
|
1039
|
+
current: { enabled: currentEnabled, disabledValueKind },
|
|
1040
|
+
proposed: { enabled: input.enabled, disabledValueKind: input.enabled ? 'absent' : 'literal' },
|
|
1041
|
+
expressionReplacement,
|
|
1042
|
+
configChange: sanitizeConfiguredServerLifecycleResult(configChange),
|
|
1043
|
+
expectedBackup: { policy: 'required', recoveryCopy: true },
|
|
1044
|
+
expectedReload: {
|
|
1045
|
+
policy: 'observe_after_write',
|
|
1046
|
+
possibleStatuses: ['observed', 'runtime_not_running', 'reload_disabled', 'failed'],
|
|
1047
|
+
},
|
|
1048
|
+
runtimeImpact,
|
|
1049
|
+
warnings,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
readConfiguredServerState(targetName, targetSource, document = this.options.readConfigDocument()) {
|
|
1053
|
+
const parsed = document;
|
|
695
1054
|
const source = this.resolveConfiguredServerSource(targetName, targetSource, parsed);
|
|
696
1055
|
const currentConfig = parsed?.[source]?.[targetName];
|
|
697
1056
|
if (!currentConfig) {
|
|
698
1057
|
throw new AdminConfiguredServerNotFoundError(targetName);
|
|
699
1058
|
}
|
|
700
|
-
return {
|
|
1059
|
+
return {
|
|
1060
|
+
currentConfig,
|
|
1061
|
+
serverDefaults: parsed?.serverDefaults,
|
|
1062
|
+
source,
|
|
1063
|
+
collision: Boolean((source === 'mcpTemplates' ? parsed?.mcpServers : parsed?.mcpTemplates)?.[targetName]),
|
|
1064
|
+
};
|
|
701
1065
|
}
|
|
702
1066
|
resolveConfiguredServerSource(targetName, targetSource, document = this.options.readConfigDocument()) {
|
|
703
1067
|
if (targetSource)
|
|
@@ -715,6 +1079,9 @@ export class AdminConfiguredServerService {
|
|
|
715
1079
|
async previewConfiguredServerEditResult(input, currentConfig, serverDefaults, requireCriticalConnectivity = false, source = 'mcpServers') {
|
|
716
1080
|
const normalizedEdit = normalizeEditDraft(input.edit);
|
|
717
1081
|
const sourceValidation = validateEditForSource(source, normalizedEdit.edit);
|
|
1082
|
+
const rawTemplateHeaderValidation = source === 'mcpTemplates'
|
|
1083
|
+
? validateRawTemplateHeaderEdit(input.edit)
|
|
1084
|
+
: { status: 'valid', errors: [] };
|
|
718
1085
|
const currentReadModel = createConfiguredServerReadModel(input.targetName, mergeGlobalAndServerConfig(serverDefaults, currentConfig), source);
|
|
719
1086
|
const secretValidation = validateSecretEditCapabilities(normalizedEdit.edit, currentReadModel.secretInputs);
|
|
720
1087
|
const transportApplicabilityValidation = validateTransportFieldApplicability(currentConfig, normalizedEdit.edit);
|
|
@@ -724,7 +1091,10 @@ export class AdminConfiguredServerService {
|
|
|
724
1091
|
const proposedConfig = applyEditDraft(currentConfig, applicableEdit, serverDefaults, input.targetName);
|
|
725
1092
|
const proposedReadModel = createConfiguredServerReadModel(proposedTargetName, mergeGlobalAndServerConfig(serverDefaults, proposedConfig), source);
|
|
726
1093
|
const proposedTransportType = configuredTransportType(proposedConfig);
|
|
727
|
-
const serverValidation = validatePreviewServerConfig(proposedConfig);
|
|
1094
|
+
const serverValidation = validatePreviewServerConfig(proposedConfig, source);
|
|
1095
|
+
const templateValidation = source === 'mcpTemplates'
|
|
1096
|
+
? templateDefinitionValidation(proposedConfig)
|
|
1097
|
+
: { status: 'valid', errors: [] };
|
|
728
1098
|
const currentToolInventory = this.options.readToolInventory
|
|
729
1099
|
? await this.options.readToolInventory({
|
|
730
1100
|
targetName: input.targetName,
|
|
@@ -734,7 +1104,7 @@ export class AdminConfiguredServerService {
|
|
|
734
1104
|
})
|
|
735
1105
|
: undefined;
|
|
736
1106
|
const toolEditValidation = validateConfiguredToolEdit(applicableEdit, currentToolInventory);
|
|
737
|
-
const validation = mergeValidation(mergeValidation(mergeValidation(mergeValidation(normalizedEdit.validation, secretValidation), transportApplicabilityValidation), overrideValidation), mergeValidation(mergeValidation(serverValidation, sourceValidation), toolEditValidation));
|
|
1107
|
+
const validation = mergeValidation(mergeValidation(mergeValidation(mergeValidation(normalizedEdit.validation, secretValidation), transportApplicabilityValidation), overrideValidation), mergeValidation(mergeValidation(mergeValidation(mergeValidation(serverValidation, sourceValidation), toolEditValidation), rawTemplateHeaderValidation), templateValidation));
|
|
738
1108
|
const diff = createPreviewDiff(currentReadModel, proposedReadModel, applicableEdit);
|
|
739
1109
|
pushDiff(diff, ['disabledTools'], currentConfig.disabledTools ?? [], proposedConfig.disabledTools ?? [], [
|
|
740
1110
|
'tool_visibility',
|
|
@@ -742,6 +1112,9 @@ export class AdminConfiguredServerService {
|
|
|
742
1112
|
pushDiff(diff, ['toolDescriptionOverrides'], currentConfig.toolDescriptionOverrides ?? {}, proposedConfig.toolDescriptionOverrides ?? {}, ['tool_metadata']);
|
|
743
1113
|
appendClearedOverrideDiff(diff, currentReadModel, proposedReadModel, applicableEdit);
|
|
744
1114
|
const changed = validation.status === 'valid' && diff.length > 0;
|
|
1115
|
+
const retirementRequired = source === 'mcpTemplates' &&
|
|
1116
|
+
(proposedTargetName !== input.targetName ||
|
|
1117
|
+
templateRuntimeHash(currentConfig) !== templateRuntimeHash(proposedConfig));
|
|
745
1118
|
const endpointChangedWithPreservedSecrets = endpointAuthorityChanged(currentConfig, proposedConfig) &&
|
|
746
1119
|
hasPreservedSecretInputs(currentReadModel.secretInputs.filter((input) => !proposedTransportType || transportFieldAppliesToType(input.fieldPath[0], proposedTransportType)), applicableEdit);
|
|
747
1120
|
const proposedToolInventory = this.options.readToolInventory
|
|
@@ -773,17 +1146,32 @@ export class AdminConfiguredServerService {
|
|
|
773
1146
|
validation,
|
|
774
1147
|
diff,
|
|
775
1148
|
configChange: previewConfigChange(input.targetName, changed, source),
|
|
776
|
-
connectivityCheck:
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1149
|
+
connectivityCheck: source === 'mcpTemplates'
|
|
1150
|
+
? { status: 'skipped', reason: 'template_structural_preview' }
|
|
1151
|
+
: await this.previewConnectivityCheck({
|
|
1152
|
+
targetName: proposedTargetName,
|
|
1153
|
+
serverConfig: proposedConfig,
|
|
1154
|
+
enabled: proposedReadModel.enabled,
|
|
1155
|
+
validationStatus: validation.status,
|
|
1156
|
+
connectionCriticalChanged: diff.some((entry) => entry.riskFlags.includes('connection_critical')),
|
|
1157
|
+
force: input.connectivityCheck === 'manual' ||
|
|
1158
|
+
(requireCriticalConnectivity && diff.some((entry) => entry.riskFlags.includes('connection_critical'))),
|
|
1159
|
+
endpointChangedWithPreservedSecrets,
|
|
1160
|
+
}),
|
|
786
1161
|
...(toolSelection ? { toolSelection } : {}),
|
|
1162
|
+
...(source === 'mcpTemplates'
|
|
1163
|
+
? {
|
|
1164
|
+
templateAnalysis: analyzeTemplateServerDefinition(proposedConfig),
|
|
1165
|
+
runtimeImpact: {
|
|
1166
|
+
activeInstanceCount: this.options.getTemplateActiveInstanceCount?.(input.targetName) ?? 0,
|
|
1167
|
+
retirementRequired: changed && retirementRequired,
|
|
1168
|
+
createsInstance: false,
|
|
1169
|
+
},
|
|
1170
|
+
}
|
|
1171
|
+
: {}),
|
|
1172
|
+
warnings: source === 'mcpTemplates'
|
|
1173
|
+
? ['Structural preview only: Request Context values are not rendered and no backend is contacted.']
|
|
1174
|
+
: [],
|
|
787
1175
|
};
|
|
788
1176
|
}
|
|
789
1177
|
async previewConnectivityCheck(input) {
|
|
@@ -810,6 +1198,42 @@ export class AdminConfiguredServerService {
|
|
|
810
1198
|
serverConfig: input.serverConfig,
|
|
811
1199
|
});
|
|
812
1200
|
}
|
|
1201
|
+
async observeTemplateRetirement(templateName, configChange, retirementRequired, activeInstancesBefore, instanceIdentities) {
|
|
1202
|
+
if (!retirementRequired) {
|
|
1203
|
+
return {
|
|
1204
|
+
activeInstancesBefore,
|
|
1205
|
+
retiredInstances: 0,
|
|
1206
|
+
activeInstancesAfter: activeInstancesBefore,
|
|
1207
|
+
retirementObserved: true,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (configChange.reload.status === 'failed' || !this.options.observeTemplateDefinitionRetirement) {
|
|
1211
|
+
const activeInstancesAfter = this.options.getTemplateActiveInstanceCount?.(templateName) ?? activeInstancesBefore;
|
|
1212
|
+
return {
|
|
1213
|
+
activeInstancesBefore,
|
|
1214
|
+
retiredInstances: Math.max(0, activeInstancesBefore - activeInstancesAfter),
|
|
1215
|
+
activeInstancesAfter,
|
|
1216
|
+
retirementObserved: activeInstancesAfter === 0,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
const observed = await this.options.observeTemplateDefinitionRetirement(templateName, instanceIdentities);
|
|
1221
|
+
return {
|
|
1222
|
+
...observed,
|
|
1223
|
+
activeInstancesBefore,
|
|
1224
|
+
retiredInstances: Math.max(0, activeInstancesBefore - observed.activeInstancesAfter),
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
catch {
|
|
1228
|
+
return {
|
|
1229
|
+
activeInstancesBefore,
|
|
1230
|
+
retiredInstances: 0,
|
|
1231
|
+
activeInstancesAfter: this.options.getTemplateActiveInstanceCount?.(templateName) ?? activeInstancesBefore,
|
|
1232
|
+
retirementObserved: false,
|
|
1233
|
+
error: 'Template instance retirement could not be confirmed.',
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
813
1237
|
async previewConfiguredServerCreateResult(input, forceConnectivity = false, document = this.options.readConfigDocument() ?? {}) {
|
|
814
1238
|
const prepared = await this.resolveConfiguredServerCreate(input.draft, document);
|
|
815
1239
|
const proposedReadModel = prepared.config
|
|
@@ -818,17 +1242,19 @@ export class AdminConfiguredServerService {
|
|
|
818
1242
|
const validation = prepared.validation;
|
|
819
1243
|
const diff = proposedReadModel ? createConfiguredServerCreateDiff(proposedReadModel, prepared.draft) : [];
|
|
820
1244
|
const changed = validation.status === 'valid' && prepared.workflow.status === 'preview';
|
|
821
|
-
const connectivityCheck = prepared.
|
|
822
|
-
?
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1245
|
+
const connectivityCheck = prepared.targetSource === 'mcpTemplates'
|
|
1246
|
+
? { status: 'skipped', reason: 'template_structural_preview' }
|
|
1247
|
+
: prepared.config
|
|
1248
|
+
? await this.previewConnectivityCheck({
|
|
1249
|
+
targetName: prepared.targetName,
|
|
1250
|
+
serverConfig: prepared.config,
|
|
1251
|
+
enabled: prepared.enabled,
|
|
1252
|
+
validationStatus: validation.status,
|
|
1253
|
+
connectionCriticalChanged: true,
|
|
1254
|
+
force: forceConnectivity || input.connectivityCheck === 'manual',
|
|
1255
|
+
endpointChangedWithPreservedSecrets: false,
|
|
1256
|
+
})
|
|
1257
|
+
: { status: 'skipped', reason: 'validation_failed' };
|
|
832
1258
|
const preview = {
|
|
833
1259
|
targetName: prepared.targetName,
|
|
834
1260
|
previewFingerprint: configuredServerCreatePreviewFingerprint({
|
|
@@ -841,12 +1267,25 @@ export class AdminConfiguredServerService {
|
|
|
841
1267
|
}),
|
|
842
1268
|
validation,
|
|
843
1269
|
diff,
|
|
844
|
-
configChange: previewCreateConfigChange(prepared.targetName, prepared.workflow.status, changed),
|
|
1270
|
+
configChange: previewCreateConfigChange(prepared.targetName, prepared.workflow.status, changed, prepared.targetSource),
|
|
845
1271
|
connectivityCheck,
|
|
846
1272
|
expectedReload: {
|
|
847
1273
|
policy: 'observe_after_write',
|
|
848
1274
|
possibleStatuses: ['observed', 'runtime_not_running', 'reload_disabled', 'failed'],
|
|
849
1275
|
},
|
|
1276
|
+
...(prepared.targetSource === 'mcpTemplates' && prepared.config
|
|
1277
|
+
? {
|
|
1278
|
+
templateAnalysis: analyzeTemplateServerDefinition(prepared.config),
|
|
1279
|
+
runtimeImpact: {
|
|
1280
|
+
activeInstanceCount: 0,
|
|
1281
|
+
retirementRequired: false,
|
|
1282
|
+
createsInstance: false,
|
|
1283
|
+
},
|
|
1284
|
+
}
|
|
1285
|
+
: {}),
|
|
1286
|
+
warnings: prepared.targetSource === 'mcpTemplates'
|
|
1287
|
+
? ['Creating a Template Server definition does not create a runtime instance.']
|
|
1288
|
+
: [],
|
|
850
1289
|
};
|
|
851
1290
|
return preview;
|
|
852
1291
|
}
|
|
@@ -862,8 +1301,11 @@ export class AdminConfiguredServerService {
|
|
|
862
1301
|
},
|
|
863
1302
|
});
|
|
864
1303
|
const workflowResult = await workflow.run({ mode: 'preview', source: normalized.source });
|
|
865
|
-
const config = workflowResult.config;
|
|
866
|
-
const
|
|
1304
|
+
const config = normalized.targetSource === 'mcpTemplates' ? createTemplateServerConfig(normalized.draft) : workflowResult.config;
|
|
1305
|
+
const templateValidation = normalized.targetSource === 'mcpTemplates' && config
|
|
1306
|
+
? templateDefinitionValidation(config)
|
|
1307
|
+
: { status: 'valid', errors: [] };
|
|
1308
|
+
const validation = mergeValidation(mergeValidation(mergeValidation(normalized.validation, validateConfiguredServerCreateSecrets(normalized.draft)), createWorkflowValidation(workflowResult, config, normalized.targetSource)), templateValidation);
|
|
867
1309
|
return {
|
|
868
1310
|
draft: normalized.draft,
|
|
869
1311
|
source: normalized.source,
|
|
@@ -872,20 +1314,71 @@ export class AdminConfiguredServerService {
|
|
|
872
1314
|
workflow: workflowResult,
|
|
873
1315
|
config,
|
|
874
1316
|
validation,
|
|
1317
|
+
targetSource: normalized.targetSource,
|
|
875
1318
|
};
|
|
876
1319
|
}
|
|
877
1320
|
}
|
|
1321
|
+
function createTemplateServerConfig(draft) {
|
|
1322
|
+
if (!draft.transport)
|
|
1323
|
+
return undefined;
|
|
1324
|
+
const config = cloneUnknown(draft.transport);
|
|
1325
|
+
if (draft.tags?.length)
|
|
1326
|
+
config.tags = [...draft.tags];
|
|
1327
|
+
const secretContainer = config.type === 'stdio' ? 'env' : 'headers';
|
|
1328
|
+
const secrets = configuredServerCreateSecretValues(draft, secretContainer);
|
|
1329
|
+
if (Object.keys(secrets).length > 0) {
|
|
1330
|
+
const current = config[secretContainer];
|
|
1331
|
+
const currentRecord = current && !Array.isArray(current) ? current : {};
|
|
1332
|
+
config[secretContainer] = { ...currentRecord, ...secrets };
|
|
1333
|
+
}
|
|
1334
|
+
return config;
|
|
1335
|
+
}
|
|
1336
|
+
function templateDefinitionValidation(config) {
|
|
1337
|
+
const analysis = analyzeTemplateServerDefinition(config);
|
|
1338
|
+
const headerErrors = Object.entries(config.headers ?? {}).flatMap(([name, value]) => typeof value === 'string' && value.includes('{{')
|
|
1339
|
+
? [
|
|
1340
|
+
{
|
|
1341
|
+
fieldPath: ['transport', 'headers', name],
|
|
1342
|
+
code: 'template_header_not_rendered',
|
|
1343
|
+
message: 'Template expressions are not supported in headers because the runtime does not render them.',
|
|
1344
|
+
},
|
|
1345
|
+
]
|
|
1346
|
+
: []);
|
|
1347
|
+
const errors = [...analysis.syntax.errors, ...headerErrors];
|
|
1348
|
+
return {
|
|
1349
|
+
status: errors.length === 0 ? 'valid' : 'invalid',
|
|
1350
|
+
errors,
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
function validateRawTemplateHeaderEdit(value) {
|
|
1354
|
+
const record = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
1355
|
+
const transport = record.transport && typeof record.transport === 'object' && !Array.isArray(record.transport)
|
|
1356
|
+
? record.transport
|
|
1357
|
+
: {};
|
|
1358
|
+
const headers = transport.headers && typeof transport.headers === 'object' && !Array.isArray(transport.headers)
|
|
1359
|
+
? transport.headers
|
|
1360
|
+
: {};
|
|
1361
|
+
const errors = Object.entries(headers).flatMap(([name, entry]) => typeof entry === 'string' && entry.includes('{{')
|
|
1362
|
+
? [
|
|
1363
|
+
{
|
|
1364
|
+
fieldPath: ['transport', 'headers', name],
|
|
1365
|
+
code: 'template_header_not_rendered',
|
|
1366
|
+
message: 'Template expressions are not supported in headers because the runtime does not render them.',
|
|
1367
|
+
},
|
|
1368
|
+
]
|
|
1369
|
+
: []);
|
|
1370
|
+
return { status: errors.length === 0 ? 'valid' : 'invalid', errors };
|
|
1371
|
+
}
|
|
878
1372
|
function validateEditForSource(source, edit) {
|
|
879
1373
|
if (source !== 'mcpTemplates')
|
|
880
1374
|
return { status: 'valid', errors: [] };
|
|
881
|
-
const
|
|
882
|
-
const unsupported = Object.keys(edit).filter((key) => !supportedTemplateMetadata.has(key));
|
|
1375
|
+
const unsupported = Object.keys(edit).filter((key) => key === 'enabled');
|
|
883
1376
|
return {
|
|
884
1377
|
status: unsupported.length === 0 ? 'valid' : 'invalid',
|
|
885
1378
|
errors: unsupported.map((key) => ({
|
|
886
1379
|
fieldPath: [key],
|
|
887
1380
|
code: 'template_edit_field_unsupported',
|
|
888
|
-
message: 'Template Server
|
|
1381
|
+
message: 'Template Server definitions do not support enable or disable operations.',
|
|
889
1382
|
})),
|
|
890
1383
|
};
|
|
891
1384
|
}
|
|
@@ -928,6 +1421,59 @@ function assertSuccessfulConfiguredServerEdit(configChange) {
|
|
|
928
1421
|
throw new AdminConfiguredServerApplyError('configured_server_apply_failed');
|
|
929
1422
|
}
|
|
930
1423
|
}
|
|
1424
|
+
function assertSuccessfulConfiguredServerDelete(configChange) {
|
|
1425
|
+
switch (configChange.status) {
|
|
1426
|
+
case 'changed':
|
|
1427
|
+
return;
|
|
1428
|
+
case 'source_conflict':
|
|
1429
|
+
throw new AdminConfiguredServerApplyError('configured_server_stale_preview');
|
|
1430
|
+
case 'not_found':
|
|
1431
|
+
throw new AdminConfiguredServerApplyError('configured_server_already_removed');
|
|
1432
|
+
default:
|
|
1433
|
+
throw new AdminConfiguredServerApplyError('configured_server_delete_failed');
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function assertSuccessfulConfiguredServerLifecycle(configChange) {
|
|
1437
|
+
switch (configChange.status) {
|
|
1438
|
+
case 'changed':
|
|
1439
|
+
case 'unchanged':
|
|
1440
|
+
return;
|
|
1441
|
+
case 'source_conflict':
|
|
1442
|
+
case 'not_found':
|
|
1443
|
+
throw new AdminConfiguredServerApplyError('configured_server_stale_preview');
|
|
1444
|
+
default:
|
|
1445
|
+
throw new AdminConfiguredServerApplyError('configured_server_apply_failed');
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function sanitizeConfiguredServerDeleteResult(configChange) {
|
|
1449
|
+
return sanitizeConfiguredServerMutationResult(configChange, 'Runtime reload failed after the configuration was deleted.');
|
|
1450
|
+
}
|
|
1451
|
+
function sanitizeConfiguredServerLifecycleResult(configChange) {
|
|
1452
|
+
return sanitizeConfiguredServerMutationResult(configChange, 'Runtime reload failed after the lifecycle configuration was persisted.');
|
|
1453
|
+
}
|
|
1454
|
+
function sanitizeConfiguredServerMutationResult(configChange, reloadFailureMessage) {
|
|
1455
|
+
return {
|
|
1456
|
+
...configChange,
|
|
1457
|
+
configPath: '[redacted]',
|
|
1458
|
+
backup: configChange.backup.created ? { created: true, path: '[redacted]' } : { created: false },
|
|
1459
|
+
retentionCleanup: {
|
|
1460
|
+
attempted: configChange.retentionCleanup.attempted,
|
|
1461
|
+
deletedPaths: configChange.retentionCleanup.deletedPaths.map(() => '[redacted]'),
|
|
1462
|
+
warnings: configChange.retentionCleanup.warnings.map(() => 'A recovery backup retention cleanup failed.'),
|
|
1463
|
+
},
|
|
1464
|
+
reload: configChange.reload.status === 'failed'
|
|
1465
|
+
? { status: 'failed', error: reloadFailureMessage }
|
|
1466
|
+
: configChange.reload,
|
|
1467
|
+
warnings: configChange.warnings.map(() => 'A recovery backup retention cleanup failed.'),
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function sanitizeConfiguredServerIdentityConfirmationFacts(facts) {
|
|
1471
|
+
if (!facts)
|
|
1472
|
+
return undefined;
|
|
1473
|
+
const allowed = ['previewConfirmed', 'targetIdentityConfirmed'];
|
|
1474
|
+
const sanitized = Object.fromEntries(allowed.filter((key) => key in facts).map((key) => [key, facts[key]]));
|
|
1475
|
+
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
|
|
1476
|
+
}
|
|
931
1477
|
function sanitizeApplyConfirmationFacts(facts) {
|
|
932
1478
|
if (!facts)
|
|
933
1479
|
return undefined;
|
|
@@ -1149,9 +1695,13 @@ function isSecretCapableRawTransportEdit(key, value) {
|
|
|
1149
1695
|
if (key === 'url') {
|
|
1150
1696
|
return isSecretCapableUrlEdit(value);
|
|
1151
1697
|
}
|
|
1152
|
-
if (key === 'headers'
|
|
1698
|
+
if (key === 'headers') {
|
|
1153
1699
|
return true;
|
|
1154
1700
|
}
|
|
1701
|
+
if (key === 'env' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1702
|
+
return Object.entries(value).some(([name, entry]) => !(typeof entry === 'string' && isSafeTemplateExpressionValue(entry)) &&
|
|
1703
|
+
(isSecretLikeKey(name) || containsSecretLikeField(entry)));
|
|
1704
|
+
}
|
|
1155
1705
|
if (key === 'oauth' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1156
1706
|
return containsStringOrSecretLikeField(value);
|
|
1157
1707
|
}
|
|
@@ -1774,7 +2324,7 @@ function deleteNestedValue(target, fieldPath) {
|
|
|
1774
2324
|
}
|
|
1775
2325
|
delete cursor[fieldPath[fieldPath.length - 1]];
|
|
1776
2326
|
}
|
|
1777
|
-
function validatePreviewServerConfig(serverConfig) {
|
|
2327
|
+
function validatePreviewServerConfig(serverConfig, source = 'mcpServers') {
|
|
1778
2328
|
const errors = [];
|
|
1779
2329
|
const type = typeof serverConfig.type === 'string' ? serverConfig.type : undefined;
|
|
1780
2330
|
const url = typeof serverConfig.url === 'string' ? serverConfig.url : undefined;
|
|
@@ -1815,7 +2365,7 @@ function validatePreviewServerConfig(serverConfig) {
|
|
|
1815
2365
|
message: 'Transport type must be stdio, http, sse, or streamableHttp.',
|
|
1816
2366
|
});
|
|
1817
2367
|
}
|
|
1818
|
-
if (url !== undefined && !isValidUrlOrEnvReference(url)) {
|
|
2368
|
+
if (url !== undefined && !isValidUrlOrEnvReference(url, source === 'mcpTemplates')) {
|
|
1819
2369
|
errors.push({
|
|
1820
2370
|
fieldPath: ['transport', 'url'],
|
|
1821
2371
|
code: 'invalid_url',
|
|
@@ -1849,7 +2399,9 @@ function mergeValidation(editValidation, serverValidation) {
|
|
|
1849
2399
|
errors,
|
|
1850
2400
|
};
|
|
1851
2401
|
}
|
|
1852
|
-
function isValidUrlOrEnvReference(value) {
|
|
2402
|
+
function isValidUrlOrEnvReference(value, allowHandlebars = false) {
|
|
2403
|
+
if (value.includes('{{'))
|
|
2404
|
+
return allowHandlebars && /\{\{[^}]+\}\}/u.test(value);
|
|
1853
2405
|
if (/\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*/u.test(value)) {
|
|
1854
2406
|
return true;
|
|
1855
2407
|
}
|
|
@@ -1874,6 +2426,19 @@ function previewConfigChange(targetName, changed, source = 'mcpServers') {
|
|
|
1874
2426
|
warnings: [],
|
|
1875
2427
|
};
|
|
1876
2428
|
}
|
|
2429
|
+
function previewDeleteConfigChange(targetName, source) {
|
|
2430
|
+
return {
|
|
2431
|
+
status: 'changed',
|
|
2432
|
+
operation: 'remove',
|
|
2433
|
+
configPath: '[redacted]',
|
|
2434
|
+
target: { name: targetName, source },
|
|
2435
|
+
changed: true,
|
|
2436
|
+
backup: { created: false },
|
|
2437
|
+
retentionCleanup: { attempted: false, deletedPaths: [], warnings: [] },
|
|
2438
|
+
reload: { status: 'skipped' },
|
|
2439
|
+
warnings: [],
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
1877
2442
|
function previewFingerprint(input) {
|
|
1878
2443
|
return `preview_${createHash('sha256')
|
|
1879
2444
|
.update(stableStringify({
|
|
@@ -2086,9 +2651,11 @@ function stableStringify(value) {
|
|
|
2086
2651
|
}
|
|
2087
2652
|
return JSON.stringify(value);
|
|
2088
2653
|
}
|
|
2089
|
-
function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServers', revision = fingerprintConfiguredServerTarget(serverConfig), instructionOverride = serverConfig.instructionOverride, hasInstructionOverride = Object.hasOwn(serverConfig, 'instructionOverride')) {
|
|
2654
|
+
function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServers', revision = fingerprintConfiguredServerTarget(serverConfig), instructionOverride = serverConfig.instructionOverride, hasInstructionOverride = Object.hasOwn(serverConfig, 'instructionOverride'), collision = false, activeInstanceCount = 0, mutationAvailable = true) {
|
|
2090
2655
|
const secretInputs = [];
|
|
2091
2656
|
const transport = {};
|
|
2657
|
+
const templateAnalysis = source === 'mcpTemplates' ? analyzeTemplateServerDefinition(serverConfig) : undefined;
|
|
2658
|
+
const invalidTemplateFields = new Set(templateAnalysis?.syntax.errors.map((error) => fieldKey(error.fieldPath)) ?? []);
|
|
2092
2659
|
for (const [key, value] of Object.entries(serverConfig)) {
|
|
2093
2660
|
if (key === 'disabled' ||
|
|
2094
2661
|
key === 'tags' ||
|
|
@@ -2098,7 +2665,7 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
|
|
|
2098
2665
|
continue;
|
|
2099
2666
|
}
|
|
2100
2667
|
if (key === 'env') {
|
|
2101
|
-
transport.env = redactNamedSecretRecord(value, ['env'], secretInputs);
|
|
2668
|
+
transport.env = redactNamedSecretRecord(value, ['env'], secretInputs, source === 'mcpTemplates', invalidTemplateFields);
|
|
2102
2669
|
continue;
|
|
2103
2670
|
}
|
|
2104
2671
|
if (key === 'headers') {
|
|
@@ -2107,6 +2674,9 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
|
|
|
2107
2674
|
}
|
|
2108
2675
|
if (key === 'args') {
|
|
2109
2676
|
transport.args = sanitizeCommandArgs(value, secretInputs);
|
|
2677
|
+
if (Array.isArray(transport.args)) {
|
|
2678
|
+
transport.args = transport.args.map((entry, index) => invalidTemplateFields.has(fieldKey(['transport', 'args', String(index)])) ? redactedValue() : entry);
|
|
2679
|
+
}
|
|
2110
2680
|
continue;
|
|
2111
2681
|
}
|
|
2112
2682
|
if (key === 'oauth') {
|
|
@@ -2114,7 +2684,13 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
|
|
|
2114
2684
|
continue;
|
|
2115
2685
|
}
|
|
2116
2686
|
if (key === 'url' && typeof value === 'string') {
|
|
2117
|
-
transport.url =
|
|
2687
|
+
transport.url = invalidTemplateFields.has(fieldKey(['transport', 'url']))
|
|
2688
|
+
? redactedValue()
|
|
2689
|
+
: redactUrlQuerySecrets(value, secretInputs);
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
if (invalidTemplateFields.has(fieldKey(['transport', key]))) {
|
|
2693
|
+
transport[key] = redactedValue();
|
|
2118
2694
|
continue;
|
|
2119
2695
|
}
|
|
2120
2696
|
transport[key] = sanitizeUnknownValue(value, [key], secretInputs);
|
|
@@ -2133,15 +2709,11 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
|
|
|
2133
2709
|
tags: normalizeTags(serverConfig.tags),
|
|
2134
2710
|
transportSummary: createTransportSummary(serverConfig, transport),
|
|
2135
2711
|
mutationAvailability: {
|
|
2136
|
-
available:
|
|
2137
|
-
operations:
|
|
2712
|
+
available: mutationAvailable,
|
|
2713
|
+
operations: ['enable', 'disable'],
|
|
2714
|
+
deleteAvailable: mutationAvailable,
|
|
2138
2715
|
},
|
|
2139
|
-
actionState:
|
|
2140
|
-
? createActionState(name, enabled)
|
|
2141
|
-
: {
|
|
2142
|
-
enable: { available: false, label: `Enable ${name}`, disabledReason: 'already_enabled' },
|
|
2143
|
-
disable: { available: false, label: `Disable ${name}`, disabledReason: 'already_disabled' },
|
|
2144
|
-
},
|
|
2716
|
+
actionState: createActionState(name, enabled),
|
|
2145
2717
|
transport,
|
|
2146
2718
|
secretInputs,
|
|
2147
2719
|
instructionOverride: !hasInstructionOverride
|
|
@@ -2149,6 +2721,16 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
|
|
|
2149
2721
|
: instructionOverride === ''
|
|
2150
2722
|
? { state: 'suppress', value: '' }
|
|
2151
2723
|
: { state: 'replace', value: instructionOverride },
|
|
2724
|
+
definition: {
|
|
2725
|
+
kind: source === 'mcpTemplates' ? 'template' : 'static',
|
|
2726
|
+
qualifiedId: `${source}/${name}`,
|
|
2727
|
+
authority: collision ? (source === 'mcpTemplates' ? 'authoritative' : 'shadowed') : 'sole',
|
|
2728
|
+
},
|
|
2729
|
+
...(templateAnalysis ? { templateAnalysis } : {}),
|
|
2730
|
+
runtime: {
|
|
2731
|
+
objectKind: 'definition',
|
|
2732
|
+
activeInstanceCount: source === 'mcpTemplates' ? activeInstanceCount : 0,
|
|
2733
|
+
},
|
|
2152
2734
|
};
|
|
2153
2735
|
}
|
|
2154
2736
|
const STDIO_TRANSPORT_TYPES = ['stdio'];
|
|
@@ -2257,7 +2839,7 @@ const TRANSPORT_FIELD_DEFINITIONS = [
|
|
|
2257
2839
|
const TRANSPORT_FIELD_DEFINITION_BY_KEY = new Map(TRANSPORT_FIELD_DEFINITIONS.map((definition) => [definition.key, definition]));
|
|
2258
2840
|
function createConfiguredServerEditContract(server, rawServerConfig, serverDefaults) {
|
|
2259
2841
|
const secretFieldKeys = new Set(server.secretInputs.map((input) => fieldKey(input.fieldPath)));
|
|
2260
|
-
const transportFields = TRANSPORT_FIELD_DEFINITIONS.map((definition) => {
|
|
2842
|
+
const transportFields = TRANSPORT_FIELD_DEFINITIONS.filter((definition) => definition.key !== 'template').map((definition) => {
|
|
2261
2843
|
const value = Object.prototype.hasOwnProperty.call(server.transport, definition.key)
|
|
2262
2844
|
? server.transport[definition.key]
|
|
2263
2845
|
: definition.defaultValue;
|
|
@@ -2276,7 +2858,7 @@ function createConfiguredServerEditContract(server, rawServerConfig, serverDefau
|
|
|
2276
2858
|
singleTargetEdit: true,
|
|
2277
2859
|
rename: { supported: true },
|
|
2278
2860
|
create: { supported: false },
|
|
2279
|
-
delete: { supported:
|
|
2861
|
+
delete: { supported: server.mutationAvailability.deleteAvailable === true },
|
|
2280
2862
|
bulkEdit: { supported: false },
|
|
2281
2863
|
rawJson: { supported: false },
|
|
2282
2864
|
preview: { supported: true },
|
|
@@ -2299,7 +2881,7 @@ function createConfiguredServerEditContract(server, rawServerConfig, serverDefau
|
|
|
2299
2881
|
label: 'Enabled',
|
|
2300
2882
|
control: 'switch',
|
|
2301
2883
|
value: server.enabled,
|
|
2302
|
-
editable:
|
|
2884
|
+
editable: server.source === 'mcpServers',
|
|
2303
2885
|
},
|
|
2304
2886
|
{
|
|
2305
2887
|
fieldPath: ['tags'],
|
|
@@ -2315,6 +2897,15 @@ function createConfiguredServerEditContract(server, rawServerConfig, serverDefau
|
|
|
2315
2897
|
label: 'Transport',
|
|
2316
2898
|
fields: transportFields,
|
|
2317
2899
|
},
|
|
2900
|
+
...(server.source === 'mcpTemplates'
|
|
2901
|
+
? [
|
|
2902
|
+
{
|
|
2903
|
+
id: 'template',
|
|
2904
|
+
label: 'Template instances',
|
|
2905
|
+
fields: templateLifecycleFields(server.transport.template, rawServerConfig.disabled),
|
|
2906
|
+
},
|
|
2907
|
+
]
|
|
2908
|
+
: []),
|
|
2318
2909
|
{
|
|
2319
2910
|
id: 'secrets',
|
|
2320
2911
|
label: 'Secrets',
|
|
@@ -2323,6 +2914,31 @@ function createConfiguredServerEditContract(server, rawServerConfig, serverDefau
|
|
|
2323
2914
|
],
|
|
2324
2915
|
};
|
|
2325
2916
|
}
|
|
2917
|
+
function templateLifecycleFields(value, disabled) {
|
|
2918
|
+
const template = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
2919
|
+
const extraction = template.extractionOptions && typeof template.extractionOptions === 'object'
|
|
2920
|
+
? template.extractionOptions
|
|
2921
|
+
: {};
|
|
2922
|
+
return [
|
|
2923
|
+
templateField(['disabled'], 'Disabled expression', 'text', disabled ?? ''),
|
|
2924
|
+
templateField(['shareable'], 'Share instances', 'switch', template.shareable ?? true),
|
|
2925
|
+
templateField(['perClient'], 'One instance per client', 'switch', template.perClient ?? false),
|
|
2926
|
+
templateField(['maxInstances'], 'Maximum instances', 'number', template.maxInstances),
|
|
2927
|
+
templateField(['idleTimeout'], 'Idle timeout (ms)', 'number', template.idleTimeout),
|
|
2928
|
+
templateField(['extractionOptions', 'includeOptional'], 'Include optional context', 'switch', extraction.includeOptional ?? false),
|
|
2929
|
+
templateField(['extractionOptions', 'includeEnvironment'], 'Include environment context', 'switch', extraction.includeEnvironment ?? false),
|
|
2930
|
+
];
|
|
2931
|
+
}
|
|
2932
|
+
function templateField(nestedPath, label, control, value) {
|
|
2933
|
+
return {
|
|
2934
|
+
fieldPath: nestedPath[0] === 'disabled' ? ['transport', 'disabled'] : ['transport', 'template', ...nestedPath],
|
|
2935
|
+
label,
|
|
2936
|
+
control,
|
|
2937
|
+
value,
|
|
2938
|
+
editable: true,
|
|
2939
|
+
applicableSources: ['mcpTemplates'],
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2326
2942
|
function omitSecretValues(value, fieldPath, secretFieldKeys) {
|
|
2327
2943
|
if (secretFieldKeys.has(fieldKey(fieldPath))) {
|
|
2328
2944
|
return undefined;
|
|
@@ -2567,7 +3183,7 @@ function wouldUseLocalStdioTransport(serverConfig) {
|
|
|
2567
3183
|
function normalizeTags(tags) {
|
|
2568
3184
|
return Array.isArray(tags) ? tags.filter((tag) => typeof tag === 'string') : [];
|
|
2569
3185
|
}
|
|
2570
|
-
function redactNamedSecretRecord(value, basePath, secretInputs) {
|
|
3186
|
+
function redactNamedSecretRecord(value, basePath, secretInputs, exposeTemplateExpressions = false, invalidTemplateFields = new Set()) {
|
|
2571
3187
|
if (Array.isArray(value)) {
|
|
2572
3188
|
return value.map((entry, index) => {
|
|
2573
3189
|
const label = typeof entry === 'string' && entry.includes('=') ? entry.split('=')[0] : String(index);
|
|
@@ -2579,12 +3195,44 @@ function redactNamedSecretRecord(value, basePath, secretInputs) {
|
|
|
2579
3195
|
return {};
|
|
2580
3196
|
}
|
|
2581
3197
|
const redacted = {};
|
|
2582
|
-
for (const key of Object.
|
|
3198
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3199
|
+
if (invalidTemplateFields.has(fieldKey(['transport', ...basePath, key]))) {
|
|
3200
|
+
redacted[key] = redactedValue();
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
if (exposeTemplateExpressions &&
|
|
3204
|
+
typeof entry === 'string' &&
|
|
3205
|
+
isSafeTemplateExpressionValue(entry) &&
|
|
3206
|
+
!containsSecretLikeString(entry)) {
|
|
3207
|
+
redacted[key] = entry;
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
2583
3210
|
redacted[key] = redactedValue();
|
|
2584
3211
|
secretInputs.push(secretInput([...basePath, key], key));
|
|
2585
3212
|
}
|
|
2586
3213
|
return redacted;
|
|
2587
3214
|
}
|
|
3215
|
+
function isSafeTemplateExpressionValue(value) {
|
|
3216
|
+
if (!value.includes('{{'))
|
|
3217
|
+
return false;
|
|
3218
|
+
const withoutExpressions = value.replace(/\{\{\{?[\s\S]*?\}\}\}?/gu, '');
|
|
3219
|
+
if (!/^[\s,;:|/_.-]*$/u.test(withoutExpressions))
|
|
3220
|
+
return false;
|
|
3221
|
+
try {
|
|
3222
|
+
return !templateAstContainsStringLiteral(Handlebars.parse(value));
|
|
3223
|
+
}
|
|
3224
|
+
catch {
|
|
3225
|
+
return false;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
function templateAstContainsStringLiteral(value) {
|
|
3229
|
+
if (!value || typeof value !== 'object')
|
|
3230
|
+
return false;
|
|
3231
|
+
if (Array.isArray(value))
|
|
3232
|
+
return value.some(templateAstContainsStringLiteral);
|
|
3233
|
+
const node = value;
|
|
3234
|
+
return node.type === 'StringLiteral' || Object.values(node).some(templateAstContainsStringLiteral);
|
|
3235
|
+
}
|
|
2588
3236
|
function redactOAuthConfig(value, secretInputs) {
|
|
2589
3237
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2590
3238
|
return {};
|