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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- iCloud sync of app settings (general, appearance, editor, data grid, history, tabs, keyboard, AI) no longer silently does nothing when a category's payload fails to decode. Each of the eight category branches previously wrapped the decode in `try?`, so a record written by a newer schema version would fall through with no log, no error, and no UI signal: the user would think their settings synced when they hadn't. Decode failures now skip the category and log which one failed and why.
- Keychain reads no longer collapse a cancelled Touch ID prompt, a failed biometric auth, or any unknown OSStatus into "not found". The `KeychainResult` enum now distinguishes `.userCancelled`, `.authFailed`, and `.error(OSStatus)` from `.notFound`, and the read paths in connection passwords, SSH profile secrets, AI provider keys, and the license key log each case with its own message. Previously a cancelled prompt looked identical to a missing entry, so the caller would treat the password as gone and silently re-save with an empty string on the next write, producing duplicate keychain entries or a connection saved with a blank password.
- Terminal PTY writes retry on `EINTR` instead of treating any non-positive return as "we're done". A signal mid-write previously truncated the input the user typed; the loop would exit silently and the keystrokes were partially sent. The new path retries on `EINTR`, logs the byte position and errno on any other non-recoverable failure, and reports a return value of zero distinctly so the cause is visible in Console.
- MCP HTTP transport no longer writes an empty body when JSON encoding of the response envelope fails. Five sites in `MCPInboundExchange` and `MCPHttpRequestRouter` previously fell back to `Data()`, sending zero bytes to the client which then saw a protocol violation and either disconnected or hung. The encode-failure paths now log and substitute a static `{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"internal_error"}}` envelope; the pairing-exchange success path falls back to `internalServerError` with a small JSON error body.
- Result-grid cells on rows marked for deletion keep their dropdown / date / JSON / blob chevron visible at reduced opacity instead of hiding it, so the cell type is still legible while clearly inactive. Click on the dimmed chevron is a no-op; FK arrow navigation is unchanged. Matches the macOS HIG "disabled appearance" guideline.
- Foreign key navigation from a table with unsaved edits opens the referenced table in a new window tab to preserve the edit buffer. Closing that new tab no longer wipes the original tab's data grid. Previously the new tab's teardown broadcast a connection-scoped event that other coordinators on the same connection received, causing them to release their cell data.
- Tables sidebar refreshes automatically after a successful SQL import; the refresh notification now fires after the success sheet's dismissal animation, so the main window is key when the observer runs (#1114)
Expand Down
21 changes: 18 additions & 3 deletions TablePro/Core/MCP/Transport/MCPHttpRequestRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import os
struct MCPHttpRequestRouter: Sendable {
private static let logger = Logger(subsystem: "com.TablePro", category: "MCP.HttpRouter")

private static let staticInternalErrorEnvelope = Data(
#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"internal_error"}}"#.utf8
)

typealias InboundEmitter = @Sendable (MCPInboundExchange) -> AsyncStream<MCPInboundExchange>.Continuation.YieldResult
typealias SseStarter = @Sendable (UUID, MCPSessionId, HttpConnectionContext) async -> Void
typealias ResponderSinkFactory = @Sendable (HttpConnectionContext) -> any MCPResponderSink
Expand Down Expand Up @@ -115,8 +119,13 @@ struct MCPHttpRequestRouter: Sendable {
Self.logger.info("Integrations exchange succeeded (token len=\(token.count, privacy: .public))")
let label = await Self.resolveTokenLabel(for: token)
MCPAuditLogger.logPairingExchange(outcome: .success, tokenName: label, ip: ip)
let payload = (try? JSONEncoder().encode(ExchangeResponse(token: token))) ?? Data()
await context.writePlainJsonResponse(status: .ok, body: payload)
do {
let payload = try JSONEncoder().encode(ExchangeResponse(token: token))
await context.writePlainJsonResponse(status: .ok, body: payload)
} catch {
Self.logger.error("Encode ExchangeResponse failed: \(error.localizedDescription, privacy: .public)")
await context.writePlainJsonError(status: .internalServerError, message: "internal_error")
}
await context.cancel()
case .failure(let error):
let mapped = Self.mapExchangeError(error)
Expand Down Expand Up @@ -422,7 +431,13 @@ struct MCPHttpRequestRouter: Sendable {
requestId: JsonRpcId?
) async {
let envelope = error.toJsonRpcErrorResponse(id: requestId)
let data = (try? JSONEncoder().encode(envelope)) ?? Data()
let data: Data
do {
data = try JSONEncoder().encode(envelope)
} catch {
Self.logger.error("Encode top-level error envelope failed: \(error.localizedDescription, privacy: .public); using static fallback")
data = Self.staticInternalErrorEnvelope
}
await context.writeJsonResponse(
data: data,
status: error.httpStatus,
Expand Down
28 changes: 25 additions & 3 deletions TablePro/Core/MCP/Transport/MCPInboundExchange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public protocol MCPResponderSink: Sendable {
public actor MCPExchangeResponder {
private static let logger = Logger(subsystem: "com.TablePro", category: "MCP.HttpServer")

private static let staticInternalErrorEnvelope = Data(
#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"internal_error"}}"#.utf8
)

private let sink: MCPResponderSink
private var completed: Bool = false
private let requestId: JsonRpcId?
Expand All @@ -71,8 +75,14 @@ public actor MCPExchangeResponder {
do {
body = try JsonRpcCodec.encode(message)
} catch {
Self.logger.error("Encode response failed: \(error.localizedDescription, privacy: .public)")
let fallback = MCPProtocolError.internalError(detail: "encode failed").toJsonRpcErrorResponse(id: requestId)
body = (try? JSONEncoder().encode(fallback)) ?? Data()
do {
body = try JSONEncoder().encode(fallback)
} catch {
Self.logger.error("Encode internal_error envelope failed: \(error.localizedDescription, privacy: .public); using static fallback")
body = Self.staticInternalErrorEnvelope
}
}

await sink.writeJson(body, status: .ok, sessionId: sessionId, extraHeaders: [])
Expand All @@ -87,7 +97,13 @@ public actor MCPExchangeResponder {
completed = true

let envelope = error.toJsonRpcErrorResponse(id: responseId ?? requestId)
let data = (try? JSONEncoder().encode(envelope)) ?? Data()
let data: Data
do {
data = try JSONEncoder().encode(envelope)
} catch {
Self.logger.error("Encode error envelope failed: \(error.localizedDescription, privacy: .public); using static fallback")
data = Self.staticInternalErrorEnvelope
}
await sink.writeJson(data, status: error.httpStatus, sessionId: nil, extraHeaders: error.extraHeaders)
await sink.closeConnection()
}
Expand Down Expand Up @@ -138,7 +154,13 @@ public actor MCPExchangeResponder {
completed = true

let envelope = error.toJsonRpcErrorResponse(id: requestId)
let data = (try? JSONEncoder().encode(envelope)) ?? Data()
let data: Data
do {
data = try JSONEncoder().encode(envelope)
} catch {
Self.logger.error("Encode reject envelope failed: \(error.localizedDescription, privacy: .public); using static fallback")
data = Self.staticInternalErrorEnvelope
}
await sink.writeJson(data, status: error.httpStatus, sessionId: nil, extraHeaders: error.extraHeaders)
await sink.closeConnection()
}
Expand Down
Loading