·10 min read

Exploring AI Driven Coding: Using Xcode 26.3 MCP Tools in Cursor, Claude Code and Codex

While digging through Xcode 26.3's internals at 3 AM, I discovered something unusual from Xcode's walled garden that made me genuinely excited: the team built a bridge that lets you use Xcode's AI tools from any MCP client.

Not just from Xcode's built-in Claude or Codex agents. From Cursor. From Claude CLI. From anything that speaks MCP.

Apple even has official documentation for this, but it is does not cover third-party tools like Cursor.

Prerequisites: Enable Xcode Tools MCP Server

Before any external tool can connect, you need to enable the MCP server in Xcode:

  1. Open Xcode > Settings (or press ⌘,)
  2. Select Intelligence in the sidebar
  3. Under Model Context Protocol, toggle Xcode Tools on

This tells Xcode to accept incoming MCP connections from external agents.

The mcpbridge

The xcrun mcpbridge bridges (pun-intended?) a binary that translates MCP protocol requests into Xcode's internal XPC calls:

┌─────────────┐    MCP Protocol    ┌────────────┐    XPC    ┌─────────┐
│   Cursor    │ ◄────────────────► │ mcpbridge  │ ◄───────► │  Xcode  │
│ (MCP Client)│                    │  (Bridge)  │           │  (IDE)  │
└─────────────┘                    └────────────┘           └─────────┘

Xcode must be running with a project open for this to work. The bridge connects to Xcode's process and exposes all 20 of its native MCP tools.

Claude Code and Codex CLI

Apple provides official one-liner commands for Claude Code and Codex. For Claude Code, run:

claude mcp add --transport stdio xcode -- xcrun mcpbridge

For Codex, run:

codex mcp add xcode -- xcrun mcpbridge

To verify the configuration worked:

claude mcp list
# or
codex mcp list

That is all you need for the official CLI tools. But what about Cursor or other VS Code forks?

Setting Up in Cursor

There are three ways to add xcode-tools to Cursor, from easiest to most manual:

Option 1: One-Click Install

Click this link to install xcode-tools directly:

Add xcode-tools to Cursor

Cursor will prompt you to confirm the installation. Click "Install" and you are done.

Option 2: GUI

  1. Open Cursor Settings (⌘,)
  2. Go to Features > MCP
  3. Click + Add New MCP Server
  4. Select stdio as the transport type
  5. Enter xcode-tools as the name
  6. Enter xcrun mcpbridge as the command

Option 3: JSON Config

Add this to ~/.cursor/mcp.json:

{
  "mcpServers": {
    "xcode-tools": {
      "command": "xcrun",
      "args": ["mcpbridge"]
    }
  }
}

The mcpbridge auto-detects the Xcode PID. You do not need to specify it.

Known Limitation in Xcode 26.3 RC 1 (Fixed in RC 2)

If you are on Xcode 26.3 RC 1 and try using xcode-tools in Cursor with the basic configuration above, you may encounter this error:

MCP error -32600: Tool XcodeListWindows has an output schema but did not return structured content

This was a known limitation in Xcode 26.3 RC 1. According to the MCP specification, when a tool declares an outputSchema, the response must include a structuredContent field. Apple's mcpbridge returned the data in content but not in structuredContent.

This has been fixed in Xcode 26.3 RC 2. The mcpbridge now correctly returns structuredContent, so Cursor works with the standard xcrun mcpbridge configuration — no wrapper needed.

If you are still on RC 1, you can use the wrapper workaround below. Otherwise, skip ahead to the next section.

Wrapper workaround for Xcode 26.3 RC 1

Wrap mcpbridge with a script that copies content into structuredContent.

Use this hardened version (it also avoids common hangs by monitoring child liveness and using bounded shutdown). Create this file at ~/bin/mcpbridge-wrapper:

#!/usr/bin/env python3
"""
Wrapper for xcrun mcpbridge that adds structuredContent to responses.
 
Hardened for long-running MCP sessions:
- monitors child liveness
- avoids silent thread crashes on malformed payloads
- uses bounded shutdown (terminate -> kill) to prevent hangs
"""
 
import json
import subprocess
import sys
import threading
import time
 
SHUTDOWN_WAIT_SECONDS = 2.0
 
 
def log(message):
    try:
        sys.stderr.write(f"[mcpbridge-wrapper] {message}\n")
        sys.stderr.flush()
    except Exception:
        pass
 
 
def ensure_structured_content(result):
    if "content" not in result or "structuredContent" in result:
        return
 
    content = result.get("content", [])
    if not isinstance(content, list):
        return
 
    if not content:
        result["structuredContent"] = content
        return
 
    for item in content:
        if not isinstance(item, dict) or item.get("type") != "text":
            continue
 
        text = item.get("text", "")
        if not isinstance(text, str):
            result["structuredContent"] = {"text": str(text)}
            return
 
        try:
            result["structuredContent"] = json.loads(text)
        except json.JSONDecodeError:
            result["structuredContent"] = {"text": text}
        return
 
    result["structuredContent"] = content
 
 
def process_response(line):
    try:
        data = json.loads(line)
    except json.JSONDecodeError:
        return line
    except Exception as exc:
        log(f"JSON parse failed unexpectedly: {exc!r}")
        return line
 
    try:
        if isinstance(data, dict):
            result = data.get("result")
            if isinstance(result, dict):
                ensure_structured_content(result)
        return json.dumps(data)
    except Exception as exc:
        log(f"Response transformation failed: {exc!r}")
        return line
 
 
def pipe_output(proc, stop_event):
    stdout = proc.stdout
    if stdout is None:
        stop_event.set()
        return
 
    try:
        for line in stdout:
            raw = line.rstrip("\n")
            try:
                processed = process_response(raw)
            except Exception as exc:
                log(f"process_response crashed: {exc!r}")
                processed = raw
 
            try:
                sys.stdout.write(processed + "\n")
                sys.stdout.flush()
            except BrokenPipeError:
                stop_event.set()
                break
            except Exception as exc:
                log(f"stdout forwarding failed: {exc!r}")
                stop_event.set()
                break
    except Exception as exc:
        log(f"stdout reader failed: {exc!r}")
    finally:
        stop_event.set()
 
 
def pipe_input(proc, stop_event):
    stdin = proc.stdin
    if stdin is None:
        stop_event.set()
        return
 
    try:
        for line in sys.stdin:
            if stop_event.is_set() or proc.poll() is not None:
                break
            try:
                stdin.write(line)
                stdin.flush()
            except (BrokenPipeError, OSError):
                stop_event.set()
                break
    except KeyboardInterrupt:
        stop_event.set()
    except Exception as exc:
        log(f"stdin forwarding failed: {exc!r}")
        stop_event.set()
    finally:
        try:
            stdin.close()
        except Exception:
            pass
 
 
def shutdown_child(proc):
    if proc.poll() is not None:
        return
 
    try:
        proc.terminate()
        proc.wait(timeout=SHUTDOWN_WAIT_SECONDS)
    except subprocess.TimeoutExpired:
        try:
            proc.kill()
        except Exception:
            pass
        try:
            proc.wait(timeout=SHUTDOWN_WAIT_SECONDS)
        except Exception:
            pass
    except Exception:
        pass
 
 
def main():
    try:
        proc = subprocess.Popen(
            ["xcrun", "mcpbridge"] + sys.argv[1:],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=sys.stderr,
            text=True,
            bufsize=1,
        )
    except Exception as exc:
        log(f"Failed to launch xcrun mcpbridge: {exc!r}")
        return 1
 
    stop_event = threading.Event()
    output_thread = threading.Thread(
        target=pipe_output, args=(proc, stop_event), daemon=True
    )
    input_thread = threading.Thread(
        target=pipe_input, args=(proc, stop_event), daemon=True
    )
    output_thread.start()
    input_thread.start()
 
    try:
        while not stop_event.is_set():
            if proc.poll() is not None:
                stop_event.set()
                break
            time.sleep(0.1)
    except KeyboardInterrupt:
        stop_event.set()
    finally:
        stop_event.set()
        shutdown_child(proc)
        output_thread.join(timeout=1.0)
        input_thread.join(timeout=1.0)
 
    return proc.returncode if proc.returncode is not None else 0
 
 
if __name__ == "__main__":
    raise SystemExit(main())

Make it executable:

chmod +x ~/bin/mcpbridge-wrapper

Then update your ~/.cursor/mcp.json to use the wrapper:

{
  "mcpServers": {
    "xcode-tools": {
      "command": "/Users/YOUR_USERNAME/bin/mcpbridge-wrapper"
    }
  }
}

Replace YOUR_USERNAME with your actual username.

The auto-detection logic:

  1. If exactly one Xcode process is running, it connects to that
  2. If multiple Xcode instances are running, it uses xcode-select to pick the right one
  3. If Xcode is not running, it exits with an error

Restart Cursor (or reload the window), and you should see the xcode-tools server appear in your MCP tools list.

The Permission Dialog

When the MCP client first tries to connect, Xcode will ask for permission. Click "Allow" and you are good to go. This is Apple's way of ensuring you explicitly grant access to Xcode's capabilities. The dialog shows the exact path to the agent binary and its PID.

The Xcode MCP Tools

Here is everything you get access to in the Xcode 26.3 MCP tools:

  • XcodeRead - Read files from the project
  • XcodeWrite - Write files to the project
  • XcodeUpdate - Edit files with str_replace-style patches
  • XcodeGlob - Find files by pattern
  • XcodeGrep - Search file contents
  • XcodeLS - List directory contents
  • XcodeMakeDir - Create directories
  • XcodeRM - Remove files
  • XcodeMV - Move/rename files
  • BuildProject - Build the Xcode project
  • GetBuildLog - Get build output
  • RunAllTests - Run all tests
  • RunSomeTests - Run specific tests
  • GetTestList - List available tests
  • XcodeListNavigatorIssues - Get Xcode issues/errors
  • XcodeRefreshCodeIssuesInFile - Get live diagnostics
  • ExecuteSnippet - Run code in a REPL-like environment
  • RenderPreview - Render SwiftUI previews as images
  • DocumentationSearch - Search Apple docs and WWDC videos
  • XcodeListWindows - List open Xcode windows

How the Tools Work

Most tools require a tabIdentifier to specify which Xcode window to operate on. The agent handles this automatically:

  1. Open your project in Xcode first (the tools operate on whatever is open):
open MyApp.xcodeproj
# or
open MyApp.xcworkspace
  1. Ask the agent to do something like "build my project"

  2. The agent automatically:

    • Calls XcodeListWindows to discover open windows
    • Gets the tabIdentifier (e.g., windowtab1) and workspace path
    • Uses that identifier in the actual tool call

Here is what that looks like in practice when you ask "build my project":

Agent: I'll first need to get the tabIdentifier by listing open Xcode windows.
 
→ XcodeListWindows()
← { "message": "* tabIdentifier: windowtab1, workspacePath: /Users/you/MyApp.xcodeproj" }
 
Agent: I see Xcode has MyApp.xcodeproj open. I'll build that.
 
→ BuildProject({ "tabIdentifier": "windowtab1" })
← { "buildResult": "The project built successfully.", "elapsedTime": 2.17, "errors": [] }

DocumentationSearch

This searches Apple's entire documentation corpus and WWDC video transcripts. The semantic search is powered by what Apple internally calls "Squirrel MLX", their MLX-accelerated embedding system optimized for Apple Silicon.

When you ask about a framework, it can pull relevant context from WWDC sessions you might have missed. The search covers everything from iOS 15 to iOS 26 documentation, all indexed and searchable semantically.

RenderPreview

This renders your SwiftUI previews and returns actual images. Your AI agent can literally see what your UI looks like. Ask it to tweak a color, it can verify the change visually.

This is something no other IDE offers to external agents. The agent can iterate on UI changes with visual feedback.

ExecuteSnippet

A Swift REPL-like environment. Test code snippets without creating a file or running a full build. Great for quickly validating logic or testing API calls.

Adding Context with AGENTS.md

Apple recommends adding hints about Xcode and your project to configuration files like AGENTS.md or CLAUDE.md in your project root. This helps the agent understand your project structure:

# Project Context
 
## Build System
- This is an iOS 26 SwiftUI project
- Use `BuildProject` to compile, not shell commands
- SwiftUI previews available via `RenderPreview`
 
## Testing
- Run tests with `RunAllTests` or `RunSomeTests`
- Test results available via Xcode's test navigator
 
## Documentation
- Use `DocumentationSearch` to find Apple API docs
- WWDC session transcripts are searchable

Manual PID Configuration (Edge Cases)

In rare cases where auto-detection does not work (e.g., running multiple Xcode versions simultaneously), you can manually specify which Xcode to connect to:

{
  "mcpServers": {
    "xcode-tools": {
      "command": "xcrun",
      "args": ["mcpbridge"],
      "env": {
        "MCP_XCODE_PID": "12345"
      }
    }
  }
}

To get the PID:

pgrep -x Xcode

The PID stays the same as long as Xcode is running.

Session ID (Advanced)

The mcpbridge also accepts an optional MCP_XCODE_SESSION_ID environment variable described as "a UUID identifying an Xcode tool session." Xcode generates these automatically for its internal agents (you can see them in ~/Library/Developer/Xcode/CodingAssistant/codex/config.toml).

For external clients like Cursor, I have not found a scenario where you would need to set this manually as the auto-detection works fine without it.

Xcode Alerts

When an external agent connects to Xcode, you will see an indicator in Xcode showing that an external tool is connected and active. This is a nice touch for security awareness as you always know when something or who is accessing your project.

Customizing Xcode's Built-in Agents

If you want to customize the Codex or Claude agents that run inside Xcode (not external ones), you can use configuration files in:

  • Codex: ~/Library/Developer/Xcode/CodingAssistant/codex/
  • Claude Agent: ~/Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/

These mirror the standard .codex and .claude configuration directories but are kept separate so Xcode does not interere withyour existing configurations.

What's Next

You can build workflows that combine Xcode's native capabilities (building, testing, previewing) with other MCP servers like Figma for design-to-code pipelines.

The fact that Apple exposed this as a standard MCP interface rather than keeping it locked to their own agents suggests they want the ecosystem to integrate with Xcode in new ways. The official documentation even encourages it.

I am curious to see what workflows people build with this. If you create something interesting, let me know on X!

Happy Xcoding!

Post Topics

Explore more in these categories:

Related Articles

Exploring AI: Cosine Similarity for RAG using Accelerate and Swift

Learn how to implement cosine similarity using Accelerate framework for iOS and macOS apps. Build Retrieval-Augmented Generation (RAG) systems breaking down complex mathematics into simple explanations and practical Swift code examples. Optimize document search with vector similarity calculations.