xcode-mcp-server 1.0.4__py3-none-any.whl → 1.0.6__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.
@@ -1,18 +1,80 @@
1
1
  """Xcode MCP Server - Model Context Protocol server for Xcode integration"""
2
2
 
3
+ __version__ = "1.0.6"
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
- # Initialize allowed folders
9
- __main__.ALLOWED_FOLDERS = __main__.get_allowed_folders()
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"Allowed folders: {__main__.ALLOWED_FOLDERS}", file=sys.stderr)
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
- __version__ = "1.0.4"
18
- __all__ = ["main"]
80
+ __all__ = ["main", "__version__"]
@@ -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
- else:
39
- print("Warning: Allowed folders was not specified.", file=sys.stderr)
40
- print("Set XCODEMCP_ALLOWED_FOLDERS environment variable to a colon-separated list of allowed folders.", file=sys.stderr)
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"Trying $HOME, {home}", file=sys.stderr)
43
- folder_list_str = home
59
+ print(f"Using default: $HOME = {home}", file=sys.stderr)
60
+ folders_to_process = [home]
44
61
 
45
- # Process the list
46
- folder_list = folder_list_str.split(":")
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
- # try to fetch folder list
94
- ALLOWED_FOLDERS = get_allowed_folders()
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
- # @mcp.tool()
177
- # def reinit_dirs() -> str:
178
- # """
179
- # Reinitialize the allowed folders.
180
- # """
181
- # global ALLOWED_FOLDERS
182
- # ALLOWED_FOLDERS = get_allowed_folders()
183
- # return f"Allowed folders reinitialized to: {ALLOWED_FOLDERS}"
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 searched.
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
- # Security check
211
- if not is_path_allowed(project_path):
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
- # Check if the path exists
216
- if os.path.exists(project_path):
217
- # Show the basic file structure
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
- #mdfind -onlyin /Users/andrew/Documents/ncc_source/cursor 'kMDItemFSName == "*.xcodeproj" || kMDItemFSName == "*.xcworkspace"'
220
- mdfindResult = subprocess.run(['mdfind', '-onlyin', project_path, 'kMDItemFSName == "*.xcodeproj" || kMDItemFSName == "*.xcworkspace"'],
221
- capture_output=True, text=True, check=True)
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
- return result
286
+ if result:
287
+ all_results.extend(result.split('\n'))
224
288
  except Exception as e:
225
- raise XCodeMCPError(f"Error listing files in {project_path}: {str(e)}")
226
- # return f"Error listing files in {project_path}: {str(e)}"
227
- else:
228
- raise InvalidParameterError(f"Project path does not exist: {project_path}")
229
- # return f"Project path does not exist: {project_path}"
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
- # return "Error: project_path cannot be empty"
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
- # Show the basic file structure
256
- try:
257
- result = subprocess.run(['find', project_path, '-type', 'f', '-name', '*.swift', '-o', '-name', '*.h', '-o', '-name', '*.m'],
258
- capture_output=True, text=True, check=True)
259
- files = result.stdout.strip().split('\n')
260
- if not files or (len(files) == 1 and files[0] == ''):
261
- raise InvalidParameterError(f"No source files found in {project_path}")
262
- # return f"No source files found in {project_path}"
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
- return f"Project at {project_path} contains {len(files)} source files:\n" + '\n'.join(files)
265
- except Exception as e:
266
- raise XCodeMCPError(f"Error listing files in {project_path}: {str(e)}")
267
- # return f"Error listing files in {project_path}: {str(e)}"
268
- else:
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
- # return f"Project path does not exist: {project_path}"
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
- # TODO: Implement build command using AppleScript or shell
298
- script = f'''
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
- end tell
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
- # Run the server if executed directly
784
+ # Main entry point for the server
535
785
  if __name__ == "__main__":
536
- # Initialize allowed folders
537
- ALLOWED_FOLDERS = get_allowed_folders()
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"Allowed folders: {ALLOWED_FOLDERS}", file=sys.stderr)
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.4
3
+ Version: 1.0.6
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,113 @@ If no allowed folders are specified, access will be restricted and tools will re
59
60
 
60
61
  ## Setup
61
62
 
62
- 1. Install dependencies:
63
+ 1. Configure Claude for Desktop:
63
64
 
64
- ```bash
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
- # Or using uv (recommended)
69
- uv pip install -r requirements.txt
70
- ```
67
+ ```brew install uv```
68
+
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:
71
73
 
72
- If you don't have pip installed, you can do:
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "xcode-mcp-server": {
78
+ "command": "uvx",
79
+ "args": [
80
+ "xcode-mcp-server"
81
+ ]
82
+ }
83
+ }
84
+ }
73
85
  ```
74
- brew install pip
86
+
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:
88
+
89
+ ```json
90
+ {
91
+ "mcpServers": {
92
+ "xcode-mcp-server": {
93
+ "command": "uvx",
94
+ "args": [
95
+ "xcode-mcp-server"
96
+ ],
97
+ "env": {
98
+ "XCODEMCP_ALLOWED_FOLDERS": "/Users/andrew/my_project:/Users/andrew/Documents/source"
99
+ }
100
+ }
101
+ }
102
+ }
75
103
  ```
76
104
 
77
- 2. Configure Claude for Desktop:
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:
111
+
112
+ claude mcp add --scope user --transport stdio `which uvx` xcode-mcp-server
113
+
114
+ 3. Add xcode-mcp-server to **Cursor AI**
78
115
 
79
- Open/create your Claude for Desktop configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` and add:
116
+ - Install Cursor, of course
117
+ - In Cursor, navigate to: Cursor --> Settings --> Cursor Settings
118
+ - Then choose 'Tools & Integrations'
119
+ - Tap the + button for 'New MCP Server'
120
+
121
+ The steps above will get you editing the file ~/.cursor/mcp.json, which you could also edit directly, if you prefer. Add a section for 'xcode-mcp-server' in the 'mcpServers' section - like this:
80
122
 
81
123
  ```json
82
124
  {
83
125
  "mcpServers": {
84
- "xcode": {
85
- "command": "python3",
126
+ "xcode-mcp-server": {
127
+ "command": "uvx",
86
128
  "args": [
87
- "/ABSOLUTE/PATH/TO/xcode_mcp.py"
129
+ "xcode-mcp-server"
130
+ ]
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ 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:
137
+
138
+ ```json
139
+ {
140
+ "mcpServers": {
141
+ "xcode-mcp-server": {
142
+ "command": "uvx",
143
+ "args": [
144
+ "xcode-mcp-server"
88
145
  ],
89
146
  "env": {
90
- "XCODEMCP_ALLOWED_FOLDERS": "/path/to/projects:/path/to/other/projects"
147
+ "XCODEMCP_ALLOWED_FOLDERS": "/Users/andrew/my_project:/Users/andrew/Documents/source"
91
148
  }
92
149
  }
93
150
  }
94
151
  }
95
152
  ```
96
153
 
97
- Replace `/ABSOLUTE/PATH/TO/xcode_mcp.py` with the actual path to your xcode_mcp.py file, and set appropriate allowed folders in the `env` section.
154
+ Be sure to hit Command-S to save the file.
155
+
156
+ If you omit the `env` section, access will default to your $HOME directory.
157
+
158
+ ### Test it out
159
+ - Open cursor to your favorite xcode project (just open the root folder of the project or git repo), and tell Cursor something like:
160
+
161
+ build this project using xcode-mcp-server
162
+
163
+ You'll get a permission prompt from Cursor and then one from macOS, and after that you should be off and running.
98
164
 
99
165
  ## Usage
100
166
 
101
167
  1. Open Xcode with a project
102
168
  2. Start Claude for Desktop
169
+ - If xcode-mcp-server failed to initialize properly, you'll see errors
103
170
  3. Look for the hammer icon to find available Xcode tools
104
171
  4. Use natural language to interact with Xcode, for example:
105
172
  - "Build the project at /path/to/MyProject.xcodeproj"
@@ -148,6 +215,5 @@ When testing in the MCP Inspector, provide input values as quoted strings:
148
215
 
149
216
  ## Limitations
150
217
 
151
- - Runtime output retrieval is not yet implemented
152
218
  - Project hierarchy is a simple file listing implementation
153
219
  - AppleScript syntax may need adjustments for specific Xcode versions # xcode-mcp-server
@@ -0,0 +1,6 @@
1
+ xcode_mcp_server/__init__.py,sha256=-yQHFLCSDJZJS02NJ6mpGaL0PGEk2Zgct4ozciGy3Ls,3028
2
+ xcode_mcp_server/__main__.py,sha256=Sonqk9OfjgBxqIa41t9qR477DJLDg9EIjYIwkdyhAhk,30952
3
+ xcode_mcp_server-1.0.6.dist-info/METADATA,sha256=F2LFLWTjrey2gau1GoJBrop7pGsQOiH0dazp5ywheZA,6592
4
+ xcode_mcp_server-1.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ xcode_mcp_server-1.0.6.dist-info/entry_points.txt,sha256=u3sbPCAACGxesL3YtGByZRj6hXkL_FqncBmUMW1SEzo,59
6
+ xcode_mcp_server-1.0.6.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,,