Research Date: 2025-10-13 Purpose: Understand session management architecture and patterns to inform test_sim tool integration
Lines 1-48: Singleton in-memory store for session defaults
Key Features:
-
Type Definition (lines 3-13):
SessionDefaultsincludes:projectPath,workspacePath(mutually exclusive project identifiers)scheme,configuration(build settings)simulatorName,simulatorId(mutually exclusive simulator identifiers)deviceId(device identifier)useLatestOS(simulator OS selection)arch(architecture: 'arm64' | 'x86_64')
-
Storage Pattern (lines 15-45):
setDefaults(partial): Merges new defaults into existing ones (line 18-21)clear(keys?): Clears all or specific keys (lines 23-36)get<K>(key): Retrieves single default value (lines 38-40)getAll(): Returns copy of all defaults (lines 42-44)
Critical Detail: Line 47 exports singleton instance sessionStore
Lines 63-174: createSessionAwareTool<TParams> function
Key Features:
-
Requirement Types (lines 63-65):
allOf: All specified keys must be presentoneOf: At least one specified key must be present- Optional custom error messages
-
Argument Sanitization (lines 92-96):
- Treats
nullandundefinedas "not provided" - Only includes explicitly provided values in sanitizedArgs
- Critical: Empty object
{}from client means "use all session defaults"
- Treats
-
Factory-Level Mutual Exclusivity Check (lines 98-110):
- Checks
exclusivePairsBEFORE merging session defaults - Rejects if user provides multiple values from an exclusive pair
- Example:
{ projectPath: '/a', workspacePath: '/b' }→ error
- Checks
-
Session Defaults Merge (line 113):
merged = { ...sessionStore.getAll(), ...sanitizedArgs }- Explicit args override session defaults
-
Exclusive Pair Pruning (lines 115-128):
- Only when user provides a concrete value
- Drops conflicting session defaults from the pair
- Example: If session has
simulatorNamebut user providessimulatorId, dropsimulatorNamefrom merged
-
Requirements Validation (lines 130-155):
allOf: All keys must be in merged (line 132-141)oneOf: At least one key must be in merged (line 142-154)- Returns friendly error messages with session-set-defaults hints
-
Schema Validation (line 157):
- Validates merged result against internal schema
- All XOR constraints enforced by schema's
.refine()calls
-
Error Handling (lines 159-173):
- Zod errors formatted with helpful tips
- Suggests using session-set-defaults tool
Current State: Does NOT use session-aware factory
Schema Structure:
- Base Schema (lines 19-68): All fields including session-manageable ones
- With XOR Validation (lines 74-84):
- Line 75-76: Requires at least one of projectPath/workspacePath
- Line 78-80: Rejects both projectPath AND workspacePath
- Line 81-84: Platform validation (rejects macOS)
Handler Pattern (lines 135-162):
- Uses manual try-catch with inline validation
- Validates with
testSimulatorSchema.parse(args) - Formats Zod errors manually
- Calls
test_simLogic(validatedParams, executor)
Key Observations:
- No session defaults integration
- Manual error formatting duplicates factory logic
- XOR constraints in schema but not in factory-level checks
- Schema expects both XOR sides to be optional but requires one
Pattern: Uses createTypedTool (NOT session-aware)
Handler (lines 324-330):
handler: createTypedTool<TestMacosParams>(
testMacosSchema as z.ZodType<TestMacosParams>,
(params: TestMacosParams) => {
return testMacosLogic(params, getDefaultCommandExecutor(), getDefaultFileSystemExecutor());
},
getDefaultCommandExecutor,
)Key Observations:
- Uses standard typed tool factory
- No session defaults support
- XOR validation only in schema (lines 52-58)
Pattern: Uses createTypedTool (NOT session-aware)
Handler (lines 281-294):
- Similar to test_macos
- No session defaults integration
- XOR validation in schema only (lines 51-57)
Lines 168-186: Complete session-aware implementation
Key Pattern Elements:
-
Dual Schema Structure:
- Internal Schema (lines 56-82): Full validation with XOR constraints
- Public Schema (lines 157-166): Omits session-managed fields
-
Public Schema Creation (lines 157-166):
const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
simulatorId: true,
simulatorName: true,
useLatestOS: true,
// platform is NOT omitted - it's available for clients to specify
} as const);- Handler Configuration (lines 172-185):
handler: createSessionAwareTool<BuildSimulatorParams>({
internalSchema: buildSimulatorSchema as unknown as z.ZodType<BuildSimulatorParams>,
logicFunction: build_simLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [
{ allOf: ['scheme'], message: 'scheme is required' },
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
{ oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
],
exclusivePairs: [
['projectPath', 'workspacePath'],
['simulatorId', 'simulatorName'],
],
})Key Observations:
- Internal schema has XOR constraints via
.refine() - Factory requirements declare what's needed (allOf/oneOf)
- Factory exclusivePairs enable session pruning
- Public schema exposes only non-session fields
- Logic function unchanged - receives fully validated params
Lines 522-540: Identical pattern to build_sim
Observations:
- Same dual schema approach
- Same requirements and exclusivePairs
- Demonstrates consistent pattern across tools
Step 1: Base Schema Object
const baseSchemaObject = z.object({
projectPath: z.string().optional().describe('...'),
workspacePath: z.string().optional().describe('...'),
// ... other fields
});Step 2: Preprocessor Application
const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);Purpose: Converts empty strings to undefined for cleaner optional field handling
Step 3: XOR Constraint Addition
const toolSchema = baseSchema
.refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
message: 'Either projectPath or workspacePath is required.',
})
.refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
});Step 4: Type Inference
export type ToolParams = z.infer<typeof toolSchema>;Step 5: Public Schema (Session-Aware Only)
const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
// ... session-managed fields
} as const);Two-Step Validation:
- At least one required:
val.projectPath !== undefined || val.workspacePath !== undefined - Not both:
!(val.projectPath !== undefined && val.workspacePath !== undefined)
Why Two Refines?:
- First ensures at least one is provided
- Second ensures they're mutually exclusive
- Together create true XOR behavior
Purpose: Enable session defaults and provide friendly errors
Factory Checks (createSessionAwareTool):
- Sanitization: Null/undefined treated as "not provided"
- Factory-Level XOR Check: Rejects multiple explicit values from exclusivePairs
- Session Merge: Combines session + explicit args (explicit wins)
- Session Pruning: Removes conflicting session defaults per exclusivePairs
- Requirements Check: Validates allOf/oneOf on merged data
- Then passes to schema validation
Purpose: Enforce type safety and business rules
Schema Validation:
- Validates merged data against internal schema
- Enforces XOR via
.refine()calls - Ensures all type constraints
- Final safety net
Factory Level:
- User-Friendly: "Missing required session defaults" vs raw Zod error
- Session Logic: Handles merge, pruning, precedence
- XOR Enforcement: Prevents explicit conflicts BEFORE merge
Schema Level:
- Type Safety: Ensures final data structure correctness
- Business Rules: All domain constraints enforced
- Safety Net: Catches any factory logic bugs
Input: { simulatorId: 'ABC' }
Session Defaults: { scheme: 'App', projectPath: '/x', simulatorName: 'iPhone 16' }
Factory Processing:
- Sanitize:
simulatorIdis concrete - Factory XOR: Only one value from
[simulatorId, simulatorName]→ OK - Merge:
{ scheme: 'App', projectPath: '/x', simulatorId: 'ABC', simulatorName: 'iPhone 16' } - Prune: User provided
simulatorId, so dropsimulatorName→{ scheme: 'App', projectPath: '/x', simulatorId: 'ABC' } - Requirements: Check allOf/oneOf → OK
- Pass to schema
Schema Validation:
- XOR refinement: Only one of
simulatorId/simulatorName→ OK - Type checks: All fields valid → OK
- Return validated params to logic
Result: Logic receives { scheme: 'App', projectPath: '/x', simulatorId: 'ABC' }
Lines 46-56: Basic merge behavior
- Sets session defaults
- Calls handler with empty object
- Verifies logic receives merged params
Lines 58-85: Explicit args override session
- Session has one value
- Args provide different value
- Verifies arg wins
Lines 87-92: allOf requirement validation
- Missing required field
- Returns friendly error message
Lines 94-99: oneOf requirement validation
- None of the options provided
- Returns friendly error with session-set-defaults hint
Lines 101-111: Zod error formatting
- Invalid type provided
- Formats error with "Tip: set session defaults"
Lines 113-134: Session pruning with null
- User provides
nullfor conflicting field - Session default NOT pruned (null = not provided)
Lines 136-157: Session pruning with undefined
- User provides
undefinedfor conflicting field - Session default NOT pruned (undefined = not provided)
Lines 159-189: Factory-level XOR check
- User provides both sides of exclusive pair
- Factory rejects BEFORE schema validation
- Error: "Mutually exclusive parameters provided"
- Sanitization Works:
nullandundefineddon't trigger pruning - Factory XOR Enforced: Multiple explicit values rejected early
- Session Pruning Logic: Only concrete user values trigger pruning
- Friendly Errors: All error paths provide helpful messages
- Schema Still Validates: Factory doesn't bypass schema checks
Current test_sim (src/mcp/tools/simulator/test_sim.ts):
- Schema (lines 74-84): XOR constraints via
.refine() - Handler (lines 135-162): Manual validation, no session support
- Public Schema (line 134):
baseSchemaObject.shape- exposes ALL fields
To match build_sim pattern:
-
Keep Internal Schema (lines 74-84):
- XOR constraints stay
- Used for final validation
- No changes needed
-
Add Public Schema:
- Omit session-managed fields
- Expose only per-call configuration
-
Replace Handler (lines 135-162):
- Use
createSessionAwareTool - Define requirements (allOf/oneOf)
- Define exclusivePairs
- Use
-
Result:
- Factory handles session merge + pruning + requirements
- Schema validates final merged data
- Logic function unchanged
Factory exclusivePairs:
- Prevent user from providing both explicit values
- Enable session default pruning
Schema refines:
- Validate final merged data structure
- Ensure business rules after merge
- Safety net for factory bugs
Both Are Needed:
- Factory: User-facing validation + session logic
- Schema: Type safety + final correctness
From Grep Results (src/mcp/tools/):
simulator/build_sim.ts(lines 168-186)simulator/build_run_sim.ts(lines 522-540)
Based on schema-helpers usage, these tools likely follow similar patterns:
simulator/stop_app_sim.tssimulator/launch_app_sim.tssimulator/get_sim_app_path.tsmacos/build_macos.tsmacos/build_run_macos.tsmacos/get_mac_app_path.tsdevice/build_device.tsdevice/get_device_app_path.ts
project-discovery/show_build_settings.tsproject-discovery/list_schemes.ts
utilities/clean.ts
All session-aware tools follow the same pattern:
- Internal schema with XOR constraints
- Public schema omitting session fields
- Handler using
createSessionAwareTool - Requirements and exclusivePairs defined
- Logic function unchanged
-
Tool Test (
src/mcp/tools/simulator/__tests__/test_sim.test.ts):- Add session defaults test cases
- Test requirement validation errors
- Test exclusivePairs behavior
- Ensure logic tests unchanged (using direct logic function calls)
-
Integration Pattern:
- Follow test patterns from
build_sim.__tests__/build_sim.test.ts - Test session merge behavior
- Test public schema (omitted fields not visible)
- Follow test patterns from
Critical: NO Vitest mocking allowed
Pattern:
import { test_simLogic } from '../test_sim.ts';
import { createMockExecutor } from '../../../test-utils/mock-executors.ts';
it('should use session defaults', async () => {
sessionStore.setDefaults({
scheme: 'Test',
projectPath: '/path/proj.xcodeproj',
simulatorId: 'SIM-123'
});
const mockExecutor = createMockExecutor({
success: true,
output: 'TEST SUCCEEDED'
});
// Test handler (includes session merge)
const result = await toolHandler({});
// Or test logic directly (bypass session, test pure logic)
const params = { scheme: 'Test', projectPath: '/x', simulatorId: 'S' };
const logicResult = await test_simLogic(params, mockExecutor);
});File: src/mcp/tools/simulator/test_sim.ts
Changes:
- After line 87 (type definition), add public schema:
// Public schema = internal minus session-managed fields
const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
simulatorId: true,
simulatorName: true,
useLatestOS: true,
// platform is NOT omitted - it's available for clients to specify
// testRunnerEnv is NOT omitted - per-test configuration
} as const);- Replace handler (lines 135-162) with:
export default {
name: 'test_sim',
description: 'Runs tests on a simulator.',
schema: publicSchemaObject.shape, // Public schema for MCP clients
handler: createSessionAwareTool<TestSimulatorParams>({
internalSchema: testSimulatorSchema as unknown as z.ZodType<TestSimulatorParams>,
logicFunction: test_simLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [
{ allOf: ['scheme'], message: 'scheme is required' },
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
{ oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
],
exclusivePairs: [
['projectPath', 'workspacePath'],
['simulatorId', 'simulatorName'],
],
}),
};- Add import:
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';File: src/mcp/tools/simulator/__tests__/test_sim.test.ts
Add Test Cases:
- Session defaults merge test
- Explicit args override session test
- Requirements validation tests
- ExclusivePairs behavior tests
- Public schema validation (omitted fields)
Pattern: Follow build_sim.__tests__/build_sim.test.ts structure
Checklist:
-
npm run typecheckpasses -
npm run lintpasses -
npm run testpasses - Manual test with mcpli/reloaderoo
- Description follows Tool Description Policy (concise)
-
Separation of Concerns:
- Factory: Session management, user-friendly errors
- Schema: Type safety, business rules
- Logic: Pure business logic, unchanged
-
Progressive Enhancement:
- Tools work without session defaults (explicit args)
- Session defaults reduce repetition
- Both modes supported seamlessly
-
Type Safety Maintained:
- Factory validates requirements before schema
- Schema validates final merged data
- Logic receives guaranteed-valid params
-
Testing Strategy:
- Test logic functions directly with mock executors
- Test factory behavior with integration tests
- No Vitest mocking - dependency injection only
-
Don't Remove Schema XOR Constraints:
- Factory exclusivePairs ≠ schema refinements
- Both serve different purposes
- Both are required
-
Don't Change Logic Signatures:
- Logic functions stay unchanged
- Only handlers change to session-aware
- Tests of logic remain valid
-
Don't Omit All Optional Fields from Public Schema:
- Only session-managed fields omitted
- Per-call configuration (like
platform) stays public - Test-specific fields (like
testRunnerEnv) stay public
-
Don't Forget Preprocessing:
z.preprocess(nullifyEmptyStrings, ...)stays- Handles empty string → undefined conversion
- Critical for optional field behavior
- SessionStore:
/src/utils/session-store.ts(lines 1-48) - Factory:
/src/utils/typed-tool-factory.ts(lines 63-174) - Schema Helpers:
/src/utils/schema-helpers.ts(lines 1-25)
- Set Defaults:
/src/mcp/tools/session-management/session_set_defaults.ts - Clear Defaults:
/src/mcp/tools/session-management/session_clear_defaults.ts - Show Defaults:
/src/mcp/tools/session-management/session_show_defaults.ts
- build_sim:
/src/mcp/tools/simulator/build_sim.ts(lines 168-186) - build_run_sim:
/src/mcp/tools/simulator/build_run_sim.ts(lines 522-540)
- test_sim:
/src/mcp/tools/simulator/test_sim.ts(lines 1-164) - Not session-aware - test_macos:
/src/mcp/tools/macos/test_macos.ts(lines 1-332) - Not session-aware - test_device:
/src/mcp/tools/device/test_device.ts(lines 1-296) - Not session-aware
- Factory Tests:
/src/utils/__tests__/session-aware-tool-factory.test.ts(lines 1-191) - SessionStore Tests:
/src/utils/__tests__/session-store.test.ts - build_sim Tests:
/src/mcp/tools/simulator/__tests__/build_sim.test.ts
- Session Plan:
/docs/session_management_plan.md(lines 1-485) - Testing Guide:
/docs/TESTING.md - Architecture:
/docs/ARCHITECTURE.md
The session management system is a well-designed middleware layer that:
- Preserves existing logic - no changes to business logic functions
- Provides friendly UX - clear error messages with actionable hints
- Maintains type safety - factory AND schema validation
- Enables progressive enhancement - works with or without session defaults
The test_sim tool can be migrated by:
- Adding a public schema that omits session fields
- Replacing the handler with createSessionAwareTool
- Defining requirements and exclusivePairs
- Keeping the internal schema and logic unchanged
This pattern is proven by build_sim and build_run_sim implementations, and follows the architectural principles documented in the session management plan.