@aiwg/cli 2026.10.7 → 2026.10.8
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/dist/src/cli/handlers/cost-report.js +7 -1
- package/dist/src/cli/handlers/decision.js +33 -8
- package/dist/src/cli/handlers/local-executor.js +33 -6
- package/dist/src/cost/fleet-report.js +20 -22
- package/dist/src/decision/driver.js +2 -2
- package/dist/src/decision/jev-credentials.js +18 -5
- package/dist/src/decision/ledger.js +230 -34
- package/dist/src/mcp/cli.mjs +105 -19
- package/dist/src/mcp/credentials.d.mts +4 -0
- package/dist/src/mcp/credentials.mjs +44 -3
- package/dist/src/mcp/grok-build-config.mjs +2 -0
- package/dist/src/mcp/omp-config.mjs +2 -0
- package/dist/src/mcp/registry.js +19 -15
- package/dist/src/mcp/registry.mjs +2 -18
- package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
- package/dist/src/models/grok-build-models.js +20 -0
- package/dist/src/providers/provider-definitions.js +2 -2
- package/dist/src/serve/shared-host-scheduler.js +4 -1
- package/dist/src/storage/backends/fortemi.js +107 -49
- package/package.json +1 -1
package/dist/src/mcp/registry.js
CHANGED
|
@@ -7,7 +7,7 @@ import { manageGrokBuildMcp } from './grok-build-config.mjs';
|
|
|
7
7
|
import { replaceServer } from './toml-editor.mjs';
|
|
8
8
|
import { resolveOmpPaths } from '../providers/omp-paths.mjs';
|
|
9
9
|
import { applyJsonToolFilterPlan, claudeSettingsPath, prepareClaudePermissions, planToolFilters } from './tool-filters.mjs';
|
|
10
|
-
import { assertCredentialPolicy, resolveCredentialPolicy, validateEnvReferenceName } from './credentials.mjs';
|
|
10
|
+
import { assertCredentialPolicy, resolveCredentialPolicy, validateEnvReferenceName, credentialReferenceInput, validateCredentialReferences, assertEnvironmentCredentialReferences } from './credentials.mjs';
|
|
11
11
|
/**
|
|
12
12
|
* MCP Server Registry
|
|
13
13
|
*
|
|
@@ -23,6 +23,7 @@ import { qualifyWorkerInvocationProfile, requiredWorkerControls } from '../model
|
|
|
23
23
|
import { dirname, isAbsolute, resolve } from 'path';
|
|
24
24
|
import { homedir } from 'os';
|
|
25
25
|
import { resolveConfigDir } from '../config/user-config.js';
|
|
26
|
+
import { resolveSensitiveInput } from '../auth/sensitive-input.js';
|
|
26
27
|
// ============================================
|
|
27
28
|
// Registry
|
|
28
29
|
// ============================================
|
|
@@ -32,22 +33,24 @@ const DEFAULT_REGISTRY = {
|
|
|
32
33
|
kind: 'McpServerRegistry',
|
|
33
34
|
servers: {},
|
|
34
35
|
};
|
|
35
|
-
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
/** Resolve references only for AIWG's in-memory transport, never for a config writer. */
|
|
37
|
+
export async function resolveMcpCredentialMaps(server, dependencies = {}) {
|
|
38
|
+
const result = { env: server.env ? { ...server.env } : undefined, headers: server.headers ? { ...server.headers } : undefined };
|
|
39
|
+
for (const [field, target, flag] of [['envFrom', 'env', '--env-from'], ['headerEnv', 'headers', '--header-env']]) {
|
|
40
|
+
for (const [key, reference] of Object.entries(server[field] ?? {})) {
|
|
41
|
+
const input = credentialReferenceInput(reference, flag);
|
|
42
|
+
// Explicit locators prevent terminal prompts and implicit stdin consumption.
|
|
43
|
+
try {
|
|
44
|
+
const value = await resolveSensitiveInput({ ...input, prefix: 'value' }, dependencies);
|
|
45
|
+
result[target] = { ...result[target], [key]: value };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : 'MCP credential input failed';
|
|
49
|
+
throw new Error(message.replace(/--value-(file|store|env)/g, (_, source) => `${flag} KEY=${source === 'env' ? 'ENV_NAME' : source === 'file' ? 'file:ABSOLUTE_PATH' : 'store:NAME'}`));
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
}
|
|
53
|
+
return result;
|
|
51
54
|
}
|
|
52
55
|
export function workerServerDigest(server) {
|
|
53
56
|
const { workerQualification, description, injectedProviders, addedAt, updatedAt, ...execution } = server;
|
|
@@ -300,6 +303,7 @@ function tomlInlineTable(values) {
|
|
|
300
303
|
return `{ ${Object.entries(values).map(([name, value]) => `${tomlKey(name)} = ${tomlString(value)}`).join(', ')} }`;
|
|
301
304
|
}
|
|
302
305
|
export function buildServerToml(server) {
|
|
306
|
+
assertEnvironmentCredentialReferences(server, 'codex');
|
|
303
307
|
const lines = [];
|
|
304
308
|
lines.push(`[mcp_servers.${tomlKey(server.name)}]`);
|
|
305
309
|
if (server.type === 'stdio') {
|
|
@@ -8,7 +8,7 @@ import { manageGrokBuildMcp } from './grok-build-config.mjs';
|
|
|
8
8
|
import { replaceServer } from './toml-editor.mjs';
|
|
9
9
|
import { resolveOmpPaths } from '../providers/omp-paths.mjs';
|
|
10
10
|
import { applyJsonToolFilterPlan, claudeSettingsPath, prepareClaudePermissions, planToolFilters } from './tool-filters.mjs';
|
|
11
|
-
import { assertCredentialPolicy, resolveCredentialPolicy, validateEnvReferenceName } from './credentials.mjs';
|
|
11
|
+
import { assertCredentialPolicy, resolveCredentialPolicy, validateEnvReferenceName, validateCredentialReferences, assertEnvironmentCredentialReferences } from './credentials.mjs';
|
|
12
12
|
/**
|
|
13
13
|
* MCP Server Registry (Runtime ESM)
|
|
14
14
|
*
|
|
@@ -72,23 +72,6 @@ const DEFAULT_REGISTRY = {
|
|
|
72
72
|
servers: {},
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
const ENV_REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
76
|
-
|
|
77
|
-
function validateCredentialReferences(def) {
|
|
78
|
-
for (const [header, envName] of Object.entries(def.headerEnv || {})) {
|
|
79
|
-
if (!header.trim()) throw new Error('MCP header-env header name must not be empty');
|
|
80
|
-
if (!ENV_REFERENCE_NAME.test(envName)) {
|
|
81
|
-
throw new Error(`Invalid MCP header environment variable reference "${envName}"`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
for (const [key, envName] of Object.entries(def.envFrom || {})) {
|
|
85
|
-
if (!ENV_REFERENCE_NAME.test(key)) throw new Error(`Invalid MCP env-from variable name "${key}"`);
|
|
86
|
-
if (!ENV_REFERENCE_NAME.test(envName)) {
|
|
87
|
-
throw new Error(`Invalid MCP env-from environment variable reference "${envName}"`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
75
|
// The typed worker dispatch boundary owns qualification. Legacy runtime edits
|
|
93
76
|
// invalidate its binding without importing the TypeScript policy owner.
|
|
94
77
|
export function workerServerDigest(server) {
|
|
@@ -280,6 +263,7 @@ function tomlInlineTable(values) {
|
|
|
280
263
|
}
|
|
281
264
|
|
|
282
265
|
export function buildServerToml(server) {
|
|
266
|
+
assertEnvironmentCredentialReferences(server, 'codex');
|
|
283
267
|
const lines = [];
|
|
284
268
|
lines.push(`[mcp_servers.${tomlKey(server.name)}]`);
|
|
285
269
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Governed Agentic Sandbox fleet and activity MCP surface.
|
|
3
3
|
*
|
|
4
|
-
* Credentials are file-
|
|
4
|
+
* Credentials are private-file or native-store configuration, never tool arguments.
|
|
5
5
|
* Every tool in this module uses the management bearer domain; executor-plane
|
|
6
6
|
* credentials are deliberately out of scope and cannot be substituted.
|
|
7
7
|
*
|
|
8
8
|
* @implements #2015
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { access } from 'node:fs/promises';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { integer, dateTime } from '../../schema/zod-validation.mjs';
|
|
14
14
|
import { assertNoWorkerBinding, mcpError, mcpJson } from '../helpers.mjs';
|
|
@@ -36,17 +36,27 @@ function configuredBaseUrl(env = process.env) {
|
|
|
36
36
|
return url.toString().replace(/\/$/, '');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async function
|
|
39
|
+
async function sensitiveInputModule() {
|
|
40
|
+
// Source MCP modules and the published dist tree share the compiled owner.
|
|
41
|
+
for (const location of [new URL('../../auth/sensitive-input.js', import.meta.url), new URL('../../../dist/src/auth/sensitive-input.js', import.meta.url)]) {
|
|
42
|
+
try { await access(location); } catch { continue; }
|
|
43
|
+
return import(location.href);
|
|
44
|
+
}
|
|
45
|
+
throw new Error('Sandbox credentials require the compiled AIWG auth module, which this installation is missing. Run `aiwg doctor` to repair the installation.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function managementBearer(env = process.env, readStore) {
|
|
40
49
|
const tokenFile = String(env.AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE ?? '').trim();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) {
|
|
45
|
-
throw new Error('sandbox management token file must not be accessible by group or other users');
|
|
50
|
+
const tokenStore = String(env.AIWG_SANDBOX_MANAGEMENT_TOKEN_STORE ?? '').trim();
|
|
51
|
+
if (!tokenFile && !tokenStore) {
|
|
52
|
+
throw new Error('Sandbox management bearer requires AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE or AIWG_SANDBOX_MANAGEMENT_TOKEN_STORE.');
|
|
46
53
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
const { readPrivateValueFile, resolveSensitiveInput } = await sensitiveInputModule();
|
|
55
|
+
return resolveSensitiveInput(tokenFile ? { prefix: 'token', file: tokenFile } : { prefix: 'token', store: tokenStore }, {
|
|
56
|
+
isTTY: false,
|
|
57
|
+
readFile: (pathname) => readPrivateValueFile(pathname, 'AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE'),
|
|
58
|
+
...(readStore ? { readStore } : {}),
|
|
59
|
+
});
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
function containsRestricted(value) {
|
|
@@ -138,32 +148,40 @@ function typedResult(body, status, validate) {
|
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
export class AgenticSandboxMcpClient {
|
|
141
|
-
constructor({ env = process.env, fetch = globalThis.fetch.bind(globalThis) } = {}) {
|
|
151
|
+
constructor({ env = process.env, fetch = globalThis.fetch.bind(globalThis), readStore } = {}) {
|
|
142
152
|
this.env = env;
|
|
143
153
|
this.fetch = fetch;
|
|
154
|
+
this.readStore = readStore;
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
async request(path, { method = 'GET', body, headers = {}, validate = (value) => safeResponse(value) } = {}) {
|
|
147
158
|
assertNoWorkerBinding(body, 'mcp');
|
|
148
159
|
const baseUrl = configuredBaseUrl(this.env);
|
|
149
|
-
const token = await managementBearer(this.env);
|
|
150
|
-
const
|
|
151
|
-
method,
|
|
152
|
-
headers: { accept: 'application/json', authorization: `Bearer ${token}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...headers },
|
|
153
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
154
|
-
});
|
|
155
|
-
let parsed;
|
|
156
|
-
try {
|
|
157
|
-
parsed = await response.json();
|
|
158
|
-
} catch {
|
|
159
|
-
if (response.status === 404 || response.status === 405) parsed = {};
|
|
160
|
-
else return mcpError(`sandbox returned non-JSON HTTP ${response.status}`);
|
|
161
|
-
}
|
|
160
|
+
const token = await managementBearer(this.env, this.readStore);
|
|
161
|
+
const redact = (text) => String(text).split(token).join('[REDACTED]');
|
|
162
162
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const response = await this.fetch(`${baseUrl}${path}`, {
|
|
164
|
+
method,
|
|
165
|
+
headers: { accept: 'application/json', authorization: `Bearer ${token}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...headers },
|
|
166
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
167
|
+
});
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
const value = await response.json();
|
|
171
|
+
// Upstream errors and metadata can reflect the authorization header.
|
|
172
|
+
parsed = JSON.parse(JSON.stringify(value).split(JSON.stringify(token).slice(1, -1)).join('[REDACTED]'));
|
|
173
|
+
} catch {
|
|
174
|
+
if (response.status === 404 || response.status === 405) parsed = {};
|
|
175
|
+
else return mcpError(`sandbox returned non-JSON HTTP ${response.status}`);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
assertNoWorkerBinding(parsed, 'mcp');
|
|
179
|
+
return typedResult(parsed, response.status, validate);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return mcpError(`${error.code ?? 'sandbox_malformed_response'}: ${error.message}`);
|
|
182
|
+
}
|
|
165
183
|
} catch (error) {
|
|
166
|
-
|
|
184
|
+
throw new Error(redact(error instanceof Error ? error.message : 'Sandbox management request failed'));
|
|
167
185
|
}
|
|
168
186
|
}
|
|
169
187
|
}
|
|
@@ -83,6 +83,13 @@ export function parseGrokBuildModelConfig(text, scope) {
|
|
|
83
83
|
parsed.projectModelFields = true;
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
|
+
// Presence only: never retain the command or endpoint in discovery output.
|
|
87
|
+
if ((table === 'auth' || table === 'grok_com_config') && key === 'auth_provider_command') {
|
|
88
|
+
parsed.hasExternalAuthProvider = Boolean(unquote(value)?.trim());
|
|
89
|
+
}
|
|
90
|
+
if (table === 'endpoints' && ['models_base_url', 'models_list_url', 'models_endpoint'].includes(key)) {
|
|
91
|
+
parsed.hasCustomModelsEndpoint ||= Boolean(unquote(value)?.trim());
|
|
92
|
+
}
|
|
86
93
|
if (table === 'models') {
|
|
87
94
|
if (key === 'default')
|
|
88
95
|
parsed.defaultModel = unquote(value);
|
|
@@ -299,6 +306,19 @@ export async function discoverGrokBuildModels(options = {}) {
|
|
|
299
306
|
const discovered = inspectModels(report, validationDiagnostics);
|
|
300
307
|
const parsedByLayer = await Promise.all(layers.map(layer => layer.role === 'env_overlay'
|
|
301
308
|
&& layer.path.includes('(inline)') ? parseEnvOverlay(env) : readLayer(layer)));
|
|
309
|
+
// Reviewed upstream 2bdd1d6a: external auth plus a custom models endpoint
|
|
310
|
+
// discards [model.*] and ordinary allowed_models. Inspect has no effective
|
|
311
|
+
// catalog, so config reconstruction cannot establish selectable aliases.
|
|
312
|
+
// Conservatively withhold discovery when any active layer names this setup;
|
|
313
|
+
// do not execute the auth provider or fetch its endpoint to fill the gap.
|
|
314
|
+
const externalAuth = Boolean(env.GROK_AUTH_PROVIDER_COMMAND?.trim())
|
|
315
|
+
|| parsedByLayer.some(layer => layer?.hasExternalAuthProvider);
|
|
316
|
+
const customEndpoint = Boolean(env.GROK_MODELS_BASE_URL?.trim() || env.GROK_MODELS_LIST_URL?.trim())
|
|
317
|
+
|| parsedByLayer.some(layer => layer?.hasCustomModelsEndpoint);
|
|
318
|
+
if (externalAuth && customEndpoint) {
|
|
319
|
+
return { ...base, ...(runtimeVersion ? { runtimeVersion } : {}), errorKind: 'invalid-output',
|
|
320
|
+
error: 'Grok Build external auth with a custom models endpoint requires its runtime catalog; config model aliases and ordinary allowed_models are ignored upstream. Automatic discovery is deferred for this configuration.' };
|
|
321
|
+
}
|
|
302
322
|
let requirementsDefault;
|
|
303
323
|
let requirementsAllowed;
|
|
304
324
|
for (let i = 0; i < layers.length; i++) {
|
|
@@ -687,9 +687,9 @@ const BUILT_IN_SEEDS = [
|
|
|
687
687
|
source: 'https://github.com/xai-org/grok-build',
|
|
688
688
|
version: '1.0.38',
|
|
689
689
|
// Reviewed public tree (docs + inspect schema), not an all-zero placeholder.
|
|
690
|
-
revision: '
|
|
690
|
+
revision: '2bdd1d6a6369de0e8c68132ea4539e9abd9e14a8',
|
|
691
691
|
runtime: 'Grok Build CLI',
|
|
692
|
-
lastVerified: '2026-
|
|
692
|
+
lastVerified: '2026-10-26',
|
|
693
693
|
},
|
|
694
694
|
surfaces: {
|
|
695
695
|
primary: 'grok-build',
|
|
@@ -233,7 +233,10 @@ export class FileAdmissionStore {
|
|
|
233
233
|
const file = openSync(this.path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
234
234
|
try {
|
|
235
235
|
const stat = fstatSync(file);
|
|
236
|
-
|
|
236
|
+
// nlink is 0 when a writer atomically renamed a new snapshot over the
|
|
237
|
+
// file after this reader opened it; the open descriptor is still a
|
|
238
|
+
// complete earlier snapshot. Only extra hard links are refused.
|
|
239
|
+
if (!stat.isFile() || stat.nlink > 1 || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.() || stat.size > 16 * 1024 * 1024)
|
|
237
240
|
throw new Error('host authority must be a private bounded regular file');
|
|
238
241
|
return JSON.parse(readFileSync(file, 'utf8'));
|
|
239
242
|
}
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* @issue #972
|
|
38
38
|
*/
|
|
39
39
|
import { createHash } from 'node:crypto';
|
|
40
|
+
import { debuglog } from 'node:util';
|
|
40
41
|
const FORTEMI_QUERY_LIMIT = 50;
|
|
41
42
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
42
43
|
function schemaProperties(tool) {
|
|
@@ -371,8 +372,8 @@ export class FortemiAdapter {
|
|
|
371
372
|
* Implemented as a lazy import so tests that inject a stub never load
|
|
372
373
|
* the SDK or touch the registry.
|
|
373
374
|
*/
|
|
374
|
-
export const createDefaultMcpClient = async (serverName, registryOverride, environment = process.env) => {
|
|
375
|
-
const { McpServerRegistry } = await import('../../mcp/registry.js');
|
|
375
|
+
export const createDefaultMcpClient = async (serverName, registryOverride, environment = process.env, sensitiveInput = {}) => {
|
|
376
|
+
const { McpServerRegistry, resolveMcpCredentialMaps } = await import('../../mcp/registry.js');
|
|
376
377
|
const registry = registryOverride ?? new McpServerRegistry();
|
|
377
378
|
const server = await registry.get(serverName);
|
|
378
379
|
if (!server) {
|
|
@@ -381,60 +382,117 @@ export const createDefaultMcpClient = async (serverName, registryOverride, envir
|
|
|
381
382
|
}
|
|
382
383
|
// Lazy imports keep unit tests that inject a stub isolated from transports.
|
|
383
384
|
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
env: server.env,
|
|
391
|
-
});
|
|
385
|
+
const hasNewReferences = [...Object.values(server.headerEnv ?? {}), ...Object.values(server.envFrom ?? {})]
|
|
386
|
+
.some(reference => /^(?:file|store):/.test(reference));
|
|
387
|
+
// Keep legacy header/environment behavior unchanged. New locators are resolved
|
|
388
|
+
// only at connection time and never fed back to the registry or config writers.
|
|
389
|
+
if (hasNewReferences && server.type === 'stdio' && debuglog('child_process').enabled) {
|
|
390
|
+
throw new Error('MCP file/store stdio credentials require NODE_DEBUG child_process logging to be disabled.');
|
|
392
391
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
392
|
+
const resolved = hasNewReferences
|
|
393
|
+
? await resolveMcpCredentialMaps(server, { ...sensitiveInput, environment })
|
|
394
|
+
: undefined;
|
|
395
|
+
const secrets = resolved ? [
|
|
396
|
+
...Object.keys(server.envFrom ?? {}).map(key => resolved.env?.[key]),
|
|
397
|
+
...Object.keys(server.headerEnv ?? {}).map(key => resolved.headers?.[key]),
|
|
398
|
+
].filter((value) => Boolean(value)).sort((a, b) => b.length - a.length) : [];
|
|
399
|
+
const redact = (text) => secrets.reduce((text, secret) => text.split(secret).join('[REDACTED]'), text);
|
|
400
|
+
const protect = async (operation) => {
|
|
401
|
+
try {
|
|
402
|
+
return await operation();
|
|
396
403
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (server.type === 'http') {
|
|
400
|
-
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
401
|
-
transport = new StreamableHTTPClientTransport(url, {
|
|
402
|
-
requestInit: { headers },
|
|
403
|
-
});
|
|
404
|
+
catch (error) {
|
|
405
|
+
throw new Error(redact(error instanceof Error ? error.message : 'MCP transport failed'));
|
|
404
406
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
407
|
+
};
|
|
408
|
+
const protectResult = (value) => {
|
|
409
|
+
if (typeof value === 'string')
|
|
410
|
+
return redact(value);
|
|
411
|
+
if (Array.isArray(value))
|
|
412
|
+
return value.map(protectResult);
|
|
413
|
+
if (value && typeof value === 'object')
|
|
414
|
+
return Object.fromEntries(Object.entries(value).map(([key, value]) => [redact(key), protectResult(value)]));
|
|
415
|
+
return value;
|
|
416
|
+
};
|
|
417
|
+
try {
|
|
418
|
+
let transport;
|
|
419
|
+
if (server.type === 'stdio') {
|
|
420
|
+
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
|
|
421
|
+
transport = new StdioClientTransport({
|
|
422
|
+
command: server.command ?? '',
|
|
423
|
+
args: server.args ?? [],
|
|
424
|
+
env: resolved?.env ?? server.env,
|
|
425
|
+
...(hasNewReferences ? { stderr: 'pipe' } : {}),
|
|
417
426
|
});
|
|
418
427
|
}
|
|
419
428
|
else {
|
|
420
|
-
|
|
429
|
+
if (!server.url) {
|
|
430
|
+
throw new Error(`storage(fortemi): MCP server "${serverName}" has no URL`);
|
|
431
|
+
}
|
|
432
|
+
const url = validateRemoteMcpUrl(server.url);
|
|
433
|
+
const headers = resolved?.headers ?? resolveMcpRequestHeaders(server, environment);
|
|
434
|
+
if (resolved) {
|
|
435
|
+
for (const header of Object.keys(server.headerEnv ?? {})) {
|
|
436
|
+
if (header.toLowerCase() === 'authorization')
|
|
437
|
+
headers[header] = `Bearer ${headers[header]}`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (server.type === 'http') {
|
|
441
|
+
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
442
|
+
transport = new StreamableHTTPClientTransport(url, {
|
|
443
|
+
requestInit: { headers },
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else if (server.type === 'sse') {
|
|
447
|
+
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
|
448
|
+
transport = new SSEClientTransport(url, {
|
|
449
|
+
requestInit: { headers },
|
|
450
|
+
eventSourceInit: {
|
|
451
|
+
fetch: async (input, init) => {
|
|
452
|
+
const merged = new Headers(init?.headers);
|
|
453
|
+
for (const [name, value] of Object.entries(headers))
|
|
454
|
+
merged.set(name, value);
|
|
455
|
+
return fetch(input, { ...init, headers: merged });
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
throw new Error(`storage(fortemi): unsupported MCP transport "${String(server.type)}"`);
|
|
462
|
+
}
|
|
421
463
|
}
|
|
464
|
+
const client = new Client({ name: 'aiwg-storage-fortemi-adapter', version: '1.0.0' }, { capabilities: {} });
|
|
465
|
+
if (hasNewReferences && server.type === 'stdio') {
|
|
466
|
+
// The recipient's stderr can contain credentials. Drain without forwarding.
|
|
467
|
+
transport.stderr?.on('data', () => { });
|
|
468
|
+
}
|
|
469
|
+
if (hasNewReferences)
|
|
470
|
+
await protect(() => client.connect(transport));
|
|
471
|
+
else
|
|
472
|
+
await client.connect(transport);
|
|
473
|
+
return {
|
|
474
|
+
async callTool(name, args) {
|
|
475
|
+
const call = async () => unwrapMcpToolResult(await client.callTool({ name, arguments: args }));
|
|
476
|
+
return hasNewReferences ? protectResult(await protect(call)) : call();
|
|
477
|
+
},
|
|
478
|
+
async listTools() {
|
|
479
|
+
return hasNewReferences ? protectResult(await protect(() => client.listTools())) : client.listTools();
|
|
480
|
+
},
|
|
481
|
+
serverVersion() {
|
|
482
|
+
return hasNewReferences ? protectResult(client.getServerVersion()) : client.getServerVersion();
|
|
483
|
+
},
|
|
484
|
+
async close() {
|
|
485
|
+
if (hasNewReferences)
|
|
486
|
+
await protect(() => client.close());
|
|
487
|
+
else
|
|
488
|
+
await client.close();
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
if (!hasNewReferences)
|
|
494
|
+
throw error;
|
|
495
|
+
throw new Error(redact(error instanceof Error ? error.message : 'MCP transport failed'));
|
|
422
496
|
}
|
|
423
|
-
const client = new Client({ name: 'aiwg-storage-fortemi-adapter', version: '1.0.0' }, { capabilities: {} });
|
|
424
|
-
await client.connect(transport);
|
|
425
|
-
return {
|
|
426
|
-
async callTool(name, args) {
|
|
427
|
-
return unwrapMcpToolResult(await client.callTool({ name, arguments: args }));
|
|
428
|
-
},
|
|
429
|
-
async listTools() {
|
|
430
|
-
return client.listTools();
|
|
431
|
-
},
|
|
432
|
-
serverVersion() {
|
|
433
|
-
return client.getServerVersion();
|
|
434
|
-
},
|
|
435
|
-
async close() {
|
|
436
|
-
await client.close();
|
|
437
|
-
},
|
|
438
|
-
};
|
|
439
497
|
};
|
|
440
498
|
//# sourceMappingURL=fortemi.js.map
|