Xcode26.3#125
Conversation
# Conflicts: # App/InjectionNext/Info.plist
c1bfa8d to
6f045d4
Compare
77f94c1 to
543d978
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends the client↔server handshake to send the running app’s executable path (so the server can derive an app name), adjusts the SwiftPM target layout/module naming, and tweaks the file-watcher pattern as part of the “rework log parsing” effort noted in the PR description.
Changes:
- Add a new
InjectionResponsecase (InjectionExecutable) and send the client executable path during connection. - Update the server to consume the new response and set
Reloader.appNamebased on the executable. - Rename the SwiftPM target to
InjectionBundleand update project/version metadata; extend watcher regex to include.o.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Sources/InjectionNextC/include/InjectionClient.h | Adds a new response enum case used in the socket protocol. |
| Sources/InjectionNext/InjectionNext.swift | Sends the executable path to the server as part of initial connection metadata. |
| Package.swift | Renames the SwiftPM target/module to InjectionBundle while keeping product name InjectionNext. |
| App/InjectionNext/InjectionServer.swift | Handles the new .executable response and derives Reloader.appName. |
| App/InjectionNext/InjectionHybrid.swift | Expands file-watcher injectable pattern to include .o files. |
| App/InjectionNext/Info.plist | Increments build number. |
| App/InjectionNext.xcodeproj/project.pbxproj | Bumps marketing version and sets module name for InjectionBundle target. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
bc96a65 to
bcaa6d7
Compare
8caebae to
6136594
Compare
91d4b4e to
31f5348
Compare
fb084c6 to
2186111
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
* Add MCP server for AI-driven control of InjectionNext - ControlServer: local TCP server (localhost:8919) that exposes app actions as JSON commands (watch project, enable devices, get status, etc.) - LogBuffer: ring buffer capturing injection logs, compilation errors, and file watcher activity for AI consumption - MCP server (Node.js): 13 tools exposing InjectionNext to AI agents via the Model Context Protocol (get_status, watch_project, get_logs, etc.) - Hook log() and InjectionServer.log/error into LogBuffer for real-time debug console access Made-with: Cursor * Update README.md * Address review: opt-in mcpServer default, harden ControlServer - Add Defaults.mcpServer boolean (set via `defaults write`); ControlServer and LogBuffer only start when enabled. - Make LogBuffer.shared optional; all call sites use optional chaining. - Clamp get_logs limit to 0...500 to prevent negative/overflow traps. - Add 64KB max request size guard on TCP read loop. - Add zod as direct dependency and engines field in package.json. - Update README with enable step and correct Node.js version. Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.
Files not reviewed (1)
- mcp-server/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private func sendResponse(_ sock: Int32, success: Bool, data: [String: Any]? = nil, error: String? = nil) { | ||
| var response: [String: Any] = ["success": success] | ||
| if let error = error { response["error"] = error } | ||
| if let data = data { response["data"] = data } | ||
| guard let jsonData = try? JSONSerialization.data(withJSONObject: response), | ||
| let jsonStr = String(data: jsonData, encoding: .utf8) else { return } | ||
| let line = jsonStr + "\n" | ||
| _ = line.withCString { ptr in | ||
| send(sock, ptr, strlen(ptr), 0) | ||
| } |
There was a problem hiding this comment.
sendResponse writes the entire JSON line with a single send() call. send() is allowed to return after writing only a prefix of the buffer (partial write), which can truncate responses and cause the Node client to fail JSON parsing. Consider looping until all bytes are written (and handle EPIPE/ECONNRESET) before returning.
| private func sendResponse(_ sock: Int32, success: Bool, data: [String: Any]? = nil, error: String? = nil) { | |
| var response: [String: Any] = ["success": success] | |
| if let error = error { response["error"] = error } | |
| if let data = data { response["data"] = data } | |
| guard let jsonData = try? JSONSerialization.data(withJSONObject: response), | |
| let jsonStr = String(data: jsonData, encoding: .utf8) else { return } | |
| let line = jsonStr + "\n" | |
| _ = line.withCString { ptr in | |
| send(sock, ptr, strlen(ptr), 0) | |
| } | |
| private func sendAll(_ sock: Int32, data: Data) { | |
| data.withUnsafeBytes { rawBuffer in | |
| guard let baseAddress = rawBuffer.baseAddress else { return } | |
| var bytesSent = 0 | |
| while bytesSent < rawBuffer.count { | |
| let remaining = rawBuffer.count - bytesSent | |
| let pointer = baseAddress.advanced(by: bytesSent) | |
| let sent = send(sock, pointer, remaining, 0) | |
| if sent > 0 { | |
| bytesSent += sent | |
| continue | |
| } | |
| if sent == -1 { | |
| if errno == EINTR { | |
| continue | |
| } | |
| if errno == EPIPE || errno == ECONNRESET { | |
| return | |
| } | |
| } | |
| return | |
| } | |
| } | |
| } | |
| private func sendResponse(_ sock: Int32, success: Bool, data: [String: Any]? = nil, error: String? = nil) { | |
| var response: [String: Any] = ["success": success] | |
| if let error = error { response["error"] = error } | |
| if let data = data { response["data"] = data } | |
| guard var jsonData = try? JSONSerialization.data(withJSONObject: response) else { return } | |
| jsonData.append(UInt8(ascii: "\n")) | |
| sendAll(sock, data: jsonData) |
| private func executeAction(_ action: String, params: [String: Any]) -> ActionResult { | ||
| switch action { | ||
|
|
||
| case "status": | ||
| return getStatus() | ||
|
|
||
| case "watch_project": | ||
| guard let path = params["path"] as? String else { | ||
| return .fail("Missing 'path' parameter") | ||
| } | ||
| return watchProject(path: path) | ||
|
|
||
| case "stop_watching": | ||
| return stopWatching() | ||
|
|
||
| case "launch_xcode": | ||
| return launchXcode() | ||
|
|
||
| case "intercept_compiler": | ||
| return interceptCompiler() | ||
|
|
||
| case "enable_devices": | ||
| let enable = params["enable"] as? Bool ?? true | ||
| return enableDevices(enable: enable) | ||
|
|
||
| case "unhide_symbols": | ||
| return unhideSymbols() | ||
|
|
||
| case "get_last_error": | ||
| return getLastError() | ||
|
|
||
| case "prepare_swiftui_source": | ||
| return prepareSwiftUISource() | ||
|
|
||
| case "prepare_swiftui_project": | ||
| return prepareSwiftUIProject() | ||
|
|
||
| case "set_xcode_path": | ||
| guard let path = params["path"] as? String else { | ||
| return .fail("Missing 'path' parameter") | ||
| } | ||
| return setXcodePath(path: path) | ||
|
|
||
| case "get_logs": | ||
| let since = params["since"] as? TimeInterval ?? 0 | ||
| let limit = max(0, min(params["limit"] as? Int ?? 200, 500)) | ||
| return getLogs(since: since, limit: limit) | ||
|
|
||
| case "clear_logs": | ||
| return clearLogs() | ||
|
|
||
| default: | ||
| return .fail("Unknown action: \(action)") | ||
| } |
There was a problem hiding this comment.
The control server exposes privileged actions (launch Xcode, start watchers, toggle devices, etc.) on a localhost TCP port with no authentication/authorization. Even though it is opt-in, any local process can connect and drive these actions once enabled. Consider requiring a shared secret/token (e.g., in UserDefaults/env) and rejecting requests missing/invalid credentials, or using a Unix domain socket with filesystem permissions.
| @@ -21,13 +21,13 @@ let package = Package( | |||
| // Targets are the basic building blocks of a package, defining a module or a test suite. | |||
| // Targets can depend on other targets in this package and products from dependencies. | |||
| .target( | |||
| name: "InjectionNext", dependencies: ["InjectionNextC"], | |||
| swiftSettings: [.define("DEBUG_ONLY")]), | |||
| name: "InjectionBundle", dependencies: ["InjectionNextC"], | |||
| path: "Sources/InjectionNext", swiftSettings: [.define("DEBUG_ONLY")]), | |||
| .target( | |||
| name: "InjectionNextC", | |||
| cSettings: [.define("DEBUG_ONLY"), .define("FISHHOOK_EXPORT")]), | |||
| .testTarget( | |||
| name: "InjectionNextTests", | |||
| dependencies: ["InjectionNext"]), | |||
| dependencies: ["InjectionBundle"]), | |||
There was a problem hiding this comment.
The SwiftPM target was renamed from "InjectionNext" to "InjectionBundle", but the existing tests still import the module name (e.g. "@testable import InjectionNext"). This change will make the test target fail to compile unless the test imports are updated (or the target/module name is kept as InjectionNext).
Log parsing fallback. May need to back-port to InjectionIII if EMIT_FRONTEND_COMMAND_LINES stops working.