@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.
- package/build/admin/assets/{admin-OAuthServicesWorkspace-B8y2weAX.js → admin-OAuthServicesWorkspace-BHyDoEos.js} +1 -1
- package/build/admin/assets/admin-console-DnlZpz5X.js +3 -0
- package/build/admin/assets/admin-vendor-62eL-qdH.js +1 -0
- package/build/admin/assets/admin-vendor-BbMrl7MK.js +3 -0
- package/build/admin/assets/admin-vendor-DpjVwd81.js +62 -0
- package/build/admin/index.html +4 -1
- package/build/auth/sdkOAuthServerProvider.d.ts.map +1 -1
- package/build/auth/sdkOAuthServerProvider.js +49 -48
- package/build/auth/sdkOAuthServerProvider.js.map +1 -1
- package/build/auth/storage/authCodeRepository.js +2 -2
- package/build/auth/storage/authCodeRepository.js.map +1 -1
- package/build/auth/storage/authRequestRepository.js +2 -2
- package/build/auth/storage/authRequestRepository.js.map +1 -1
- package/build/auth/storage/fileStorageService.d.ts +39 -0
- package/build/auth/storage/fileStorageService.d.ts.map +1 -1
- package/build/auth/storage/fileStorageService.js +171 -38
- package/build/auth/storage/fileStorageService.js.map +1 -1
- package/build/auth/storage/oauthStorageService.d.ts +5 -0
- package/build/auth/storage/oauthStorageService.d.ts.map +1 -1
- package/build/auth/storage/oauthStorageService.js +6 -0
- package/build/auth/storage/oauthStorageService.js.map +1 -1
- package/build/commands/mcp/install.d.ts +47 -3
- package/build/commands/mcp/install.d.ts.map +1 -1
- package/build/commands/mcp/install.js +22 -14
- package/build/commands/mcp/install.js.map +1 -1
- package/build/commands/registry/options.d.ts +3 -0
- package/build/commands/registry/options.d.ts.map +1 -1
- package/build/commands/registry/options.js +27 -0
- package/build/commands/registry/options.js.map +1 -1
- package/build/commands/registry/search.d.ts.map +1 -1
- package/build/commands/registry/search.js +2 -10
- package/build/commands/registry/search.js.map +1 -1
- package/build/commands/registry/show.d.ts.map +1 -1
- package/build/commands/registry/show.js +2 -10
- package/build/commands/registry/show.js.map +1 -1
- package/build/commands/registry/status.d.ts.map +1 -1
- package/build/commands/registry/status.js +2 -10
- package/build/commands/registry/status.js.map +1 -1
- package/build/commands/registry/versions.d.ts.map +1 -1
- package/build/commands/registry/versions.js +2 -10
- package/build/commands/registry/versions.js.map +1 -1
- package/build/commands/serve/asyncLoadingOptions.d.ts +16 -0
- package/build/commands/serve/asyncLoadingOptions.d.ts.map +1 -1
- package/build/commands/serve/asyncLoadingOptions.js +29 -0
- package/build/commands/serve/asyncLoadingOptions.js.map +1 -1
- package/build/commands/serve/index.d.ts +24 -0
- package/build/commands/serve/index.d.ts.map +1 -1
- package/build/commands/serve/index.js +24 -0
- package/build/commands/serve/index.js.map +1 -1
- package/build/commands/serve/serve.d.ts +6 -0
- package/build/commands/serve/serve.d.ts.map +1 -1
- package/build/commands/serve/serve.js +45 -2
- package/build/commands/serve/serve.js.map +1 -1
- package/build/commands/serve/serveBackground.d.ts +2 -1
- package/build/commands/serve/serveBackground.d.ts.map +1 -1
- package/build/commands/serve/serveBackground.js +6 -0
- package/build/commands/serve/serveBackground.js.map +1 -1
- package/build/commands/wait/wait.js +1 -1
- package/build/commands/wait/wait.js.map +1 -1
- package/build/config/configContext.d.ts +2 -0
- package/build/config/configContext.d.ts.map +1 -1
- package/build/config/configContext.js +5 -0
- package/build/config/configContext.js.map +1 -1
- package/build/config/configLoader.d.ts +8 -0
- package/build/config/configLoader.d.ts.map +1 -1
- package/build/config/configLoader.js +35 -11
- package/build/config/configLoader.js.map +1 -1
- package/build/config/configManager.d.ts +5 -0
- package/build/config/configManager.d.ts.map +1 -1
- package/build/config/configManager.js +83 -6
- package/build/config/configManager.js.map +1 -1
- package/build/config/configWatcher.d.ts +2 -0
- package/build/config/configWatcher.d.ts.map +1 -1
- package/build/config/configWatcher.js +9 -4
- package/build/config/configWatcher.js.map +1 -1
- package/build/config/configuredServerTargets.d.ts +1 -0
- package/build/config/configuredServerTargets.d.ts.map +1 -1
- package/build/config/configuredServerTargets.js +6 -0
- package/build/config/configuredServerTargets.js.map +1 -1
- package/build/config/envProcessor.d.ts +2 -1
- package/build/config/envProcessor.d.ts.map +1 -1
- package/build/config/envProcessor.js +16 -9
- package/build/config/envProcessor.js.map +1 -1
- package/build/config/runtimeScopeEnv.d.ts +15 -0
- package/build/config/runtimeScopeEnv.d.ts.map +1 -0
- package/build/config/runtimeScopeEnv.js +231 -0
- package/build/config/runtimeScopeEnv.js.map +1 -0
- package/build/config/runtimeTargetFingerprint.d.ts +3 -0
- package/build/config/runtimeTargetFingerprint.d.ts.map +1 -0
- package/build/config/runtimeTargetFingerprint.js +66 -0
- package/build/config/runtimeTargetFingerprint.js.map +1 -0
- package/build/config/types.d.ts +5 -0
- package/build/config/types.d.ts.map +1 -1
- package/build/config/types.js +1 -0
- package/build/config/types.js.map +1 -1
- package/build/constants/mcp.d.ts +1 -1
- package/build/constants/mcp.js +1 -1
- package/build/core/capabilities/capabilityPagination.d.ts.map +1 -1
- package/build/core/capabilities/capabilityPagination.js +5 -4
- package/build/core/capabilities/capabilityPagination.js.map +1 -1
- package/build/core/capabilities/configuredToolSnapshot.d.ts +22 -0
- package/build/core/capabilities/configuredToolSnapshot.d.ts.map +1 -1
- package/build/core/capabilities/configuredToolSnapshot.js +39 -0
- package/build/core/capabilities/configuredToolSnapshot.js.map +1 -1
- package/build/core/client/clientManager.d.ts.map +1 -1
- package/build/core/client/clientManager.js +25 -13
- package/build/core/client/clientManager.js.map +1 -1
- package/build/core/client/connectionHandler.d.ts.map +1 -1
- package/build/core/client/connectionHandler.js +9 -6
- package/build/core/client/connectionHandler.js.map +1 -1
- package/build/core/configChangeHandler.d.ts +3 -0
- package/build/core/configChangeHandler.d.ts.map +1 -1
- package/build/core/configChangeHandler.js +21 -4
- package/build/core/configChangeHandler.js.map +1 -1
- package/build/core/loading/backendLoadingPolicy.d.ts +11 -0
- package/build/core/loading/backendLoadingPolicy.d.ts.map +1 -0
- package/build/core/loading/backendLoadingPolicy.js +10 -0
- package/build/core/loading/backendLoadingPolicy.js.map +1 -0
- package/build/core/loading/mcpLoadingManager.d.ts +8 -24
- package/build/core/loading/mcpLoadingManager.d.ts.map +1 -1
- package/build/core/loading/mcpLoadingManager.js +121 -73
- package/build/core/loading/mcpLoadingManager.js.map +1 -1
- package/build/core/server/agentConfig.d.ts +26 -0
- package/build/core/server/agentConfig.d.ts.map +1 -1
- package/build/core/server/agentConfig.js +11 -0
- package/build/core/server/agentConfig.js.map +1 -1
- package/build/core/server/backendStdioSupervisor.d.ts.map +1 -1
- package/build/core/server/backendStdioSupervisor.js +2 -1
- package/build/core/server/backendStdioSupervisor.js.map +1 -1
- package/build/core/server/clientInstanceFactory.d.ts +2 -1
- package/build/core/server/clientInstanceFactory.d.ts.map +1 -1
- package/build/core/server/clientInstanceFactory.js +9 -2
- package/build/core/server/clientInstanceFactory.js.map +1 -1
- package/build/core/server/clientInstancePool.d.ts +5 -6
- package/build/core/server/clientInstancePool.d.ts.map +1 -1
- package/build/core/server/clientInstancePool.js +123 -86
- package/build/core/server/clientInstancePool.js.map +1 -1
- package/build/core/server/clientInstancePoolTypes.d.ts +11 -2
- package/build/core/server/clientInstancePoolTypes.d.ts.map +1 -1
- package/build/core/server/clientInstancePoolTypes.js +8 -3
- package/build/core/server/clientInstancePoolTypes.js.map +1 -1
- package/build/core/server/mcpServerLifecycleManager.d.ts +0 -4
- package/build/core/server/mcpServerLifecycleManager.d.ts.map +1 -1
- package/build/core/server/mcpServerLifecycleManager.js +18 -40
- package/build/core/server/mcpServerLifecycleManager.js.map +1 -1
- package/build/core/server/requestContextPreparation.d.ts.map +1 -1
- package/build/core/server/requestContextPreparation.js +2 -1
- package/build/core/server/requestContextPreparation.js.map +1 -1
- package/build/core/server/serverManager.d.ts +1 -0
- package/build/core/server/serverManager.d.ts.map +1 -1
- package/build/core/server/serverManager.js +33 -1
- package/build/core/server/serverManager.js.map +1 -1
- package/build/core/server/templateServerManager.d.ts +22 -1
- package/build/core/server/templateServerManager.d.ts.map +1 -1
- package/build/core/server/templateServerManager.js +107 -13
- package/build/core/server/templateServerManager.js.map +1 -1
- package/build/core/tools/handlers/serverManagementHandler.d.ts +7 -7
- package/build/core/tools/internal/adapters/helpers/configTransformers.d.ts +1 -1
- package/build/core/tools/internal/schemas/index.d.ts +2 -2
- package/build/core/tools/internal/schemas/installation.d.ts +1 -1
- package/build/core/tools/internal/schemas/management.d.ts +1 -1
- package/build/core/types/transport.d.ts +88 -8
- package/build/core/types/transport.d.ts.map +1 -1
- package/build/core/types/transport.js +78 -1
- package/build/core/types/transport.js.map +1 -1
- package/build/domains/admin/adminConfiguredServerService.d.ts +160 -2
- package/build/domains/admin/adminConfiguredServerService.d.ts.map +1 -1
- package/build/domains/admin/adminConfiguredServerService.js +761 -91
- package/build/domains/admin/adminConfiguredServerService.js.map +1 -1
- package/build/domains/admin/adminDomain.d.ts +11 -0
- package/build/domains/admin/adminDomain.d.ts.map +1 -1
- package/build/domains/admin/adminDomain.js +12 -0
- package/build/domains/admin/adminDomain.js.map +1 -1
- package/build/domains/admin/adminOperationService.d.ts +1 -0
- package/build/domains/admin/adminOperationService.d.ts.map +1 -1
- package/build/domains/admin/adminOperationService.js +6 -2
- package/build/domains/admin/adminOperationService.js.map +1 -1
- package/build/domains/admin/configuredToolInventory.d.ts +19 -1
- package/build/domains/admin/configuredToolInventory.d.ts.map +1 -1
- package/build/domains/admin/configuredToolInventory.js +78 -31
- package/build/domains/admin/configuredToolInventory.js.map +1 -1
- package/build/domains/admin/runtimeConfiguredToolInspectionService.d.ts +21 -0
- package/build/domains/admin/runtimeConfiguredToolInspectionService.d.ts.map +1 -0
- package/build/domains/admin/runtimeConfiguredToolInspectionService.js +255 -0
- package/build/domains/admin/runtimeConfiguredToolInspectionService.js.map +1 -0
- package/build/domains/admin/templateServerDefinitionAnalyzer.d.ts +26 -0
- package/build/domains/admin/templateServerDefinitionAnalyzer.d.ts.map +1 -0
- package/build/domains/admin/templateServerDefinitionAnalyzer.js +148 -0
- package/build/domains/admin/templateServerDefinitionAnalyzer.js.map +1 -0
- package/build/domains/config-change/configChange.d.ts +1 -1
- package/build/domains/config-change/configChange.d.ts.map +1 -1
- package/build/domains/config-change/configChange.js +168 -49
- package/build/domains/config-change/configChange.js.map +1 -1
- package/build/domains/config-change/types.d.ts +16 -2
- package/build/domains/config-change/types.d.ts.map +1 -1
- package/build/domains/registry/mcpRegistryClient.d.ts +3 -3
- package/build/domains/registry/mcpRegistryClient.d.ts.map +1 -1
- package/build/domains/registry/mcpRegistryClient.js +8 -7
- package/build/domains/registry/mcpRegistryClient.js.map +1 -1
- package/build/logger/secureLogger.d.ts.map +1 -1
- package/build/logger/secureLogger.js +160 -23
- package/build/logger/secureLogger.js.map +1 -1
- package/build/server.d.ts +2 -1
- package/build/server.d.ts.map +1 -1
- package/build/server.js +8 -7
- package/build/server.js.map +1 -1
- package/build/transport/http/middlewares/securityMiddleware.d.ts +6 -0
- package/build/transport/http/middlewares/securityMiddleware.d.ts.map +1 -1
- package/build/transport/http/middlewares/securityMiddleware.js +35 -29
- package/build/transport/http/middlewares/securityMiddleware.js.map +1 -1
- package/build/transport/http/routes/adminRoutes.d.ts +17 -1
- package/build/transport/http/routes/adminRoutes.d.ts.map +1 -1
- package/build/transport/http/routes/adminRoutes.js +331 -15
- package/build/transport/http/routes/adminRoutes.js.map +1 -1
- package/build/transport/http/routes/healthRoutes.d.ts +6 -1
- package/build/transport/http/routes/healthRoutes.d.ts.map +1 -1
- package/build/transport/http/routes/healthRoutes.js +7 -3
- package/build/transport/http/routes/healthRoutes.js.map +1 -1
- package/build/transport/http/server.d.ts.map +1 -1
- package/build/transport/http/server.js +25 -8
- package/build/transport/http/server.js.map +1 -1
- package/build/transport/http/storage/streamableSessionRepository.d.ts +1 -0
- package/build/transport/http/storage/streamableSessionRepository.d.ts.map +1 -1
- package/build/transport/http/storage/streamableSessionRepository.js +6 -3
- package/build/transport/http/storage/streamableSessionRepository.js.map +1 -1
- package/build/transport/transportFactory.d.ts.map +1 -1
- package/build/transport/transportFactory.js +34 -12
- package/build/transport/transportFactory.js.map +1 -1
- package/package.json +6 -1
- 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
|
-
|
|
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 =
|
|
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
|
-
{
|
|
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
|
-
...(
|
|
334
|
-
? { source
|
|
335
|
-
: workflowStatus === '
|
|
336
|
-
? { source: '
|
|
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.
|
|
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 =
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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 (
|
|
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}
|
|
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
|
-
|
|
672
|
-
const
|
|
914
|
+
previewConfiguredServerDeleteResult(input) {
|
|
915
|
+
const document = this.options.readConfigDocument();
|
|
916
|
+
const otherSource = input.targetSource === 'mcpServers' ? 'mcpTemplates' : 'mcpServers';
|
|
917
|
+
if (!document?.[input.targetSource]?.[input.targetName] && document?.[otherSource]?.[input.targetName]) {
|
|
918
|
+
throw new AdminConfiguredServerApplyError('configured_server_source_changed');
|
|
919
|
+
}
|
|
920
|
+
const { currentConfig, serverDefaults, collision } = this.readConfiguredServerState(input.targetName, input.targetSource, document);
|
|
921
|
+
const targetFingerprint = fingerprintConfiguredServerTarget(currentConfig);
|
|
922
|
+
const activeInstanceCount = input.targetSource === 'mcpTemplates'
|
|
923
|
+
? (this.options.getTemplateActiveInstanceCount?.(input.targetName) ?? 0)
|
|
924
|
+
: 0;
|
|
925
|
+
const definition = createConfiguredServerReadModel(input.targetName, mergeGlobalAndServerConfig(serverDefaults, currentConfig), input.targetSource, targetFingerprint, Object.hasOwn(currentConfig, 'instructionOverride') ? currentConfig.instructionOverride : undefined, Object.hasOwn(currentConfig, 'instructionOverride'), collision, activeInstanceCount, this.options.mutationAvailability?.available ?? true);
|
|
926
|
+
const qualifiedId = definition.definition?.qualifiedId ?? `${input.targetSource}/${input.targetName}`;
|
|
927
|
+
const authority = definition.definition?.authority ?? 'sole';
|
|
928
|
+
const runtimeImpact = input.targetSource === 'mcpTemplates'
|
|
929
|
+
? { kind: 'template', activeInstanceCount, retirement: 'reload_scheduled' }
|
|
930
|
+
: { kind: 'static', configuredBackendRemoval: 'after_reload' };
|
|
931
|
+
const warnings = [
|
|
932
|
+
'Deletion removes only this source-qualified definition and does not cascade to presets, filters, credentials, registry state, or individually selected live instances.',
|
|
933
|
+
...(collision
|
|
934
|
+
? [`The same name in ${input.targetSource === 'mcpServers' ? 'mcpTemplates' : 'mcpServers'} is preserved.`]
|
|
935
|
+
: []),
|
|
936
|
+
...(input.targetSource === 'mcpTemplates' && activeInstanceCount > 0
|
|
937
|
+
? [
|
|
938
|
+
`Reload schedules retirement of ${activeInstanceCount} active Template Server instance${activeInstanceCount === 1 ? '' : 's'}.`,
|
|
939
|
+
]
|
|
940
|
+
: []),
|
|
941
|
+
];
|
|
942
|
+
const previewFingerprint = `delete_preview_${createHash('sha256')
|
|
943
|
+
.update(stableStringify({
|
|
944
|
+
schemaVersion: 1,
|
|
945
|
+
qualifiedId,
|
|
946
|
+
targetFingerprint,
|
|
947
|
+
authority,
|
|
948
|
+
collision,
|
|
949
|
+
runtimeImpact,
|
|
950
|
+
warnings,
|
|
951
|
+
}))
|
|
952
|
+
.digest('hex')}`;
|
|
953
|
+
return {
|
|
954
|
+
target: definition.target,
|
|
955
|
+
qualifiedId,
|
|
956
|
+
targetFingerprint,
|
|
957
|
+
previewFingerprint,
|
|
958
|
+
authority,
|
|
959
|
+
removal: { definition, preservesSameNamedOtherSource: collision, cascades: false },
|
|
960
|
+
configChange: previewDeleteConfigChange(input.targetName, input.targetSource),
|
|
961
|
+
expectedBackup: { policy: 'required', recoveryCopy: true },
|
|
962
|
+
expectedReload: {
|
|
963
|
+
policy: 'observe_after_write',
|
|
964
|
+
possibleStatuses: ['observed', 'runtime_not_running', 'reload_disabled', 'failed'],
|
|
965
|
+
},
|
|
966
|
+
runtimeImpact,
|
|
967
|
+
warnings,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async previewConfiguredServerLifecycleResult(input) {
|
|
971
|
+
const document = this.options.readConfigDocument();
|
|
972
|
+
const currentConfig = document?.mcpTemplates?.[input.targetName];
|
|
973
|
+
if (!currentConfig) {
|
|
974
|
+
if (document?.mcpServers?.[input.targetName]) {
|
|
975
|
+
throw new AdminConfiguredServerApplyError('configured_server_source_changed');
|
|
976
|
+
}
|
|
977
|
+
throw new AdminConfiguredServerNotFoundError(input.targetName);
|
|
978
|
+
}
|
|
979
|
+
const targetFingerprint = fingerprintConfiguredServerTarget(currentConfig);
|
|
980
|
+
const qualifiedId = `mcpTemplates/${input.targetName}`;
|
|
981
|
+
const disabled = currentConfig.disabled;
|
|
982
|
+
const currentEnabled = !isConfiguredServerTargetDisabled(disabled);
|
|
983
|
+
const disabledValueKind = disabled === undefined
|
|
984
|
+
? 'absent'
|
|
985
|
+
: typeof disabled === 'string' && disabled.includes('{{')
|
|
986
|
+
? 'context_expression'
|
|
987
|
+
: 'literal';
|
|
988
|
+
const expressionReplacement = {
|
|
989
|
+
occurs: disabledValueKind === 'context_expression' && !input.enabled,
|
|
990
|
+
replacement: input.enabled ? 'enabled_absent' : 'disabled_true',
|
|
991
|
+
};
|
|
992
|
+
const activeInstanceCount = this.options.getTemplateActiveInstanceCount?.(input.targetName) ?? 0;
|
|
993
|
+
const configChange = await this.options.configChangeService.previewConfiguredServerTargetEnabledState({
|
|
994
|
+
targetName: input.targetName,
|
|
995
|
+
targetSource: input.targetSource,
|
|
996
|
+
enabled: input.enabled,
|
|
997
|
+
backup: 'skip',
|
|
998
|
+
});
|
|
999
|
+
if (configChange.status === 'not_found' || configChange.status === 'source_conflict') {
|
|
1000
|
+
throw new AdminConfiguredServerApplyError('configured_server_stale_preview');
|
|
1001
|
+
}
|
|
1002
|
+
if (configChange.status === 'failed') {
|
|
1003
|
+
throw new AdminConfiguredServerApplyError('configured_server_apply_failed');
|
|
1004
|
+
}
|
|
1005
|
+
const warnings = [
|
|
1006
|
+
...(expressionReplacement.occurs
|
|
1007
|
+
? ['The context-rendered disabled expression will be replaced with literal true without evaluating Request Context.']
|
|
1008
|
+
: []),
|
|
1009
|
+
...(input.enabled
|
|
1010
|
+
? ['Re-enable restores eligibility only; instances and Request Sessions are created lazily by future matching requests.']
|
|
1011
|
+
: activeInstanceCount > 0
|
|
1012
|
+
? [`Successful reload retires ${activeInstanceCount} active Template Server instance${activeInstanceCount === 1 ? '' : 's'} and removes their Request Session memberships.`]
|
|
1013
|
+
: []),
|
|
1014
|
+
];
|
|
1015
|
+
const runtimeImpact = {
|
|
1016
|
+
activeInstanceCount,
|
|
1017
|
+
retirement: !input.enabled && configChange.changed ? 'after_successful_reload' : 'not_required',
|
|
1018
|
+
recreation: 'lazy_future_match_only',
|
|
1019
|
+
};
|
|
1020
|
+
const previewFingerprint = `lifecycle_preview_${createHash('sha256')
|
|
1021
|
+
.update(stableStringify({
|
|
1022
|
+
schemaVersion: 1,
|
|
1023
|
+
qualifiedId,
|
|
1024
|
+
targetFingerprint,
|
|
1025
|
+
currentEnabled,
|
|
1026
|
+
proposedEnabled: input.enabled,
|
|
1027
|
+
disabledValueKind,
|
|
1028
|
+
expressionReplacement,
|
|
1029
|
+
changed: configChange.changed,
|
|
1030
|
+
runtimeImpact,
|
|
1031
|
+
warnings,
|
|
1032
|
+
}))
|
|
1033
|
+
.digest('hex')}`;
|
|
1034
|
+
return {
|
|
1035
|
+
target: { type: 'configured_server', id: input.targetName, source: input.targetSource },
|
|
1036
|
+
qualifiedId,
|
|
1037
|
+
targetFingerprint,
|
|
1038
|
+
previewFingerprint,
|
|
1039
|
+
current: { enabled: currentEnabled, disabledValueKind },
|
|
1040
|
+
proposed: { enabled: input.enabled, disabledValueKind: input.enabled ? 'absent' : 'literal' },
|
|
1041
|
+
expressionReplacement,
|
|
1042
|
+
configChange: sanitizeConfiguredServerLifecycleResult(configChange),
|
|
1043
|
+
expectedBackup: { policy: 'required', recoveryCopy: true },
|
|
1044
|
+
expectedReload: {
|
|
1045
|
+
policy: 'observe_after_write',
|
|
1046
|
+
possibleStatuses: ['observed', 'runtime_not_running', 'reload_disabled', 'failed'],
|
|
1047
|
+
},
|
|
1048
|
+
runtimeImpact,
|
|
1049
|
+
warnings,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
readConfiguredServerState(targetName, targetSource, document = this.options.readConfigDocument()) {
|
|
1053
|
+
const parsed = document;
|
|
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 {
|
|
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:
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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.
|
|
800
|
-
?
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
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 =
|
|
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 =
|
|
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:
|
|
2115
|
-
operations:
|
|
2712
|
+
available: mutationAvailable,
|
|
2713
|
+
operations: ['enable', 'disable'],
|
|
2714
|
+
deleteAvailable: mutationAvailable,
|
|
2116
2715
|
},
|
|
2117
|
-
actionState:
|
|
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:
|
|
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:
|
|
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.
|
|
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 {};
|