xcode-mcp-server 1.0.4__py3-none-any.whl → 1.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xcode_mcp_server/__init__.py +67 -5
- xcode_mcp_server/__main__.py +390 -82
- {xcode_mcp_server-1.0.4.dist-info → xcode_mcp_server-1.0.5.dist-info}/METADATA +36 -20
- xcode_mcp_server-1.0.5.dist-info/RECORD +6 -0
- xcode_mcp_server-1.0.4.dist-info/RECORD +0 -6
- {xcode_mcp_server-1.0.4.dist-info → xcode_mcp_server-1.0.5.dist-info}/WHEEL +0 -0
- {xcode_mcp_server-1.0.4.dist-info → xcode_mcp_server-1.0.5.dist-info}/entry_points.txt +0 -0
xcode_mcp_server/__init__.py
CHANGED
@@ -1,18 +1,80 @@
|
|
1
1
|
"""Xcode MCP Server - Model Context Protocol server for Xcode integration"""
|
2
2
|
|
3
|
+
__version__ = "1.0.5"
|
4
|
+
|
3
5
|
def main():
|
4
6
|
"""Entry point for the xcode-mcp-server command"""
|
5
7
|
import sys
|
8
|
+
import os
|
9
|
+
import argparse
|
10
|
+
import subprocess
|
6
11
|
from . import __main__
|
7
12
|
|
8
|
-
#
|
9
|
-
|
13
|
+
# Parse command line arguments
|
14
|
+
parser = argparse.ArgumentParser(description="Xcode MCP Server")
|
15
|
+
parser.add_argument("--version", action="version", version=f"xcode-mcp-server {__version__}")
|
16
|
+
parser.add_argument("--allowed", action="append", help="Add an allowed folder path (can be used multiple times)")
|
17
|
+
parser.add_argument("--show-notifications", action="store_true", help="Enable notifications for tool invocations")
|
18
|
+
parser.add_argument("--hide-notifications", action="store_true", help="Disable notifications for tool invocations")
|
19
|
+
args = parser.parse_args()
|
20
|
+
|
21
|
+
# Handle notification settings
|
22
|
+
if args.show_notifications and args.hide_notifications:
|
23
|
+
print("Error: Cannot use both --show-notifications and --hide-notifications", file=sys.stderr)
|
24
|
+
sys.exit(1)
|
25
|
+
elif args.show_notifications:
|
26
|
+
__main__.NOTIFICATIONS_ENABLED = True
|
27
|
+
print("Notifications enabled", file=sys.stderr)
|
28
|
+
elif args.hide_notifications:
|
29
|
+
__main__.NOTIFICATIONS_ENABLED = False
|
30
|
+
print("Notifications disabled", file=sys.stderr)
|
31
|
+
|
32
|
+
# Initialize allowed folders from environment and command line
|
33
|
+
__main__.ALLOWED_FOLDERS = __main__.get_allowed_folders(args.allowed)
|
34
|
+
|
35
|
+
# Check if we have any allowed folders
|
36
|
+
if not __main__.ALLOWED_FOLDERS:
|
37
|
+
error_msg = """
|
38
|
+
========================================================================
|
39
|
+
ERROR: Xcode MCP Server cannot start - No valid allowed folders!
|
40
|
+
========================================================================
|
41
|
+
|
42
|
+
No valid folders were found to allow access to.
|
43
|
+
|
44
|
+
To fix this, you can either:
|
45
|
+
|
46
|
+
1. Set the XCODEMCP_ALLOWED_FOLDERS environment variable:
|
47
|
+
export XCODEMCP_ALLOWED_FOLDERS="/path/to/folder1:/path/to/folder2"
|
48
|
+
|
49
|
+
2. Use the --allowed command line option:
|
50
|
+
xcode-mcp-server --allowed /path/to/folder1 --allowed /path/to/folder2
|
51
|
+
|
52
|
+
3. Ensure your $HOME directory exists and is accessible
|
53
|
+
|
54
|
+
All specified folders must:
|
55
|
+
- Be absolute paths
|
56
|
+
- Exist on the filesystem
|
57
|
+
- Be directories (not files)
|
58
|
+
- Not contain '..' components
|
59
|
+
|
60
|
+
========================================================================
|
61
|
+
"""
|
62
|
+
print(error_msg, file=sys.stderr)
|
63
|
+
|
64
|
+
# Show macOS notification
|
65
|
+
try:
|
66
|
+
subprocess.run(['osascript', '-e',
|
67
|
+
'display alert "Xcode MCP Server Error" message "No valid allowed folders found. Check your configuration."'],
|
68
|
+
capture_output=True)
|
69
|
+
except:
|
70
|
+
pass # Ignore notification errors
|
71
|
+
|
72
|
+
sys.exit(1)
|
10
73
|
|
11
74
|
# Debug info
|
12
|
-
print(f"
|
75
|
+
print(f"Total allowed folders: {__main__.ALLOWED_FOLDERS}", file=sys.stderr)
|
13
76
|
|
14
77
|
# Run the server
|
15
78
|
__main__.mcp.run()
|
16
79
|
|
17
|
-
|
18
|
-
__all__ = ["main"]
|
80
|
+
__all__ = ["main", "__version__"]
|
xcode_mcp_server/__main__.py
CHANGED
@@ -3,6 +3,7 @@ import os
|
|
3
3
|
import sys
|
4
4
|
import subprocess
|
5
5
|
import json
|
6
|
+
import argparse
|
6
7
|
from typing import Optional, Dict, List, Any, Tuple, Set
|
7
8
|
from dataclasses import dataclass
|
8
9
|
|
@@ -10,6 +11,7 @@ from mcp.server.fastmcp import FastMCP, Context
|
|
10
11
|
|
11
12
|
# Global variables for allowed folders
|
12
13
|
ALLOWED_FOLDERS: Set[str] = set()
|
14
|
+
NOTIFICATIONS_ENABLED = False # No type annotation to avoid global declaration issues
|
13
15
|
|
14
16
|
class XCodeMCPError(Exception):
|
15
17
|
def __init__(self, message, code=None):
|
@@ -23,28 +25,42 @@ class AccessDeniedError(XCodeMCPError):
|
|
23
25
|
class InvalidParameterError(XCodeMCPError):
|
24
26
|
pass
|
25
27
|
|
26
|
-
def get_allowed_folders() -> Set[str]:
|
28
|
+
def get_allowed_folders(command_line_folders: Optional[List[str]] = None) -> Set[str]:
|
27
29
|
"""
|
28
|
-
Get the allowed folders from environment variable.
|
30
|
+
Get the allowed folders from environment variable and command line.
|
29
31
|
Validates that paths are absolute, exist, and are directories.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
command_line_folders: List of folders provided via command line
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
Set of validated folder paths
|
30
38
|
"""
|
31
39
|
allowed_folders = set()
|
40
|
+
folders_to_process = []
|
32
41
|
|
33
42
|
# Get from environment variable
|
34
43
|
folder_list_str = os.environ.get("XCODEMCP_ALLOWED_FOLDERS")
|
35
44
|
|
36
45
|
if folder_list_str:
|
37
46
|
print(f"Using allowed folders from environment: {folder_list_str}", file=sys.stderr)
|
38
|
-
|
39
|
-
|
40
|
-
|
47
|
+
folders_to_process.extend(folder_list_str.split(":"))
|
48
|
+
|
49
|
+
# Add command line folders
|
50
|
+
if command_line_folders:
|
51
|
+
print(f"Adding {len(command_line_folders)} folder(s) from command line", file=sys.stderr)
|
52
|
+
folders_to_process.extend(command_line_folders)
|
53
|
+
|
54
|
+
# If no folders specified, use $HOME
|
55
|
+
if not folders_to_process:
|
56
|
+
print("Warning: No allowed folders specified via environment or command line.", file=sys.stderr)
|
57
|
+
print("Set XCODEMCP_ALLOWED_FOLDERS environment variable or use --allowed flag.", file=sys.stderr)
|
41
58
|
home = os.environ.get("HOME", "/")
|
42
|
-
print(f"
|
43
|
-
|
59
|
+
print(f"Using default: $HOME = {home}", file=sys.stderr)
|
60
|
+
folders_to_process = [home]
|
44
61
|
|
45
|
-
# Process
|
46
|
-
|
47
|
-
for folder in folder_list:
|
62
|
+
# Process all folders
|
63
|
+
for folder in folders_to_process:
|
48
64
|
folder = folder.rstrip("/") # Normalize by removing trailing slash
|
49
65
|
|
50
66
|
# Skip empty entries
|
@@ -90,11 +106,8 @@ def is_path_allowed(project_path: str) -> bool:
|
|
90
106
|
|
91
107
|
# If no allowed folders are specified, nothing is allowed
|
92
108
|
if not ALLOWED_FOLDERS:
|
93
|
-
|
94
|
-
|
95
|
-
if not ALLOWED_FOLDERS:
|
96
|
-
print(f"Warning: not ALLOWED_FOLDERS: {', '.join(ALLOWED_FOLDERS)}", file=sys.stderr)
|
97
|
-
return False
|
109
|
+
print(f"Warning: ALLOWED_FOLDERS is empty, denying access", file=sys.stderr)
|
110
|
+
return False
|
98
111
|
|
99
112
|
# Normalize the path
|
100
113
|
project_path = os.path.abspath(project_path).rstrip("/")
|
@@ -115,7 +128,31 @@ def is_path_allowed(project_path: str) -> bool:
|
|
115
128
|
return False
|
116
129
|
|
117
130
|
# Initialize the MCP server
|
118
|
-
mcp = FastMCP("Xcode MCP Server"
|
131
|
+
mcp = FastMCP("Xcode MCP Server",
|
132
|
+
instructions="""
|
133
|
+
This server provides access to the Xcode IDE. For any project intended
|
134
|
+
for Apple platforms, such as iOS or macOS, this MCP server is the best
|
135
|
+
way to build or run .xcodeproj or .xcworkspace Xcode projects, and should
|
136
|
+
always be preferred over using `xcodebuild`, `swift build`, or
|
137
|
+
`swift package build`. Building with this tool ensures the build happens
|
138
|
+
exactly the same way as when the user builds with Xcode, with all the same
|
139
|
+
settings, so you will get the same results the user sees. The user can also
|
140
|
+
see any results immediately and a subsequent build and run by the user will
|
141
|
+
happen almost instantly for the user.
|
142
|
+
|
143
|
+
You might start with `get_frontmost_project` to see if the user currently
|
144
|
+
has an Xcode project already open.
|
145
|
+
|
146
|
+
You can call `get_xcode_projects` to find Xcode project (.xcodeproj) and
|
147
|
+
Xcode workspace (.xcworkspace) folders under a given root folder.
|
148
|
+
|
149
|
+
You can call `get_project_schemes` to get the build scheme names for a given
|
150
|
+
.xcodeproj or .xcworkspace.
|
151
|
+
|
152
|
+
Call build_project to build the project and get back the first 25 lines of
|
153
|
+
error output. `build_project` will default to the active scheme if none is provided.
|
154
|
+
"""
|
155
|
+
)
|
119
156
|
|
120
157
|
# Helper functions for Xcode interaction
|
121
158
|
def get_frontmost_project() -> str:
|
@@ -171,62 +208,91 @@ def run_applescript(script: str) -> Tuple[bool, str]:
|
|
171
208
|
except subprocess.CalledProcessError as e:
|
172
209
|
return False, e.stderr.strip()
|
173
210
|
|
211
|
+
def show_notification(title: str, message: str):
|
212
|
+
"""Show a macOS notification if notifications are enabled"""
|
213
|
+
if NOTIFICATIONS_ENABLED:
|
214
|
+
try:
|
215
|
+
subprocess.run(['osascript', '-e',
|
216
|
+
f'display notification "{message}" with title "{title}"'],
|
217
|
+
capture_output=True)
|
218
|
+
except:
|
219
|
+
pass # Ignore notification errors
|
220
|
+
|
174
221
|
# MCP Tools for Xcode
|
175
222
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
223
|
+
@mcp.tool()
|
224
|
+
def version() -> str:
|
225
|
+
"""
|
226
|
+
Get the current version of the Xcode MCP Server.
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
The version string of the server
|
230
|
+
"""
|
231
|
+
show_notification("Xcode MCP", "Getting server version")
|
232
|
+
return f"Xcode MCP Server version {__import__('xcode_mcp_server').__version__}"
|
184
233
|
|
185
234
|
|
186
235
|
@mcp.tool()
|
187
|
-
def get_xcode_projects(search_path: str) -> str:
|
236
|
+
def get_xcode_projects(search_path: str = "") -> str:
|
188
237
|
"""
|
189
238
|
Search the given search_path to find .xcodeproj (Xcode project) and
|
190
239
|
.xcworkspace (Xcode workspace) paths. If the search_path is empty,
|
191
240
|
all paths to which this tool has been granted access are searched.
|
241
|
+
Searching all paths to which this tool has been granted access can
|
242
|
+
uses `mdfind` (Spotlight indexing) to find the relevant files, and
|
243
|
+
so will only return .xcodeproj and .xcworkspace folders that are
|
244
|
+
indexed.
|
192
245
|
|
193
246
|
Args:
|
194
|
-
search_path: Path to
|
247
|
+
search_path: Path to search. If empty, searches all allowed folders.
|
195
248
|
|
196
249
|
Returns:
|
197
250
|
A string which is a newline-separated list of .xcodeproj and
|
198
251
|
.xcworkspace paths found. If none are found, returns an empty string.
|
199
252
|
"""
|
200
|
-
|
201
|
-
|
202
|
-
project_path = search_path
|
203
|
-
|
204
|
-
# Validate input
|
205
|
-
if not search_path or search_path.strip() == "":
|
206
|
-
project_path = "/Users/andrew/Documents/ncc_source"
|
207
|
-
# return "Error: project_path cannot be empty"
|
208
|
-
|
253
|
+
global ALLOWED_FOLDERS
|
209
254
|
|
210
|
-
#
|
211
|
-
|
212
|
-
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
213
|
-
# return f"Error: Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable."
|
255
|
+
# Determine paths to search
|
256
|
+
paths_to_search = []
|
214
257
|
|
215
|
-
|
216
|
-
|
217
|
-
|
258
|
+
if not search_path or search_path.strip() == "":
|
259
|
+
# Search all allowed folders
|
260
|
+
show_notification("Xcode MCP", f"Searching all {len(ALLOWED_FOLDERS)} allowed folders for Xcode projects")
|
261
|
+
paths_to_search = list(ALLOWED_FOLDERS)
|
262
|
+
else:
|
263
|
+
# Search specific path
|
264
|
+
project_path = search_path.strip()
|
265
|
+
|
266
|
+
# Security check
|
267
|
+
if not is_path_allowed(project_path):
|
268
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
269
|
+
|
270
|
+
# Check if the path exists
|
271
|
+
if not os.path.exists(project_path):
|
272
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
273
|
+
|
274
|
+
show_notification("Xcode MCP", f"Searching {project_path} for Xcode projects")
|
275
|
+
paths_to_search = [project_path]
|
276
|
+
|
277
|
+
# Search for projects in all paths
|
278
|
+
all_results = []
|
279
|
+
for path in paths_to_search:
|
218
280
|
try:
|
219
|
-
#
|
220
|
-
mdfindResult = subprocess.run(['mdfind', '-onlyin',
|
221
|
-
|
281
|
+
# Use mdfind to search for Xcode projects
|
282
|
+
mdfindResult = subprocess.run(['mdfind', '-onlyin', path,
|
283
|
+
'kMDItemFSName == "*.xcodeproj" || kMDItemFSName == "*.xcworkspace"'],
|
284
|
+
capture_output=True, text=True, check=True)
|
222
285
|
result = mdfindResult.stdout.strip()
|
223
|
-
|
286
|
+
if result:
|
287
|
+
all_results.extend(result.split('\n'))
|
224
288
|
except Exception as e:
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
289
|
+
print(f"Warning: Error searching in {path}: {str(e)}", file=sys.stderr)
|
290
|
+
continue
|
291
|
+
|
292
|
+
# Remove duplicates and sort
|
293
|
+
unique_results = sorted(set(all_results))
|
294
|
+
|
295
|
+
return '\n'.join(unique_results) if unique_results else ""
|
230
296
|
|
231
297
|
|
232
298
|
@mcp.tool()
|
@@ -235,7 +301,8 @@ def get_project_hierarchy(project_path: str) -> str:
|
|
235
301
|
Get the hierarchy of the specified Xcode project or workspace.
|
236
302
|
|
237
303
|
Args:
|
238
|
-
project_path: Path to an Xcode project/workspace directory
|
304
|
+
project_path: Path to an Xcode project/workspace directory, which must
|
305
|
+
end in '.xcodeproj' or '.xcworkspace' and must exist.
|
239
306
|
|
240
307
|
Returns:
|
241
308
|
A string representation of the project hierarchy
|
@@ -243,41 +310,178 @@ def get_project_hierarchy(project_path: str) -> str:
|
|
243
310
|
# Validate input
|
244
311
|
if not project_path or project_path.strip() == "":
|
245
312
|
raise InvalidParameterError("project_path cannot be empty")
|
246
|
-
|
313
|
+
|
314
|
+
project_path = project_path.strip()
|
315
|
+
|
316
|
+
# Verify path ends with .xcodeproj or .xcworkspace
|
317
|
+
if not (project_path.endswith('.xcodeproj') or project_path.endswith('.xcworkspace')):
|
318
|
+
raise InvalidParameterError("project_path must end with '.xcodeproj' or '.xcworkspace'")
|
319
|
+
|
320
|
+
show_notification("Xcode MCP", f"Getting hierarchy for {os.path.basename(project_path)}")
|
247
321
|
|
248
322
|
# Security check
|
249
323
|
if not is_path_allowed(project_path):
|
250
324
|
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
251
|
-
# return f"Error: Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable."
|
252
325
|
|
253
326
|
# Check if the path exists
|
254
|
-
if os.path.exists(project_path):
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
327
|
+
if not os.path.exists(project_path):
|
328
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
329
|
+
|
330
|
+
# Get the parent directory to scan
|
331
|
+
parent_dir = os.path.dirname(project_path)
|
332
|
+
project_name = os.path.basename(project_path)
|
333
|
+
|
334
|
+
# Build the hierarchy
|
335
|
+
def build_hierarchy(path: str, prefix: str = "", is_last: bool = True, base_path: str = "") -> List[str]:
|
336
|
+
"""Recursively build a visual hierarchy of files and folders"""
|
337
|
+
lines = []
|
338
|
+
|
339
|
+
if not base_path:
|
340
|
+
base_path = path
|
263
341
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
342
|
+
# Get relative path for display
|
343
|
+
rel_path = os.path.relpath(path, os.path.dirname(base_path))
|
344
|
+
|
345
|
+
# Add current item
|
346
|
+
if path != base_path:
|
347
|
+
connector = "└── " if is_last else "├── "
|
348
|
+
name = os.path.basename(path)
|
349
|
+
if os.path.isdir(path):
|
350
|
+
name += "/"
|
351
|
+
lines.append(prefix + connector + name)
|
352
|
+
|
353
|
+
# Update prefix for children
|
354
|
+
extension = " " if is_last else "│ "
|
355
|
+
prefix = prefix + extension
|
356
|
+
|
357
|
+
# If it's a directory, recurse into it (with restrictions)
|
358
|
+
if os.path.isdir(path):
|
359
|
+
# Skip certain directories
|
360
|
+
if os.path.basename(path) in ['.build', 'build']:
|
361
|
+
return lines
|
362
|
+
|
363
|
+
# Don't recurse into .xcodeproj or .xcworkspace directories
|
364
|
+
if path.endswith('.xcodeproj') or path.endswith('.xcworkspace'):
|
365
|
+
return lines
|
366
|
+
|
367
|
+
try:
|
368
|
+
items = sorted(os.listdir(path))
|
369
|
+
# Filter out hidden files except for important ones
|
370
|
+
items = [item for item in items if not item.startswith('.') or item in ['.gitignore', '.swift-version']]
|
371
|
+
|
372
|
+
for i, item in enumerate(items):
|
373
|
+
item_path = os.path.join(path, item)
|
374
|
+
is_last_item = (i == len(items) - 1)
|
375
|
+
lines.extend(build_hierarchy(item_path, prefix, is_last_item, base_path))
|
376
|
+
except PermissionError:
|
377
|
+
pass
|
378
|
+
|
379
|
+
return lines
|
380
|
+
|
381
|
+
# Build hierarchy starting from parent directory
|
382
|
+
hierarchy_lines = [parent_dir + "/"]
|
383
|
+
|
384
|
+
try:
|
385
|
+
items = sorted(os.listdir(parent_dir))
|
386
|
+
# Filter out hidden files and build directories
|
387
|
+
items = [item for item in items if not item.startswith('.') or item in ['.gitignore', '.swift-version']]
|
388
|
+
|
389
|
+
for i, item in enumerate(items):
|
390
|
+
item_path = os.path.join(parent_dir, item)
|
391
|
+
is_last_item = (i == len(items) - 1)
|
392
|
+
hierarchy_lines.extend(build_hierarchy(item_path, "", is_last_item, parent_dir))
|
393
|
+
|
394
|
+
except Exception as e:
|
395
|
+
raise XCodeMCPError(f"Error building hierarchy for {project_path}: {str(e)}")
|
396
|
+
|
397
|
+
return '\n'.join(hierarchy_lines)
|
398
|
+
|
399
|
+
@mcp.tool()
|
400
|
+
def get_project_schemes(project_path: str) -> str:
|
401
|
+
"""
|
402
|
+
Get the available build schemes for the specified Xcode project or workspace.
|
403
|
+
|
404
|
+
Args:
|
405
|
+
project_path: Path to an Xcode project/workspace directory, which must
|
406
|
+
end in '.xcodeproj' or '.xcworkspace' and must exist.
|
407
|
+
|
408
|
+
Returns:
|
409
|
+
A newline-separated list of scheme names, with the active scheme listed first.
|
410
|
+
If no schemes are found, returns an empty string.
|
411
|
+
"""
|
412
|
+
# Validate input
|
413
|
+
if not project_path or project_path.strip() == "":
|
414
|
+
raise InvalidParameterError("project_path cannot be empty")
|
415
|
+
|
416
|
+
project_path = project_path.strip()
|
417
|
+
|
418
|
+
# Verify path ends with .xcodeproj or .xcworkspace
|
419
|
+
if not (project_path.endswith('.xcodeproj') or project_path.endswith('.xcworkspace')):
|
420
|
+
raise InvalidParameterError("project_path must end with '.xcodeproj' or '.xcworkspace'")
|
421
|
+
|
422
|
+
show_notification("Xcode MCP", f"Getting schemes for {os.path.basename(project_path)}")
|
423
|
+
|
424
|
+
# Security check
|
425
|
+
if not is_path_allowed(project_path):
|
426
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
427
|
+
|
428
|
+
# Check if the path exists
|
429
|
+
if not os.path.exists(project_path):
|
269
430
|
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
270
|
-
|
431
|
+
|
432
|
+
script = f'''
|
433
|
+
tell application "Xcode"
|
434
|
+
open "{project_path}"
|
435
|
+
|
436
|
+
set workspaceDoc to first workspace document whose path is "{project_path}"
|
437
|
+
|
438
|
+
-- Wait for it to load
|
439
|
+
repeat 60 times
|
440
|
+
if loaded of workspaceDoc is true then exit repeat
|
441
|
+
delay 0.5
|
442
|
+
end repeat
|
443
|
+
|
444
|
+
if loaded of workspaceDoc is false then
|
445
|
+
error "Xcode workspace did not load in time."
|
446
|
+
end if
|
447
|
+
|
448
|
+
-- Get active scheme name
|
449
|
+
set activeScheme to name of active scheme of workspaceDoc
|
450
|
+
|
451
|
+
-- Get all scheme names
|
452
|
+
set schemeNames to {{}}
|
453
|
+
repeat with aScheme in schemes of workspaceDoc
|
454
|
+
set end of schemeNames to name of aScheme
|
455
|
+
end repeat
|
456
|
+
|
457
|
+
-- Format output with active scheme first
|
458
|
+
set output to activeScheme & " (active)"
|
459
|
+
repeat with schemeName in schemeNames
|
460
|
+
if schemeName as string is not equal to activeScheme then
|
461
|
+
set output to output & "\\n" & schemeName
|
462
|
+
end if
|
463
|
+
end repeat
|
464
|
+
|
465
|
+
return output
|
466
|
+
end tell
|
467
|
+
'''
|
468
|
+
|
469
|
+
success, output = run_applescript(script)
|
470
|
+
|
471
|
+
if success:
|
472
|
+
return output
|
473
|
+
else:
|
474
|
+
raise XCodeMCPError(f"Failed to get schemes for {project_path}: {output}")
|
271
475
|
|
272
476
|
@mcp.tool()
|
273
477
|
def build_project(project_path: str,
|
274
|
-
scheme: str) -> str:
|
478
|
+
scheme: Optional[str] = None) -> str:
|
275
479
|
"""
|
276
480
|
Build the specified Xcode project or workspace.
|
277
481
|
|
278
482
|
Args:
|
279
483
|
project_path: Path to an Xcode project workspace or directory.
|
280
|
-
scheme: Name of the scheme to build.
|
484
|
+
scheme: Name of the scheme to build. If not provided, uses the active scheme.
|
281
485
|
|
282
486
|
Returns:
|
283
487
|
On success, returns "Build succeeded with 0 errors."
|
@@ -287,6 +491,15 @@ def build_project(project_path: str,
|
|
287
491
|
if not project_path or project_path.strip() == "":
|
288
492
|
raise InvalidParameterError("project_path cannot be empty")
|
289
493
|
|
494
|
+
project_path = project_path.strip()
|
495
|
+
|
496
|
+
# Verify path ends with .xcodeproj or .xcworkspace
|
497
|
+
if not (project_path.endswith('.xcodeproj') or project_path.endswith('.xcworkspace')):
|
498
|
+
raise InvalidParameterError("project_path must end with '.xcodeproj' or '.xcworkspace'")
|
499
|
+
|
500
|
+
scheme_desc = scheme if scheme else "active scheme"
|
501
|
+
show_notification("Xcode MCP", f"Building {scheme_desc} in {os.path.basename(project_path)}")
|
502
|
+
|
290
503
|
# Security check
|
291
504
|
if not is_path_allowed(project_path):
|
292
505
|
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
@@ -294,14 +507,13 @@ def build_project(project_path: str,
|
|
294
507
|
if not os.path.exists(project_path):
|
295
508
|
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
296
509
|
|
297
|
-
#
|
298
|
-
|
510
|
+
# Build the AppleScript
|
511
|
+
if scheme:
|
512
|
+
# Use provided scheme
|
513
|
+
script = f'''
|
299
514
|
set projectPath to "{project_path}"
|
300
515
|
set schemeName to "{scheme}"
|
301
516
|
|
302
|
-
--
|
303
|
-
-- Then run with: osascript <thisfilename>
|
304
|
-
--
|
305
517
|
tell application "Xcode"
|
306
518
|
-- 1. Open the project file
|
307
519
|
open projectPath
|
@@ -334,13 +546,51 @@ tell application "Xcode"
|
|
334
546
|
-- 7. Check result
|
335
547
|
set buildStatus to status of actionResult
|
336
548
|
if buildStatus is succeeded then
|
337
|
-
-- display dialog "Build succeeded"
|
338
549
|
return "Build succeeded."
|
339
550
|
else
|
340
551
|
return build log of actionResult
|
341
552
|
end if
|
553
|
+
end tell
|
554
|
+
'''
|
555
|
+
else:
|
556
|
+
# Use active scheme
|
557
|
+
script = f'''
|
558
|
+
set projectPath to "{project_path}"
|
342
559
|
|
343
|
-
|
560
|
+
tell application "Xcode"
|
561
|
+
-- 1. Open the project file
|
562
|
+
open projectPath
|
563
|
+
|
564
|
+
-- 2. Get the workspace document
|
565
|
+
set workspaceDoc to first workspace document whose path is projectPath
|
566
|
+
|
567
|
+
-- 3. Wait for it to load (timeout after ~30 seconds)
|
568
|
+
repeat 60 times
|
569
|
+
if loaded of workspaceDoc is true then exit repeat
|
570
|
+
delay 0.5
|
571
|
+
end repeat
|
572
|
+
|
573
|
+
if loaded of workspaceDoc is false then
|
574
|
+
error "Xcode workspace did not load in time."
|
575
|
+
end if
|
576
|
+
|
577
|
+
-- 4. Build with current active scheme
|
578
|
+
set actionResult to build workspaceDoc
|
579
|
+
|
580
|
+
-- 5. Wait for completion
|
581
|
+
repeat
|
582
|
+
if completed of actionResult is true then exit repeat
|
583
|
+
delay 0.5
|
584
|
+
end repeat
|
585
|
+
|
586
|
+
-- 6. Check result
|
587
|
+
set buildStatus to status of actionResult
|
588
|
+
if buildStatus is succeeded then
|
589
|
+
return "Build succeeded."
|
590
|
+
else
|
591
|
+
return build log of actionResult
|
592
|
+
end if
|
593
|
+
end tell
|
344
594
|
'''
|
345
595
|
|
346
596
|
success, output = run_applescript(script)
|
@@ -531,13 +781,71 @@ def get_runtime_output(project_path: str,
|
|
531
781
|
# This is a placeholder as you mentioned this functionality isn't available yet
|
532
782
|
raise XCodeMCPError("Runtime output retrieval not yet implemented")
|
533
783
|
|
534
|
-
#
|
784
|
+
# Main entry point for the server
|
535
785
|
if __name__ == "__main__":
|
536
|
-
#
|
537
|
-
|
786
|
+
# Parse command line arguments
|
787
|
+
parser = argparse.ArgumentParser(description="Xcode MCP Server")
|
788
|
+
parser.add_argument("--version", action="version", version=f"xcode-mcp-server {__import__('xcode_mcp_server').__version__}")
|
789
|
+
parser.add_argument("--allowed", action="append", help="Add an allowed folder path (can be used multiple times)")
|
790
|
+
parser.add_argument("--show-notifications", action="store_true", help="Enable notifications for tool invocations")
|
791
|
+
parser.add_argument("--hide-notifications", action="store_true", help="Disable notifications for tool invocations")
|
792
|
+
args = parser.parse_args()
|
793
|
+
|
794
|
+
# Handle notification settings
|
795
|
+
if args.show_notifications and args.hide_notifications:
|
796
|
+
print("Error: Cannot use both --show-notifications and --hide-notifications", file=sys.stderr)
|
797
|
+
sys.exit(1)
|
798
|
+
elif args.show_notifications:
|
799
|
+
NOTIFICATIONS_ENABLED = True
|
800
|
+
print("Notifications enabled", file=sys.stderr)
|
801
|
+
elif args.hide_notifications:
|
802
|
+
NOTIFICATIONS_ENABLED = False
|
803
|
+
print("Notifications disabled", file=sys.stderr)
|
804
|
+
|
805
|
+
# Initialize allowed folders from environment and command line
|
806
|
+
ALLOWED_FOLDERS = get_allowed_folders(args.allowed)
|
807
|
+
|
808
|
+
# Check if we have any allowed folders
|
809
|
+
if not ALLOWED_FOLDERS:
|
810
|
+
error_msg = """
|
811
|
+
========================================================================
|
812
|
+
ERROR: Xcode MCP Server cannot start - No valid allowed folders!
|
813
|
+
========================================================================
|
814
|
+
|
815
|
+
No valid folders were found to allow access to.
|
816
|
+
|
817
|
+
To fix this, you can either:
|
818
|
+
|
819
|
+
1. Set the XCODEMCP_ALLOWED_FOLDERS environment variable:
|
820
|
+
export XCODEMCP_ALLOWED_FOLDERS="/path/to/folder1:/path/to/folder2"
|
821
|
+
|
822
|
+
2. Use the --allowed command line option:
|
823
|
+
xcode-mcp-server --allowed /path/to/folder1 --allowed /path/to/folder2
|
824
|
+
|
825
|
+
3. Ensure your $HOME directory exists and is accessible
|
826
|
+
|
827
|
+
All specified folders must:
|
828
|
+
- Be absolute paths
|
829
|
+
- Exist on the filesystem
|
830
|
+
- Be directories (not files)
|
831
|
+
- Not contain '..' components
|
832
|
+
|
833
|
+
========================================================================
|
834
|
+
"""
|
835
|
+
print(error_msg, file=sys.stderr)
|
836
|
+
|
837
|
+
# Show macOS notification
|
838
|
+
try:
|
839
|
+
subprocess.run(['osascript', '-e',
|
840
|
+
'display alert "Xcode MCP Server Error" message "No valid allowed folders found. Check your configuration."'],
|
841
|
+
capture_output=True)
|
842
|
+
except:
|
843
|
+
pass # Ignore notification errors
|
844
|
+
|
845
|
+
sys.exit(1)
|
538
846
|
|
539
847
|
# Debug info
|
540
|
-
print(f"
|
848
|
+
print(f"Total allowed folders: {ALLOWED_FOLDERS}", file=sys.stderr)
|
541
849
|
|
542
850
|
# Run the server
|
543
851
|
mcp.run()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: xcode-mcp-server
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.5
|
4
4
|
Summary: Drew's MCP server for Xcode integration
|
5
5
|
Project-URL: Homepage, https://github.com/drewster99/xcode-mcp-server
|
6
6
|
Project-URL: Repository, https://github.com/drewster99/xcode-mcp-server
|
@@ -39,6 +39,7 @@ The server implements path-based security to prevent unauthorized access to file
|
|
39
39
|
|
40
40
|
- You must specify allowed folders using the environment variable:
|
41
41
|
- `XCODEMCP_ALLOWED_FOLDERS=/path1:/path2:/path3`
|
42
|
+
- Otherwise, all files and subfolders from your home directory ($HOME) will be allowed.
|
42
43
|
|
43
44
|
Security requirements:
|
44
45
|
- All paths must be absolute (starting with /)
|
@@ -59,47 +60,63 @@ If no allowed folders are specified, access will be restricted and tools will re
|
|
59
60
|
|
60
61
|
## Setup
|
61
62
|
|
62
|
-
1.
|
63
|
+
1. Configure Claude for Desktop:
|
63
64
|
|
64
|
-
|
65
|
-
# Using pip
|
66
|
-
pip install -r requirements.txt
|
65
|
+
First, using homebrew, install 'uv'. You might already have this on your system, but installing it via Homebrew usually ensures that `uvx` (part of `uv`) is in the $PATH that Claude Desktop vends to on-device local MCP servers:
|
67
66
|
|
68
|
-
|
69
|
-
uv pip install -r requirements.txt
|
70
|
-
```
|
67
|
+
```brew install uv```
|
71
68
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
69
|
+
Open/create your Claude for Desktop configuration file
|
70
|
+
- Open Claude Desktop --> Settings --> Developer --> Edit Config (to find the file in finder)
|
71
|
+
- It should be at `~/Library/Application Support/Claude/claude_desktop_config.json`
|
72
|
+
- Add the following:
|
76
73
|
|
77
|
-
|
74
|
+
```json
|
75
|
+
{
|
76
|
+
"mcpServers": {
|
77
|
+
"xcode-mcp-server": {
|
78
|
+
"command": "uvx",
|
79
|
+
"args": [
|
80
|
+
"xcode-mcp-server"
|
81
|
+
]
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
```
|
78
86
|
|
79
|
-
|
87
|
+
If you'd like to allow only certain projects or folders to be accessible by xcode-mcp-server, add the `env` option, with a colon-separated list of absolute folder paths, like this:
|
80
88
|
|
81
89
|
```json
|
82
90
|
{
|
83
91
|
"mcpServers": {
|
84
|
-
"xcode": {
|
85
|
-
"command": "
|
92
|
+
"xcode-mcp-server": {
|
93
|
+
"command": "uvx",
|
86
94
|
"args": [
|
87
|
-
"
|
95
|
+
"xcode-mcp-server"
|
88
96
|
],
|
89
97
|
"env": {
|
90
|
-
"XCODEMCP_ALLOWED_FOLDERS": "/
|
98
|
+
"XCODEMCP_ALLOWED_FOLDERS": "/Users/andrew/my_project:/Users/andrew/Documents/source"
|
91
99
|
}
|
92
100
|
}
|
93
101
|
}
|
94
102
|
}
|
95
103
|
```
|
96
104
|
|
97
|
-
|
105
|
+
If you omit the `env` section, access will default to your $HOME directory.
|
106
|
+
|
107
|
+
2. Add xcode-mcp-server to **Claude Code** (Anthropic's CLI-based agent)
|
108
|
+
|
109
|
+
- Install claude code
|
110
|
+
- Add xcode-mcp-server:
|
98
111
|
|
112
|
+
claude mcp add --scope user --transport stdio `which uvx` xcode-mcp-server
|
113
|
+
|
114
|
+
|
99
115
|
## Usage
|
100
116
|
|
101
117
|
1. Open Xcode with a project
|
102
118
|
2. Start Claude for Desktop
|
119
|
+
- If xcode-mcp-server failed to initialize properly, you'll see errors
|
103
120
|
3. Look for the hammer icon to find available Xcode tools
|
104
121
|
4. Use natural language to interact with Xcode, for example:
|
105
122
|
- "Build the project at /path/to/MyProject.xcodeproj"
|
@@ -148,6 +165,5 @@ When testing in the MCP Inspector, provide input values as quoted strings:
|
|
148
165
|
|
149
166
|
## Limitations
|
150
167
|
|
151
|
-
- Runtime output retrieval is not yet implemented
|
152
168
|
- Project hierarchy is a simple file listing implementation
|
153
169
|
- AppleScript syntax may need adjustments for specific Xcode versions # xcode-mcp-server
|
@@ -0,0 +1,6 @@
|
|
1
|
+
xcode_mcp_server/__init__.py,sha256=toZJvPdmw_AXREqYjK7qmPmlT9934jqCUeGFNUvXV00,3028
|
2
|
+
xcode_mcp_server/__main__.py,sha256=Sonqk9OfjgBxqIa41t9qR477DJLDg9EIjYIwkdyhAhk,30952
|
3
|
+
xcode_mcp_server-1.0.5.dist-info/METADATA,sha256=v2yqmOeWOMlCjzsurEbiK-Bk2YJKkuequ8FzetYLSTo,5059
|
4
|
+
xcode_mcp_server-1.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
xcode_mcp_server-1.0.5.dist-info/entry_points.txt,sha256=u3sbPCAACGxesL3YtGByZRj6hXkL_FqncBmUMW1SEzo,59
|
6
|
+
xcode_mcp_server-1.0.5.dist-info/RECORD,,
|
@@ -1,6 +0,0 @@
|
|
1
|
-
xcode_mcp_server/__init__.py,sha256=P7r8UnSuuEfMndcdvf2Bt_9Emzrh4dgTkWtSRxFRbeE,474
|
2
|
-
xcode_mcp_server/__main__.py,sha256=vBieXwq5-8wUc4taEQ3LgXxbDl_lwcL5zg5Mq30fSwo,19079
|
3
|
-
xcode_mcp_server-1.0.4.dist-info/METADATA,sha256=aPfuF2rP7xgg_Z8j26APXzw6d9ZN-d1ntxWVNAHzBbs,4284
|
4
|
-
xcode_mcp_server-1.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
-
xcode_mcp_server-1.0.4.dist-info/entry_points.txt,sha256=u3sbPCAACGxesL3YtGByZRj6hXkL_FqncBmUMW1SEzo,59
|
6
|
-
xcode_mcp_server-1.0.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|