@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.
Files changed (158) hide show
  1. package/build/admin/assets/{admin-OAuthServicesWorkspace-BCGj2hP4.js → admin-OAuthServicesWorkspace-BHyDoEos.js} +1 -1
  2. package/build/admin/assets/admin-console-DnlZpz5X.js +3 -0
  3. package/build/admin/assets/admin-vendor-62eL-qdH.js +1 -0
  4. package/build/admin/assets/admin-vendor-BbMrl7MK.js +3 -0
  5. package/build/admin/assets/admin-vendor-DpjVwd81.js +62 -0
  6. package/build/admin/index.html +4 -1
  7. package/build/auth/sdkOAuthServerProvider.d.ts.map +1 -1
  8. package/build/auth/sdkOAuthServerProvider.js +49 -48
  9. package/build/auth/sdkOAuthServerProvider.js.map +1 -1
  10. package/build/auth/storage/authCodeRepository.js +2 -2
  11. package/build/auth/storage/authCodeRepository.js.map +1 -1
  12. package/build/auth/storage/authRequestRepository.js +2 -2
  13. package/build/auth/storage/authRequestRepository.js.map +1 -1
  14. package/build/auth/storage/fileStorageService.d.ts +39 -0
  15. package/build/auth/storage/fileStorageService.d.ts.map +1 -1
  16. package/build/auth/storage/fileStorageService.js +171 -38
  17. package/build/auth/storage/fileStorageService.js.map +1 -1
  18. package/build/auth/storage/oauthStorageService.d.ts +5 -0
  19. package/build/auth/storage/oauthStorageService.d.ts.map +1 -1
  20. package/build/auth/storage/oauthStorageService.js +6 -0
  21. package/build/auth/storage/oauthStorageService.js.map +1 -1
  22. package/build/commands/mcp/install.d.ts +47 -3
  23. package/build/commands/mcp/install.d.ts.map +1 -1
  24. package/build/commands/mcp/install.js +22 -14
  25. package/build/commands/mcp/install.js.map +1 -1
  26. package/build/commands/registry/options.d.ts +3 -0
  27. package/build/commands/registry/options.d.ts.map +1 -1
  28. package/build/commands/registry/options.js +27 -0
  29. package/build/commands/registry/options.js.map +1 -1
  30. package/build/commands/registry/search.d.ts.map +1 -1
  31. package/build/commands/registry/search.js +2 -10
  32. package/build/commands/registry/search.js.map +1 -1
  33. package/build/commands/registry/show.d.ts.map +1 -1
  34. package/build/commands/registry/show.js +2 -10
  35. package/build/commands/registry/show.js.map +1 -1
  36. package/build/commands/registry/status.d.ts.map +1 -1
  37. package/build/commands/registry/status.js +2 -10
  38. package/build/commands/registry/status.js.map +1 -1
  39. package/build/commands/registry/versions.d.ts.map +1 -1
  40. package/build/commands/registry/versions.js +2 -10
  41. package/build/commands/registry/versions.js.map +1 -1
  42. package/build/commands/serve/asyncLoadingOptions.d.ts +16 -0
  43. package/build/commands/serve/asyncLoadingOptions.d.ts.map +1 -1
  44. package/build/commands/serve/asyncLoadingOptions.js +29 -0
  45. package/build/commands/serve/asyncLoadingOptions.js.map +1 -1
  46. package/build/commands/serve/index.d.ts +24 -0
  47. package/build/commands/serve/index.d.ts.map +1 -1
  48. package/build/commands/serve/index.js +24 -0
  49. package/build/commands/serve/index.js.map +1 -1
  50. package/build/commands/serve/serve.d.ts +6 -0
  51. package/build/commands/serve/serve.d.ts.map +1 -1
  52. package/build/commands/serve/serve.js +45 -2
  53. package/build/commands/serve/serve.js.map +1 -1
  54. package/build/commands/serve/serveBackground.d.ts +2 -1
  55. package/build/commands/serve/serveBackground.d.ts.map +1 -1
  56. package/build/commands/serve/serveBackground.js +6 -0
  57. package/build/commands/serve/serveBackground.js.map +1 -1
  58. package/build/commands/wait/wait.js +1 -1
  59. package/build/commands/wait/wait.js.map +1 -1
  60. package/build/config/configManager.d.ts.map +1 -1
  61. package/build/config/configManager.js +19 -6
  62. package/build/config/configManager.js.map +1 -1
  63. package/build/config/configuredServerTargets.d.ts +1 -0
  64. package/build/config/configuredServerTargets.d.ts.map +1 -1
  65. package/build/config/configuredServerTargets.js +6 -0
  66. package/build/config/configuredServerTargets.js.map +1 -1
  67. package/build/constants/mcp.d.ts +1 -1
  68. package/build/constants/mcp.js +1 -1
  69. package/build/core/loading/backendLoadingPolicy.d.ts +11 -0
  70. package/build/core/loading/backendLoadingPolicy.d.ts.map +1 -0
  71. package/build/core/loading/backendLoadingPolicy.js +10 -0
  72. package/build/core/loading/backendLoadingPolicy.js.map +1 -0
  73. package/build/core/loading/mcpLoadingManager.d.ts +8 -24
  74. package/build/core/loading/mcpLoadingManager.d.ts.map +1 -1
  75. package/build/core/loading/mcpLoadingManager.js +116 -70
  76. package/build/core/loading/mcpLoadingManager.js.map +1 -1
  77. package/build/core/server/agentConfig.d.ts +26 -0
  78. package/build/core/server/agentConfig.d.ts.map +1 -1
  79. package/build/core/server/agentConfig.js +11 -0
  80. package/build/core/server/agentConfig.js.map +1 -1
  81. package/build/core/server/clientInstancePool.d.ts +4 -6
  82. package/build/core/server/clientInstancePool.d.ts.map +1 -1
  83. package/build/core/server/clientInstancePool.js +63 -63
  84. package/build/core/server/clientInstancePool.js.map +1 -1
  85. package/build/core/server/clientInstancePoolTypes.d.ts +9 -2
  86. package/build/core/server/clientInstancePoolTypes.d.ts.map +1 -1
  87. package/build/core/server/clientInstancePoolTypes.js +8 -3
  88. package/build/core/server/clientInstancePoolTypes.js.map +1 -1
  89. package/build/core/server/requestContextPreparation.d.ts.map +1 -1
  90. package/build/core/server/requestContextPreparation.js +2 -1
  91. package/build/core/server/requestContextPreparation.js.map +1 -1
  92. package/build/core/server/serverManager.d.ts.map +1 -1
  93. package/build/core/server/serverManager.js +2 -1
  94. package/build/core/server/serverManager.js.map +1 -1
  95. package/build/core/server/templateServerManager.d.ts +14 -1
  96. package/build/core/server/templateServerManager.d.ts.map +1 -1
  97. package/build/core/server/templateServerManager.js +64 -9
  98. package/build/core/server/templateServerManager.js.map +1 -1
  99. package/build/core/tools/internal/schemas/index.d.ts +1 -1
  100. package/build/core/tools/internal/schemas/installation.d.ts +1 -1
  101. package/build/core/types/transport.d.ts +82 -2
  102. package/build/core/types/transport.d.ts.map +1 -1
  103. package/build/core/types/transport.js +72 -0
  104. package/build/core/types/transport.js.map +1 -1
  105. package/build/domains/admin/adminConfiguredServerService.d.ts +146 -2
  106. package/build/domains/admin/adminConfiguredServerService.d.ts.map +1 -1
  107. package/build/domains/admin/adminConfiguredServerService.js +729 -81
  108. package/build/domains/admin/adminConfiguredServerService.js.map +1 -1
  109. package/build/domains/admin/adminDomain.d.ts +5 -0
  110. package/build/domains/admin/adminDomain.d.ts.map +1 -1
  111. package/build/domains/admin/adminDomain.js +11 -0
  112. package/build/domains/admin/adminDomain.js.map +1 -1
  113. package/build/domains/admin/adminOperationService.d.ts +1 -0
  114. package/build/domains/admin/adminOperationService.d.ts.map +1 -1
  115. package/build/domains/admin/adminOperationService.js +6 -2
  116. package/build/domains/admin/adminOperationService.js.map +1 -1
  117. package/build/domains/admin/templateServerDefinitionAnalyzer.d.ts +26 -0
  118. package/build/domains/admin/templateServerDefinitionAnalyzer.d.ts.map +1 -0
  119. package/build/domains/admin/templateServerDefinitionAnalyzer.js +148 -0
  120. package/build/domains/admin/templateServerDefinitionAnalyzer.js.map +1 -0
  121. package/build/domains/config-change/configChange.d.ts +1 -1
  122. package/build/domains/config-change/configChange.d.ts.map +1 -1
  123. package/build/domains/config-change/configChange.js +168 -49
  124. package/build/domains/config-change/configChange.js.map +1 -1
  125. package/build/domains/config-change/types.d.ts +16 -2
  126. package/build/domains/config-change/types.d.ts.map +1 -1
  127. package/build/domains/registry/mcpRegistryClient.d.ts +3 -3
  128. package/build/domains/registry/mcpRegistryClient.d.ts.map +1 -1
  129. package/build/domains/registry/mcpRegistryClient.js +8 -7
  130. package/build/domains/registry/mcpRegistryClient.js.map +1 -1
  131. package/build/logger/secureLogger.d.ts.map +1 -1
  132. package/build/logger/secureLogger.js +160 -23
  133. package/build/logger/secureLogger.js.map +1 -1
  134. package/build/server.d.ts +2 -1
  135. package/build/server.d.ts.map +1 -1
  136. package/build/server.js +8 -7
  137. package/build/server.js.map +1 -1
  138. package/build/transport/http/middlewares/securityMiddleware.d.ts +6 -0
  139. package/build/transport/http/middlewares/securityMiddleware.d.ts.map +1 -1
  140. package/build/transport/http/middlewares/securityMiddleware.js +35 -29
  141. package/build/transport/http/middlewares/securityMiddleware.js.map +1 -1
  142. package/build/transport/http/routes/adminRoutes.d.ts +17 -1
  143. package/build/transport/http/routes/adminRoutes.d.ts.map +1 -1
  144. package/build/transport/http/routes/adminRoutes.js +281 -17
  145. package/build/transport/http/routes/adminRoutes.js.map +1 -1
  146. package/build/transport/http/routes/healthRoutes.d.ts +6 -1
  147. package/build/transport/http/routes/healthRoutes.d.ts.map +1 -1
  148. package/build/transport/http/routes/healthRoutes.js +7 -3
  149. package/build/transport/http/routes/healthRoutes.js.map +1 -1
  150. package/build/transport/http/server.d.ts.map +1 -1
  151. package/build/transport/http/server.js +21 -3
  152. package/build/transport/http/server.js.map +1 -1
  153. package/build/transport/http/storage/streamableSessionRepository.d.ts +1 -0
  154. package/build/transport/http/storage/streamableSessionRepository.d.ts.map +1 -1
  155. package/build/transport/http/storage/streamableSessionRepository.js +6 -3
  156. package/build/transport/http/storage/streamableSessionRepository.js.map +1 -1
  157. package/package.json +1 -1
  158. 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
- { fieldPath: ['enabled'], label: 'Enabled', control: 'switch', value: true, editable: true },
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
- ...(workflowStatus === 'template_conflict'
325
- ? { source: 'mcpTemplates' }
326
- : workflowStatus === 'exists'
327
- ? { source: 'mcpServers' }
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.enabled &&
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 = await this.options.configChangeService.createStaticConfiguredServerTarget({
442
- targetName: prepared.targetName,
443
- serverConfig: prepared.config,
444
- operation: 'install',
445
- backup: 'skip',
446
- expectedConfigFingerprint: configFingerprint,
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}:${input.targetName}` },
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}:${input.targetName}` },
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}:${input.targetName}` },
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}:${input.targetName}` },
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 (connectionCritical &&
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}:${input.targetName}` },
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
- readConfiguredServerState(targetName, targetSource) {
694
- const parsed = this.options.readConfigDocument();
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 { currentConfig, serverDefaults: parsed?.serverDefaults, source };
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: await this.previewConnectivityCheck({
777
- targetName: proposedTargetName,
778
- serverConfig: proposedConfig,
779
- enabled: proposedReadModel.enabled,
780
- validationStatus: validation.status,
781
- connectionCriticalChanged: diff.some((entry) => entry.riskFlags.includes('connection_critical')),
782
- force: input.connectivityCheck === 'manual' ||
783
- (requireCriticalConnectivity && diff.some((entry) => entry.riskFlags.includes('connection_critical'))),
784
- endpointChangedWithPreservedSecrets,
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.config
822
- ? await this.previewConnectivityCheck({
823
- targetName: prepared.targetName,
824
- serverConfig: prepared.config,
825
- enabled: prepared.enabled,
826
- validationStatus: validation.status,
827
- connectionCriticalChanged: true,
828
- force: forceConnectivity || input.connectivityCheck === 'manual',
829
- endpointChangedWithPreservedSecrets: false,
830
- })
831
- : { status: 'skipped', reason: 'validation_failed' };
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 validation = mergeValidation(mergeValidation(normalized.validation, validateConfiguredServerCreateSecrets(normalized.draft)), createWorkflowValidation(workflowResult, config));
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 supportedTemplateMetadata = new Set(['instructionOverride', 'disabledTools', 'toolDescriptionOverrides']);
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 edits may only change instruction and tool metadata.',
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' || key === 'env') {
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 = redactUrlQuerySecrets(value, secretInputs);
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: source === 'mcpServers',
2137
- operations: source === 'mcpServers' ? ['enable', 'disable'] : [],
2712
+ available: mutationAvailable,
2713
+ operations: ['enable', 'disable'],
2714
+ deleteAvailable: mutationAvailable,
2138
2715
  },
2139
- actionState: source === 'mcpServers'
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: false },
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: true,
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.keys(value)) {
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 {};