Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/SESSION_DEFAULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,49 @@ The persistence is patch-only: only keys provided in that call are written (plus

You can also manually create the config file to essentially seed the defaults at startup; see [CONFIGURATION.md](CONFIGURATION.md) for more information.

## Namespaced profiles
Session defaults support named profiles so one workspace can keep separate defaults for iOS/watchOS/macOS (or any custom profile names).

- Use `session_use_defaults_profile` to switch the active profile (existing profiles only).
- Existing tools (`session_set_defaults`, `session_show_defaults`, build/test tools) use the active profile automatically.
- `session_set_defaults` can also accept `profile` to switch and set in one call; use `createIfNotExists: true` to create a new profile intentionally.
- Profiles are strictly isolated: values are not inherited from global defaults or other profiles.
- The unnamed global defaults profile exists for backwards compatibility and is the default active profile when no named profile is selected.
- There is always an active profile context: either a named profile or `global`.
- Use `global: true` to switch back to the unnamed global profile.
- Set `persist: true` on `session_use_defaults_profile` to write `activeSessionDefaultsProfile` in `.xcodebuildmcp/config.yaml`.

## Recommended startup flow (monorepo / multi-target)
Copy/paste this sequence when starting a new session:

```json
{"name":"session_use_defaults_profile","arguments":{"profile":"ios","persist":true}}
{"name":"session_set_defaults","arguments":{
"workspacePath":"/repo/MyApp.xcworkspace",
"scheme":"MyApp-iOS",
"simulatorName":"iPhone 16 Pro",
"persist":true
}}
{"name":"session_show_defaults","arguments":{}}
```

Switch targets later in the same session:

```json
{"name":"session_use_defaults_profile","arguments":{"profile":"watch","persist":true}}
{"name":"session_set_defaults","arguments":{
"workspacePath":"/repo/MyApp.xcworkspace",
"scheme":"MyApp-watchOS",
"simulatorName":"Apple Watch Series 10 (45mm)",
"persist":true
}}
```

Isolation example:
- Global profile has `workspacePath: /repo/MyApp.xcworkspace`
- Active profile is `watch` with only `scheme` set
- Result: `watch` does not see global `workspacePath` until you set it on `watch` or switch back to `global`

## Related docs
- Configuration options: [CONFIGURATION.md](CONFIGURATION.md)
- Tools reference: [TOOLS.md](TOOLS.md)
2 changes: 1 addition & 1 deletion docs/TOOLS-CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,4 @@ XcodeBuildMCP provides 73 canonical tools organized into 13 workflow groups.

---

*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-17T14:53:05.834Z UTC*
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-17T21:48:36.993Z UTC*
17 changes: 9 additions & 8 deletions docs/TOOLS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# XcodeBuildMCP MCP Tools Reference

This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP provides 78 canonical tools organized into 15 workflow groups for comprehensive Apple development workflows.
This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP provides 79 canonical tools organized into 15 workflow groups for comprehensive Apple development workflows.

## Workflow Groups

Expand Down Expand Up @@ -126,11 +126,12 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov


### Session Management (`session-management`)
**Purpose**: Manage session defaults for project/workspace paths, scheme, configuration, simulator/device settings. (4 tools)
**Purpose**: Manage session defaults for project/workspace paths, scheme, configuration, simulator/device settings. (5 tools)

- `session_clear_defaults` - Clear session defaults.
- `session_set_defaults` - Set the session defaults, should be called at least once to set tool defaults.
- `session_show_defaults` - Show session defaults.
- `session_clear_defaults` - Clear session defaults for the active profile or a specified profile.
- `session_set_defaults` - Set session defaults for the active profile, or for a specified profile and make it active.
- `session_show_defaults` - Show the current active defaults.
- `session_use_defaults_profile` - Switch the active session defaults profile.
- `sync_xcode_defaults` - Sync session defaults (scheme, simulator) from Xcode's current IDE selection.


Expand Down Expand Up @@ -198,10 +199,10 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov

## Summary Statistics

- **Canonical Tools**: 78
- **Total Tools**: 102
- **Canonical Tools**: 79
- **Total Tools**: 103
- **Workflow Groups**: 15

---

*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-17T14:53:05.834Z UTC*
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-17T21:48:36.993Z UTC*
21 changes: 21 additions & 0 deletions example_projects/.xcodebuildmcp/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
schemaVersion: 1
sessionDefaultsProfiles:
calculator:
workspacePath: ./iOS_Calculator/CalculatorApp.xcworkspace
scheme: CalculatorApp
simulatorName: iPhone 17 Pro
simulatorId: B38FE93D-578B-454B-BE9A-C6FA0CE5F096
simulatorPlatform: iOS Simulator
ios-test:
projectPath: ./iOS/MCPTest.xcodeproj
scheme: MCPTest
simulatorName: iPhone 17 Pro
simulatorId: B38FE93D-578B-454B-BE9A-C6FA0CE5F096
simulatorPlatform: iOS Simulator
macos-test:
projectPath: ./macOS/MCPTest.xcodeproj
scheme: MCPTest
spm:
projectPath: ./spm
scheme: spm
activeSessionDefaultsProfile: spm
2 changes: 1 addition & 1 deletion manifests/tools/session_clear_defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module: mcp/tools/session-management/session_clear_defaults
names:
mcp: session_clear_defaults
cli: clear-defaults
description: Clear session defaults.
description: Clear session defaults for the active profile or a specified profile.
annotations:
title: Clear Session Defaults
destructiveHint: true
2 changes: 1 addition & 1 deletion manifests/tools/session_set_defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module: mcp/tools/session-management/session_set_defaults
names:
mcp: session_set_defaults
cli: set-defaults
description: Set the session defaults, should be called at least once to set tool defaults.
description: Set session defaults for the active profile, or for a specified profile and make it active.
annotations:
title: Set Session Defaults
destructiveHint: true
2 changes: 1 addition & 1 deletion manifests/tools/session_show_defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module: mcp/tools/session-management/session_show_defaults
names:
mcp: session_show_defaults
cli: show-defaults
description: Show session defaults.
description: Show the current active defaults.
annotations:
title: Show Session Defaults
readOnlyHint: true
9 changes: 9 additions & 0 deletions manifests/tools/session_use_defaults_profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
id: session_use_defaults_profile
module: mcp/tools/session-management/session_use_defaults_profile
names:
mcp: session_use_defaults_profile
cli: use-defaults-profile
description: Switch the active session defaults profile.
annotations:
title: Use Session Defaults Profile
readOnlyHint: false
1 change: 1 addition & 0 deletions manifests/workflows/session-management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ selection:
autoInclude: true
tools:
- session_show_defaults
- session_use_defaults_profile
- session_set_defaults
- session_clear_defaults
- sync_xcode_defaults
12 changes: 7 additions & 5 deletions skills/xcodebuildmcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ If a capability is missing, assume your tool list may be hiding tools (search/pr

### Session defaults

Before you call any other tools, you **must** call `session_show_defaults` to show the current defaults, ensure you then fill in the appropriate missing defaults. You may need to call one or more discovery/list tools to obtain the values needed.
Before you call any other tools, you **must** call `session_show_defaults` to show the current defaults, then fill in any missing defaults. You may need discovery/list tools first to obtain valid values.

- `session_set_defaults`
- Set the session defaults, should be called at least once to set tool defaults.
- `session_show_defaults`
- Show session defaults.
- Show the current active defaults (including the active profile name).
- `session_set_defaults`
- Set defaults for the current active profile, or set defaults for a specific profile via `profile`.
- `session_use_defaults_profile`
- Switch the active defaults profile.
- `session_clear_defaults`
- Clear session defaults.
- Clear defaults (current active profile by default, or a specific profile when provided).

### Project discovery

Expand Down
2 changes: 1 addition & 1 deletion src/cli/yargs-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function buildYargsApp(opts: YargsAppOptions): ReturnType<typeof yargs> {
setLogLevel(level);
}
})
.version(version)
.version(String(version))
.help()
.alias('h', 'help')
.alias('v', 'version')
Expand Down
1 change: 1 addition & 0 deletions src/core/manifest/__tests__/load-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('load-manifest', () => {
expect(manifest.tools.has('build_sim')).toBe(true);
expect(manifest.tools.has('discover_projs')).toBe(true);
expect(manifest.tools.has('session_show_defaults')).toBe(true);
expect(manifest.tools.has('session_use_defaults_profile')).toBe(true);
});

it('should validate tool references in workflows', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,9 @@
return { success: result.success, output: result.output };
});
const xcodeAvailable = Boolean(
xcodeVersion.version ||

Check warning on line 224 in src/daemon.ts

View workflow job for this annotation

GitHub Actions / build-and-test (24.x)

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator
xcodeVersion.buildVersion ||

Check warning on line 225 in src/daemon.ts

View workflow job for this annotation

GitHub Actions / build-and-test (24.x)

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator
xcodeVersion.developerDir ||

Check warning on line 226 in src/daemon.ts

View workflow job for this annotation

GitHub Actions / build-and-test (24.x)

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator
xcodeVersion.xcodebuildPath,
);
const axeVersion = await getAxeVersionMetadata(async (command) => {
Expand Down Expand Up @@ -392,7 +392,7 @@
pid: process.pid,
startedAt,
enabledWorkflows: daemonWorkflows,
version,
version: String(version),
});

writeLine(`Daemon started (PID: ${process.pid})`);
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/tools/doctor/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export async function runDoctor(
);

const doctorInfo = {
serverVersion: version,
serverVersion: String(version),
timestamp: new Date().toISOString(),
system: systemInfo,
node: nodeInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,62 @@ describe('session-clear-defaults tool', () => {
expect(current.arch).toBe('arm64');
});

it('should clear all when all=true', async () => {
it('should clear all profiles only when all=true', async () => {
sessionStore.setActiveProfile('ios');
sessionStore.setDefaults({ scheme: 'IOS' });
sessionStore.setActiveProfile(null);
const result = await sessionClearDefaultsLogic({ all: true });
expect(result.isError).toBe(false);
expect(result.content[0].text).toBe('Session defaults cleared');
expect(result.content[0].text).toBe('All session defaults cleared');

const current = sessionStore.getAll();
expect(Object.keys(current).length).toBe(0);
expect(sessionStore.listProfiles()).toEqual([]);
expect(sessionStore.getActiveProfile()).toBeNull();
});

it('should clear all when no params provided', async () => {
it('should clear only active profile when no params provided', async () => {
sessionStore.setActiveProfile('ios');
sessionStore.setDefaults({ scheme: 'IOS', projectPath: '/ios/project.xcodeproj' });
sessionStore.setActiveProfile(null);
sessionStore.setDefaults({ scheme: 'Global' });
sessionStore.setActiveProfile('ios');

const result = await sessionClearDefaultsLogic({});
expect(result.isError).toBe(false);
const current = sessionStore.getAll();
expect(Object.keys(current).length).toBe(0);

expect(sessionStore.getAll()).toEqual({});
expect(sessionStore.listProfiles()).toEqual([]);

sessionStore.setActiveProfile(null);
expect(sessionStore.getAll().scheme).toBe('Global');
});

it('should clear a specific profile when profile is provided', async () => {
sessionStore.setActiveProfile('ios');
sessionStore.setDefaults({ scheme: 'IOS' });
sessionStore.setActiveProfile('watch');
sessionStore.setDefaults({ scheme: 'Watch' });
sessionStore.setActiveProfile('watch');

const result = await sessionClearDefaultsLogic({ profile: 'ios' });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('profile "ios"');

expect(sessionStore.listProfiles()).toEqual(['watch']);
expect(sessionStore.getAll().scheme).toBe('Watch');
});

it('should error when the specified profile does not exist', async () => {
const result = await sessionClearDefaultsLogic({ profile: 'missing' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('does not exist');
});

it('should reject all=true when combined with scoped arguments', async () => {
const result = await sessionClearDefaultsLogic({ all: true, profile: 'ios' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('cannot be combined');
});

it('should validate keys enum', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,106 @@ describe('session-set-defaults tool', () => {
expect(parsed.sessionDefaults?.simulatorName).toBeUndefined();
});

it('sets defaults on existing named profile and activates it', async () => {
sessionStore.setActiveProfile('ios');
sessionStore.setDefaults({ scheme: 'OldIOS' });
sessionStore.setActiveProfile(null);

const result = await sessionSetDefaultsLogic(
{
profile: 'ios',
scheme: 'NewIOS',
simulatorName: 'iPhone 16',
},
createContext(),
);

expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Activated profile "ios".');
expect(sessionStore.getActiveProfile()).toBe('ios');
expect(sessionStore.getAll().scheme).toBe('NewIOS');
expect(sessionStore.getAll().simulatorName).toBe('iPhone 16');
});

it('returns error when profile does not exist and createIfNotExists is false', async () => {
const result = await sessionSetDefaultsLogic(
{
profile: 'missing',
scheme: 'NewIOS',
},
createContext(),
);

expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Profile "missing" does not exist');
expect(result.content[0].text).toContain('createIfNotExists=true');
});

it('creates profile when createIfNotExists is true and activates it', async () => {
const result = await sessionSetDefaultsLogic(
{
profile: 'ios',
createIfNotExists: true,
scheme: 'NewIOS',
},
createContext(),
);

expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Created and activated profile "ios".');
expect(sessionStore.getActiveProfile()).toBe('ios');
expect(sessionStore.getAll().scheme).toBe('NewIOS');
});

it('persists defaults and active profile when profile is provided', async () => {
const yaml = [
'schemaVersion: 1',
'sessionDefaultsProfiles:',
' ios:',
' scheme: "Old"',
'',
].join('\n');

const writes: { path: string; content: string }[] = [];
let persistedYaml = yaml;
const fs = createMockFileSystemExecutor({
existsSync: (targetPath: string) => targetPath === configPath,
readFile: async (targetPath: string) => {
if (targetPath !== configPath) {
throw new Error(`Unexpected readFile path: ${targetPath}`);
}
return persistedYaml;
},
writeFile: async (targetPath: string, content: string) => {
writes.push({ path: targetPath, content });
persistedYaml = content;
},
});

await initConfigStore({ cwd, fs });
sessionStore.setActiveProfile('ios');
sessionStore.setActiveProfile(null);

await sessionSetDefaultsLogic(
{
profile: 'ios',
scheme: 'NewIOS',
simulatorName: 'iPhone 16',
persist: true,
},
createContext(),
);

expect(writes.length).toBe(2);
const parsed = parseYaml(writes[writes.length - 1].content) as {
sessionDefaultsProfiles?: Record<string, Record<string, unknown>>;
activeSessionDefaultsProfile?: string;
};
expect(parsed.sessionDefaultsProfiles?.ios?.scheme).toBe('NewIOS');
expect(parsed.sessionDefaultsProfiles?.ios?.simulatorName).toBe('iPhone 16');
expect(parsed.activeSessionDefaultsProfile).toBe('ios');
});

it('should not persist when persist is true but no defaults were provided', async () => {
const result = await sessionSetDefaultsLogic({ persist: true }, createContext());

Expand Down
Loading
Loading