Skip to content

Commit a741c4c

Browse files
committed
fix: final review — stop() race, cancellation tracking, xlsx removal, stale callback guard
1 parent 343bb4f commit a741c4c

6 files changed

Lines changed: 29 additions & 21 deletions

File tree

TablePro/Core/MCP/MCPAuthGuard.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,15 @@ actor MCPAuthGuard {
138138
}
139139
group.addTask {
140140
try await Task.sleep(for: .seconds(30))
141+
approvalTask.cancel()
141142
throw MCPError.timeout(
142143
String(localized: "User approval timed out after 30 seconds")
143144
)
144145
}
145146
guard let result = try await group.next() else {
146147
throw MCPError.internalError("No result from approval prompt")
147148
}
149+
approvalTask.cancel()
148150
group.cancelAll()
149151
return result
150152
}

TablePro/Core/MCP/MCPConnectionBridge.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -389,20 +389,19 @@ actor MCPConnectionBridge {
389389
}
390390
}
391391

392+
let (driver, _) = try await resolveDriver(connectionId)
393+
392394
let tables: [TableInfo]
393395
if !cachedTables.isEmpty {
394396
tables = cachedTables
395397
} else {
396-
let (driver, _) = try await resolveDriver(connectionId)
397398
tables = try await DatabaseManager.shared.trackOperation(sessionId: connectionId) {
398399
try await driver.fetchTables()
399400
}
400401
}
401402

402-
// Limit to first 100 tables to prevent excessive round-trips
403403
let limitedTables = Array(tables.prefix(100))
404404

405-
let (driver, _) = try await resolveDriver(connectionId)
406405
var tableSchemas: [JSONValue] = []
407406
for table in limitedTables {
408407
let columns = try await DatabaseManager.shared.trackOperation(sessionId: connectionId) {

TablePro/Core/MCP/MCPRouter.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,16 +268,30 @@ final class MCPRouter: Sendable {
268268
return encodeError(MCPError.internalError("Server not fully initialized"), id: id)
269269
}
270270

271+
let session = await server.session(for: sessionId)
272+
let toolTask = Task {
273+
try await handler(name, arguments, sessionId)
274+
}
275+
if let session {
276+
await session.addRunningTask(id, task: Task { _ = try? await toolTask.value })
277+
}
278+
271279
do {
272-
let toolResult = try await handler(name, arguments, sessionId)
280+
let toolResult = try await toolTask.value
281+
if let session { _ = await session.removeRunningTask(id) }
273282
let resultData = try encoder.encode(toolResult)
274283
guard let resultValue = try? decoder.decode(JSONValue.self, from: resultData) else {
275284
return encodeError(MCPError.internalError("Failed to encode tool result"), id: id)
276285
}
277286
return encodeRawResult(resultValue, id: id, sessionId: sessionId)
287+
} catch is CancellationError {
288+
if let session { _ = await session.removeRunningTask(id) }
289+
return encodeError(MCPError.timeout("Request was cancelled"), id: id)
278290
} catch let mcpError as MCPError {
291+
if let session { _ = await session.removeRunningTask(id) }
279292
return encodeError(mcpError, id: id)
280293
} catch {
294+
if let session { _ = await session.removeRunningTask(id) }
281295
return encodeError(MCPError.internalError(error.localizedDescription), id: id)
282296
}
283297
}
@@ -651,8 +665,8 @@ extension MCPRouter {
651665
]),
652666
"format": .object([
653667
"type": "string",
654-
"description": "Export format: csv, json, sql, or xlsx",
655-
"enum": .array([.string("csv"), .string("json"), .string("sql"), .string("xlsx")])
668+
"description": "Export format: csv, json, or sql",
669+
"enum": .array([.string("csv"), .string("json"), .string("sql")])
656670
]),
657671
"query": .object([
658672
"type": "string",

TablePro/Core/MCP/MCPServer.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,18 +106,15 @@ actor MCPServer {
106106

107107
if let currentListener = listener {
108108
listener = nil
109-
currentListener.cancel()
110-
// Wait for the listener to fully release the port
111109
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
112110
currentListener.stateUpdateHandler = { state in
113111
if case .cancelled = state {
114112
continuation.resume()
115113
}
116114
}
115+
currentListener.cancel()
117116
}
118117
}
119-
120-
stateCallback(.stopped)
121118
}
122119

123120
var sessionCount: Int {

TablePro/Core/MCP/MCPServerManager.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ final class MCPServerManager {
2424
private(set) var connectedClients: [MCPServer.SessionSnapshot] = []
2525
private var server: MCPServer?
2626
private var clientRefreshTask: Task<Void, Never>?
27+
private var serverGeneration: Int = 0
2728

2829
var isRunning: Bool {
2930
if case .running = state { return true } else { return false }
@@ -43,9 +44,12 @@ final class MCPServerManager {
4344
await stop()
4445
}
4546

47+
serverGeneration += 1
48+
let generation = serverGeneration
4649
let newServer = MCPServer { [weak self] newState in
4750
Task { @MainActor in
48-
self?.state = newState
51+
guard let self, self.serverGeneration == generation else { return }
52+
self.state = newState
4953
}
5054
}
5155

TablePro/Core/MCP/MCPToolHandler.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,8 @@ final class MCPToolHandler: Sendable {
221221
let outputPath = optionalString(args, key: "output_path")
222222
let maxRows = optionalInt(args, key: "max_rows", default: 50_000, clamp: 1...100_000)
223223

224-
guard ["csv", "json", "sql", "xlsx"].contains(format) else {
225-
throw MCPError.invalidParams("Unsupported format: \(format). Must be csv, json, sql, or xlsx")
226-
}
227-
228-
// XLSX is binary and cannot be returned inline
229-
if format == "xlsx" && outputPath == nil {
230-
throw MCPError.invalidParams("XLSX export requires output_path since it is a binary format")
224+
guard ["csv", "json", "sql"].contains(format) else {
225+
throw MCPError.invalidParams("Unsupported format: \(format). Must be csv, json, or sql")
231226
}
232227

233228
guard query != nil || tables != nil else {
@@ -280,9 +275,6 @@ final class MCPToolHandler: Sendable {
280275
formatted = formatJSON(columns: columnNames, rows: rows)
281276
case "sql":
282277
formatted = formatSQL(table: label, columns: columnNames, rows: rows)
283-
case "xlsx":
284-
// XLSX is handled via file write below; generate CSV as intermediate
285-
formatted = formatCSV(columns: columnNames, rows: rows)
286278
default:
287279
formatted = formatCSV(columns: columnNames, rows: rows)
288280
}

0 commit comments

Comments
 (0)