Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Packages/Sources/RxCodeSync/SyncClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,15 +258,23 @@ public actor SyncClient: PeerManagerHost {
/// schedule a renewal before it expires. No mapping ⇒ WAN-direct simply isn't
/// offered and the relay carries traffic.
private func refreshWANMapping() async {
guard !Task.isCancelled else { return }
guard let port = await advertiser?.listeningPort() else { return }
guard !Task.isCancelled else { return }
let mapping = await portMapper.requestMapping(internalPort: port)
guard !Task.isCancelled else { return }
currentWANMapping = mapping
wanRenewTask?.cancel()
guard let mapping, mapping.lifetimeSeconds > 0 else { return }
// Renew at half the granted lifetime.
let renewAfter = max(60, Int(mapping.lifetimeSeconds) / 2)
wanRenewTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(renewAfter) * 1_000_000_000)
do {
try await Task.sleep(nanoseconds: UInt64(renewAfter) * 1_000_000_000)
} catch {
return
}
guard !Task.isCancelled else { return }
await self?.refreshWANMapping()
}
}
Expand Down
4 changes: 4 additions & 0 deletions Packages/Sources/RxCodeSync/Transport/PortMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,21 @@ public actor PortMapper {
/// Request a public address + TCP mapping for `internalPort`. Returns nil on
/// any failure (no gateway, no NAT-PMP support, timeout).
public func requestMapping(internalPort: UInt16, lifetime: UInt32 = 3600) async -> PortMapping? {
guard !Task.isCancelled else { return nil }
guard let gateway = Self.defaultGatewayIPv4() else {
logger.info("[PortMapper] no default gateway found — WAN-direct unavailable")
return nil
}
guard let publicIP = await sendExternalAddressRequest(gateway: gateway) else {
guard !Task.isCancelled else { return nil }
logger.info("[PortMapper] gateway \(gateway, privacy: .public) did not answer NAT-PMP — WAN-direct unavailable")
return nil
}
guard !Task.isCancelled else { return nil }
guard let external = await sendMapRequest(gateway: gateway, internalPort: internalPort, lifetime: lifetime) else {
return nil
}
guard !Task.isCancelled else { return nil }
logger.info("[PortMapper] mapped \(internalPort, privacy: .public) → \(publicIP, privacy: .public):\(external.port, privacy: .public) lifetime=\(external.lifetime, privacy: .public)s")
return PortMapping(publicIP: publicIP, externalPort: external.port, lifetimeSeconds: external.lifetime)
}
Expand Down
8 changes: 8 additions & 0 deletions Packages/Tests/RxCodeCoreTests/MCPToolCallTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ struct MCPToolCallTests {
#expect(message.toolCalls.first?.isError == false)
#expect(message.toolCalls.first?.result == "")
}

@Test("Memory tools use search instead of a get-by-id tool")
func memoryToolsUseSearch() {
let names = Set(IDEToolRegistry.allTools.map(\.name))

#expect(names.contains("ide__memory_search"))
#expect(!names.contains("ide__memory_get"))
}
}
17 changes: 17 additions & 0 deletions RxCode.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
DF46526A2FCD34B3002D9562 /* JSONSchemaValidator in Frameworks */ = {isa = PBXBuildFile; productRef = DF4652692FCD34B3002D9562 /* JSONSchemaValidator */; };
DF46683A2FCDB56B002D9562 /* JSONSchemaForm in Frameworks */ = {isa = PBXBuildFile; productRef = DF4668392FCDB56B002D9562 /* JSONSchemaForm */; };
DF46683C2FCDB56B002D9562 /* JSONSchemaValidator in Frameworks */ = {isa = PBXBuildFile; productRef = DF46683B2FCDB56B002D9562 /* JSONSchemaValidator */; };
DF98F2373004F38B00BBB638 /* MCP in Frameworks */ = {isa = PBXBuildFile; productRef = DF98F2363004F38B00BBB638 /* MCP */; };
DFA0CCD12FB4CC01005991E1 /* PlanDecisionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA0CCC02FB4CC01005991E1 /* PlanDecisionTests.swift */; };
DFA0CCD22FB4CC01005991E1 /* PlanCardViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA0CCC12FB4CC01005991E1 /* PlanCardViewTests.swift */; };
DFA0CCD42FB4CC01005991E1 /* RxCodeChatKit in Frameworks */ = {isa = PBXBuildFile; productRef = DFA0CCC32FB4CC01005991E1 /* RxCodeChatKit */; };
Expand Down Expand Up @@ -324,6 +325,7 @@
DF462DCB2FC73FA8002D9562 /* RxAuthSwiftUI in Frameworks */,
E6D001072FA0000100000001 /* RxCodeSync in Frameworks */,
DF462A602FC6EDCE002D9562 /* RxAuthSwift in Frameworks */,
DF98F2373004F38B00BBB638 /* MCP in Frameworks */,
DF23FF1D2FBB42F7008929A6 /* WaterfallGrid in Frameworks */,
FB0000030000000000000001 /* FirebaseAnalytics in Frameworks */,
DF462DC92FC73FA8002D9562 /* RxAuthSwift in Frameworks */,
Expand Down Expand Up @@ -637,6 +639,7 @@
DF4652672FCD34B3002D9562 /* JSONSchemaForm */,
DF4652692FCD34B3002D9562 /* JSONSchemaValidator */,
DFAA00022FCD34B3002D9562 /* JSONSchema */,
DF98F2363004F38B00BBB638 /* MCP */,
);
productName = RxCode;
productReference = E67335382F7356F600FD26C7 /* RxCode.app */;
Expand Down Expand Up @@ -701,6 +704,7 @@
DF4652662FCD34B3002D9562 /* XCRemoteSwiftPackageReference "swift-jsonschema-form" */,
DFAA00032FCD34B3002D9562 /* XCRemoteSwiftPackageReference "swift-json-schema" */,
DFA8E3B22FD1A27F00F33E7F /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */,
DF98F2353004F38B00BBB638 /* XCRemoteSwiftPackageReference "swift-sdk" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = E67335392F7356F600FD26C7 /* Products */;
Expand Down Expand Up @@ -1565,6 +1569,14 @@
kind = branch;
};
};
DF98F2353004F38B00BBB638 /* XCRemoteSwiftPackageReference "swift-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/modelcontextprotocol/swift-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.12.1;
};
};
DFA8E3B22FD1A27F00F33E7F /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SDWebImage/SDWebImageSVGCoder";
Expand Down Expand Up @@ -1703,6 +1715,11 @@
package = DF4652662FCD34B3002D9562 /* XCRemoteSwiftPackageReference "swift-jsonschema-form" */;
productName = JSONSchemaValidator;
};
DF98F2363004F38B00BBB638 /* MCP */ = {
isa = XCSwiftPackageProductDependency;
package = DF98F2353004F38B00BBB638 /* XCRemoteSwiftPackageReference "swift-sdk" */;
productName = MCP;
};
DFA0CCC32FB4CC01005991E1 /* RxCodeChatKit */ = {
isa = XCSwiftPackageProductDependency;
productName = RxCodeChatKit;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions RxCode/App/AppCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ final class AppCore {
let cliStore: CLISessionStore
let claude: ClaudeService
let workspaceRegistry: WorkspaceRegistry
/// Process-wide listener for the optional, user-configured memory MCP API.
/// Its handler follows the frontmost workspace through `WorkspaceManager`.
let memoryMCPServer = IDEMCPServer()

init() {
let metaStore = SessionMetaStore()
Expand Down
16 changes: 16 additions & 0 deletions RxCode/App/AppState+Agents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import SwiftUI
extension AppState {
// MARK: - Memory

func configureMemoryMCPServer(enabled: Bool, port: Int, exposedTools: Set<MCPServerTool>) async {
await core.memoryMCPServer.setHandler(self)
do {
try await core.memoryMCPServer.configureMemoryAPI(
enabled: enabled && memoryEnabled,
port: port,
exposedTools: exposedTools
)
memoryMCPServerRunning = enabled && memoryEnabled
memoryMCPServerError = nil
} catch {
memoryMCPServerRunning = false
memoryMCPServerError = error.localizedDescription
}
}

func allMemoryItems() async -> [MemoryItem] {
await memoryService.allMemories()
}
Expand Down
2 changes: 2 additions & 0 deletions RxCode/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@
}

var memoryRevision = 0
var memoryMCPServerRunning = false
var memoryMCPServerError: String?

// MARK: - Notifications

Expand Down Expand Up @@ -1359,4 +1361,4 @@
return message
}
}
}

Check warning on line 1364 in RxCode/App/AppState.swift

View workflow job for this annotation

GitHub Actions / swiftlint

File should contain 600 lines or less excluding comments and whitespaces: currently contains 779 (file_length)
21 changes: 21 additions & 0 deletions RxCode/App/WorkspaceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ final class WorkspaceManager {
let workspace = snapshot.all.first { $0.id == workspaceID } ?? snapshot.active
let appState = AppState(core: core, workspace: workspace)
appStatesByWorkspaceID[workspace.id] = appState
if workspace.id == frontmostWorkspaceID {
bindMemoryMCPServer(to: appState)
}
startPerformanceMonitorIfNeeded()
return appState
}
Expand All @@ -65,6 +68,24 @@ final class WorkspaceManager {
func markFrontmost(_ workspaceID: String) {
frontmostWorkspaceID = workspaceID
transferMobileSyncOwnership(to: workspaceID)
bindMemoryMCPServer(to: appState(for: workspaceID))
}

private func bindMemoryMCPServer(to appState: AppState) {
let defaults = UserDefaults.standard
let enabled = defaults.bool(forKey: IDEMCPServer.memoryAPIEnabledDefaultsKey)
let savedPort = defaults.integer(forKey: IDEMCPServer.memoryAPIPortDefaultsKey)
let port = savedPort == 0 ? IDEMCPServer.memoryAPIDefaultPort : savedPort
let exposedTools = IDEMCPServer.exposedTools(
from: defaults.string(forKey: IDEMCPServer.exposedToolsDefaultsKey)
)
Task {
await appState.configureMemoryMCPServer(
enabled: enabled,
port: port,
exposedTools: exposedTools
)
}
}

/// Hand mobile-sync ownership to `workspaceID`, tearing it down on the
Expand Down
36 changes: 36 additions & 0 deletions RxCode/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -3296,6 +3296,9 @@
}
}
}
},
"Choose which RxCode tools are available to other local MCP clients." : {

},
"Choose…" : {
"localizations" : {
Expand Down Expand Up @@ -6394,6 +6397,9 @@
}
}
}
},
"Enable MCP Server" : {

},
"Enable Memory" : {
"localizations" : {
Expand All @@ -6410,6 +6416,9 @@
}
}
}
},
"Enable Memory to start the MCP server." : {

},
"Enabled" : {
"localizations" : {
Expand Down Expand Up @@ -6727,6 +6736,9 @@
}
}
}
},
"Expose selected RxCode tools to other local MCP clients." : {

},
"Extra environment" : {
"localizations" : {
Expand Down Expand Up @@ -8327,6 +8339,9 @@
}
}
}
},
"Keep RxCode running while another MCP client uses this server. Ports 19847–19946 are reserved for chat sessions." : {

},
"Keep summaries on the thread model, or route titles and briefings through an OpenAI-compatible endpoint." : {
"localizations" : {
Expand Down Expand Up @@ -9165,6 +9180,9 @@
}
}
}
},
"MCP Server" : {

},
"MCP server disconnected" : {
"comment" : "Notification title when an MCP server transitions from connected to failed.",
Expand Down Expand Up @@ -10900,6 +10918,9 @@
}
}
}
},
"Off" : {

},
"Offline" : {
"localizations" : {
Expand Down Expand Up @@ -11810,6 +11831,9 @@
}
}
}
},
"Port" : {

},
"PR #%lld" : {
"localizations" : {
Expand Down Expand Up @@ -13362,6 +13386,9 @@
}
}
}
},
"Running" : {

},
"Running swift build" : {
"localizations" : {
Expand Down Expand Up @@ -14560,6 +14587,9 @@
}
}
}
},
"Server stopped" : {

},
"Servers" : {
"localizations" : {
Expand Down Expand Up @@ -15674,6 +15704,9 @@
}
}
}
},
"Streamable HTTP" : {

},
"streaming stops" : {
"extractionState" : "stale",
Expand Down Expand Up @@ -16439,6 +16472,9 @@
}
}
}
},
"Tools to Expose" : {

},
"Transport" : {
"localizations" : {
Expand Down
Loading
Loading