xcode-mcp-server 1.0.0__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.
@@ -0,0 +1,21 @@
|
|
1
|
+
"""Xcode MCP Server - Model Context Protocol server for Xcode integration"""
|
2
|
+
|
3
|
+
from .__main__ import mcp, ALLOWED_FOLDERS, get_allowed_folders
|
4
|
+
|
5
|
+
def main():
|
6
|
+
"""Entry point for the xcode-mcp-server command"""
|
7
|
+
import sys
|
8
|
+
from .__main__ import mcp, ALLOWED_FOLDERS, get_allowed_folders
|
9
|
+
|
10
|
+
# Initialize allowed folders
|
11
|
+
global ALLOWED_FOLDERS
|
12
|
+
ALLOWED_FOLDERS = get_allowed_folders()
|
13
|
+
|
14
|
+
# Debug info
|
15
|
+
print(f"Allowed folders: {ALLOWED_FOLDERS}", file=sys.stderr)
|
16
|
+
|
17
|
+
# Run the server
|
18
|
+
mcp.run()
|
19
|
+
|
20
|
+
__version__ = "1.0.0"
|
21
|
+
__all__ = ["mcp", "main"]
|
@@ -0,0 +1,541 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import subprocess
|
5
|
+
import json
|
6
|
+
from typing import Optional, Dict, List, Any, Tuple, Set
|
7
|
+
from dataclasses import dataclass
|
8
|
+
|
9
|
+
from mcp.server.fastmcp import FastMCP, Context
|
10
|
+
|
11
|
+
# Global variables for allowed folders
|
12
|
+
ALLOWED_FOLDERS: Set[str] = set()
|
13
|
+
|
14
|
+
class XCodeMCPError(Exception):
|
15
|
+
def __init__(self, message, code=None):
|
16
|
+
self.message = message
|
17
|
+
self.code = code
|
18
|
+
super().__init__(self.message)
|
19
|
+
|
20
|
+
class AccessDeniedError(XCodeMCPError):
|
21
|
+
pass
|
22
|
+
|
23
|
+
class InvalidParameterError(XCodeMCPError):
|
24
|
+
pass
|
25
|
+
|
26
|
+
def get_allowed_folders() -> Set[str]:
|
27
|
+
"""
|
28
|
+
Get the allowed folders from environment variable.
|
29
|
+
Validates that paths are absolute, exist, and are directories.
|
30
|
+
"""
|
31
|
+
allowed_folders = set()
|
32
|
+
|
33
|
+
# Get from environment variable
|
34
|
+
folder_list_str = os.environ.get("XCODEMCP_ALLOWED_FOLDERS", "monkies")
|
35
|
+
|
36
|
+
if folder_list_str:
|
37
|
+
print(f"Using allowed folders from environment: {folder_list_str}", file=sys.stderr)
|
38
|
+
else:
|
39
|
+
print("Warning: No allowed folders specified. Access will be restricted.", file=sys.stderr)
|
40
|
+
print("Set XCODEMCP_ALLOWED_FOLDERS environment variable to specify allowed folders.", file=sys.stderr)
|
41
|
+
return allowed_folders
|
42
|
+
|
43
|
+
# Process the list
|
44
|
+
folder_list = folder_list_str.split(":")
|
45
|
+
for folder in folder_list:
|
46
|
+
folder = folder.rstrip("/") # Normalize by removing trailing slash
|
47
|
+
|
48
|
+
# Skip empty entries
|
49
|
+
if not folder:
|
50
|
+
print(f"Warning: Skipping empty folder entry", file=sys.stderr)
|
51
|
+
continue
|
52
|
+
|
53
|
+
# Check if path is absolute
|
54
|
+
if not os.path.isabs(folder):
|
55
|
+
print(f"Warning: Skipping non-absolute path: {folder}", file=sys.stderr)
|
56
|
+
continue
|
57
|
+
|
58
|
+
# Check if path contains ".." components
|
59
|
+
if ".." in folder:
|
60
|
+
print(f"Warning: Skipping path with '..' components: {folder}", file=sys.stderr)
|
61
|
+
continue
|
62
|
+
|
63
|
+
# Check if path exists and is a directory
|
64
|
+
if not os.path.exists(folder):
|
65
|
+
print(f"Warning: Skipping non-existent path: {folder}", file=sys.stderr)
|
66
|
+
continue
|
67
|
+
|
68
|
+
if not os.path.isdir(folder):
|
69
|
+
print(f"Warning: Skipping non-directory path: {folder}", file=sys.stderr)
|
70
|
+
continue
|
71
|
+
|
72
|
+
# Add to allowed folders
|
73
|
+
allowed_folders.add(folder)
|
74
|
+
print(f"Added allowed folder: {folder}", file=sys.stderr)
|
75
|
+
|
76
|
+
return allowed_folders
|
77
|
+
|
78
|
+
def is_path_allowed(project_path: str) -> bool:
|
79
|
+
"""
|
80
|
+
Check if a project path is allowed based on the allowed folders list.
|
81
|
+
Path must be a subfolder or direct match of an allowed folder.
|
82
|
+
"""
|
83
|
+
|
84
|
+
global ALLOWED_FOLDERS
|
85
|
+
if not project_path:
|
86
|
+
print(f"Warning: not project_path: {project_path}", file=sys.stderr)
|
87
|
+
return False
|
88
|
+
|
89
|
+
# If no allowed folders are specified, nothing is allowed
|
90
|
+
if not ALLOWED_FOLDERS:
|
91
|
+
# try to fetch folder list
|
92
|
+
ALLOWED_FOLDERS = get_allowed_folders()
|
93
|
+
if not ALLOWED_FOLDERS:
|
94
|
+
print(f"Warning: not ALLOWED_FOLDERS: {', '.join(ALLOWED_FOLDERS)}", file=sys.stderr)
|
95
|
+
return False
|
96
|
+
|
97
|
+
# Normalize the path
|
98
|
+
project_path = os.path.abspath(project_path).rstrip("/")
|
99
|
+
|
100
|
+
# Check if path is in allowed folders
|
101
|
+
print(f"Warning: Normalized project_path: {project_path}", file=sys.stderr)
|
102
|
+
for allowed_folder in ALLOWED_FOLDERS:
|
103
|
+
# Direct match
|
104
|
+
if project_path == allowed_folder:
|
105
|
+
print(f"direct match to {allowed_folder}", file=sys.stderr)
|
106
|
+
return True
|
107
|
+
|
108
|
+
# Path is a subfolder
|
109
|
+
if project_path.startswith(allowed_folder + "/"):
|
110
|
+
print(f"Match to startswith {allowed_folder}", file=sys.stderr)
|
111
|
+
return True
|
112
|
+
print(f"no match of {project_path} with allowed folder {allowed_folder}", file=sys.stderr)
|
113
|
+
return False
|
114
|
+
|
115
|
+
# Initialize the MCP server
|
116
|
+
mcp = FastMCP("Xcode MCP Server")
|
117
|
+
|
118
|
+
# Helper functions for Xcode interaction
|
119
|
+
def get_frontmost_project() -> str:
|
120
|
+
"""
|
121
|
+
Get the path to the frontmost Xcode project/workspace.
|
122
|
+
Returns empty string if no project is open.
|
123
|
+
"""
|
124
|
+
script = '''
|
125
|
+
tell application "Xcode"
|
126
|
+
if it is running then
|
127
|
+
try
|
128
|
+
tell application "System Events"
|
129
|
+
tell process "Xcode"
|
130
|
+
set frontWindow to name of front window
|
131
|
+
end tell
|
132
|
+
end tell
|
133
|
+
|
134
|
+
set docPath to ""
|
135
|
+
try
|
136
|
+
set docPath to path of document 1
|
137
|
+
end try
|
138
|
+
|
139
|
+
return docPath
|
140
|
+
on error errMsg
|
141
|
+
return "ERROR: " & errMsg
|
142
|
+
end try
|
143
|
+
else
|
144
|
+
return "ERROR: Xcode is not running"
|
145
|
+
end if
|
146
|
+
end tell
|
147
|
+
'''
|
148
|
+
try:
|
149
|
+
result = subprocess.run(['osascript', '-e', script],
|
150
|
+
capture_output=True, text=True, check=True)
|
151
|
+
output = result.stdout.strip()
|
152
|
+
|
153
|
+
# Check if we got an error message from our AppleScript
|
154
|
+
if output.startswith("ERROR:"):
|
155
|
+
print(f"AppleScript error: {output}")
|
156
|
+
return ""
|
157
|
+
|
158
|
+
return output
|
159
|
+
except subprocess.CalledProcessError as e:
|
160
|
+
print(f"Error executing AppleScript: {e.stderr}")
|
161
|
+
return ""
|
162
|
+
|
163
|
+
def run_applescript(script: str) -> Tuple[bool, str]:
|
164
|
+
"""Run an AppleScript and return success status and output"""
|
165
|
+
try:
|
166
|
+
result = subprocess.run(['osascript', '-e', script],
|
167
|
+
capture_output=True, text=True, check=True)
|
168
|
+
return True, result.stdout.strip()
|
169
|
+
except subprocess.CalledProcessError as e:
|
170
|
+
return False, e.stderr.strip()
|
171
|
+
|
172
|
+
# MCP Tools for Xcode
|
173
|
+
|
174
|
+
# @mcp.tool()
|
175
|
+
# def reinit_dirs() -> str:
|
176
|
+
# """
|
177
|
+
# Reinitialize the allowed folders.
|
178
|
+
# """
|
179
|
+
# global ALLOWED_FOLDERS
|
180
|
+
# ALLOWED_FOLDERS = get_allowed_folders()
|
181
|
+
# return f"Allowed folders reinitialized to: {ALLOWED_FOLDERS}"
|
182
|
+
|
183
|
+
|
184
|
+
@mcp.tool()
|
185
|
+
def get_xcode_projects(search_path: str) -> str:
|
186
|
+
"""
|
187
|
+
Search the given search_path to find .xcodeproj (Xcode project) and
|
188
|
+
.xcworkspace (Xcode workspace) paths. If the search_path is empty,
|
189
|
+
all paths to which this tool has been granted access are searched.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
search_path: Path to searched.
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
A string which is a newline-separated list of .xcodeproj and
|
196
|
+
.xcworkspace paths found. If none are found, returns an empty string.
|
197
|
+
"""
|
198
|
+
|
199
|
+
|
200
|
+
project_path = search_path
|
201
|
+
|
202
|
+
# Validate input
|
203
|
+
if not search_path or search_path.strip() == "":
|
204
|
+
project_path = "/Users/andrew/Documents/ncc_source"
|
205
|
+
# return "Error: project_path cannot be empty"
|
206
|
+
|
207
|
+
|
208
|
+
# Security check
|
209
|
+
if not is_path_allowed(project_path):
|
210
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
211
|
+
# return f"Error: Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable."
|
212
|
+
|
213
|
+
# Check if the path exists
|
214
|
+
if os.path.exists(project_path):
|
215
|
+
# Show the basic file structure
|
216
|
+
try:
|
217
|
+
#mdfind -onlyin /Users/andrew/Documents/ncc_source/cursor 'kMDItemFSName == "*.xcodeproj" || kMDItemFSName == "*.xcworkspace"'
|
218
|
+
mdfindResult = subprocess.run(['mdfind', '-onlyin', project_path, 'kMDItemFSName == "*.xcodeproj" || kMDItemFSName == "*.xcworkspace"'],
|
219
|
+
capture_output=True, text=True, check=True)
|
220
|
+
result = mdfindResult.stdout.strip()
|
221
|
+
return result
|
222
|
+
except Exception as e:
|
223
|
+
raise XCodeMCPError(f"Error listing files in {project_path}: {str(e)}")
|
224
|
+
# return f"Error listing files in {project_path}: {str(e)}"
|
225
|
+
else:
|
226
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
227
|
+
# return f"Project path does not exist: {project_path}"
|
228
|
+
|
229
|
+
|
230
|
+
@mcp.tool()
|
231
|
+
def get_project_hierarchy(project_path: str) -> str:
|
232
|
+
"""
|
233
|
+
Get the hierarchy of the specified Xcode project or workspace.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
project_path: Path to an Xcode project/workspace directory.
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
A string representation of the project hierarchy
|
240
|
+
"""
|
241
|
+
# Validate input
|
242
|
+
if not project_path or project_path.strip() == "":
|
243
|
+
raise InvalidParameterError("project_path cannot be empty")
|
244
|
+
# return "Error: project_path cannot be empty"
|
245
|
+
|
246
|
+
# Security check
|
247
|
+
if not is_path_allowed(project_path):
|
248
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
249
|
+
# return f"Error: Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable."
|
250
|
+
|
251
|
+
# Check if the path exists
|
252
|
+
if os.path.exists(project_path):
|
253
|
+
# Show the basic file structure
|
254
|
+
try:
|
255
|
+
result = subprocess.run(['find', project_path, '-type', 'f', '-name', '*.swift', '-o', '-name', '*.h', '-o', '-name', '*.m'],
|
256
|
+
capture_output=True, text=True, check=True)
|
257
|
+
files = result.stdout.strip().split('\n')
|
258
|
+
if not files or (len(files) == 1 and files[0] == ''):
|
259
|
+
raise InvalidParameterError(f"No source files found in {project_path}")
|
260
|
+
# return f"No source files found in {project_path}"
|
261
|
+
|
262
|
+
return f"Project at {project_path} contains {len(files)} source files:\n" + '\n'.join(files)
|
263
|
+
except Exception as e:
|
264
|
+
raise XCodeMCPError(f"Error listing files in {project_path}: {str(e)}")
|
265
|
+
# return f"Error listing files in {project_path}: {str(e)}"
|
266
|
+
else:
|
267
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
268
|
+
# return f"Project path does not exist: {project_path}"
|
269
|
+
|
270
|
+
@mcp.tool()
|
271
|
+
def build_project(project_path: str,
|
272
|
+
scheme: str) -> str:
|
273
|
+
"""
|
274
|
+
Build the specified Xcode project or workspace.
|
275
|
+
|
276
|
+
Args:
|
277
|
+
project_path: Path to an Xcode project/workspace directory.
|
278
|
+
scheme: Name of the scheme to build.
|
279
|
+
|
280
|
+
Returns:
|
281
|
+
On success, returns "Build succeeded with 0 errors."
|
282
|
+
On failure, returns the first (up to) 100 error lines from the build log.
|
283
|
+
"""
|
284
|
+
# Validate input
|
285
|
+
if not project_path or project_path.strip() == "":
|
286
|
+
raise InvalidParameterError("project_path cannot be empty")
|
287
|
+
|
288
|
+
# Security check
|
289
|
+
if not is_path_allowed(project_path):
|
290
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
291
|
+
|
292
|
+
if not os.path.exists(project_path):
|
293
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
294
|
+
|
295
|
+
# TODO: Implement build command using AppleScript or shell
|
296
|
+
script = f'''
|
297
|
+
set projectPath to "{project_path}"
|
298
|
+
set schemeName to "{scheme}"
|
299
|
+
|
300
|
+
--
|
301
|
+
-- Then run with: osascript <thisfilename>
|
302
|
+
--
|
303
|
+
tell application "Xcode"
|
304
|
+
-- 1. Open the project file
|
305
|
+
open projectPath
|
306
|
+
|
307
|
+
-- 2. Get the workspace document
|
308
|
+
set workspaceDoc to first workspace document whose path is projectPath
|
309
|
+
|
310
|
+
-- 3. Wait for it to load (timeout after ~30 seconds)
|
311
|
+
repeat 60 times
|
312
|
+
if loaded of workspaceDoc is true then exit repeat
|
313
|
+
delay 0.5
|
314
|
+
end repeat
|
315
|
+
|
316
|
+
if loaded of workspaceDoc is false then
|
317
|
+
error "Xcode workspace did not load in time."
|
318
|
+
end if
|
319
|
+
|
320
|
+
-- 4. Set the active scheme
|
321
|
+
set active scheme of workspaceDoc to (first scheme of workspaceDoc whose name is schemeName)
|
322
|
+
|
323
|
+
-- 5. Build
|
324
|
+
set actionResult to build workspaceDoc
|
325
|
+
|
326
|
+
-- 6. Wait for completion
|
327
|
+
repeat
|
328
|
+
if completed of actionResult is true then exit repeat
|
329
|
+
delay 0.5
|
330
|
+
end repeat
|
331
|
+
|
332
|
+
-- 7. Check result
|
333
|
+
set buildStatus to status of actionResult
|
334
|
+
if buildStatus is succeeded then
|
335
|
+
-- display dialog "Build succeeded"
|
336
|
+
return "Build succeeded."
|
337
|
+
else
|
338
|
+
return build log of actionResult
|
339
|
+
end if
|
340
|
+
|
341
|
+
end tell
|
342
|
+
'''
|
343
|
+
|
344
|
+
success, output = run_applescript(script)
|
345
|
+
|
346
|
+
if success:
|
347
|
+
if output == "Build succeeded.":
|
348
|
+
return "Build succeeded with 0 errors."
|
349
|
+
else:
|
350
|
+
output_lines = output.split("\n")
|
351
|
+
error_lines = [line for line in output_lines if "error" in line]
|
352
|
+
|
353
|
+
# Limit to first 100 error lines
|
354
|
+
if len(error_lines) > 100:
|
355
|
+
error_lines = error_lines[:100]
|
356
|
+
error_lines.append("... (truncated to first 100 error lines)")
|
357
|
+
|
358
|
+
error_list = "\n".join(error_lines)
|
359
|
+
return f"Build failed with errors:\n{error_list}"
|
360
|
+
else:
|
361
|
+
raise XCodeMCPError(f"Build failed to start for scheme {scheme} in project {project_path}: {output}")
|
362
|
+
|
363
|
+
@mcp.tool()
|
364
|
+
def run_project(project_path: str,
|
365
|
+
scheme: Optional[str] = None) -> str:
|
366
|
+
"""
|
367
|
+
Run the specified Xcode project or workspace.
|
368
|
+
|
369
|
+
Args:
|
370
|
+
project_path: Path to an Xcode project/workspace directory.
|
371
|
+
scheme: Optional scheme to run. If not provided, uses the active scheme.
|
372
|
+
|
373
|
+
Returns:
|
374
|
+
Output message
|
375
|
+
"""
|
376
|
+
# Validate input
|
377
|
+
if not project_path or project_path.strip() == "":
|
378
|
+
raise InvalidParameterError("project_path cannot be empty")
|
379
|
+
|
380
|
+
# Security check
|
381
|
+
if not is_path_allowed(project_path):
|
382
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
383
|
+
|
384
|
+
if not os.path.exists(project_path):
|
385
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
386
|
+
|
387
|
+
# TODO: Implement run command using AppleScript
|
388
|
+
script = f'''
|
389
|
+
tell application "Xcode"
|
390
|
+
open "{project_path}"
|
391
|
+
delay 1
|
392
|
+
set frontWindow to front window
|
393
|
+
tell frontWindow
|
394
|
+
set currentWorkspace to workspace
|
395
|
+
run currentWorkspace
|
396
|
+
end tell
|
397
|
+
end tell
|
398
|
+
'''
|
399
|
+
|
400
|
+
success, output = run_applescript(script)
|
401
|
+
|
402
|
+
if success:
|
403
|
+
return "Run started successfully"
|
404
|
+
else:
|
405
|
+
raise XCodeMCPError(f"Run failed to start: {output}")
|
406
|
+
|
407
|
+
@mcp.tool()
|
408
|
+
def get_build_errors(project_path: str) -> str:
|
409
|
+
"""
|
410
|
+
Get the build errors for the specified Xcode project or workspace.
|
411
|
+
|
412
|
+
Args:
|
413
|
+
project_path: Path to an Xcode project/workspace directory.
|
414
|
+
|
415
|
+
Returns:
|
416
|
+
A string containing the build errors or a message if there are none
|
417
|
+
"""
|
418
|
+
# Validate input
|
419
|
+
if not project_path or project_path.strip() == "":
|
420
|
+
raise InvalidParameterError("project_path cannot be empty")
|
421
|
+
|
422
|
+
# Security check
|
423
|
+
if not is_path_allowed(project_path):
|
424
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
425
|
+
|
426
|
+
if not os.path.exists(project_path):
|
427
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
428
|
+
|
429
|
+
# TODO: Implement error retrieval using AppleScript or by parsing logs
|
430
|
+
script = f'''
|
431
|
+
tell application "Xcode"
|
432
|
+
open "{project_path}"
|
433
|
+
delay 1
|
434
|
+
set frontWindow to front window
|
435
|
+
tell frontWindow
|
436
|
+
set currentWorkspace to workspace
|
437
|
+
set issuesList to get issues
|
438
|
+
set issuesText to ""
|
439
|
+
set issueCount to 0
|
440
|
+
|
441
|
+
repeat with anIssue in issuesList
|
442
|
+
if issueCount ≥ 100 then exit repeat
|
443
|
+
set issuesText to issuesText & "- " & message of anIssue & "\n"
|
444
|
+
set issueCount to issueCount + 1
|
445
|
+
end repeat
|
446
|
+
|
447
|
+
return issuesText
|
448
|
+
end tell
|
449
|
+
end tell
|
450
|
+
'''
|
451
|
+
|
452
|
+
# This script syntax may need to be adjusted based on actual AppleScript capabilities
|
453
|
+
success, output = run_applescript(script)
|
454
|
+
|
455
|
+
if success and output:
|
456
|
+
return output
|
457
|
+
elif success:
|
458
|
+
return "No build errors found."
|
459
|
+
else:
|
460
|
+
raise XCodeMCPError(f"Failed to retrieve build errors: {output}")
|
461
|
+
|
462
|
+
@mcp.tool()
|
463
|
+
def clean_project(project_path: str) -> str:
|
464
|
+
"""
|
465
|
+
Clean the specified Xcode project or workspace.
|
466
|
+
|
467
|
+
Args:
|
468
|
+
project_path: Path to an Xcode project/workspace directory.
|
469
|
+
|
470
|
+
Returns:
|
471
|
+
Output message
|
472
|
+
"""
|
473
|
+
# Validate input
|
474
|
+
if not project_path or project_path.strip() == "":
|
475
|
+
raise InvalidParameterError("project_path cannot be empty")
|
476
|
+
|
477
|
+
# Security check
|
478
|
+
if not is_path_allowed(project_path):
|
479
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
480
|
+
|
481
|
+
if not os.path.exists(project_path):
|
482
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
483
|
+
|
484
|
+
# TODO: Implement clean command using AppleScript
|
485
|
+
script = f'''
|
486
|
+
tell application "Xcode"
|
487
|
+
open "{project_path}"
|
488
|
+
delay 1
|
489
|
+
set frontWindow to front window
|
490
|
+
tell frontWindow
|
491
|
+
set currentWorkspace to workspace
|
492
|
+
clean currentWorkspace
|
493
|
+
end tell
|
494
|
+
end tell
|
495
|
+
'''
|
496
|
+
|
497
|
+
success, output = run_applescript(script)
|
498
|
+
|
499
|
+
if success:
|
500
|
+
return "Clean completed successfully"
|
501
|
+
else:
|
502
|
+
raise XCodeMCPError(f"Clean failed: {output}")
|
503
|
+
|
504
|
+
@mcp.tool()
|
505
|
+
def get_runtime_output(project_path: str,
|
506
|
+
max_lines: int = 100) -> str:
|
507
|
+
"""
|
508
|
+
Get the runtime output from the console for the specified Xcode project.
|
509
|
+
|
510
|
+
Args:
|
511
|
+
project_path: Path to an Xcode project/workspace directory.
|
512
|
+
max_lines: Maximum number of lines to retrieve. Defaults to 100.
|
513
|
+
|
514
|
+
Returns:
|
515
|
+
Console output as a string
|
516
|
+
"""
|
517
|
+
# Validate input
|
518
|
+
if not project_path or project_path.strip() == "":
|
519
|
+
raise InvalidParameterError("project_path cannot be empty")
|
520
|
+
|
521
|
+
# Security check
|
522
|
+
if not is_path_allowed(project_path):
|
523
|
+
raise AccessDeniedError(f"Access to path '{project_path}' is not allowed. Set XCODEMCP_ALLOWED_FOLDERS environment variable.")
|
524
|
+
|
525
|
+
if not os.path.exists(project_path):
|
526
|
+
raise InvalidParameterError(f"Project path does not exist: {project_path}")
|
527
|
+
|
528
|
+
# TODO: Implement console output retrieval
|
529
|
+
# This is a placeholder as you mentioned this functionality isn't available yet
|
530
|
+
raise XCodeMCPError("Runtime output retrieval not yet implemented")
|
531
|
+
|
532
|
+
# Run the server if executed directly
|
533
|
+
if __name__ == "__main__":
|
534
|
+
# Initialize allowed folders
|
535
|
+
ALLOWED_FOLDERS = get_allowed_folders()
|
536
|
+
|
537
|
+
# Debug info
|
538
|
+
print(f"Allowed folders: {ALLOWED_FOLDERS}", file=sys.stderr)
|
539
|
+
|
540
|
+
# Run the server
|
541
|
+
mcp.run()
|
@@ -0,0 +1,153 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: xcode-mcp-server
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: MCP server for Xcode integration
|
5
|
+
Project-URL: Homepage, https://github.com/yourusername/xcode-mcp-server
|
6
|
+
Project-URL: Repository, https://github.com/yourusername/xcode-mcp-server
|
7
|
+
Project-URL: Issues, https://github.com/yourusername/xcode-mcp-server/issues
|
8
|
+
Author-email: Your Name <your.email@example.com>
|
9
|
+
License: MIT
|
10
|
+
Keywords: mcp,server,xcode
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
12
|
+
Classifier: Intended Audience :: Developers
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
20
|
+
Requires-Python: >=3.8
|
21
|
+
Requires-Dist: mcp[cli]>=1.2.0
|
22
|
+
Description-Content-Type: text/markdown
|
23
|
+
|
24
|
+
# Xcode MCP Server
|
25
|
+
|
26
|
+
An MCP (Model Context Protocol) server for controlling and interacting with Xcode from AI assistants like Claude.
|
27
|
+
|
28
|
+
## Features
|
29
|
+
|
30
|
+
- Get project hierarchy
|
31
|
+
- Build and run projects
|
32
|
+
- Retrieve build errors
|
33
|
+
- Get runtime output (placeholder)
|
34
|
+
- Clean projects
|
35
|
+
|
36
|
+
## Security
|
37
|
+
|
38
|
+
The server implements path-based security to prevent unauthorized access to files outside of allowed directories:
|
39
|
+
|
40
|
+
- You must specify allowed folders using the environment variable:
|
41
|
+
- `XCODEMCP_ALLOWED_FOLDERS=/path1:/path2:/path3`
|
42
|
+
|
43
|
+
Security requirements:
|
44
|
+
- All paths must be absolute (starting with /)
|
45
|
+
- No path components with `..` are allowed
|
46
|
+
- All paths must exist and be directories
|
47
|
+
|
48
|
+
Example:
|
49
|
+
```bash
|
50
|
+
# Set the environment variable
|
51
|
+
export XCODEMCP_ALLOWED_FOLDERS=/Users/username/Projects:/Users/username/checkouts
|
52
|
+
python3 xcode_mcp.py
|
53
|
+
|
54
|
+
# Or inline with the MCP command
|
55
|
+
XCODEMCP_ALLOWED_FOLDERS=/Users/username/Projects mcp dev xcode_mcp.py
|
56
|
+
```
|
57
|
+
|
58
|
+
If no allowed folders are specified, access will be restricted and tools will return error messages.
|
59
|
+
|
60
|
+
## Setup
|
61
|
+
|
62
|
+
1. Install dependencies:
|
63
|
+
|
64
|
+
```bash
|
65
|
+
# Using pip
|
66
|
+
pip install -r requirements.txt
|
67
|
+
|
68
|
+
# Or using uv (recommended)
|
69
|
+
uv pip install -r requirements.txt
|
70
|
+
```
|
71
|
+
|
72
|
+
If you don't have pip installed, you can do:
|
73
|
+
```
|
74
|
+
brew install pip
|
75
|
+
```
|
76
|
+
|
77
|
+
2. Configure Claude for Desktop:
|
78
|
+
|
79
|
+
Open/create your Claude for Desktop configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` and add:
|
80
|
+
|
81
|
+
```json
|
82
|
+
{
|
83
|
+
"mcpServers": {
|
84
|
+
"xcode": {
|
85
|
+
"command": "python3",
|
86
|
+
"args": [
|
87
|
+
"/ABSOLUTE/PATH/TO/xcode_mcp.py"
|
88
|
+
],
|
89
|
+
"env": {
|
90
|
+
"XCODEMCP_ALLOWED_FOLDERS": "/path/to/projects:/path/to/other/projects"
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
```
|
96
|
+
|
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.
|
98
|
+
|
99
|
+
## Usage
|
100
|
+
|
101
|
+
1. Open Xcode with a project
|
102
|
+
2. Start Claude for Desktop
|
103
|
+
3. Look for the hammer icon to find available Xcode tools
|
104
|
+
4. Use natural language to interact with Xcode, for example:
|
105
|
+
- "Build the project at /path/to/MyProject.xcodeproj"
|
106
|
+
- "Run the app in /path/to/MyProject"
|
107
|
+
- "What build errors are there in /path/to/MyProject.xcodeproj?"
|
108
|
+
- "Clean the project at /path/to/MyProject"
|
109
|
+
|
110
|
+
### Parameter Format
|
111
|
+
|
112
|
+
All tools require a `project_path` parameter pointing to an Xcode project/workspace directory:
|
113
|
+
|
114
|
+
```
|
115
|
+
"/path/to/your/project.xcodeproj"
|
116
|
+
```
|
117
|
+
|
118
|
+
or
|
119
|
+
|
120
|
+
```
|
121
|
+
"/path/to/your/project"
|
122
|
+
```
|
123
|
+
|
124
|
+
## Development
|
125
|
+
|
126
|
+
The server is built with the MCP Python SDK and uses AppleScript to communicate with Xcode.
|
127
|
+
|
128
|
+
To test the server locally without Claude, use:
|
129
|
+
|
130
|
+
```bash
|
131
|
+
# Set the environment variable first
|
132
|
+
export XCODEMCP_ALLOWED_FOLDERS=/Users/username/Projects
|
133
|
+
mcp dev xcode_mcp.py
|
134
|
+
|
135
|
+
# Or inline with the command
|
136
|
+
XCODEMCP_ALLOWED_FOLDERS=/Users/username/Projects mcp dev xcode_mcp.py
|
137
|
+
```
|
138
|
+
|
139
|
+
This will open the MCP Inspector interface where you can test the tools directly.
|
140
|
+
|
141
|
+
### Testing in MCP Inspector
|
142
|
+
|
143
|
+
When testing in the MCP Inspector, provide input values as quoted strings:
|
144
|
+
|
145
|
+
```
|
146
|
+
"/Users/username/Projects/MyApp"
|
147
|
+
```
|
148
|
+
|
149
|
+
## Limitations
|
150
|
+
|
151
|
+
- Runtime output retrieval is not yet implemented
|
152
|
+
- Project hierarchy is a simple file listing implementation
|
153
|
+
- AppleScript syntax may need adjustments for specific Xcode versions # xcode-mcp-server
|
@@ -0,0 +1,6 @@
|
|
1
|
+
xcode_mcp_server/__init__.py,sha256=isXCmR_ASdjNbBCThfuDBh5mukoOQLlhYZIxZci5l6E,578
|
2
|
+
xcode_mcp_server/__main__.py,sha256=9V6gTwUEYPkf5bVtCjNyjw3_YF-zpVhOYbzdFJTTWAk,18997
|
3
|
+
xcode_mcp_server-1.0.0.dist-info/METADATA,sha256=3G4TaoqRHu8o98cK3Qd3Tj4vIR3bPK76UcWCQRZeRrw,4281
|
4
|
+
xcode_mcp_server-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
xcode_mcp_server-1.0.0.dist-info/entry_points.txt,sha256=u3sbPCAACGxesL3YtGByZRj6hXkL_FqncBmUMW1SEzo,59
|
6
|
+
xcode_mcp_server-1.0.0.dist-info/RECORD,,
|