@wix/pathgrade 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/agents/claude/denied-mcp-event-store.d.ts +8 -0
  2. package/dist/agents/claude/denied-mcp-event-store.js +17 -0
  3. package/dist/agents/claude/mcp-tool-name.d.ts +12 -0
  4. package/dist/agents/claude/mcp-tool-name.js +22 -0
  5. package/dist/agents/claude/sdk-message-projector.d.ts +5 -0
  6. package/dist/agents/claude/sdk-message-projector.js +31 -3
  7. package/dist/agents/claude/sdk-options.d.ts +4 -2
  8. package/dist/agents/claude/sdk-options.js +2 -0
  9. package/dist/agents/claude/tool-permission-bridge.d.ts +18 -0
  10. package/dist/agents/claude/tool-permission-bridge.js +86 -0
  11. package/dist/agents/claude.d.ts +1 -1
  12. package/dist/agents/claude.js +43 -5
  13. package/dist/agents/codex-app-server/agent.js +162 -3
  14. package/dist/agents/codex.js +4 -0
  15. package/dist/agents/cursor.js +12 -13
  16. package/dist/providers/mcp-config.d.ts +24 -16
  17. package/dist/providers/mcp-config.js +128 -20
  18. package/dist/providers/mcp-runtime-mounting.d.ts +46 -0
  19. package/dist/providers/mcp-runtime-mounting.js +155 -0
  20. package/dist/providers/sandbox.d.ts +1 -1
  21. package/dist/providers/sandbox.js +2 -1
  22. package/dist/providers/workspace.d.ts +1 -1
  23. package/dist/providers/workspace.js +2 -2
  24. package/dist/sdk/agent.js +7 -4
  25. package/dist/sdk/index.d.ts +6 -1
  26. package/dist/sdk/index.js +2 -0
  27. package/dist/sdk/managed-session.d.ts +3 -0
  28. package/dist/sdk/managed-session.js +2 -0
  29. package/dist/sdk/mcp-evidence.d.ts +29 -0
  30. package/dist/sdk/mcp-evidence.js +96 -0
  31. package/dist/sdk/mcp-safety.d.ts +30 -0
  32. package/dist/sdk/mcp-safety.js +61 -0
  33. package/dist/sdk/types.d.ts +12 -2
  34. package/dist/sdk/types.js +2 -2
  35. package/dist/tool-events.d.ts +1 -1
  36. package/dist/types.d.ts +6 -0
  37. package/dist/utils/timeout.js +11 -10
  38. package/package.json +3 -2
@@ -1,9 +1,8 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
1
  import { BaseAgent, getWorkspacePath } from '../types.js';
4
2
  import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../tool-events.js';
5
3
  import { getVisibleAssistantMessage } from '../sdk/visible-turn.js';
6
4
  import { prependRuntimePolicies } from '../sdk/runtime-policy.js';
5
+ import { assertUnsupportedLiveMcpSafetyRuntime, mountMcpForCursor, } from '../providers/mcp-runtime-mounting.js';
7
6
  /**
8
7
  * Cursor tool_call discriminants observed in the discovery spike. Probed in
9
8
  * order — first present key wins. Explicit probing (vs. `Object.keys(…)[0]`)
@@ -171,6 +170,12 @@ export class CursorAgent extends BaseAgent {
171
170
  return getVisibleAssistantMessage(result);
172
171
  }
173
172
  async runTurn(instruction, workspacePath, runCommand, sessionId, options) {
173
+ await assertUnsupportedLiveMcpSafetyRuntime({
174
+ runtimeName: 'Cursor',
175
+ workspacePath,
176
+ mcpConfigPath: options?.mcpConfigPath,
177
+ mcpSafety: options?.mcpSafety,
178
+ });
174
179
  // Runtime policies are injected into the first-turn prompt only.
175
180
  // On resumed turns (sessionId set) the policy was already delivered,
176
181
  // so we skip re-prepending. The Claude SDK driver does not prepend
@@ -188,18 +193,12 @@ export class CursorAgent extends BaseAgent {
188
193
  const promptPath = '"${TMPDIR:-/tmp}/.pathgrade-cursor-prompt.md"';
189
194
  const b64 = Buffer.from(effectiveInstruction).toString('base64');
190
195
  await runCommand(`mkdir -p "\${TMPDIR:-/tmp}" && echo '${b64}' | base64 -d > ${promptPath}`);
191
- // Materialize .cursor/mcp.json from pathgrade's MCP config so the
192
- // CLI's native loader picks it up. Idempotent per turn.
193
- if (options?.mcpConfigPath) {
194
- const srcMcp = path.join(workspacePath, options.mcpConfigPath);
195
- if (await fs.pathExists(srcMcp)) {
196
- const cursorDir = path.join(workspacePath, '.cursor');
197
- await fs.ensureDir(cursorDir);
198
- await fs.copy(srcMcp, path.join(cursorDir, 'mcp.json'), { overwrite: true });
199
- }
200
- }
196
+ const mcpMount = await mountMcpForCursor({
197
+ workspacePath,
198
+ mcpConfigPath: options?.mcpConfigPath,
199
+ });
201
200
  const modelFlag = options?.model ? ` --model ${options.model}` : '';
202
- const mcpFlag = options?.mcpConfigPath ? ' --approve-mcps' : '';
201
+ const mcpFlag = mcpMount.approveMcps ? ' --approve-mcps' : '';
203
202
  const resumeFlag = sessionId ? ` --resume ${this.sanitizeSessionId(sessionId)}` : '';
204
203
  const command = `${CURSOR_EXECUTABLE} -p --output-format stream-json --trust --force --workspace "${workspacePath}"${resumeFlag}${modelFlag}${mcpFlag} "$(cat ${promptPath})" < /dev/null`;
205
204
  const result = await runCommand(command);
@@ -3,32 +3,40 @@ import type { MockMcpServerDescriptor } from '../core/mcp-mock.types.js';
3
3
  * Stdio-server shape pathgrade writes to `.pathgrade-mcp.json`. Lines up
4
4
  * directly with the SDK's `McpStdioServerConfig` (`sdk.d.ts:1005`) — the
5
5
  * optional `type: 'stdio'` discriminator is omitted because the JSON
6
- * `writeMcpConfig` produces does not include it.
6
+ * `stageMcpConfig` produces does not include it.
7
7
  */
8
8
  export interface McpStdioEntry {
9
9
  command: string;
10
10
  args?: string[];
11
11
  env?: Record<string, string>;
12
+ startup_timeout_sec?: number;
13
+ tool_timeout_sec?: number;
12
14
  }
13
- /** Object form of MCP server entries the SDK driver passes to `Options.mcpServers`. */
14
- export type McpServersObject = Record<string, McpStdioEntry>;
15
- export type McpSpec = {
15
+ export interface McpStreamableHttpEntry {
16
+ type?: 'streamable-http' | 'http';
17
+ url: string;
18
+ headers?: Record<string, string>;
19
+ http_headers?: Record<string, string>;
20
+ env_http_headers?: Record<string, string>;
21
+ bearer_token_env_var?: string;
22
+ startup_timeout_sec?: number;
23
+ tool_timeout_sec?: number;
24
+ }
25
+ export type McpConfigFileEntry = McpStdioEntry | McpStreamableHttpEntry;
26
+ export type McpDeclaration = {
16
27
  configFile: string;
17
28
  } | {
18
29
  mock: MockMcpServerDescriptor | MockMcpServerDescriptor[];
19
30
  };
31
+ export declare const MCP_CONFIG_FILENAME = ".pathgrade-mcp.json";
32
+ export declare function readStagedMcpServers(workspacePath: string, mcpConfigPath?: string): Promise<Record<string, McpConfigFileEntry> | undefined>;
33
+ export declare function missingMcpSecretReferences(mcpServers: Record<string, McpConfigFileEntry> | undefined, env: Record<string, string | undefined>): string[];
34
+ export declare function assertMcpSecretReferencesReady(opts: {
35
+ workspacePath: string;
36
+ mcpConfigPath?: string;
37
+ env: Record<string, string | undefined>;
38
+ }): Promise<void>;
20
39
  export interface McpConfigResult {
21
40
  mcpConfigPath: string | undefined;
22
41
  }
23
- export declare function writeMcpConfig(workspacePath: string, mcp: McpSpec | undefined): Promise<McpConfigResult>;
24
- /**
25
- * Read the JSON `writeMcpConfig` writes into `<workspace>/.pathgrade-mcp.json`
26
- * and return the inner `mcpServers` object the Claude SDK driver hands to
27
- * `Options.mcpServers`. Returns `undefined` when the file is absent (the
28
- * "no MCP" path through the driver — the fixture didn't declare an `mcp` spec).
29
- *
30
- * The on-disk path is preserved because the Cursor agent driver still
31
- * consumes it via `mcpConfigPath` (`src/agents/cursor.ts`) — Claude moves to
32
- * the object form, Cursor stays on the file.
33
- */
34
- export declare function loadMcpServersForSdk(workspacePath: string): Promise<McpServersObject | undefined>;
42
+ export declare function stageMcpConfig(workspacePath: string, mcp: McpDeclaration | undefined): Promise<McpConfigResult>;
@@ -1,7 +1,132 @@
1
1
  import fs from 'fs-extra';
2
2
  import * as path from 'path';
3
- const MCP_CONFIG_FILENAME = '.pathgrade-mcp.json';
4
- export async function writeMcpConfig(workspacePath, mcp) {
3
+ export const MCP_CONFIG_FILENAME = '.pathgrade-mcp.json';
4
+ const GENERATED_MOCK_SERVER_FILENAME = '.pathgrade-mcp-mock-server.cjs';
5
+ function isRecord(value) {
6
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ }
8
+ function isStringRecord(value) {
9
+ return isRecord(value)
10
+ && Object.values(value).every((entry) => typeof entry === 'string');
11
+ }
12
+ function assertOptionalStringRecord(serverName, fieldName, value) {
13
+ if (value !== undefined && !isStringRecord(value)) {
14
+ throw new Error(`Invalid MCP server "${serverName}": ${fieldName} must be a string record`);
15
+ }
16
+ }
17
+ function assertOptionalStringArray(serverName, fieldName, value) {
18
+ if (value !== undefined && (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string'))) {
19
+ throw new Error(`Invalid MCP server "${serverName}": ${fieldName} must be a string array`);
20
+ }
21
+ }
22
+ function assertOptionalNumber(serverName, fieldName, value) {
23
+ if (value !== undefined && typeof value !== 'number') {
24
+ throw new Error(`Invalid MCP server "${serverName}": ${fieldName} must be a number`);
25
+ }
26
+ }
27
+ function validateMcpConfigFileEntry(serverName, entry) {
28
+ if (!isRecord(entry)) {
29
+ throw new Error(`Invalid MCP server "${serverName}": entry must be an object`);
30
+ }
31
+ const hasCommand = entry.command !== undefined;
32
+ const hasUrl = entry.url !== undefined;
33
+ if (hasCommand && hasUrl) {
34
+ throw new Error(`Invalid MCP server "${serverName}": must not define both command and url`);
35
+ }
36
+ if (hasCommand) {
37
+ if (typeof entry.command !== 'string') {
38
+ throw new Error(`Invalid MCP server "${serverName}": command must be a string`);
39
+ }
40
+ assertOptionalStringArray(serverName, 'args', entry.args);
41
+ assertOptionalStringRecord(serverName, 'env', entry.env);
42
+ assertOptionalNumber(serverName, 'startup_timeout_sec', entry.startup_timeout_sec);
43
+ assertOptionalNumber(serverName, 'tool_timeout_sec', entry.tool_timeout_sec);
44
+ return entry;
45
+ }
46
+ if (!hasUrl) {
47
+ throw new Error(`Invalid MCP server "${serverName}": expected command or url`);
48
+ }
49
+ if (entry.type !== undefined && entry.type !== 'streamable-http' && entry.type !== 'http') {
50
+ throw new Error(`Unsupported MCP server type for "${serverName}": ${String(entry.type)}`);
51
+ }
52
+ if (typeof entry.url !== 'string') {
53
+ throw new Error(`Invalid MCP server "${serverName}": url must be a string`);
54
+ }
55
+ assertOptionalStringRecord(serverName, 'headers', entry.headers);
56
+ assertOptionalStringRecord(serverName, 'http_headers', entry.http_headers);
57
+ assertOptionalStringRecord(serverName, 'env_http_headers', entry.env_http_headers);
58
+ if (entry.bearer_token_env_var !== undefined && typeof entry.bearer_token_env_var !== 'string') {
59
+ throw new Error(`Invalid MCP server "${serverName}": bearer_token_env_var must be a string`);
60
+ }
61
+ assertOptionalNumber(serverName, 'startup_timeout_sec', entry.startup_timeout_sec);
62
+ assertOptionalNumber(serverName, 'tool_timeout_sec', entry.tool_timeout_sec);
63
+ return entry;
64
+ }
65
+ function readMcpServers(parsed) {
66
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers))
67
+ return undefined;
68
+ return Object.fromEntries(Object.entries(parsed.mcpServers).map(([name, entry]) => [
69
+ name,
70
+ validateMcpConfigFileEntry(name, entry),
71
+ ]));
72
+ }
73
+ export async function readStagedMcpServers(workspacePath, mcpConfigPath = MCP_CONFIG_FILENAME) {
74
+ const configPath = path.join(workspacePath, mcpConfigPath);
75
+ if (!(await fs.pathExists(configPath)))
76
+ return undefined;
77
+ return readMcpServers(await fs.readJson(configPath));
78
+ }
79
+ export function missingMcpSecretReferences(mcpServers, env) {
80
+ if (!mcpServers)
81
+ return [];
82
+ const missing = [];
83
+ for (const [serverName, entry] of Object.entries(mcpServers)) {
84
+ if ('env_http_headers' in entry) {
85
+ for (const envVar of Object.values(entry.env_http_headers ?? {})) {
86
+ if (!env[envVar])
87
+ missing.push(`${serverName}: env_http_headers references ${envVar}`);
88
+ }
89
+ }
90
+ if ('bearer_token_env_var' in entry && entry.bearer_token_env_var && !env[entry.bearer_token_env_var]) {
91
+ missing.push(`${serverName}: bearer_token_env_var references ${entry.bearer_token_env_var}`);
92
+ }
93
+ }
94
+ return missing;
95
+ }
96
+ export async function assertMcpSecretReferencesReady(opts) {
97
+ const mcpServers = await readStagedMcpServers(opts.workspacePath, opts.mcpConfigPath);
98
+ const missing = missingMcpSecretReferences(mcpServers, opts.env);
99
+ if (missing.length > 0) {
100
+ throw new Error(`Missing MCP secret references: ${missing.join('; ')}`);
101
+ }
102
+ }
103
+ async function resolveMockServerScript(workspacePath) {
104
+ const compiledScript = path.resolve(import.meta.dirname, '../mcp-mock-server.js');
105
+ if (await fs.pathExists(compiledScript))
106
+ return compiledScript;
107
+ const sourceScript = path.resolve(import.meta.dirname, '../mcp-mock-server.ts');
108
+ if (!await fs.pathExists(sourceScript)) {
109
+ throw new Error(`Mock MCP server source not found: ${sourceScript}`);
110
+ }
111
+ const ts = await import('typescript');
112
+ const source = await fs.readFile(sourceScript, 'utf-8');
113
+ const transpiled = ts.transpileModule(source, {
114
+ compilerOptions: {
115
+ target: ts.ScriptTarget.ES2022,
116
+ module: ts.ModuleKind.CommonJS,
117
+ esModuleInterop: true,
118
+ },
119
+ fileName: sourceScript,
120
+ });
121
+ const generatedScript = path.join(workspacePath, GENERATED_MOCK_SERVER_FILENAME);
122
+ await fs.writeFile(generatedScript, [
123
+ '// Generated by PathGrade from src/mcp-mock-server.ts.',
124
+ '// This trial-local file lets external agent runtimes spawn the mock server with plain node.',
125
+ transpiled.outputText,
126
+ ].join('\n'));
127
+ return generatedScript;
128
+ }
129
+ export async function stageMcpConfig(workspacePath, mcp) {
5
130
  if (!mcp)
6
131
  return { mcpConfigPath: undefined };
7
132
  const mcpConfigPath = MCP_CONFIG_FILENAME;
@@ -21,12 +146,12 @@ export async function writeMcpConfig(workspacePath, mcp) {
21
146
  }
22
147
  seen.add(mock.config.name);
23
148
  }
149
+ const mockServerScript = await resolveMockServerScript(workspacePath);
24
150
  const mcpServers = {};
25
151
  for (const mock of mocks) {
26
152
  const sanitizedName = mock.config.name.replace(/[^a-zA-Z0-9-]/g, '-');
27
153
  const fixturePath = path.join(workspacePath, `.pathgrade-mcp-mock-${sanitizedName}.json`);
28
154
  await fs.writeJson(fixturePath, mock.config, { spaces: 2 });
29
- const mockServerScript = path.resolve(import.meta.dirname, '../mcp-mock-server.js');
30
155
  mcpServers[mock.config.name] = {
31
156
  command: 'node',
32
157
  args: [mockServerScript, fixturePath],
@@ -36,20 +161,3 @@ export async function writeMcpConfig(workspacePath, mcp) {
36
161
  }
37
162
  return { mcpConfigPath };
38
163
  }
39
- /**
40
- * Read the JSON `writeMcpConfig` writes into `<workspace>/.pathgrade-mcp.json`
41
- * and return the inner `mcpServers` object the Claude SDK driver hands to
42
- * `Options.mcpServers`. Returns `undefined` when the file is absent (the
43
- * "no MCP" path through the driver — the fixture didn't declare an `mcp` spec).
44
- *
45
- * The on-disk path is preserved because the Cursor agent driver still
46
- * consumes it via `mcpConfigPath` (`src/agents/cursor.ts`) — Claude moves to
47
- * the object form, Cursor stays on the file.
48
- */
49
- export async function loadMcpServersForSdk(workspacePath) {
50
- const configPath = path.join(workspacePath, MCP_CONFIG_FILENAME);
51
- if (!(await fs.pathExists(configPath)))
52
- return undefined;
53
- const parsed = (await fs.readJson(configPath));
54
- return parsed.mcpServers;
55
- }
@@ -0,0 +1,46 @@
1
+ import type { McpStdioEntry } from './mcp-config.js';
2
+ import type { McpSafetyOptions } from '../sdk/mcp-safety.js';
3
+ export type ClaudeSdkMcpServerEntry = McpStdioEntry | {
4
+ type: 'http';
5
+ url: string;
6
+ headers?: Record<string, string>;
7
+ };
8
+ /** Object form of MCP server entries the SDK driver passes to `Options.mcpServers`. */
9
+ export type McpServersObject = Record<string, ClaudeSdkMcpServerEntry>;
10
+ export type CodexMcpServerEntry = McpStdioEntry | {
11
+ url: string;
12
+ http_headers?: Record<string, string>;
13
+ env_http_headers?: Record<string, string>;
14
+ bearer_token_env_var?: string;
15
+ startup_timeout_sec?: number;
16
+ tool_timeout_sec?: number;
17
+ };
18
+ export interface CodexAppServerMcpConfig extends Record<string, unknown> {
19
+ mcp_servers: Record<string, unknown>;
20
+ }
21
+ export interface McpRuntimeMountingOptions {
22
+ workspacePath?: string;
23
+ mcpConfigPath?: string;
24
+ }
25
+ interface WorkspaceMcpRuntimeMountingOptions extends McpRuntimeMountingOptions {
26
+ workspacePath: string;
27
+ runtimeEnv?: Record<string, string>;
28
+ }
29
+ interface ClaudeLiveMcpSafetyPreflightOptions extends WorkspaceMcpRuntimeMountingOptions {
30
+ mcpSafety?: McpSafetyOptions;
31
+ }
32
+ interface UnsupportedLiveMcpSafetyRuntimeOptions extends WorkspaceMcpRuntimeMountingOptions {
33
+ runtimeName: string;
34
+ mcpSafety?: McpSafetyOptions;
35
+ }
36
+ export interface CursorMcpRuntimeMount {
37
+ approveMcps: boolean;
38
+ }
39
+ export declare function assertClaudeLiveMcpSafetyPreflight(options: ClaudeLiveMcpSafetyPreflightOptions): Promise<void>;
40
+ export declare function assertUnsupportedLiveMcpSafetyRuntime(options: UnsupportedLiveMcpSafetyRuntimeOptions): Promise<void>;
41
+ export declare function assertStdioMcpServersStartForClaudeSdk(options: WorkspaceMcpRuntimeMountingOptions): Promise<void>;
42
+ export declare function mountMcpForClaudeSdk(options: WorkspaceMcpRuntimeMountingOptions): Promise<McpServersObject | undefined>;
43
+ export declare function mountMcpForCodexAppServer(options: WorkspaceMcpRuntimeMountingOptions): Promise<CodexAppServerMcpConfig | undefined>;
44
+ export declare function mountMcpForCursor(options: WorkspaceMcpRuntimeMountingOptions): Promise<CursorMcpRuntimeMount>;
45
+ export declare function assertMcpRuntimeMountingSupportedForCodexExec(options: McpRuntimeMountingOptions): Promise<void>;
46
+ export {};
@@ -0,0 +1,155 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
5
+ import { readStagedMcpServers } from './mcp-config.js';
6
+ function isStdioEntry(entry) {
7
+ return typeof entry.command === 'string';
8
+ }
9
+ function headersForRemote(entry) {
10
+ return entry.headers ?? entry.http_headers;
11
+ }
12
+ function compact(entry) {
13
+ return Object.fromEntries(Object.entries(entry).filter(([, value]) => value !== undefined));
14
+ }
15
+ function toClaudeSdkMcpServerEntry(entry) {
16
+ if (isStdioEntry(entry))
17
+ return entry;
18
+ return compact({
19
+ type: 'http',
20
+ url: entry.url,
21
+ headers: headersForRemote(entry),
22
+ });
23
+ }
24
+ function toCodexAppServerMcpServerEntry(entry) {
25
+ if (isStdioEntry(entry))
26
+ return entry;
27
+ return compact({
28
+ url: entry.url,
29
+ http_headers: headersForRemote(entry),
30
+ env_http_headers: entry.env_http_headers,
31
+ bearer_token_env_var: entry.bearer_token_env_var,
32
+ startup_timeout_sec: entry.startup_timeout_sec,
33
+ tool_timeout_sec: entry.tool_timeout_sec,
34
+ });
35
+ }
36
+ function isLiveMcpSafetyMode(options) {
37
+ const runMode = options?.runMode ?? 'mock';
38
+ return runMode === 'live-readonly' || runMode === 'live-sandbox' || runMode === 'live';
39
+ }
40
+ function describeClaudeIncompatibleFields(serverName, entry) {
41
+ if (isStdioEntry(entry))
42
+ return [];
43
+ const fields = [];
44
+ if (entry.env_http_headers !== undefined)
45
+ fields.push('env_http_headers');
46
+ if (entry.bearer_token_env_var !== undefined)
47
+ fields.push('bearer_token_env_var');
48
+ if (entry.headers !== undefined && entry.http_headers !== undefined) {
49
+ fields.push('headers and http_headers');
50
+ }
51
+ return fields.map((field) => `${serverName}: ${field}`);
52
+ }
53
+ function formatMcpStartupError(serverName, error, stderr) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ const stderrSuffix = stderr.trim() ? `\nstderr:\n${stderr.trim()}` : '';
56
+ return new Error(`MCP server ${serverName} failed to start: ${message}${stderrSuffix}`);
57
+ }
58
+ async function assertStdioMcpServerStarts(workspacePath, serverName, entry, runtimeEnv) {
59
+ const timeoutMs = Math.max(1, entry.startup_timeout_sec ?? 15) * 1000;
60
+ const transport = new StdioClientTransport({
61
+ command: entry.command,
62
+ args: entry.args,
63
+ env: {
64
+ ...(runtimeEnv ?? {}),
65
+ ...(entry.env ?? {}),
66
+ },
67
+ cwd: workspacePath,
68
+ stderr: 'pipe',
69
+ });
70
+ let stderr = '';
71
+ transport.stderr?.on('data', (chunk) => {
72
+ stderr += chunk.toString();
73
+ });
74
+ const client = new Client({ name: 'pathgrade-stdio-mcp-preflight', version: '0.5.0' }, { capabilities: {} });
75
+ try {
76
+ await client.connect(transport, { timeout: timeoutMs });
77
+ await client.listTools(undefined, { timeout: timeoutMs });
78
+ }
79
+ catch (error) {
80
+ throw formatMcpStartupError(serverName, error, stderr);
81
+ }
82
+ finally {
83
+ await client.close().catch(() => undefined);
84
+ await transport.close().catch(() => undefined);
85
+ }
86
+ }
87
+ export async function assertClaudeLiveMcpSafetyPreflight(options) {
88
+ if (!options.mcpConfigPath || !isLiveMcpSafetyMode(options.mcpSafety))
89
+ return;
90
+ const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
91
+ if (!mcpServers)
92
+ return;
93
+ if (options.mcpSafety?.liveOptIn !== true) {
94
+ throw new Error('Claude live MCP safety requires mcpSafety.liveOptIn: true before MCP runtime mounting.');
95
+ }
96
+ const unsupported = Object.entries(mcpServers).flatMap(([serverName, entry]) => describeClaudeIncompatibleFields(serverName, entry));
97
+ if (unsupported.length > 0) {
98
+ throw new Error(`Claude live MCP safety does not support runtime-incompatible MCP config fields: ${unsupported.join('; ')}`);
99
+ }
100
+ }
101
+ export async function assertUnsupportedLiveMcpSafetyRuntime(options) {
102
+ if (!options.mcpConfigPath || !isLiveMcpSafetyMode(options.mcpSafety))
103
+ return;
104
+ const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
105
+ if (!mcpServers)
106
+ return;
107
+ throw new Error(`${options.runtimeName} live MCP safety enforcement is not supported. Use mock mode or a runtime that enforces mcpSafety before live MCP mounting.`);
108
+ }
109
+ export async function assertStdioMcpServersStartForClaudeSdk(options) {
110
+ const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
111
+ if (!mcpServers)
112
+ return;
113
+ for (const [name, entry] of Object.entries(mcpServers)) {
114
+ if (!isStdioEntry(entry))
115
+ continue;
116
+ await assertStdioMcpServerStarts(options.workspacePath, name, entry, options.runtimeEnv);
117
+ }
118
+ }
119
+ export async function mountMcpForClaudeSdk(options) {
120
+ const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
121
+ if (!mcpServers)
122
+ return undefined;
123
+ return Object.fromEntries(Object.entries(mcpServers).map(([name, entry]) => [
124
+ name,
125
+ toClaudeSdkMcpServerEntry(entry),
126
+ ]));
127
+ }
128
+ export async function mountMcpForCodexAppServer(options) {
129
+ const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
130
+ if (!mcpServers)
131
+ return undefined;
132
+ return {
133
+ mcp_servers: Object.fromEntries(Object.entries(mcpServers).map(([name, entry]) => [
134
+ name,
135
+ toCodexAppServerMcpServerEntry(entry),
136
+ ])),
137
+ };
138
+ }
139
+ export async function mountMcpForCursor(options) {
140
+ if (!options.mcpConfigPath)
141
+ return { approveMcps: false };
142
+ await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
143
+ const srcMcp = path.join(options.workspacePath, options.mcpConfigPath);
144
+ if (!(await fs.pathExists(srcMcp)))
145
+ return { approveMcps: false };
146
+ const cursorDir = path.join(options.workspacePath, '.cursor');
147
+ await fs.ensureDir(cursorDir);
148
+ await fs.copy(srcMcp, path.join(cursorDir, 'mcp.json'), { overwrite: true });
149
+ return { approveMcps: true };
150
+ }
151
+ export async function assertMcpRuntimeMountingSupportedForCodexExec(options) {
152
+ if (!options.mcpConfigPath)
153
+ return;
154
+ throw new Error('Codex exec does not support MCP Runtime Mounting. Use the codex app-server transport for fixtures that stage MCP config.');
155
+ }
@@ -4,7 +4,7 @@ export interface SandboxConfig {
4
4
  skillDir?: string;
5
5
  copyFromHome?: string[];
6
6
  env?: Record<string, string>;
7
- mcp?: import('./mcp-config.js').McpSpec;
7
+ mcp?: import('./mcp-config.js').McpDeclaration;
8
8
  /**
9
9
  * Glob patterns to ignore when copying workspace and skill directories.
10
10
  * Replaces the default ignore list entirely. Pass `[]` to disable filtering.
@@ -29,7 +29,8 @@ export async function createSandbox(spec) {
29
29
  const skillName = path.basename(skillPath);
30
30
  const skillFilter = (src) => {
31
31
  const rel = path.relative(skillPath, src);
32
- if (rel === 'test' || rel.startsWith(`test${path.sep}`)) {
32
+ const [topLevelDir] = rel.split(path.sep);
33
+ if (topLevelDir === 'test' || topLevelDir === 'tests') {
33
34
  return false;
34
35
  }
35
36
  return copyFilter(src);
@@ -1,6 +1,6 @@
1
1
  import { type SandboxConfig } from './sandbox.js';
2
2
  import type { CommandResult } from '../types.js';
3
- export type { McpSpec } from './mcp-config.js';
3
+ export type { McpDeclaration } from './mcp-config.js';
4
4
  export interface Workspace {
5
5
  readonly path: string;
6
6
  readonly mcpConfigPath: string | undefined;
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import { createSandbox } from './sandbox.js';
5
- import { writeMcpConfig } from './mcp-config.js';
5
+ import { stageMcpConfig } from './mcp-config.js';
6
6
  import { sandboxExec } from './sandbox-exec.js';
7
7
  import { resolveCredentials } from './credentials.js';
8
8
  async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
@@ -38,7 +38,7 @@ export async function prepareWorkspace(spec) {
38
38
  Object.assign(sandboxEnv, creds.env);
39
39
  await copyPathsFromHostHome(creds.copyFromHome, homePath);
40
40
  await linkPathsFromHostHome(creds.linkFromHome ?? [], homePath);
41
- const { mcpConfigPath } = await writeMcpConfig(workspacePath, mcp);
41
+ const { mcpConfigPath } = await stageMcpConfig(workspacePath, mcp);
42
42
  let disposed = false;
43
43
  return {
44
44
  path: workspacePath,
package/dist/sdk/agent.js CHANGED
@@ -39,7 +39,8 @@ class AgentImpl {
39
39
  lastConversationResult = null;
40
40
  verbose;
41
41
  transport;
42
- constructor(ws, agentName, llm, timeoutSetting, conversationWindow, modelOpt, debugOpt, debugName, debugBaseDir, verbose, transport) {
42
+ mcpSafety;
43
+ constructor(ws, agentName, llm, timeoutSetting, conversationWindow, modelOpt, debugOpt, debugName, debugBaseDir, verbose, transport, mcpSafety) {
43
44
  this.ws = ws;
44
45
  this.agentName = agentName;
45
46
  this.llm = llm;
@@ -51,6 +52,7 @@ class AgentImpl {
51
52
  this.debugBaseDir = debugBaseDir;
52
53
  this.verbose = verbose;
53
54
  this.transport = transport;
55
+ this.mcpSafety = mcpSafety;
54
56
  }
55
57
  get messages() {
56
58
  return this._messages;
@@ -79,6 +81,7 @@ class AgentImpl {
79
81
  llm: this.llm,
80
82
  ...(askUserTimeoutMs !== undefined ? { askUserTimeoutMs } : {}),
81
83
  ...(this.transport !== undefined ? { transport: this.transport } : {}),
84
+ ...(this.mcpSafety !== undefined ? { mcpSafety: this.mcpSafety } : {}),
82
85
  });
83
86
  }
84
87
  resolveTimeoutSec(mode, maxTurns) {
@@ -345,11 +348,11 @@ export async function createAgent(opts) {
345
348
  const timeoutSetting = opts.timeout ?? 300;
346
349
  // Capture test context now, while vitest state is available
347
350
  const testCtx = opts.debug ? resolveTestContext() : { name: '', dir: '' };
348
- const { timeout: _, mcpMock, agent: __, debug: ___, model: ____, transport: _____, ...rest } = opts;
351
+ const { timeout: _, mcpMock, mcpConfigFile, agent: __, debug: ___, model: ____, transport: _____, mcpSafety: ______, ...rest } = opts;
349
352
  const workspace = await prepareWorkspace({
350
353
  ...rest,
351
354
  agent: agentName,
352
- mcp: mcpMock ? { mock: mcpMock } : undefined,
355
+ mcp: mcpConfigFile ? { configFile: mcpConfigFile } : mcpMock ? { mock: mcpMock } : undefined,
353
356
  });
354
357
  // Create agent LLM once, using the fully-resolved sandbox env (includes
355
358
  // keychain OAuth tokens, API keys, safe host vars).
@@ -363,7 +366,7 @@ export async function createAgent(opts) {
363
366
  sink: verboseSinkOverride ?? undefined,
364
367
  testName: testCtx.name || undefined,
365
368
  });
366
- const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport);
369
+ const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport, opts.mcpSafety);
367
370
  lifecycle.trackAgent(agent);
368
371
  return agent;
369
372
  }
@@ -2,6 +2,8 @@ export { createAgent } from './agent.js';
2
2
  export { resolveAgentName, resolveCodexTransport, InvalidTransportEnvError, } from './agent-resolution.js';
3
3
  export { AgentCrashError } from './agent-crash.js';
4
4
  export { check, score, judge, toolUsage } from './scorers.js';
5
+ export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, } from './mcp-evidence.js';
6
+ export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
5
7
  export { evaluate, EvalScorerError } from './evaluate.js';
6
8
  export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
7
9
  export { createPersona } from './persona.js';
@@ -16,7 +18,7 @@ export { toAskUserToolEvent } from './ask-bus/projection.js';
16
18
  export type { AskUserToolEvent, AskUserToolEventArguments, AskUserToolEventQuestionArgument, } from './ask-bus/projection.js';
17
19
  export { buildAskBatchLogEntries } from './agent-result-log.js';
18
20
  export { getAgentCapabilities } from './types.js';
19
- export type { AgentTransport, AgentCapabilities, AgentName } from './types.js';
21
+ export type { AgentTransport, AgentCapabilities, AgentName, McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './types.js';
20
22
  export type { AskBus, AskBatch, AskQuestion, AskOption, AskAnswer, AskResolution, AskBatchSnapshot, AskAnswerSnapshot, AskResolutionSnapshot, AskHandle, AskHandler, AskSource, AskLifecycle, AskAnswerSource, Unsubscribe as AskBusUnsubscribe, } from './ask-bus/types.js';
21
23
  export type { Agent, AgentOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, } from './types.js';
22
24
  export type { ConversationWindow, ConversationWindowOptions } from './conversation-window.js';
@@ -24,6 +26,9 @@ export type { JudgePipelineOptions } from './judge-pipeline.js';
24
26
  export type { RunScorerOptions } from './run-scorer.js';
25
27
  export type { OnScorerErrorMode } from './evaluate.js';
26
28
  export type { RunSnapshot } from './snapshots.js';
29
+ export type { ExpectedMcpStartupStatus, ExpectedMcpToolCall, McpStartupStatusEvidence, McpToolCallEvidence, } from './mcp-evidence.js';
30
+ export type { McpPolicyDenialReason, McpToolCallRequest, McpToolPolicyDecision, } from './mcp-safety.js';
31
+ export type { ToolEvent } from '../tool-events.js';
27
32
  export type { LLMPort, EvalRuntime } from './eval-runtime.js';
28
33
  export { createLLMClient, ProviderNotSupportedError } from '../utils/llm.js';
29
34
  export type { CreateLLMClientOptions, LLMProviderAdapter, TokenUsage as LLMTokenUsage } from '../utils/llm.js';
package/dist/sdk/index.js CHANGED
@@ -3,6 +3,8 @@ export { createAgent } from './agent.js';
3
3
  export { resolveAgentName, resolveCodexTransport, InvalidTransportEnvError, } from './agent-resolution.js';
4
4
  export { AgentCrashError } from './agent-crash.js';
5
5
  export { check, score, judge, toolUsage } from './scorers.js';
6
+ export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, } from './mcp-evidence.js';
7
+ export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
6
8
  export { evaluate, EvalScorerError } from './evaluate.js';
7
9
  export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
8
10
  export { createPersona } from './persona.js';
@@ -1,6 +1,7 @@
1
1
  import type { AgentTurnResult, LogEntry } from '../types.js';
2
2
  import type { Workspace } from '../providers/workspace.js';
3
3
  import type { AgentName, AgentTransport, Message } from './types.js';
4
+ import type { McpSafetyOptions } from './mcp-safety.js';
4
5
  import type { LLMPort } from '../utils/llm-types.js';
5
6
  import type { AskBus } from './ask-bus/types.js';
6
7
  export interface ManagedSessionDeps {
@@ -28,6 +29,8 @@ export interface ManagedSessionDeps {
28
29
  * driver capabilities fall back to the default (`exec` semantics).
29
30
  */
30
31
  transport?: AgentTransport;
32
+ /** Live MCP Server Safety policy to enforce in supported harnesses. */
33
+ mcpSafety?: McpSafetyOptions;
31
34
  }
32
35
  export interface ManagedSession {
33
36
  /** Full lifecycle: log start/result, push messages, check exit code. */
@@ -23,6 +23,8 @@ export function createManagedSession(deps) {
23
23
  ...(llm ? { llm } : {}),
24
24
  askBus,
25
25
  ...(transport !== undefined ? { transport } : {}),
26
+ ...(deps.mcpSafety !== undefined ? { mcpSafety: deps.mcpSafety } : {}),
27
+ getAbortSignal: () => currentSignal,
26
28
  };
27
29
  let session = null;
28
30
  let setupDone = false;