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:
- Open Xcode > Settings (or press
⌘,) - Select Intelligence in the sidebar
- 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 mcpbridgeFor Codex, run:
codex mcp add xcode -- xcrun mcpbridgeTo verify the configuration worked:
claude mcp list
# or
codex mcp listThat 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:
Cursor will prompt you to confirm the installation. Click "Install" and you are done.
Option 2: GUI
- Open Cursor Settings (⌘,)
- Go to Features > MCP
- Click + Add New MCP Server
- Select stdio as the transport type
- Enter
xcode-toolsas the name - Enter
xcrun mcpbridgeas 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 contentThis 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-wrapperThen 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:
- If exactly one Xcode process is running, it connects to that
- If multiple Xcode instances are running, it uses
xcode-selectto pick the right one - 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 projectXcodeWrite- Write files to the projectXcodeUpdate- Edit files with str_replace-style patchesXcodeGlob- Find files by patternXcodeGrep- Search file contentsXcodeLS- List directory contentsXcodeMakeDir- Create directoriesXcodeRM- Remove filesXcodeMV- Move/rename filesBuildProject- Build the Xcode projectGetBuildLog- Get build outputRunAllTests- Run all testsRunSomeTests- Run specific testsGetTestList- List available testsXcodeListNavigatorIssues- Get Xcode issues/errorsXcodeRefreshCodeIssuesInFile- Get live diagnosticsExecuteSnippet- Run code in a REPL-like environmentRenderPreview- Render SwiftUI previews as imagesDocumentationSearch- Search Apple docs and WWDC videosXcodeListWindows- 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:
- Open your project in Xcode first (the tools operate on whatever is open):
open MyApp.xcodeproj
# or
open MyApp.xcworkspace-
Ask the agent to do something like "build my project"
-
The agent automatically:
- Calls
XcodeListWindowsto discover open windows - Gets the
tabIdentifier(e.g.,windowtab1) and workspace path - Uses that identifier in the actual tool call
- Calls
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 searchableManual 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 XcodeThe 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:






