Skip to content

Commit eba0474

Browse files
committed
Add session-aware opt-out
1 parent 1bdaaf5 commit eba0474

3 files changed

Lines changed: 75 additions & 15 deletions

File tree

src/utils/__tests__/session-aware-tool-factory.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,26 @@ describe('createSessionAwareTool', () => {
9898
expect(result.content[0].text).toContain('Provide a project or workspace');
9999
});
100100

101+
it('uses opt-out messaging when session defaults schema is disabled', async () => {
102+
const original = process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS;
103+
process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS = 'true';
104+
105+
try {
106+
const result = await handler({ projectPath: '/p.xcodeproj', simulatorId: 'SIM-1' });
107+
expect(result.isError).toBe(true);
108+
const text = result.content[0].text;
109+
expect(text).toContain('Missing required parameters');
110+
expect(text).toContain('scheme is required');
111+
expect(text).not.toContain('session defaults');
112+
} finally {
113+
if (original === undefined) {
114+
delete process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS;
115+
} else {
116+
process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS = original;
117+
}
118+
}
119+
});
120+
101121
it('should surface Zod validation errors with tip when invalid', async () => {
102122
const badHandler = createSessionAwareTool<any>({
103123
internalSchema,

src/utils/environment.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ export function getDefaultEnvironmentDetector(): EnvironmentDetector {
6767
return defaultEnvironmentDetector;
6868
}
6969

70+
/**
71+
* Global opt-out for session defaults in MCP tool schemas.
72+
* When enabled, tools re-expose all parameters instead of hiding session-managed fields.
73+
*/
74+
export function isSessionDefaultsSchemaOptOutEnabled(): boolean {
75+
const raw = process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS;
76+
if (!raw) return false;
77+
78+
const normalized = raw.trim().toLowerCase();
79+
return ['1', 'true', 'yes', 'on'].includes(normalized);
80+
}
81+
7082
/**
7183
* Normalizes a set of user-provided environment variables by ensuring they are
7284
* prefixed with TEST_RUNNER_. Variables already prefixed are preserved.

src/utils/typed-tool-factory.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ToolResponse } from '../types/common.ts';
1414
import type { CommandExecutor } from './execution/index.ts';
1515
import { createErrorResponse } from './responses/index.ts';
1616
import { sessionStore, type SessionDefaults } from './session-store.ts';
17+
import { isSessionDefaultsSchemaOptOutEnabled } from './environment.ts';
1718

1819
/**
1920
* Creates a type-safe tool handler that validates parameters at runtime
@@ -71,6 +72,27 @@ function missingFromMerged(
7172
return keys.filter((k) => merged[k] == null);
7273
}
7374

75+
function formatRequirementError(opts: {
76+
message: string;
77+
setHint?: string;
78+
optOutEnabled: boolean;
79+
}) {
80+
const title = opts.optOutEnabled
81+
? 'Missing required parameters'
82+
: 'Missing required session defaults';
83+
const body = opts.optOutEnabled
84+
? opts.message
85+
: [opts.message, opts.setHint].filter(Boolean).join('\n');
86+
return { title, body };
87+
}
88+
89+
export function getSessionAwareToolSchemaShape<
90+
TSession extends z.ZodRawShape,
91+
TLegacy extends z.ZodRawShape,
92+
>(opts: { sessionAware: z.ZodObject<TSession>; legacy: z.ZodObject<TLegacy> }): z.ZodRawShape {
93+
return isSessionDefaultsSchemaOptOutEnabled() ? opts.legacy.shape : opts.sessionAware.shape;
94+
}
95+
7496
export function createSessionAwareTool<TParams>(opts: {
7597
internalSchema: z.ZodType<TParams>;
7698
logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>;
@@ -132,13 +154,15 @@ export function createSessionAwareTool<TParams>(opts: {
132154
if ('allOf' in req) {
133155
const missing = missingFromMerged(req.allOf, merged);
134156
if (missing.length > 0) {
135-
return createErrorResponse(
136-
'Missing required session defaults',
137-
`${req.message ?? `Required: ${req.allOf.join(', ')}`}\n` +
138-
`Set with: session-set-defaults { ${missing
139-
.map((k) => `"${k}": "..."`)
140-
.join(', ')} }`,
141-
);
157+
const setHint = `Set with: session-set-defaults { ${missing
158+
.map((k) => `"${k}": "..."`)
159+
.join(', ')} }`;
160+
const { title, body } = formatRequirementError({
161+
message: req.message ?? `Required: ${req.allOf.join(', ')}`,
162+
setHint,
163+
optOutEnabled: isSessionDefaultsSchemaOptOutEnabled(),
164+
});
165+
return createErrorResponse(title, body);
142166
}
143167
} else if ('oneOf' in req) {
144168
const satisfied = req.oneOf.some((k) => merged[k] != null);
@@ -147,10 +171,12 @@ export function createSessionAwareTool<TParams>(opts: {
147171
const setHints = req.oneOf
148172
.map((k) => `session-set-defaults { "${k}": "..." }`)
149173
.join(' OR ');
150-
return createErrorResponse(
151-
'Missing required session defaults',
152-
`${req.message ?? `Provide one of: ${options}`}\nSet with: ${setHints}`,
153-
);
174+
const { title, body } = formatRequirementError({
175+
message: req.message ?? `Provide one of: ${options}`,
176+
setHint: `Set with: ${setHints}`,
177+
optOutEnabled: isSessionDefaultsSchemaOptOutEnabled(),
178+
});
179+
return createErrorResponse(title, body);
154180
}
155181
}
156182
}
@@ -164,10 +190,12 @@ export function createSessionAwareTool<TParams>(opts: {
164190
return `${path}: ${e.message}`;
165191
});
166192

167-
return createErrorResponse(
168-
'Parameter validation failed',
169-
`Invalid parameters:\n${errorMessages.join('\n')}\nTip: set session defaults via session-set-defaults`,
170-
);
193+
const tip = isSessionDefaultsSchemaOptOutEnabled()
194+
? ''
195+
: '\nTip: set session defaults via session-set-defaults';
196+
const details = `Invalid parameters:\n${errorMessages.join('\n')}${tip}`;
197+
198+
return createErrorResponse('Parameter validation failed', details);
171199
}
172200
throw error;
173201
}

0 commit comments

Comments
 (0)