Skip to content
Draft
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
34 changes: 34 additions & 0 deletions docs/SESSION_DEFAULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,40 @@ 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 tools (`session_set_defaults`, `session_show_defaults`, build/test tools) use the active profile automatically.
- 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
}}
```

## 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 @@ -187,4 +187,4 @@ XcodeBuildMCP provides 71 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-08T12:09:33.648Z UTC*
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-11T19:34:51.141Z UTC*
11 changes: 6 additions & 5 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 76 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 77 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_use_defaults_profile` - Select the active session defaults profile for multi-target workflows.
- `sync_xcode_defaults` - Sync session defaults (scheme, simulator) from Xcode's current IDE selection.


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

## Summary Statistics

- **Canonical Tools**: 76
- **Total Tools**: 100
- **Canonical Tools**: 77
- **Total Tools**: 101
- **Workflow Groups**: 15

---

*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-08T12:09:33.648Z UTC*
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-11T19:34:51.141Z UTC*
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: Select the active session defaults profile for multi-target workflows.
annotations:
title: Use Session Defaults Profile
readOnlyHint: true
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
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 @@ -323,7 +323,7 @@ async function main(): Promise<void> {
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 @@ -50,12 +50,17 @@ describe('session-clear-defaults tool', () => {
});

it('should clear all 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');

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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,49 @@ describe('session-set-defaults tool', () => {
expect(parsed.sessionDefaults?.simulatorName).toBeUndefined();
});

it('should persist into the active named profile when selected', async () => {
const yaml = [
'schemaVersion: 1',
'sessionDefaultsProfiles:',
' ios:',
' scheme: "Old"',
'',
].join('\n');

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

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

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,15 @@ describe('session-show-defaults tool', () => {
expect(parsed.scheme).toBe('MyScheme');
expect(parsed.simulatorId).toBe('SIM-123');
});

it('shows defaults from the active profile', async () => {
sessionStore.setDefaults({ scheme: 'GlobalScheme' });
sessionStore.setActiveProfile('ios');
sessionStore.setDefaults({ scheme: 'IOSScheme' });

const result = await handler();
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.scheme).toBe('IOSScheme');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, it, expect, beforeEach } from 'vitest';
import path from 'node:path';
import { parse as parseYaml } from 'yaml';
import { __resetConfigStoreForTests, initConfigStore } from '../../../../utils/config-store.ts';
import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import {
handler,
schema,
sessionUseDefaultsProfileLogic,
} from '../session_use_defaults_profile.ts';

describe('session-use-defaults-profile tool', () => {
beforeEach(() => {
__resetConfigStoreForTests();
sessionStore.clear();
});

const cwd = '/repo';
const configPath = path.join(cwd, '.xcodebuildmcp', 'config.yaml');

it('exports handler and schema', () => {
expect(typeof handler).toBe('function');
expect(schema).toBeDefined();
expect(typeof schema).toBe('object');
});

it('activates a named profile', async () => {
const result = await sessionUseDefaultsProfileLogic({ profile: 'ios' });
expect(result.isError).toBe(false);
expect(sessionStore.getActiveProfile()).toBe('ios');
expect(sessionStore.listProfiles()).toContain('ios');
});

it('switches back to global profile', async () => {
sessionStore.setActiveProfile('watch');
const result = await sessionUseDefaultsProfileLogic({ global: true });
expect(result.isError).toBe(false);
expect(sessionStore.getActiveProfile()).toBeNull();
});

it('returns error when both global and profile are provided', async () => {
const result = await sessionUseDefaultsProfileLogic({ global: true, profile: 'ios' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('either global=true or profile');
});

it('returns error when profile is missing and create=false', async () => {
const result = await sessionUseDefaultsProfileLogic({ profile: 'macos', create: false });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('does not exist');
});

it('returns status for empty args', async () => {
const result = await sessionUseDefaultsProfileLogic({});
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Active defaults profile: global');
});

it('persists active profile when persist=true', async () => {
const writes: { path: string; content: string }[] = [];
const fs = createMockFileSystemExecutor({
existsSync: (targetPath: string) => targetPath === configPath,
readFile: async () => ['schemaVersion: 1', ''].join('\n'),
writeFile: async (targetPath: string, content: string) => {
writes.push({ path: targetPath, content });
},
});
await initConfigStore({ cwd, fs });

const result = await sessionUseDefaultsProfileLogic({ profile: 'ios', persist: true });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Persisted active profile selection');
expect(writes).toHaveLength(1);
const parsed = parseYaml(writes[0].content) as { activeSessionDefaultsProfile?: string };
expect(parsed.activeSessionDefaultsProfile).toBe('ios');
});

it('removes active profile from config when persisting global selection', async () => {
const writes: { path: string; content: string }[] = [];
const yaml = ['schemaVersion: 1', 'activeSessionDefaultsProfile: "ios"', ''].join('\n');
const fs = createMockFileSystemExecutor({
existsSync: (targetPath: string) => targetPath === configPath,
readFile: async () => yaml,
writeFile: async (targetPath: string, content: string) => {
writes.push({ path: targetPath, content });
},
});
await initConfigStore({ cwd, fs });

const result = await sessionUseDefaultsProfileLogic({ global: true, persist: true });
expect(result.isError).toBe(false);
expect(writes).toHaveLength(1);
const parsed = parseYaml(writes[0].content) as { activeSessionDefaultsProfile?: string };
expect(parsed.activeSessionDefaultsProfile).toBeUndefined();
});
});
3 changes: 3 additions & 0 deletions src/mcp/tools/session-management/session_set_defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export async function sessionSetDefaultsLogic(
context: SessionSetDefaultsContext,
): Promise<ToolResponse> {
const notices: string[] = [];
const activeProfile = sessionStore.getActiveProfile();
const current = sessionStore.getAll();
const { persist, ...rawParams } = params;
const nextParams = removeUndefined(
Expand Down Expand Up @@ -133,6 +134,7 @@ export async function sessionSetDefaultsLogic(
const { path } = await persistSessionDefaultsPatch({
patch: nextParams,
deleteKeys: Array.from(toClear),
profile: activeProfile,
});
notices.push(`Persisted defaults to ${path}`);
}
Expand All @@ -145,6 +147,7 @@ export async function sessionSetDefaultsLogic(
executor: context.executor,
expectedRevision: revision,
reason: 'session-set-defaults',
profile: activeProfile,
persist: Boolean(persist),
simulatorId: defaultsForRefresh.simulatorId,
simulatorName: defaultsForRefresh.simulatorName,
Expand Down
Loading
Loading