Skip to content

Commit 2a808be

Browse files
committed
feat(mcp): refactor to in-app HTTP server — v6.2.0 (build 96)
Replace the v6.1.0 helper-binary + stdio + URL-scheme + status-polling architecture with a single in-process HTTP MCP server. Same 13 tools, same one-click installer, but the entire pipeline now lives inside the running app and works identically on App Store builds. New: - MCPHTTPServer (swift-nio + StatefulHTTPServerTransport) binds to 127.0.0.1:8765 at /mcp, auto-retries higher ports if 8765 is taken. - MCPToolRegistry owns the 13 tool definitions and dispatches each to direct calls into FrameHistoryDatabase / AstroRootStore / ArchiveScanner / TriageViewModel (via MCPViewModelBridge). No indirection, no polling. - FrameHistoryDatabase+MCP extension hosts the per-tool query methods (listSetupsForMCP, qualitySummaryForMCP, etc.) returning plain Codable result structs. - MCPConnectorWindow now shows the running endpoint URL with a live preflight, plus a "Test Connection" button that round-trips an initialize request to prove the server's up. - com.apple.security.network.server entitlement on the app, so a sandboxed App Store build can also accept localhost connections. Removed: - AstroBlinkMCPServer standalone tool target and the entire MCPServer/ directory. - "Embed AstroBlinkMCPServer" postBuildScript + SKIP_MCP_HELPER switch. - astroblink://scan and astroblink://mark-garbage URL verbs. - MCPCommandRunner.swift, MCPCommandStatus.swift, the saveMCPCommandStatus / updateMCPProgress / autoGarbageCandidates-via- polling helpers. Migration: - No user action. The mcp_command_status table is now unused; left in place to avoid an immediate v11 migration. - Old Claude Desktop configs pointing at the v6.1.0 helper binary are inert. Re-run "Install to Claude Desktop" from MCP Connector to overwrite with the new URL form. kAlgorithmVersion unchanged. Tested end-to-end: app launches, server binds, MCP initialize/tools/list/tools/call ping all succeed.
1 parent 88e81b7 commit 2a808be

22 files changed

Lines changed: 1644 additions & 2122 deletions

AstroTriage.xcodeproj/project.pbxproj

Lines changed: 65 additions & 180 deletions
Large diffs are not rendered by default.

AstroTriage.xcodeproj/xcshareddata/xcschemes/AstroBlinkMCPServer.xcscheme

Lines changed: 0 additions & 91 deletions
This file was deleted.

AstroTriage/App/AstroTriageApp.swift

Lines changed: 12 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,73 +5,23 @@ import SwiftUI
55
let appStoreURL = "https://apps.apple.com/app/astroblinkv2/id6760241266?mt=12"
66

77
// Singleton handler registered before SwiftUI takes over the event pipeline.
8-
// Handles all astroblink:// verbs:
9-
// open?folder=<path> — open a folder (existing)
10-
// scan?root=<path>&id=<uuid> — MCP: start headless scan
11-
// mark-garbage?id=<uuid>&setup=<hash>&night=<date> — MCP: identify/mark garbage
12-
// &dry_run=true|false
8+
// astroblink://open?folder=<path> — legacy "open a folder" verb.
9+
//
10+
// The v6.1.0 scan/mark-garbage URL verbs were removed in v6.2.0: MCP now uses
11+
// an in-process HTTP server (see MCPHTTPServer.swift), so external callers
12+
// don't need to round-trip via URL scheme + polling anymore.
1313
class URLSchemeHandler: NSObject {
1414
static let shared = URLSchemeHandler()
1515
@objc func handleGetURL(_ event: NSAppleEventDescriptor, withReply reply: NSAppleEventDescriptor) {
1616
guard let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue,
1717
let url = URL(string: urlString),
1818
url.scheme == "astroblink",
1919
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
20-
let host = components.host else { return }
21-
let params = (components.queryItems ?? []).reduce(into: [String: String]()) { acc, item in
22-
if let v = item.value { acc[item.name] = v }
23-
}
24-
25-
// Defer to allow AppKit/SwiftUI to finish launching when the URL arrives
26-
// immediately at startup. Once running, the dispatch is essentially a no-op.
20+
components.host == "open",
21+
let folderPath = components.queryItems?.first(where: { $0.name == "folder" })?.value else { return }
22+
let folderURL = URL(fileURLWithPath: folderPath)
2723
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
28-
switch host {
29-
case "open":
30-
guard let folder = params["folder"] else { return }
31-
NotificationCenter.default.post(
32-
name: .openFolderAtPath, object: URL(fileURLWithPath: folder))
33-
34-
case "scan":
35-
guard let root = params["root"], let id = params["id"] else { return }
36-
// Insert pending status so the MCP server poller sees state="pending"
37-
// immediately, even before the runner picks up the notification.
38-
let status = MCPCommandStatus(
39-
commandId: id, verb: "scan", state: "pending",
40-
startedAt: MCPCommandStatus.nowISO8601(), completedAt: nil,
41-
progressCurrent: 0, progressTotal: 0,
42-
resultSummary: nil, errorMessage: nil
43-
)
44-
try? FrameHistoryDatabase.shared.saveMCPCommandStatus(status)
45-
_ = MCPCommandRunner.shared // ensure observers registered
46-
NotificationCenter.default.post(
47-
name: .mcpScanRequested, object: nil,
48-
userInfo: ["rootPath": root, "commandId": id])
49-
50-
case "mark-garbage":
51-
guard let id = params["id"] else { return }
52-
let setupHash = params["setup"]
53-
let night = params["night"]
54-
let dryRun = (params["dry_run"] ?? "true").lowercased() != "false"
55-
let status = MCPCommandStatus(
56-
commandId: id, verb: "mark-garbage", state: "pending",
57-
startedAt: MCPCommandStatus.nowISO8601(), completedAt: nil,
58-
progressCurrent: 0, progressTotal: 0,
59-
resultSummary: nil, errorMessage: nil
60-
)
61-
try? FrameHistoryDatabase.shared.saveMCPCommandStatus(status)
62-
_ = MCPCommandRunner.shared
63-
NotificationCenter.default.post(
64-
name: .mcpMarkGarbageRequested, object: nil,
65-
userInfo: [
66-
"commandId": id,
67-
"setupHash": setupHash as Any,
68-
"night": night as Any,
69-
"dryRun": dryRun
70-
])
71-
72-
default:
73-
break // unknown verb — silently ignore
74-
}
24+
NotificationCenter.default.post(name: .openFolderAtPath, object: folderURL)
7525
}
7626
}
7727
}
@@ -372,9 +322,9 @@ class AstroBlinkV2AppDelegate: NSObject, NSApplicationDelegate {
372322
// Initialize Frame History database (local SQLite only — instant)
373323
if !isTestHost {
374324
_ = FrameHistoryDatabase.shared
375-
// Register MCP notification observers up front so any astroblink://
376-
// URL fired during early launch sees a runner ready to handle it.
377-
_ = MCPCommandRunner.shared
325+
// Start the in-process HTTP MCP server so external clients
326+
// (Claude Code, Claude Desktop) can connect on localhost:8765.
327+
MCPHTTPServer.shared.start()
378328
}
379329

380330
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {

AstroTriage/AstroTriage.entitlements

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@
2727
<true/>
2828
<key>com.apple.security.network.client</key>
2929
<true/>
30+
<key>com.apple.security.network.server</key>
31+
<true/>
3032
</dict>
3133
</plist>

0 commit comments

Comments
 (0)