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
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ A Model Context Protocol (MCP) server that provides Xcode-related tools for inte
- [MCP Resources](#mcp-resources)
- [Getting started](#getting-started)
- [Prerequisites](#prerequisites)
- [Configure your MCP client](#configure-your-mcp-client)
- [One click install](#one-click-install)
- [General installation](#general-installation)
- [Specific client installation instructions](#specific-client-installation-instructions)
Expand All @@ -33,6 +32,7 @@ A Model Context Protocol (MCP) server that provides Xcode-related tools for inte
- [Usage Example](#usage-example)
- [Client Compatibility](#client-compatibility)
- [Selective Workflow Loading (Static Mode)](#selective-workflow-loading-static-mode)
- [Session-aware opt-out](#session-aware-opt-out)
- [Code Signing for Device Deployment](#code-signing-for-device-deployment)
- [Troubleshooting](#troubleshooting)
- [Doctor Tool](#doctor-tool)
Expand Down Expand Up @@ -291,6 +291,24 @@ For clients that don't support MCP Sampling but still want to reduce context win
> [!NOTE]
> The `XCODEBUILDMCP_ENABLED_WORKFLOWS` setting only works in Static Mode. If `XCODEBUILDMCP_DYNAMIC_TOOLS=true` is set, the selective workflow setting will be ignored.

## Session-aware opt-out

By default, XcodeBuildMCP uses a session-aware mode: the LLM (or client) sets shared defaults once (simulator, device, project/workspace, scheme, etc.), and all tools reuse them—similar to choosing a scheme and simulator in Xcode’s UI so you don’t repeat them on every action. This cuts context bloat not just in each call payload, but also in the tool schemas themselves (those parameters don’t have to be described on every tool).

If you prefer the older, explicit style where each tool requires its own parameters, set `XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS=true`. This restores the legacy schemas with per-call parameters while still honoring any session defaults you choose to set.

Example MCP client configuration:
```json
"XcodeBuildMCP": {
...
"env": {
"XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS": "true"
}
}
```

Leave this unset for the streamlined session-aware experience; enable it to force explicit parameters on each tool call.

## Code Signing for Device Deployment

For device deployment features to work, code signing must be properly configured in Xcode **before** using XcodeBuildMCP device tools:
Expand Down
128 changes: 127 additions & 1 deletion example_projects/macOS/MCPTest.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@
objectVersion = 77;
objects = {

/* Begin PBXContainerItemProxy section */
8BCB4E2B2EF00E2600D60AD2 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8BA9F8182D62A17D00C22D5D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 8BA9F81F2D62A17D00C22D5D;
remoteInfo = MCPTest;
};
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
8BA9F8202D62A17D00C22D5D /* MCPTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MCPTest.app; sourceTree = BUILT_PRODUCTS_DIR; };
8BCB4E272EF00E2600D60AD2 /* MCPTestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MCPTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand All @@ -16,6 +27,11 @@
path = MCPTest;
sourceTree = "<group>";
};
8BCB4E282EF00E2600D60AD2 /* MCPTestTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = MCPTestTests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -26,13 +42,21 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
8BCB4E242EF00E2600D60AD2 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
8BA9F8172D62A17D00C22D5D = {
isa = PBXGroup;
children = (
8BA9F8222D62A17D00C22D5D /* MCPTest */,
8BCB4E282EF00E2600D60AD2 /* MCPTestTests */,
8BA9F8212D62A17D00C22D5D /* Products */,
);
sourceTree = "<group>";
Expand All @@ -41,6 +65,7 @@
isa = PBXGroup;
children = (
8BA9F8202D62A17D00C22D5D /* MCPTest.app */,
8BCB4E272EF00E2600D60AD2 /* MCPTestTests.xctest */,
);
name = Products;
sourceTree = "<group>";
Expand Down Expand Up @@ -70,19 +95,46 @@
productReference = 8BA9F8202D62A17D00C22D5D /* MCPTest.app */;
productType = "com.apple.product-type.application";
};
8BCB4E262EF00E2600D60AD2 /* MCPTestTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8BCB4E2F2EF00E2600D60AD2 /* Build configuration list for PBXNativeTarget "MCPTestTests" */;
buildPhases = (
8BCB4E232EF00E2600D60AD2 /* Sources */,
8BCB4E242EF00E2600D60AD2 /* Frameworks */,
8BCB4E252EF00E2600D60AD2 /* Resources */,
);
buildRules = (
);
dependencies = (
8BCB4E2C2EF00E2600D60AD2 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
8BCB4E282EF00E2600D60AD2 /* MCPTestTests */,
);
name = MCPTestTests;
packageProductDependencies = (
);
productName = MCPTestTests;
productReference = 8BCB4E272EF00E2600D60AD2 /* MCPTestTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
8BA9F8182D62A17D00C22D5D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 1620;
TargetAttributes = {
8BA9F81F2D62A17D00C22D5D = {
CreatedOnToolsVersion = 16.2;
};
8BCB4E262EF00E2600D60AD2 = {
CreatedOnToolsVersion = 26.0;
TestTargetID = 8BA9F81F2D62A17D00C22D5D;
};
};
};
buildConfigurationList = 8BA9F81B2D62A17D00C22D5D /* Build configuration list for PBXProject "MCPTest" */;
Expand All @@ -100,6 +152,7 @@
projectRoot = "";
targets = (
8BA9F81F2D62A17D00C22D5D /* MCPTest */,
8BCB4E262EF00E2600D60AD2 /* MCPTestTests */,
);
};
/* End PBXProject section */
Expand All @@ -112,6 +165,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
8BCB4E252EF00E2600D60AD2 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
Expand All @@ -122,8 +182,23 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
8BCB4E232EF00E2600D60AD2 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */

/* Begin PBXTargetDependency section */
8BCB4E2C2EF00E2600D60AD2 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 8BA9F81F2D62A17D00C22D5D /* MCPTest */;
targetProxy = 8BCB4E2B2EF00E2600D60AD2 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */

/* Begin XCBuildConfiguration section */
8BA9F8432D62A18100C22D5D /* Debug */ = {
isa = XCBuildConfiguration;
Expand Down Expand Up @@ -297,6 +372,48 @@
};
name = Release;
};
8BCB4E2D2EF00E2600D60AD2 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = BR6WD3M6ZD;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Example project test target requires non-existent macOS 26

The newly added MCPTestTests test target has MACOSX_DEPLOYMENT_TARGET = 26.0 in both Debug and Release configurations. macOS 26.0 doesn't exist - current macOS versions are around 14-15 (Sonoma/Sequoia). The main app target correctly uses 15.2. This configuration will prevent anyone from building the example project's test target on any real macOS system, as the specified deployment target is invalid.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact macOS 26 does exist it's just before your training data cut-off, it was released in July 2025, apple decided to rebrand to use years so in 2025 Apple released macOS 26, in 2026 they will release macOS 27 and so on.

MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.test.MCPTestTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MCPTest.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MCPTest";
};
name = Debug;
};
8BCB4E2E2EF00E2600D60AD2 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = BR6WD3M6ZD;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.test.MCPTestTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MCPTest.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MCPTest";
};
name = Release;
};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
Expand All @@ -318,6 +435,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
8BCB4E2F2EF00E2600D60AD2 /* Build configuration list for PBXNativeTarget "MCPTestTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8BCB4E2D2EF00E2600D60AD2 /* Debug */,
8BCB4E2E2EF00E2600D60AD2 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 8BA9F8182D62A17D00C22D5D /* Project object */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8BCB4E262EF00E2600D60AD2"
BuildableName = "MCPTestTests.xctest"
BlueprintName = "MCPTestTests"
ReferencedContainer = "container:MCPTest.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
Expand Down
16 changes: 16 additions & 0 deletions example_projects/macOS/MCPTestTests/MCPTestTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// MCPTestTests.swift
// MCPTestTests
//
// Created by Cameron on 15/12/2025.
//

import Testing

struct MCPTestTests {

@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}

}
22 changes: 15 additions & 7 deletions src/mcp/tools/device/build_device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath
Expand All @@ -36,6 +39,13 @@ const buildDeviceSchema = baseSchema

export type BuildDeviceParams = z.infer<typeof buildDeviceSchema>;

const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
} as const);

/**
* Business logic for building device project or workspace.
* Exported for direct testing and reuse.
Expand Down Expand Up @@ -64,12 +74,10 @@ export async function buildDeviceLogic(
export default {
name: 'build_device',
description: 'Builds an app for a connected device.',
schema: baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
} as const).shape,
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: baseSchemaObject,
}),
handler: createSessionAwareTool<BuildDeviceParams>({
internalSchema: buildDeviceSchema as unknown as z.ZodType<BuildDeviceParams>,
logicFunction: buildDeviceLogic,
Expand Down
22 changes: 15 additions & 7 deletions src/mcp/tools/device/get_device_app_path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { log } from '../../../utils/logging/index.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath, sharing common options
Expand Down Expand Up @@ -43,6 +46,13 @@ const getDeviceAppPathSchema = baseSchema
// Use z.infer for type safety
type GetDeviceAppPathParams = z.infer<typeof getDeviceAppPathSchema>;

const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
} as const);

export async function get_device_app_pathLogic(
params: GetDeviceAppPathParams,
executor: CommandExecutor,
Expand Down Expand Up @@ -147,12 +157,10 @@ export async function get_device_app_pathLogic(
export default {
name: 'get_device_app_path',
description: 'Retrieves the built app path for a connected device.',
schema: baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
} as const).shape,
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: baseSchemaObject,
}),
handler: createSessionAwareTool<GetDeviceAppPathParams>({
internalSchema: getDeviceAppPathSchema as unknown as z.ZodType<GetDeviceAppPathParams>,
logicFunction: get_device_app_pathLogic,
Expand Down
Loading
Loading