@1mcp/agent 0.35.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 (230) hide show
  1. package/build/admin/assets/{admin-OAuthServicesWorkspace-B8y2weAX.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/configContext.d.ts +2 -0
  61. package/build/config/configContext.d.ts.map +1 -1
  62. package/build/config/configContext.js +5 -0
  63. package/build/config/configContext.js.map +1 -1
  64. package/build/config/configLoader.d.ts +8 -0
  65. package/build/config/configLoader.d.ts.map +1 -1
  66. package/build/config/configLoader.js +35 -11
  67. package/build/config/configLoader.js.map +1 -1
  68. package/build/config/configManager.d.ts +5 -0
  69. package/build/config/configManager.d.ts.map +1 -1
  70. package/build/config/configManager.js +83 -6
  71. package/build/config/configManager.js.map +1 -1
  72. package/build/config/configWatcher.d.ts +2 -0
  73. package/build/config/configWatcher.d.ts.map +1 -1
  74. package/build/config/configWatcher.js +9 -4
  75. package/build/config/configWatcher.js.map +1 -1
  76. package/build/config/configuredServerTargets.d.ts +1 -0
  77. package/build/config/configuredServerTargets.d.ts.map +1 -1
  78. package/build/config/configuredServerTargets.js +6 -0
  79. package/build/config/configuredServerTargets.js.map +1 -1
  80. package/build/config/envProcessor.d.ts +2 -1
  81. package/build/config/envProcessor.d.ts.map +1 -1
  82. package/build/config/envProcessor.js +16 -9
  83. package/build/config/envProcessor.js.map +1 -1
  84. package/build/config/runtimeScopeEnv.d.ts +15 -0
  85. package/build/config/runtimeScopeEnv.d.ts.map +1 -0
  86. package/build/config/runtimeScopeEnv.js +231 -0
  87. package/build/config/runtimeScopeEnv.js.map +1 -0
  88. package/build/config/runtimeTargetFingerprint.d.ts +3 -0
  89. package/build/config/runtimeTargetFingerprint.d.ts.map +1 -0
  90. package/build/config/runtimeTargetFingerprint.js +66 -0
  91. package/build/config/runtimeTargetFingerprint.js.map +1 -0
  92. package/build/config/types.d.ts +5 -0
  93. package/build/config/types.d.ts.map +1 -1
  94. package/build/config/types.js +1 -0
  95. package/build/config/types.js.map +1 -1
  96. package/build/constants/mcp.d.ts +1 -1
  97. package/build/constants/mcp.js +1 -1
  98. package/build/core/capabilities/capabilityPagination.d.ts.map +1 -1
  99. package/build/core/capabilities/capabilityPagination.js +5 -4
  100. package/build/core/capabilities/capabilityPagination.js.map +1 -1
  101. package/build/core/capabilities/configuredToolSnapshot.d.ts +22 -0
  102. package/build/core/capabilities/configuredToolSnapshot.d.ts.map +1 -1
  103. package/build/core/capabilities/configuredToolSnapshot.js +39 -0
  104. package/build/core/capabilities/configuredToolSnapshot.js.map +1 -1
  105. package/build/core/client/clientManager.d.ts.map +1 -1
  106. package/build/core/client/clientManager.js +25 -13
  107. package/build/core/client/clientManager.js.map +1 -1
  108. package/build/core/client/connectionHandler.d.ts.map +1 -1
  109. package/build/core/client/connectionHandler.js +9 -6
  110. package/build/core/client/connectionHandler.js.map +1 -1
  111. package/build/core/configChangeHandler.d.ts +3 -0
  112. package/build/core/configChangeHandler.d.ts.map +1 -1
  113. package/build/core/configChangeHandler.js +21 -4
  114. package/build/core/configChangeHandler.js.map +1 -1
  115. package/build/core/loading/backendLoadingPolicy.d.ts +11 -0
  116. package/build/core/loading/backendLoadingPolicy.d.ts.map +1 -0
  117. package/build/core/loading/backendLoadingPolicy.js +10 -0
  118. package/build/core/loading/backendLoadingPolicy.js.map +1 -0
  119. package/build/core/loading/mcpLoadingManager.d.ts +8 -24
  120. package/build/core/loading/mcpLoadingManager.d.ts.map +1 -1
  121. package/build/core/loading/mcpLoadingManager.js +121 -73
  122. package/build/core/loading/mcpLoadingManager.js.map +1 -1
  123. package/build/core/server/agentConfig.d.ts +26 -0
  124. package/build/core/server/agentConfig.d.ts.map +1 -1
  125. package/build/core/server/agentConfig.js +11 -0
  126. package/build/core/server/agentConfig.js.map +1 -1
  127. package/build/core/server/backendStdioSupervisor.d.ts.map +1 -1
  128. package/build/core/server/backendStdioSupervisor.js +2 -1
  129. package/build/core/server/backendStdioSupervisor.js.map +1 -1
  130. package/build/core/server/clientInstanceFactory.d.ts +2 -1
  131. package/build/core/server/clientInstanceFactory.d.ts.map +1 -1
  132. package/build/core/server/clientInstanceFactory.js +9 -2
  133. package/build/core/server/clientInstanceFactory.js.map +1 -1
  134. package/build/core/server/clientInstancePool.d.ts +5 -6
  135. package/build/core/server/clientInstancePool.d.ts.map +1 -1
  136. package/build/core/server/clientInstancePool.js +123 -86
  137. package/build/core/server/clientInstancePool.js.map +1 -1
  138. package/build/core/server/clientInstancePoolTypes.d.ts +11 -2
  139. package/build/core/server/clientInstancePoolTypes.d.ts.map +1 -1
  140. package/build/core/server/clientInstancePoolTypes.js +8 -3
  141. package/build/core/server/clientInstancePoolTypes.js.map +1 -1
  142. package/build/core/server/mcpServerLifecycleManager.d.ts +0 -4
  143. package/build/core/server/mcpServerLifecycleManager.d.ts.map +1 -1
  144. package/build/core/server/mcpServerLifecycleManager.js +18 -40
  145. package/build/core/server/mcpServerLifecycleManager.js.map +1 -1
  146. package/build/core/server/requestContextPreparation.d.ts.map +1 -1
  147. package/build/core/server/requestContextPreparation.js +2 -1
  148. package/build/core/server/requestContextPreparation.js.map +1 -1
  149. package/build/core/server/serverManager.d.ts +1 -0
  150. package/build/core/server/serverManager.d.ts.map +1 -1
  151. package/build/core/server/serverManager.js +33 -1
  152. package/build/core/server/serverManager.js.map +1 -1
  153. package/build/core/server/templateServerManager.d.ts +22 -1
  154. package/build/core/server/templateServerManager.d.ts.map +1 -1
  155. package/build/core/server/templateServerManager.js +107 -13
  156. package/build/core/server/templateServerManager.js.map +1 -1
  157. package/build/core/tools/handlers/serverManagementHandler.d.ts +7 -7
  158. package/build/core/tools/internal/adapters/helpers/configTransformers.d.ts +1 -1
  159. package/build/core/tools/internal/schemas/index.d.ts +2 -2
  160. package/build/core/tools/internal/schemas/installation.d.ts +1 -1
  161. package/build/core/tools/internal/schemas/management.d.ts +1 -1
  162. package/build/core/types/transport.d.ts +88 -8
  163. package/build/core/types/transport.d.ts.map +1 -1
  164. package/build/core/types/transport.js +78 -1
  165. package/build/core/types/transport.js.map +1 -1
  166. package/build/domains/admin/adminConfiguredServerService.d.ts +160 -2
  167. package/build/domains/admin/adminConfiguredServerService.d.ts.map +1 -1
  168. package/build/domains/admin/adminConfiguredServerService.js +761 -91
  169. package/build/domains/admin/adminConfiguredServerService.js.map +1 -1
  170. package/build/domains/admin/adminDomain.d.ts +11 -0
  171. package/build/domains/admin/adminDomain.d.ts.map +1 -1
  172. package/build/domains/admin/adminDomain.js +12 -0
  173. package/build/domains/admin/adminDomain.js.map +1 -1
  174. package/build/domains/admin/adminOperationService.d.ts +1 -0
  175. package/build/domains/admin/adminOperationService.d.ts.map +1 -1
  176. package/build/domains/admin/adminOperationService.js +6 -2
  177. package/build/domains/admin/adminOperationService.js.map +1 -1
  178. package/build/domains/admin/configuredToolInventory.d.ts +19 -1
  179. package/build/domains/admin/configuredToolInventory.d.ts.map +1 -1
  180. package/build/domains/admin/configuredToolInventory.js +78 -31
  181. package/build/domains/admin/configuredToolInventory.js.map +1 -1
  182. package/build/domains/admin/runtimeConfiguredToolInspectionService.d.ts +21 -0
  183. package/build/domains/admin/runtimeConfiguredToolInspectionService.d.ts.map +1 -0
  184. package/build/domains/admin/runtimeConfiguredToolInspectionService.js +255 -0
  185. package/build/domains/admin/runtimeConfiguredToolInspectionService.js.map +1 -0
  186. package/build/domains/admin/templateServerDefinitionAnalyzer.d.ts +26 -0
  187. package/build/domains/admin/templateServerDefinitionAnalyzer.d.ts.map +1 -0
  188. package/build/domains/admin/templateServerDefinitionAnalyzer.js +148 -0
  189. package/build/domains/admin/templateServerDefinitionAnalyzer.js.map +1 -0
  190. package/build/domains/config-change/configChange.d.ts +1 -1
  191. package/build/domains/config-change/configChange.d.ts.map +1 -1
  192. package/build/domains/config-change/configChange.js +168 -49
  193. package/build/domains/config-change/configChange.js.map +1 -1
  194. package/build/domains/config-change/types.d.ts +16 -2
  195. package/build/domains/config-change/types.d.ts.map +1 -1
  196. package/build/domains/registry/mcpRegistryClient.d.ts +3 -3
  197. package/build/domains/registry/mcpRegistryClient.d.ts.map +1 -1
  198. package/build/domains/registry/mcpRegistryClient.js +8 -7
  199. package/build/domains/registry/mcpRegistryClient.js.map +1 -1
  200. package/build/logger/secureLogger.d.ts.map +1 -1
  201. package/build/logger/secureLogger.js +160 -23
  202. package/build/logger/secureLogger.js.map +1 -1
  203. package/build/server.d.ts +2 -1
  204. package/build/server.d.ts.map +1 -1
  205. package/build/server.js +8 -7
  206. package/build/server.js.map +1 -1
  207. package/build/transport/http/middlewares/securityMiddleware.d.ts +6 -0
  208. package/build/transport/http/middlewares/securityMiddleware.d.ts.map +1 -1
  209. package/build/transport/http/middlewares/securityMiddleware.js +35 -29
  210. package/build/transport/http/middlewares/securityMiddleware.js.map +1 -1
  211. package/build/transport/http/routes/adminRoutes.d.ts +17 -1
  212. package/build/transport/http/routes/adminRoutes.d.ts.map +1 -1
  213. package/build/transport/http/routes/adminRoutes.js +331 -15
  214. package/build/transport/http/routes/adminRoutes.js.map +1 -1
  215. package/build/transport/http/routes/healthRoutes.d.ts +6 -1
  216. package/build/transport/http/routes/healthRoutes.d.ts.map +1 -1
  217. package/build/transport/http/routes/healthRoutes.js +7 -3
  218. package/build/transport/http/routes/healthRoutes.js.map +1 -1
  219. package/build/transport/http/server.d.ts.map +1 -1
  220. package/build/transport/http/server.js +25 -8
  221. package/build/transport/http/server.js.map +1 -1
  222. package/build/transport/http/storage/streamableSessionRepository.d.ts +1 -0
  223. package/build/transport/http/storage/streamableSessionRepository.d.ts.map +1 -1
  224. package/build/transport/http/storage/streamableSessionRepository.js +6 -3
  225. package/build/transport/http/storage/streamableSessionRepository.js.map +1 -1
  226. package/build/transport/transportFactory.d.ts.map +1 -1
  227. package/build/transport/transportFactory.js +34 -12
  228. package/build/transport/transportFactory.js.map +1 -1
  229. package/package.json +6 -1
  230. package/build/admin/assets/admin-console-HJH52xGP.js +0 -3
@@ -1,19 +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';
8
- const ENV_PLACEHOLDER_PATTERN = /\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*/u;
9
- const previewTransportConfigSchema = transportConfigSchema.extend({
10
- url: z
11
- .string()
12
- .refine((value) => ENV_PLACEHOLDER_PATTERN.test(value) || z.string().url().safeParse(value).success, {
13
- message: 'URL must be a valid URL or environment substitution reference.',
14
- })
15
- .optional(),
16
- });
10
+ import { analyzeTemplateServerDefinition, } from './templateServerDefinitionAnalyzer.js';
17
11
  const configuredServerCreateSecretSchema = z
18
12
  .object({
19
13
  fieldPath: z.array(z.string()).min(2),
@@ -26,6 +20,7 @@ const configuredServerCreateSecretSchema = z
26
20
  .strict();
27
21
  const configuredServerCreateDraftSchema = z
28
22
  .object({
23
+ source: z.enum(['mcpServers', 'mcpTemplates']).optional().default('mcpServers'),
29
24
  name: z.string(),
30
25
  enabled: z.boolean().optional(),
31
26
  tags: z.array(z.string()).optional(),
@@ -44,6 +39,33 @@ const configuredServerCreateDraftSchema = z
44
39
  restartOnExit: z.boolean().optional(),
45
40
  maxRestarts: z.number().optional(),
46
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(),
47
69
  })
48
70
  .strict(),
49
71
  secrets: z.array(configuredServerCreateSecretSchema).optional(),
@@ -63,6 +85,12 @@ const CONFIGURED_SERVER_CREATE_TRANSPORT_FIELD_KEYS = new Set([
63
85
  'restartOnExit',
64
86
  'maxRestarts',
65
87
  'restartDelay',
88
+ 'disabled',
89
+ 'stderr',
90
+ 'inheritParentEnv',
91
+ 'envFilter',
92
+ 'oauth',
93
+ 'template',
66
94
  ]);
67
95
  function normalizeConfiguredServerCreateDraft(value) {
68
96
  const parsed = configuredServerCreateDraftSchema.safeParse(value);
@@ -79,6 +107,7 @@ function normalizeConfiguredServerCreateDraft(value) {
79
107
  localName: typeof record.name === 'string' ? record.name : '',
80
108
  transport: transportType,
81
109
  },
110
+ targetSource: record.source === 'mcpTemplates' ? 'mcpTemplates' : 'mcpServers',
82
111
  validation: {
83
112
  status: 'invalid',
84
113
  errors: parsed.error.issues.map((issue) => ({
@@ -91,6 +120,7 @@ function normalizeConfiguredServerCreateDraft(value) {
91
120
  }
92
121
  const { transport } = parsed.data;
93
122
  const draft = {
123
+ source: parsed.data.source,
94
124
  name: parsed.data.name,
95
125
  enabled: parsed.data.enabled,
96
126
  tags: parsed.data.tags,
@@ -118,7 +148,7 @@ function normalizeConfiguredServerCreateDraft(value) {
118
148
  maxRestarts: transport.maxRestarts,
119
149
  restartDelay: transport.restartDelay,
120
150
  };
121
- return { draft, source, validation: { status: 'valid', errors: [] } };
151
+ return { draft, source, targetSource: parsed.data.source, validation: { status: 'valid', errors: [] } };
122
152
  }
123
153
  function configuredServerCreateSecretValues(draft, container) {
124
154
  const values = {};
@@ -136,7 +166,7 @@ function mergeConfiguredServerCreateRecord(base, additions) {
136
166
  const merged = { ...base, ...additions };
137
167
  return Object.keys(merged).length > 0 ? merged : undefined;
138
168
  }
139
- function createWorkflowValidation(workflow, config) {
169
+ function createWorkflowValidation(workflow, config, source = 'mcpServers') {
140
170
  const errors = [];
141
171
  for (const [field, messages] of Object.entries(workflow.fieldErrors ?? {})) {
142
172
  for (const message of messages) {
@@ -155,9 +185,15 @@ function createWorkflowValidation(workflow, config) {
155
185
  });
156
186
  }
157
187
  if (config) {
158
- const parsed = previewTransportConfigSchema.safeParse(config);
188
+ const parsed = transportConfigSchema.safeParse(config);
159
189
  if (!parsed.success) {
160
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
+ }
161
197
  errors.push({
162
198
  fieldPath: ['transport', ...issue.path.map(String)],
163
199
  code: 'invalid_transport_config',
@@ -208,7 +244,7 @@ function validateConfiguredServerCreateSecrets(draft) {
208
244
  return { status: errors.length === 0 ? 'valid' : 'invalid', errors };
209
245
  }
210
246
  function createConfiguredServerCreateContract() {
211
- 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) => ({
212
248
  fieldPath: ['transport', definition.key],
213
249
  label: definition.label,
214
250
  control: definition.control,
@@ -243,12 +279,28 @@ function createConfiguredServerCreateContract() {
243
279
  id: 'identity',
244
280
  label: 'Identity',
245
281
  fields: [
282
+ {
283
+ fieldPath: ['source'],
284
+ label: 'Definition Type',
285
+ control: 'select',
286
+ value: 'mcpServers',
287
+ options: ['mcpServers', 'mcpTemplates'],
288
+ editable: true,
289
+ },
246
290
  { fieldPath: ['name'], label: 'Server Name', control: 'text', editable: true },
247
- { 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
+ },
248
299
  { fieldPath: ['tags'], label: 'Tags', control: 'tag-list', value: [], editable: true },
249
300
  ],
250
301
  },
251
302
  { id: 'transport', label: 'Transport', fields: transportFields },
303
+ { id: 'template', label: 'Template instances', fields: templateLifecycleFields() },
252
304
  ],
253
305
  };
254
306
  }
@@ -316,7 +368,7 @@ function connectivityFingerprintFacts(connectivityCheck) {
316
368
  return connectivityCheck;
317
369
  return { status: connectivityCheck.status, mode: connectivityCheck.mode };
318
370
  }
319
- function previewCreateConfigChange(targetName, workflowStatus, changed) {
371
+ function previewCreateConfigChange(targetName, workflowStatus, changed, source = 'mcpServers') {
320
372
  const status = workflowStatus === 'template_conflict'
321
373
  ? 'template_conflict'
322
374
  : workflowStatus === 'exists'
@@ -326,15 +378,17 @@ function previewCreateConfigChange(targetName, workflowStatus, changed) {
326
378
  : 'failed';
327
379
  return {
328
380
  status,
329
- operation: 'create_static',
381
+ operation: source === 'mcpTemplates' ? 'create_template' : 'create_static',
330
382
  configPath: '[redacted]',
331
383
  target: {
332
384
  name: targetName,
333
- ...(workflowStatus === 'template_conflict'
334
- ? { source: 'mcpTemplates' }
335
- : workflowStatus === 'exists'
336
- ? { source: 'mcpServers' }
337
- : {}),
385
+ ...(changed
386
+ ? { source }
387
+ : workflowStatus === 'template_conflict'
388
+ ? { source: 'mcpTemplates' }
389
+ : workflowStatus === 'exists'
390
+ ? { source: 'mcpServers' }
391
+ : {}),
338
392
  },
339
393
  changed,
340
394
  backup: { created: false },
@@ -349,6 +403,11 @@ function createDraftCandidateName(value) {
349
403
  const name = value.name;
350
404
  return typeof name === 'string' ? name : '';
351
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
+ }
352
411
  function sanitizeCreateConfirmationFacts(facts) {
353
412
  if (!facts)
354
413
  return undefined;
@@ -405,7 +464,7 @@ export class AdminConfiguredServerService {
405
464
  const candidateName = createDraftCandidateName(input.draft);
406
465
  const context = {
407
466
  ...input.context,
408
- target: { type: 'configured_server', id: candidateName },
467
+ target: { type: 'configured_server', id: `${createDraftCandidateSource(input.draft)}/${candidateName}` },
409
468
  confirmationFacts: sanitizeCreateConfirmationFacts(input.context.confirmationFacts),
410
469
  };
411
470
  return this.options.operationService.executeMutation({
@@ -424,7 +483,8 @@ export class AdminConfiguredServerService {
424
483
  if (!prepared.config) {
425
484
  throw new AdminConfiguredServerApplyError('configured_server_create_invalid');
426
485
  }
427
- if (prepared.enabled &&
486
+ if (prepared.targetSource === 'mcpServers' &&
487
+ prepared.enabled &&
428
488
  !wouldUseLocalStdioTransport(prepared.config) &&
429
489
  preview.connectivityCheck.status !== 'passed' &&
430
490
  !(preview.connectivityCheck.status === 'skipped' && preview.connectivityCheck.reason === 'checker_unavailable') &&
@@ -447,13 +507,21 @@ export class AdminConfiguredServerService {
447
507
  run: async () => {
448
508
  let configChange;
449
509
  try {
450
- configChange = await this.options.configChangeService.createStaticConfiguredServerTarget({
451
- targetName: prepared.targetName,
452
- serverConfig: prepared.config,
453
- operation: 'install',
454
- backup: 'skip',
455
- expectedConfigFingerprint: configFingerprint,
456
- });
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
+ });
457
525
  }
458
526
  catch {
459
527
  throw new AdminConfiguredServerApplyError('configured_server_create_failed');
@@ -461,8 +529,12 @@ export class AdminConfiguredServerService {
461
529
  assertSuccessfulConfiguredServerCreate(configChange);
462
530
  return {
463
531
  targetName: prepared.targetName,
532
+ targetSource: prepared.targetSource,
464
533
  previewFingerprint: preview.previewFingerprint,
465
534
  configChange,
535
+ ...(prepared.targetSource === 'mcpTemplates'
536
+ ? { runtimeImpact: { activeInstanceCount: 0, createdInstance: false } }
537
+ : {}),
466
538
  };
467
539
  },
468
540
  };
@@ -481,14 +553,14 @@ export class AdminConfiguredServerService {
481
553
  const source = configuredState.source;
482
554
  const context = {
483
555
  ...input.context,
484
- target: { type: 'configured_server', id: `${source}:${input.targetName}` },
556
+ target: { type: 'configured_server', id: `${source}/${input.targetName}` },
485
557
  };
486
558
  return this.options.operationService.executeReadOnly({
487
559
  context,
488
560
  operationName: 'getConfiguredServerDetail',
489
561
  run: async () => {
490
- const { currentConfig, serverDefaults } = configuredState;
491
- 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);
492
564
  return {
493
565
  server,
494
566
  editContract: createConfiguredServerEditContract(server, currentConfig, serverDefaults),
@@ -506,12 +578,43 @@ export class AdminConfiguredServerService {
506
578
  },
507
579
  });
508
580
  }
581
+ async refreshConfiguredToolInventory(input) {
582
+ const configuredState = this.readConfiguredServerState(input.targetName, input.targetSource);
583
+ const context = {
584
+ ...input.context,
585
+ target: { type: 'configured_server', id: `${configuredState.source}/${input.targetName}` },
586
+ };
587
+ return this.options.operationService.executeReadOnly({
588
+ context,
589
+ operationName: 'refreshConfiguredToolInventory',
590
+ run: async () => {
591
+ const refresh = this.options.refreshToolInventory;
592
+ if (!refresh) {
593
+ if (!this.options.readToolInventory) {
594
+ throw new Error('Configured tool inventory is unavailable');
595
+ }
596
+ return this.options.readToolInventory({
597
+ targetName: input.targetName,
598
+ source: configuredState.source,
599
+ config: configuredState.currentConfig,
600
+ ...(input.model ? { model: input.model } : {}),
601
+ });
602
+ }
603
+ return refresh({
604
+ targetName: input.targetName,
605
+ source: configuredState.source,
606
+ config: configuredState.currentConfig,
607
+ ...(input.model ? { model: input.model } : {}),
608
+ });
609
+ },
610
+ });
611
+ }
509
612
  async previewConfiguredServerEdit(input) {
510
613
  const configuredState = this.readConfiguredServerState(input.targetName, input.targetSource);
511
614
  const source = configuredState.source;
512
615
  const context = {
513
616
  ...input.context,
514
- target: { type: 'configured_server', id: `${source}:${input.targetName}` },
617
+ target: { type: 'configured_server', id: `${source}/${input.targetName}` },
515
618
  };
516
619
  const { currentConfig, serverDefaults } = configuredState;
517
620
  return this.options.operationService.executeDryRun({
@@ -524,7 +627,7 @@ export class AdminConfiguredServerService {
524
627
  const source = this.resolveConfiguredServerSource(input.targetName, input.targetSource);
525
628
  const context = {
526
629
  ...input.context,
527
- target: { type: 'configured_server', id: `${source}:${input.targetName}` },
630
+ target: { type: 'configured_server', id: `${source}/${input.targetName}` },
528
631
  confirmationFacts: sanitizeApplyConfirmationFacts(input.context.confirmationFacts),
529
632
  };
530
633
  return this.options.operationService.executeMutation({
@@ -549,7 +652,8 @@ export class AdminConfiguredServerService {
549
652
  if (preview.previewFingerprint !== input.previewFingerprint) {
550
653
  throw new AdminConfiguredServerApplyError('configured_server_stale_preview');
551
654
  }
552
- if (connectionCritical &&
655
+ if (source === 'mcpServers' &&
656
+ connectionCritical &&
553
657
  !wouldUseLocalStdioTransport(proposedConfig) &&
554
658
  proposedReadModel.enabled &&
555
659
  preview.connectivityCheck.status !== 'passed' &&
@@ -573,6 +677,14 @@ export class AdminConfiguredServerService {
573
677
  : []),
574
678
  ],
575
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;
576
688
  const configChange = await this.options.configChangeService.editConfiguredServerTarget({
577
689
  sourceName: input.targetName,
578
690
  targetSource: source,
@@ -582,11 +694,142 @@ export class AdminConfiguredServerService {
582
694
  expectedGlobalConfigFingerprint: fingerprintConfiguredServerDefaults(serverDefaults),
583
695
  });
584
696
  assertSuccessfulConfiguredServerEdit(configChange);
697
+ const runtimeImpact = source === 'mcpTemplates'
698
+ ? await this.observeTemplateRetirement(input.targetName, configChange, preview.runtimeImpact?.retirementRequired === true, activeInstancesBefore, instanceIdentities)
699
+ : undefined;
585
700
  return {
586
701
  originalTargetName: input.targetName,
587
702
  targetName: preview.proposedTargetName,
588
703
  previewFingerprint: preview.previewFingerprint,
589
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,
590
833
  };
591
834
  },
592
835
  };
@@ -607,7 +850,7 @@ export class AdminConfiguredServerService {
607
850
  const source = input.targetSource ?? 'mcpServers';
608
851
  const context = {
609
852
  ...input.context,
610
- target: { type: 'configured_server', id: `${source}:${input.targetName}` },
853
+ target: { type: 'configured_server', id: `${source}/${input.targetName}` },
611
854
  };
612
855
  if (input.dryRun) {
613
856
  return this.options.operationService.executeDryRun({
@@ -665,17 +908,160 @@ export class AdminConfiguredServerService {
665
908
  }
666
909
  return {
667
910
  configFingerprint: fingerprintConfiguredServerConfigDocument(parsed),
668
- 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))),
669
912
  };
670
913
  }
671
- readConfiguredServerState(targetName, targetSource) {
672
- 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;
673
1054
  const source = this.resolveConfiguredServerSource(targetName, targetSource, parsed);
674
1055
  const currentConfig = parsed?.[source]?.[targetName];
675
1056
  if (!currentConfig) {
676
1057
  throw new AdminConfiguredServerNotFoundError(targetName);
677
1058
  }
678
- 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
+ };
679
1065
  }
680
1066
  resolveConfiguredServerSource(targetName, targetSource, document = this.options.readConfigDocument()) {
681
1067
  if (targetSource)
@@ -693,6 +1079,9 @@ export class AdminConfiguredServerService {
693
1079
  async previewConfiguredServerEditResult(input, currentConfig, serverDefaults, requireCriticalConnectivity = false, source = 'mcpServers') {
694
1080
  const normalizedEdit = normalizeEditDraft(input.edit);
695
1081
  const sourceValidation = validateEditForSource(source, normalizedEdit.edit);
1082
+ const rawTemplateHeaderValidation = source === 'mcpTemplates'
1083
+ ? validateRawTemplateHeaderEdit(input.edit)
1084
+ : { status: 'valid', errors: [] };
696
1085
  const currentReadModel = createConfiguredServerReadModel(input.targetName, mergeGlobalAndServerConfig(serverDefaults, currentConfig), source);
697
1086
  const secretValidation = validateSecretEditCapabilities(normalizedEdit.edit, currentReadModel.secretInputs);
698
1087
  const transportApplicabilityValidation = validateTransportFieldApplicability(currentConfig, normalizedEdit.edit);
@@ -702,7 +1091,10 @@ export class AdminConfiguredServerService {
702
1091
  const proposedConfig = applyEditDraft(currentConfig, applicableEdit, serverDefaults, input.targetName);
703
1092
  const proposedReadModel = createConfiguredServerReadModel(proposedTargetName, mergeGlobalAndServerConfig(serverDefaults, proposedConfig), source);
704
1093
  const proposedTransportType = configuredTransportType(proposedConfig);
705
- const serverValidation = validatePreviewServerConfig(proposedConfig);
1094
+ const serverValidation = validatePreviewServerConfig(proposedConfig, source);
1095
+ const templateValidation = source === 'mcpTemplates'
1096
+ ? templateDefinitionValidation(proposedConfig)
1097
+ : { status: 'valid', errors: [] };
706
1098
  const currentToolInventory = this.options.readToolInventory
707
1099
  ? await this.options.readToolInventory({
708
1100
  targetName: input.targetName,
@@ -712,7 +1104,7 @@ export class AdminConfiguredServerService {
712
1104
  })
713
1105
  : undefined;
714
1106
  const toolEditValidation = validateConfiguredToolEdit(applicableEdit, currentToolInventory);
715
- 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));
716
1108
  const diff = createPreviewDiff(currentReadModel, proposedReadModel, applicableEdit);
717
1109
  pushDiff(diff, ['disabledTools'], currentConfig.disabledTools ?? [], proposedConfig.disabledTools ?? [], [
718
1110
  'tool_visibility',
@@ -720,6 +1112,9 @@ export class AdminConfiguredServerService {
720
1112
  pushDiff(diff, ['toolDescriptionOverrides'], currentConfig.toolDescriptionOverrides ?? {}, proposedConfig.toolDescriptionOverrides ?? {}, ['tool_metadata']);
721
1113
  appendClearedOverrideDiff(diff, currentReadModel, proposedReadModel, applicableEdit);
722
1114
  const changed = validation.status === 'valid' && diff.length > 0;
1115
+ const retirementRequired = source === 'mcpTemplates' &&
1116
+ (proposedTargetName !== input.targetName ||
1117
+ templateRuntimeHash(currentConfig) !== templateRuntimeHash(proposedConfig));
723
1118
  const endpointChangedWithPreservedSecrets = endpointAuthorityChanged(currentConfig, proposedConfig) &&
724
1119
  hasPreservedSecretInputs(currentReadModel.secretInputs.filter((input) => !proposedTransportType || transportFieldAppliesToType(input.fieldPath[0], proposedTransportType)), applicableEdit);
725
1120
  const proposedToolInventory = this.options.readToolInventory
@@ -751,17 +1146,32 @@ export class AdminConfiguredServerService {
751
1146
  validation,
752
1147
  diff,
753
1148
  configChange: previewConfigChange(input.targetName, changed, source),
754
- connectivityCheck: await this.previewConnectivityCheck({
755
- targetName: proposedTargetName,
756
- serverConfig: proposedConfig,
757
- enabled: proposedReadModel.enabled,
758
- validationStatus: validation.status,
759
- connectionCriticalChanged: diff.some((entry) => entry.riskFlags.includes('connection_critical')),
760
- force: input.connectivityCheck === 'manual' ||
761
- (requireCriticalConnectivity && diff.some((entry) => entry.riskFlags.includes('connection_critical'))),
762
- endpointChangedWithPreservedSecrets,
763
- }),
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
+ }),
764
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
+ : [],
765
1175
  };
766
1176
  }
767
1177
  async previewConnectivityCheck(input) {
@@ -788,6 +1198,42 @@ export class AdminConfiguredServerService {
788
1198
  serverConfig: input.serverConfig,
789
1199
  });
790
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
+ }
791
1237
  async previewConfiguredServerCreateResult(input, forceConnectivity = false, document = this.options.readConfigDocument() ?? {}) {
792
1238
  const prepared = await this.resolveConfiguredServerCreate(input.draft, document);
793
1239
  const proposedReadModel = prepared.config
@@ -796,17 +1242,19 @@ export class AdminConfiguredServerService {
796
1242
  const validation = prepared.validation;
797
1243
  const diff = proposedReadModel ? createConfiguredServerCreateDiff(proposedReadModel, prepared.draft) : [];
798
1244
  const changed = validation.status === 'valid' && prepared.workflow.status === 'preview';
799
- const connectivityCheck = prepared.config
800
- ? await this.previewConnectivityCheck({
801
- targetName: prepared.targetName,
802
- serverConfig: prepared.config,
803
- enabled: prepared.enabled,
804
- validationStatus: validation.status,
805
- connectionCriticalChanged: true,
806
- force: forceConnectivity || input.connectivityCheck === 'manual',
807
- endpointChangedWithPreservedSecrets: false,
808
- })
809
- : { 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' };
810
1258
  const preview = {
811
1259
  targetName: prepared.targetName,
812
1260
  previewFingerprint: configuredServerCreatePreviewFingerprint({
@@ -819,12 +1267,25 @@ export class AdminConfiguredServerService {
819
1267
  }),
820
1268
  validation,
821
1269
  diff,
822
- configChange: previewCreateConfigChange(prepared.targetName, prepared.workflow.status, changed),
1270
+ configChange: previewCreateConfigChange(prepared.targetName, prepared.workflow.status, changed, prepared.targetSource),
823
1271
  connectivityCheck,
824
1272
  expectedReload: {
825
1273
  policy: 'observe_after_write',
826
1274
  possibleStatuses: ['observed', 'runtime_not_running', 'reload_disabled', 'failed'],
827
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
+ : [],
828
1289
  };
829
1290
  return preview;
830
1291
  }
@@ -840,8 +1301,11 @@ export class AdminConfiguredServerService {
840
1301
  },
841
1302
  });
842
1303
  const workflowResult = await workflow.run({ mode: 'preview', source: normalized.source });
843
- const config = workflowResult.config;
844
- 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);
845
1309
  return {
846
1310
  draft: normalized.draft,
847
1311
  source: normalized.source,
@@ -850,20 +1314,71 @@ export class AdminConfiguredServerService {
850
1314
  workflow: workflowResult,
851
1315
  config,
852
1316
  validation,
1317
+ targetSource: normalized.targetSource,
853
1318
  };
854
1319
  }
855
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
+ }
856
1372
  function validateEditForSource(source, edit) {
857
1373
  if (source !== 'mcpTemplates')
858
1374
  return { status: 'valid', errors: [] };
859
- const supportedTemplateMetadata = new Set(['instructionOverride', 'disabledTools', 'toolDescriptionOverrides']);
860
- const unsupported = Object.keys(edit).filter((key) => !supportedTemplateMetadata.has(key));
1375
+ const unsupported = Object.keys(edit).filter((key) => key === 'enabled');
861
1376
  return {
862
1377
  status: unsupported.length === 0 ? 'valid' : 'invalid',
863
1378
  errors: unsupported.map((key) => ({
864
1379
  fieldPath: [key],
865
1380
  code: 'template_edit_field_unsupported',
866
- message: 'Template Server edits may only change instruction and tool metadata.',
1381
+ message: 'Template Server definitions do not support enable or disable operations.',
867
1382
  })),
868
1383
  };
869
1384
  }
@@ -906,6 +1421,59 @@ function assertSuccessfulConfiguredServerEdit(configChange) {
906
1421
  throw new AdminConfiguredServerApplyError('configured_server_apply_failed');
907
1422
  }
908
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
+ }
909
1477
  function sanitizeApplyConfirmationFacts(facts) {
910
1478
  if (!facts)
911
1479
  return undefined;
@@ -1127,9 +1695,13 @@ function isSecretCapableRawTransportEdit(key, value) {
1127
1695
  if (key === 'url') {
1128
1696
  return isSecretCapableUrlEdit(value);
1129
1697
  }
1130
- if (key === 'headers' || key === 'env') {
1698
+ if (key === 'headers') {
1131
1699
  return true;
1132
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
+ }
1133
1705
  if (key === 'oauth' && value && typeof value === 'object' && !Array.isArray(value)) {
1134
1706
  return containsStringOrSecretLikeField(value);
1135
1707
  }
@@ -1752,12 +2324,12 @@ function deleteNestedValue(target, fieldPath) {
1752
2324
  }
1753
2325
  delete cursor[fieldPath[fieldPath.length - 1]];
1754
2326
  }
1755
- function validatePreviewServerConfig(serverConfig) {
2327
+ function validatePreviewServerConfig(serverConfig, source = 'mcpServers') {
1756
2328
  const errors = [];
1757
2329
  const type = typeof serverConfig.type === 'string' ? serverConfig.type : undefined;
1758
2330
  const url = typeof serverConfig.url === 'string' ? serverConfig.url : undefined;
1759
2331
  const command = typeof serverConfig.command === 'string' ? serverConfig.command : undefined;
1760
- const schemaResult = previewTransportConfigSchema.safeParse(serverConfig);
2332
+ const schemaResult = transportConfigSchema.safeParse(serverConfig);
1761
2333
  if (!schemaResult.success) {
1762
2334
  for (const issue of schemaResult.error.issues) {
1763
2335
  if ((issue.path[0] === 'type' && typeof serverConfig.type === 'string') ||
@@ -1793,7 +2365,7 @@ function validatePreviewServerConfig(serverConfig) {
1793
2365
  message: 'Transport type must be stdio, http, sse, or streamableHttp.',
1794
2366
  });
1795
2367
  }
1796
- if (url !== undefined && !isValidUrlOrEnvReference(url)) {
2368
+ if (url !== undefined && !isValidUrlOrEnvReference(url, source === 'mcpTemplates')) {
1797
2369
  errors.push({
1798
2370
  fieldPath: ['transport', 'url'],
1799
2371
  code: 'invalid_url',
@@ -1827,7 +2399,9 @@ function mergeValidation(editValidation, serverValidation) {
1827
2399
  errors,
1828
2400
  };
1829
2401
  }
1830
- function isValidUrlOrEnvReference(value) {
2402
+ function isValidUrlOrEnvReference(value, allowHandlebars = false) {
2403
+ if (value.includes('{{'))
2404
+ return allowHandlebars && /\{\{[^}]+\}\}/u.test(value);
1831
2405
  if (/\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*/u.test(value)) {
1832
2406
  return true;
1833
2407
  }
@@ -1852,6 +2426,19 @@ function previewConfigChange(targetName, changed, source = 'mcpServers') {
1852
2426
  warnings: [],
1853
2427
  };
1854
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
+ }
1855
2442
  function previewFingerprint(input) {
1856
2443
  return `preview_${createHash('sha256')
1857
2444
  .update(stableStringify({
@@ -2064,9 +2651,11 @@ function stableStringify(value) {
2064
2651
  }
2065
2652
  return JSON.stringify(value);
2066
2653
  }
2067
- 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) {
2068
2655
  const secretInputs = [];
2069
2656
  const transport = {};
2657
+ const templateAnalysis = source === 'mcpTemplates' ? analyzeTemplateServerDefinition(serverConfig) : undefined;
2658
+ const invalidTemplateFields = new Set(templateAnalysis?.syntax.errors.map((error) => fieldKey(error.fieldPath)) ?? []);
2070
2659
  for (const [key, value] of Object.entries(serverConfig)) {
2071
2660
  if (key === 'disabled' ||
2072
2661
  key === 'tags' ||
@@ -2076,7 +2665,7 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
2076
2665
  continue;
2077
2666
  }
2078
2667
  if (key === 'env') {
2079
- transport.env = redactNamedSecretRecord(value, ['env'], secretInputs);
2668
+ transport.env = redactNamedSecretRecord(value, ['env'], secretInputs, source === 'mcpTemplates', invalidTemplateFields);
2080
2669
  continue;
2081
2670
  }
2082
2671
  if (key === 'headers') {
@@ -2085,6 +2674,9 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
2085
2674
  }
2086
2675
  if (key === 'args') {
2087
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
+ }
2088
2680
  continue;
2089
2681
  }
2090
2682
  if (key === 'oauth') {
@@ -2092,7 +2684,13 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
2092
2684
  continue;
2093
2685
  }
2094
2686
  if (key === 'url' && typeof value === 'string') {
2095
- 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();
2096
2694
  continue;
2097
2695
  }
2098
2696
  transport[key] = sanitizeUnknownValue(value, [key], secretInputs);
@@ -2111,15 +2709,11 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
2111
2709
  tags: normalizeTags(serverConfig.tags),
2112
2710
  transportSummary: createTransportSummary(serverConfig, transport),
2113
2711
  mutationAvailability: {
2114
- available: source === 'mcpServers',
2115
- operations: source === 'mcpServers' ? ['enable', 'disable'] : [],
2712
+ available: mutationAvailable,
2713
+ operations: ['enable', 'disable'],
2714
+ deleteAvailable: mutationAvailable,
2116
2715
  },
2117
- actionState: source === 'mcpServers'
2118
- ? createActionState(name, enabled)
2119
- : {
2120
- enable: { available: false, label: `Enable ${name}`, disabledReason: 'already_enabled' },
2121
- disable: { available: false, label: `Disable ${name}`, disabledReason: 'already_disabled' },
2122
- },
2716
+ actionState: createActionState(name, enabled),
2123
2717
  transport,
2124
2718
  secretInputs,
2125
2719
  instructionOverride: !hasInstructionOverride
@@ -2127,6 +2721,16 @@ function createConfiguredServerReadModel(name, serverConfig, source = 'mcpServer
2127
2721
  : instructionOverride === ''
2128
2722
  ? { state: 'suppress', value: '' }
2129
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
+ },
2130
2734
  };
2131
2735
  }
2132
2736
  const STDIO_TRANSPORT_TYPES = ['stdio'];
@@ -2235,7 +2839,7 @@ const TRANSPORT_FIELD_DEFINITIONS = [
2235
2839
  const TRANSPORT_FIELD_DEFINITION_BY_KEY = new Map(TRANSPORT_FIELD_DEFINITIONS.map((definition) => [definition.key, definition]));
2236
2840
  function createConfiguredServerEditContract(server, rawServerConfig, serverDefaults) {
2237
2841
  const secretFieldKeys = new Set(server.secretInputs.map((input) => fieldKey(input.fieldPath)));
2238
- const transportFields = TRANSPORT_FIELD_DEFINITIONS.map((definition) => {
2842
+ const transportFields = TRANSPORT_FIELD_DEFINITIONS.filter((definition) => definition.key !== 'template').map((definition) => {
2239
2843
  const value = Object.prototype.hasOwnProperty.call(server.transport, definition.key)
2240
2844
  ? server.transport[definition.key]
2241
2845
  : definition.defaultValue;
@@ -2254,7 +2858,7 @@ function createConfiguredServerEditContract(server, rawServerConfig, serverDefau
2254
2858
  singleTargetEdit: true,
2255
2859
  rename: { supported: true },
2256
2860
  create: { supported: false },
2257
- delete: { supported: false },
2861
+ delete: { supported: server.mutationAvailability.deleteAvailable === true },
2258
2862
  bulkEdit: { supported: false },
2259
2863
  rawJson: { supported: false },
2260
2864
  preview: { supported: true },
@@ -2277,7 +2881,7 @@ function createConfiguredServerEditContract(server, rawServerConfig, serverDefau
2277
2881
  label: 'Enabled',
2278
2882
  control: 'switch',
2279
2883
  value: server.enabled,
2280
- editable: true,
2884
+ editable: server.source === 'mcpServers',
2281
2885
  },
2282
2886
  {
2283
2887
  fieldPath: ['tags'],
@@ -2293,6 +2897,15 @@ function createConfiguredServerEditContract(server, rawServerConfig, serverDefau
2293
2897
  label: 'Transport',
2294
2898
  fields: transportFields,
2295
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
+ : []),
2296
2909
  {
2297
2910
  id: 'secrets',
2298
2911
  label: 'Secrets',
@@ -2301,6 +2914,31 @@ function createConfiguredServerEditContract(server, rawServerConfig, serverDefau
2301
2914
  ],
2302
2915
  };
2303
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
+ }
2304
2942
  function omitSecretValues(value, fieldPath, secretFieldKeys) {
2305
2943
  if (secretFieldKeys.has(fieldKey(fieldPath))) {
2306
2944
  return undefined;
@@ -2545,7 +3183,7 @@ function wouldUseLocalStdioTransport(serverConfig) {
2545
3183
  function normalizeTags(tags) {
2546
3184
  return Array.isArray(tags) ? tags.filter((tag) => typeof tag === 'string') : [];
2547
3185
  }
2548
- function redactNamedSecretRecord(value, basePath, secretInputs) {
3186
+ function redactNamedSecretRecord(value, basePath, secretInputs, exposeTemplateExpressions = false, invalidTemplateFields = new Set()) {
2549
3187
  if (Array.isArray(value)) {
2550
3188
  return value.map((entry, index) => {
2551
3189
  const label = typeof entry === 'string' && entry.includes('=') ? entry.split('=')[0] : String(index);
@@ -2557,12 +3195,44 @@ function redactNamedSecretRecord(value, basePath, secretInputs) {
2557
3195
  return {};
2558
3196
  }
2559
3197
  const redacted = {};
2560
- 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
+ }
2561
3210
  redacted[key] = redactedValue();
2562
3211
  secretInputs.push(secretInput([...basePath, key], key));
2563
3212
  }
2564
3213
  return redacted;
2565
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
+ }
2566
3236
  function redactOAuthConfig(value, secretInputs) {
2567
3237
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
2568
3238
  return {};