You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(mcp): rewrite server, bridge, and protocol layer (#964)
* refactor(mcp): phase 1 wire layer + phase 3 session/auth/ratelimit
Wire layer (TablePro/Core/MCP/Wire/) — pure value types with no transport
awareness: tagged JsonRpcMessage enum (4 cases), JsonRpcId supporting null
explicitly, JsonRpcCodec, strict-CRLF HttpRequestParser, SseEncoder/Decoder,
typed JsonRpcDecodingError. Replaces ad-hoc message types and tolerant HTTP
parser.
Session layer (Session/, Auth/, RateLimit/) — actor-based with eviction
broadcast: MCPSessionStore actor with multicast events stream, idle eviction
under MCPClock abstraction, MCPBearerTokenAuthenticator with SHA-256 token
fingerprint, rate limiter rekeyed on (clientAddress, principalFingerprint)
to fix the localhost auth-DoS issue. Idle timeout raised from 5 to 15 min.
Old types renamed with Legacy prefix to coexist during the rewrite — they
will be deleted in phase 6 once all callers are migrated.
* refactor(mcp): phase 2 transport layer
Three new transports under Core/MCP/Transport/, all built on the new
JsonRpcMessage type. Bridge transports (StdioMessageTransport, Streamable
HttpClientTransport) conform to MCPMessageTransport — symmetric inbound
stream + send. The stdio transport reads via FileHandle.bytes (no more
availableData polling) and writes only valid JSON-RPC to stdout. The
streaming HTTP client uses URLSession.bytes for incremental SSE parsing
and synthesizes JSON-RPC error envelopes from non-2xx HTTP responses so
no malformed line ever reaches the host's stdin — fixing the bridge bug
at the architecture level.
The in-app HTTP server transport (MCPHttpServerTransport) is exchange-
based: each inbound POST yields an MCPInboundExchange whose responder
handles wire-level details. Every error path now produces a JSON-RPC
envelope; the previous bare {"error":...} body shape is impossible to
emit by construction. CORS now includes Last-Event-ID. Remote access
without TLS is rejected at configuration time, not at runtime.
MCPProtocolError carries both JSON-RPC code and HTTP status; static
factories (sessionNotFound, unauthenticated, etc.) encode the spec-
correct mappings in one place.
* refactor(mcp): phase 4 protocol dispatcher + per-method handlers
ProtocolDispatcher actor routes inbound JSON-RPC by method, validates session
state and required scopes, registers cancellation tokens, builds request
contexts, invokes handlers, and translates typed errors back into JSON-RPC
envelopes. Catches MCPProtocolError specifically and falls back to
internalError for everything else.
Per-method handlers split out as small structs: Initialize, Ping, ToolsList,
ToolsCall, ResourcesList/Read/TemplatesList, plus stubs for prompts/logging/
completion. Tools live in their own registry — 19 implementations under
Protocol/Tools/ each conforming to MCPToolImplementation. JsonValueLegacy
Bridge converts at the MCPConnectionBridge boundary so the legacy JSONValue
type can stay on the existing data layer until phase 6.
Streaming progress works via MCPProgressEmitter — handlers emit progress
events that the transport routes as notifications/progress to the session
SSE stream. ExecuteQueryTool uses this at four checkpoints. Cancellation
flows through MCPCancellationToken, looked up by (requestId, sessionId)
in MCPInflightRegistry; notifications/cancelled triggers token cancel.
* refactor(mcp): phase 5 bridge rewrite
The tablepro-mcp binary is now a thin composition root over the new
transport types. BridgeMain wires up MCPStdioMessageTransport (host side)
and MCPStreamableHttpClientTransport (upstream side); BridgeProxy forwards
JsonRpcMessage objects between them with a TaskGroup so both directions
run concurrently. Errors land in os_log via MCPCompositeBridgeLogger and
never leak to stdout — the host-facing transport guarantees only valid
JSON-RPC bytes reach Claude Desktop's stdin.
Handshake module now uses Duration/ContinuousClock instead of TimeInterval
math, and properly distinguishes 'file missing' from 'process not running'
when deciding whether to relaunch.
mcp-server target's pbxproj exception list expanded to include the Wire/
and Transport/ files the bridge depends on; main.swift renamed to
BridgeMain.swift since @main on a struct is incompatible with main.swift
filename. Old MCPBridgeProxy.swift deleted.
* refactor(mcp): phase 6 wire new server, delete legacy code
MCPServerManager is now a thin composition root: it builds an
MCPHttpServerTransport, an MCPProtocolDispatcher with all method handlers
registered, an MCPSessionStore, MCPBearerTokenAuthenticator, and
MCPRateLimiter, then wires the transport's exchanges stream into the
dispatcher. Public API (start/stop/restart/lazyStart/tokenStore/state)
preserved so AppDelegate, AppSettingsManager, MCPSection, and the launch
intent router don't need changes beyond a SessionSnapshot type rename.
Legacy code deleted: MCPServer, MCPRouter, MCPRouteHandler,
MCPHTTPParser, MCPMessageTypes (JSONRPCRequest/Response/etc), MCPSession,
MCPSessionPhase, MCPRateLimiter, MCPToolHandler (+Integrations),
MCPResourceHandler, Routes/MCPProtocolHandler, Routes/IntegrationsExchange
Handler, JsonValueLegacyBridge. About 7000 lines removed.
MCPConnectionBridge migrated from legacy JSONValue to the new JsonValue
type. All tools, resource handlers, and helpers updated accordingly.
MCPError extracted into its own file (still used by the connection bridge,
auth policy, and pairing service).
Pre-existing test target compile errors fixed in passing: typos in
TableRowsMutationTests, missing 'try' in EvictionTests/RowOperationsDispatch
Tests/SortCacheInvalidationTests, obsolete sidebarLoadingState references
removed, DeeplinkParser migration in ConnectionSharingTests, MCPTokenStore
Tests updated for ConnectionAccess, plus deletion of obsolete legacy MCP
test files (MCPRouterTests, MCPAuthGuardTests, etc.) that tested code we
just deleted.
CHANGELOG entry under Unreleased explains the user-facing impact.
xcodebuild build clean for both TablePro and mcp-server schemes; swiftlint
strict zero violations on Core/MCP/.
* fix(mcp): make MCPHttpServerError messages human-readable
Conform to LocalizedError so error.localizedDescription returns the
actual reason (e.g., 'Remote access requires TLS to be enabled') instead
of the opaque NSError-bridged 'MCPHttpServerError error 0'. Also log the
configuration shape on start so failures show bindAddress, port, and tls
status.
* fix(mcp): remove conflicting port argument to NWListener
NWListener(using:on:) with parameters.requiredLocalEndpoint already set
produces EINVAL because the port and the endpoint conflict. The endpoint
in NWParameters carries both host and port; passing 'on: port' tries to
override the port at the listener constructor and the framework rejects
it with errno 22. Use NWListener(using:) and let the configured endpoint
in parameters drive the bind.
* chore: gitignore profraw + .claude worktrees, register new MCP tool strings
Auto-extracted localization keys from the new Phase 4 tool descriptions
land in Localizable.xcstrings. .gitignore extended to cover Xcode coverage
output (.profraw) and the Claude Code per-session worktree directory so
they don't show as untracked clutter on other branches.
* fix(mcp): in-app setup snippets use stdio command form
The 'Setup for Claude Desktop / Cursor' helpers were emitting JSON with
the 'url' transport key, which Claude Desktop rejects entirely (it only
speaks stdio) and which makes Cursor reach the HTTP server directly with
no bridge. Both should point at the bundled tablepro-mcp binary so the
stdio bridge handles the handshake and lazy-launch. The Claude Code
command line is updated to the same form: 'claude mcp add tablepro --
<bridge-path>' instead of '--transport http <url>'. Path is computed
from Bundle.main so it works for non-/Applications installs (Setapp,
DerivedData, etc.).
* fix(mcp): wait for connection.send to flush before cancelling
HttpConnectionContext.send was fire-and-forget — it queued the response
bytes via NWConnection.send but returned immediately. handleReceive
called context.cancel() right after, racing the flush, so URLSession
got 'connection lost' instead of the response body. Make send await the
.contentProcessed callback (via withCheckedContinuation) and propagate
the await up through writeJsonResponse, writeOptions204, writeNoContent,
writeAccepted, writeSseStreamHeaders, writeSseFrame, writeRaw. Cancel
now safely runs only after the network framework confirms the data has
been handed off.
* fix(mcp): wire audit log entries for tool calls, queries, resources, auth
ToolsCallHandler now records every tool invocation (success/denied/error)
with the principal's token label and the tool's connection_id argument
(when present). ResourcesReadHandler does the same for resources/read.
ToolQueryExecutor adds an MCPAuditLogger.logQueryExecuted entry alongside
the existing QueryHistoryStorage write so SQL execution shows up in the
Activity Log. MCPBearerTokenAuthenticator records auth success/failure/
rate-limit events with the client IP. Drops the 'legacy' label from the
domain error mapper in ResourcesReadHandler — MCPError is the data layer's
domain error type, not legacy code.
* refactor(mcp): split MCPError into MCPDataLayerError, drop JSON-RPC overlap
The legacy MCPError enum had two kinds of cases mixed together: domain
errors thrown by the data layer (notConnected, forbidden, timeout, etc.)
and JSON-RPC-shaped errors (parseError, invalidRequest, methodNotFound,
internalError, invalidParams) that overlapped with the protocol layer's
MCPProtocolError. The unused JSON-RPC cases are deleted; the still-used
ones are renamed to domain-flavored names (.invalidParams -> .invalid
Argument, .internalError -> .dataSourceError) so the data layer no longer
borrows protocol-layer terminology.
mapDomainError in ResourcesReadHandler now translates the full set of
domain errors into appropriate MCPProtocolError values (notFound ->
resourceNotFound + 404, timeout -> requestTimeout, etc.) without a
default case, so future additions to MCPDataLayerError will fail the
compile and force an explicit decision about the protocol mapping.
* refactor(mcp): propagate MCPDataLayerError rename to non-MCP callers
LaunchIntentRouter, PairingApprovalSheet, and MCPPairingServiceTests
also catch the data-layer error type; updated to the new name.
* fix(mcp): writeback legacy token format on first load + restructure CHANGELOG
MCPTokenStore now detects the old allowedConnectionIds field via a byte
scan and re-saves immediately after decode, so stale on-disk format
disappears after one launch instead of lingering forever. The decoder
side already handled both shapes — this just forces the encode pass.
CHANGELOG split the rewrite into Added (streaming progress as a real new
capability), Fixed (5 user-facing bugs that are gone), and Changed (idle
timeout + the 'this is a rewrite, but transparent to you' note).
* refactor(mcp): drop legacy allowedConnectionIds compat path
Old MCP wasn't shipped to users yet, so there's no on-disk format to
migrate. Removes the dual-decode branch and the byte-scan migration
writeback. Tokens persisted before this change with the old field name
won't decode — that's fine, they were never released.
* refactor(mcp): convert transports to actors, drop @unchecked Sendable
MCPStdioMessageTransport and MCPStreamableHttpClientTransport are now
actors. The previous outer-class-with-NSLock-around-internal-actor
pattern carried @unchecked Sendable; converting to actors gets compiler-
verified Sendable for free. Internal mutable state (readerTask, isClosed,
sessionId, pendingRequests, etc.) is actor-isolated. The 'inbound' stream
stays nonisolated public let so existing callers like BridgeProxy keep
working without await on the property access.
The 'var capturedContinuation: ...!' implicitly-unwrapped continuation
capture is replaced by a small StreamContinuationBox value type that
holds the continuation behind a lock. Sendable without @unchecked.
MCPBridgeLogger.StderrWriter is also now an actor; the sync log() API is
preserved by spawning a fire-and-forget Task — log ordering is best-
effort, which is fine for stderr.
Adds 15 new tool test files (DescribeTable, Disconnect, ExportData,
FocusQueryTab, GetConnectionStatus, GetTableDdl, ListConnections,
ListDatabases, ListRecentTabs, ListSchemas, ListTables, OpenConnection
Window, OpenTableTab, SearchQueryHistory, SwitchSchema). Each follows
the ConnectToolTests pattern: metadata + missing-arg + malformed-arg.
Fixes a switch-vs-struct typo in two tests.
* test(mcp): end-to-end bridge integration tests
Spins up real MCPHttpServerTransport + MCPStreamableHttpClientTransport +
MCPStdioMessageTransport in-process and exercises the full request/response
path through a TestBridgeProxy that replicates production BridgeProxy
logic (production class lives in the mcp-server target only, not visible
to TableProTests via pbxproj membership exceptions).
Four scenarios: happy-path init+tools/list, idle session eviction returns
-32001 envelope, server emitting non-spec body is wrapped into a JSON-RPC
error envelope by the client transport, malformed request returns a
JSON-RPC error envelope (not the legacy plain {error:...} shape).
Catches the class of wiring bugs that produced the 7 fix commits after
the initial PR open.
* fix(mcp): restore /v1/integrations/exchange HTTP route for Raycast
Phase 6 deleted Routes/IntegrationsExchangeHandler.swift on the assumption
that the bridge runs in-process with the app, so an HTTP route for
pairing-code exchange was redundant. That assumption is wrong: the
Raycast extension is a separate Node.js process that POSTs to
/v1/integrations/exchange to redeem a pairing code for a bearer token,
and any other future external integration would do the same.
Restore the route inline in MCPHttpServerTransport.dispatch — POST to
/v1/integrations/exchange parses {code, code_verifier}, calls MCPPairing
Service.shared.exchange on MainActor, returns {token} on 200 or a plain
{error: ...} JSON body on 4xx/5xx. Adds writePlainJsonResponse +
writePlainJsonError helpers on HttpConnectionContext for the non-MCP
shape this endpoint uses.
* docs(mcp): align external-api docs with the rewrite
mcp-tools.mdx — fix scope mismatches across tools (disconnect, switch_*,
confirm_destructive_operation, navigation tools), document the streaming
progress flow via _meta.progressToken, replace the HTTP-status error
table with the JsonRpcErrorCode + HTTP status pairs that match
JsonRpcErrorCode.swift / MCPProtocolError.swift, fix execute_query and
export_data shapes to match what the actual handlers return, fix
describe_table's database/schema defaulting note, add the spec-compliant
404/-32001 'Session not found' paragraph.
mcp-clients.mdx — drop the spurious '--transport stdio' flag from the
Claude Code command (stdio is the default). Add 404 and 429 entries to
the verification troubleshooting list. Document the WWW-Authenticate
header on 401.
mcp-resources.mdx — new Discovery section for resources/list and
resources/templates/list (latter was undocumented). Add the {contents:
[{uri,mimeType,text}]} envelope shape that resources/read returns.
tokens.mdx — rewrite TokenPermissions -> Set<MCPScope> mapping to match
MCPBearerTokenAuthenticator.mcpScopes. Replace the per-IP escalating
lockout description with the new flat policy from MCPRateLimitPolicy
(5 fails / 60s window / 5min lockout / (client_address, principal_
fingerprint) keyed).
url-scheme.mdx — remove the 'token=' query parameter described against
DeeplinkParser.parseQuery, which doesn't read it. Add the 51,200-char
SQL cap and a pointer to MCP execute_query for headless work.
versioning.mdx — note protocolVersion: 2025-03-26 from InitializeHandler.
features/mcp.mdx — replace 5-minute escalating lockout with the new
policy, matching tokens.mdx.
* chore(mcp): log connection accept + integrations-exchange lifecycle
Adds info/warning logs at handleNewConnection and at every branch of
handleIntegrationsExchange so we can see whether requests are arriving,
whether bodies parse, and whether the handler hits the success or
failure path. No behavior change.
* fix(mcp): use dedicated -32009 unauthenticated JSON-RPC error code
* fix(mcp): drop redundant top-level body-size guard and reject Last-Event-ID with 501
* fix(mcp): reuse transport-allocated session on initialize and surface session errors
* fix(mcp): serialize HTTP client writes through chained Task pipeline
* fix(mcp): write stderr bridge logs synchronously to avoid dropped lines on exit
* fix(mcp): encode handshake file via Codable struct shared with bridge decoder
* chore(mcp): drop unused Network import from MCPInboundExchange
* fix(mcp): localize tool inputSchema descriptions
* perf(mcp): replace linear tool lookup with precomputed dictionary
* chore(mcp): add pairing exchange audit logger + thousands separators
* fix(mcp): wire GET /mcp directly to SSE notification stream registration
* fix(mcp): dispatch each exchange in a child task to avoid serializing tool calls
* fix(mcp): negotiate initialize protocolVersion and validate header on follow-ups
* test(mcp): assert Mcp-Session-Id header lookup is case-insensitive
* fix(mcp): reflect Origin header against allowlist instead of hardcoding localhost
* fix(mcp): cancel in-flight requests and terminate sessions on token revoke
* fix(mcp): emit Retry-After header on 429 with actual lockout duration
* chore(mcp): skip app delegate startup under XCTest to avoid orphan host processes
* fix(mcp): clear stale handshake file with dead PID before writing new one
* fix(mcp): reject duplicate initialize on the same session
* feat(mcp): broadcast audit log inserts so the activity panel auto-refreshes
* feat(mcp): emit error redirect when user denies the pairing approval sheet
* feat(mcp): revoke prior tokens with the same name when re-pairing
* docs: note B1-M4 MCP fixes in CHANGELOG
* chore: untrack docs/refactor scratchpad
The hig-audit notes were committed by accident during the doc audit
pass. Untrack from index — keep on disk locally — so they don't ride
along on future clones.
* test(mcp): pass tokenId to MCPValidatedToken in bearer auth tests
* chore: strip trailing blank line in MCPBridgeLogger
* fix(mcp): downgrade unknown initialize protocolVersion instead of rejecting
The earlier 'strict negotiation' rejected any protocolVersion not in our
supported set with -32600 invalid request. Per the MCP transport spec,
the server SHOULD respond with the highest version it supports when the
client requests a newer one. Claude Code now uses 2025-11-25 (latest);
our supportedProtocolVersions is just 2025-03-26, so its initialize was
rejected and the bridge probe surfaced as 'Failed to connect'. Make
negotiate() always return a valid version, downgrading unknown values to
supportedProtocolVersion. Drop the unused 'unsupported version' guard.
* feat(mcp): full support for protocol versions 2025-06-18 and 2025-11-25
Server now advertises and negotiates 2025-11-25 (the latest spec) as
the preferred protocol version, with 2025-06-18 and 2025-03-26 as
backward-compatible fallbacks. Capabilities advertise only what we
genuinely implement — completions endpoint added (was already a stub
handler), elicitation deliberately omitted because the server doesn't
initiate sampling.
2025-11-25 features wired:
- Structured tool output (structuredContent alongside content[]):
list_*, describe_table, get_table_ddl, get_connection_status,
list_recent_tabs, search_query_history, execute_query, and
confirm_destructive_operation now return their structured data twice,
once as text for older clients and once as structuredContent for
clients that prefer the typed shape.
- Tool annotations (readOnlyHint, destructiveHint, idempotentHint,
openWorldHint, title) emitted by tools/list. Read tools are marked
readOnly+idempotent; execute_query and export_data are openWorld;
confirm_destructive_operation is destructive; switch_*, connect,
disconnect, and the open_/focus_ UI tools opt out of safety hints.
- ServerInfo now includes a 'title' field per the new spec.
Tests updated: InitializeHandler accepts all three versions and
downgrades unknown ones to 2025-11-25; ToolsListHandler emits the
annotation map; ToolsCallHandler passes structuredContent through.
* docs(versioning): document 2025-06-18 + 2025-11-25 support, capabilities, annotations
* docs(versioning): rewrite the new section in plain prose, drop em dashes
* refactor(mcp): native AsyncStream, drop HttpWriter, typed SSE terminate, widen token fingerprint
* docs(mcp): document 2025-11-25 features and fix broken external-api links
* Update .gitignore
Copy file name to clipboardExpand all lines: CHANGELOG.md
+27-2Lines changed: 27 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,19 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
10
10
### Added
11
11
12
+
- MCP: support for protocol versions `2025-06-18` and `2025-11-25` in addition to `2025-03-26`. Clients on the latest spec no longer downgrade. The server advertises the latest version it supports (`2025-11-25`) and falls back when a client requests an unknown version.
13
+
- MCP: structured tool output (`structuredContent`) on every tool. The serialized JSON still appears in `content[].text` for backward compatibility, while 2025-11-25 clients can read the parsed object directly.
14
+
- MCP: tool annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) on every tool, plus `serverInfo.title` in `initialize` responses. Read tools advertise `readOnlyHint=true`; `confirm_destructive_operation` advertises `destructiveHint=true`.
15
+
- MCP: `completions` capability advertised in `initialize` (the `completion/complete` handler was already wired).
16
+
- MCP: streaming progress notifications. Long-running tool calls (e.g. `execute_query`) now emit `notifications/progress` events to clients that pass a `_meta.progressToken` in their request.
17
+
- MCP: pairing redirect carries an explicit `error=denied` parameter when the user clicks Deny so extensions can show a clear error instead of hanging.
18
+
- MCP: re-pairing the same client name automatically revokes the previous token instead of leaving it active.
12
19
- Oracle 10G password verifier authentication. Accounts whose `password_versions` includes a 10G hash now connect successfully, matching DBeaver/JDBC/sqlplus behavior. The 10G hash is documented as legacy; rotating to a modern verifier is still recommended (#483)
13
20
- Oracle Test Connection now opens a focused diagnostic sheet for auth failures with copy-able diagnostic info, suggested actions, and a link to file an issue
14
21
- Oracle connection negotiation now matches python-oracledb's 23ai compile-capability advertisement, including TTC4 explicit boundary, TTC5 token/pipelining/sessionless flags, OCI3 sync, dequeue selectors, and sparse vector features
15
22
16
23
### Removed
17
24
18
-
- Keychain: the legacy-keychain migration (`migrateFromLegacyKeychainIfNeeded`) and the password-sync-state migration (`migratePasswordSyncState`). The first violated Apple's Data Protection keychain contract on sandboxed macOS apps and corrupted user credentials; the second toggled `kSecAttrSynchronizable` at runtime, which Apple does not document as safe. The Sync Passwords settings toggle now applies to new saves only — existing keychain items keep their original sync state, matching Apple's documented behavior. Users with stale items in the legacy keychain can clean them via Keychain Access; the running app no longer touches them.
25
+
- Keychain: the legacy-keychain migration (`migrateFromLegacyKeychainIfNeeded`) and the password-sync-state migration (`migratePasswordSyncState`). The first violated Apple's Data Protection keychain contract on sandboxed macOS apps and corrupted user credentials; the second toggled `kSecAttrSynchronizable` at runtime, which Apple does not document as safe. The Sync Passwords settings toggle now applies to new saves only, existing keychain items keep their original sync state, matching Apple's documented behavior. Users with stale items in the legacy keychain can clean them via Keychain Access; the running app no longer touches them.
19
26
20
27
### Changed
21
28
29
+
- MCP: idle session timeout raised from 5 to 15 minutes.
30
+
- MCP: complete internal rewrite of the server, stdio bridge, and protocol dispatcher for spec compliance. Public API of `MCPServerManager` and the on-disk handshake format are unchanged; clients do not need to re-pair.
22
31
- Internal: introduce `TabSession` as the foundation type for the editor tab/window subsystem rewrite. Currently a parallel structure mirroring `QueryTab`; subsequent PRs migrate state ownership and lifecycle hooks per `docs/architecture/tab-subsystem-rewrite.md`. No user-visible behavior change in this PR.
23
32
- Internal: row data and load epoch now live on `TabSession`. `TabSessionRegistry` exposes the row-access methods directly (`tableRows(for:)`, `setTableRows(_:for:)`, `evict(for:)`, etc.); the intermediate `TableRowsStore` facade is gone. All consumers (coordinator, extensions, views, command actions) now read row data from the registry. No user-visible behavior change.
24
-
- Internal: hidden-column state moves from the per-window `ColumnVisibilityManager` into each tab's `columnLayout.hiddenColumns`. The shared manager is removed; `MainContentCoordinator` exposes `hideColumn`, `showColumn`, `toggleColumnVisibility`, `showAllColumns`, `hideAllColumns`, and `pruneHiddenColumns` that mutate the active tab directly. Per-table UserDefaults persistence moves into a small `ColumnVisibilityPersistence` service. Tab-switch save/restore swap is gone — each tab is its own source of truth. No user-visible behavior change.
33
+
- Internal: hidden-column state moves from the per-window `ColumnVisibilityManager` into each tab's `columnLayout.hiddenColumns`. The shared manager is removed; `MainContentCoordinator` exposes `hideColumn`, `showColumn`, `toggleColumnVisibility`, `showAllColumns`, `hideAllColumns`, and `pruneHiddenColumns` that mutate the active tab directly. Per-table UserDefaults persistence moves into a small `ColumnVisibilityPersistence` service. Tab-switch save/restore swap is gone, each tab is its own source of truth. No user-visible behavior change.
25
34
- Internal: filter state collapses from three places (the per-window `FilterStateManager`, the `TabFilterState` snapshot on `QueryTab`, and the per-table file-based restore) to a single source: `tab.filterState`. The shared manager is removed; `MainContentCoordinator` now exposes the full filter API (`addFilter`, `applyAllFilters`, `clearFilterState`, `toggleFilterPanel`, `setFKFilter`, `saveLastFilters(for:)`, `restoreLastFilters(for:)`, `saveFilterPreset`, `loadFilterPreset`, `generateFilterPreviewSQL`, etc.) that mutates the active tab. The file-based "restore last filters" persistence in `FilterSettingsStorage` is unchanged. `FilterPanelView`, `MainStatusBarView`, `MainContentCommandActions`, `MainContentView`, and `MainEditorContentView` read filter state directly off the active tab. No user-visible behavior change.
26
35
- Internal: extract `QueryExecutor` service from `MainContentCoordinator`. Query data fetch, parallel schema fetch, schema parsing, parameter detection, row-cap policy, and DDL detection now live in `TablePro/Core/Services/Query/QueryExecutor.swift`. SQL parsing helpers (`extractTableName`, `stripTrailingOrderBy`, `parseSQLiteCheckConstraintValues`) move into `QuerySqlParser`. Coordinator methods become thin wrappers; behavior unchanged. No user-visible behavior change.
27
36
- Security: non-syncing keychain items now use `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`. This keeps local-only secrets out of unencrypted device backups (the pairing Apple recommends for local secrets). Syncing items still use `kSecAttrAccessibleAfterFirstUnlock` because iCloud Keychain requires it. Existing items keep their accessibility class until you save them again.
@@ -30,6 +39,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
30
39
31
40
### Fixed
32
41
42
+
- MCP: GET `/mcp` now opens a real SSE notification stream. Previously the GET path was routed through the request dispatcher, which had no handler for it, so the connection was closed immediately and `notifications/progress` events were dropped.
43
+
- MCP: concurrent tool calls no longer serialize at the dispatcher loop. Each exchange is dispatched in its own child task while session-state guards still serialize per-session work.
44
+
- MCP: server validates the `protocolVersion` requested in `initialize` against a supported set and rejects unknown versions with `-32600 invalid_request` instead of silently echoing back whatever the client sent.
45
+
- MCP: server validates `MCP-Protocol-Version` on follow-up requests against the negotiated version on the session.
46
+
- MCP: 429 responses now include a real `Retry-After` header derived from the rate-limiter lockout time. The audit log records the same value.
47
+
- MCP: token revocation cancels every in-flight request issued by that token and terminates its sessions.
48
+
- MCP: CORS reflects the request `Origin` against an allowlist (`localhost`, `127.0.0.1`, `claude.ai`, `app.cursor.com`) instead of unconditionally returning `Access-Control-Allow-Origin: http://localhost`. Requests without an `Origin` header (native clients) get no CORS headers.
49
+
- MCP: duplicate `initialize` on the same session now returns `invalid_request` instead of silently overwriting `clientInfo`.
50
+
- MCP: `xcodebuild test` no longer leaves an orphan `TablePro.app` running. The app delegate skips its normal startup when launched under XCTest.
51
+
- MCP: server start removes a stale handshake file written by a crashed previous PID before writing a fresh one.
52
+
- MCP: settings activity log refreshes automatically when new audit entries are written.
53
+
- MCP: stale `Mcp-Session-Id` after idle timeout now produces a JSON-RPC `-32001 "Session not found"` envelope with HTTP 404, matching the spec and letting clients re-initialize cleanly. Previously the bridge forwarded a plain `{"error":"Session not found"}` body that Claude Desktop's parser rejected, hanging the request until a 4-minute client-side timeout fired.
54
+
- MCP: stdio bridge no longer exits silently when stdin is briefly empty (was reading via `availableData`, which can't tell EOF from "no bytes right now"). Now uses `FileHandle.bytes` AsyncBytes.
55
+
- MCP: SSE responses now stream incrementally instead of buffering the entire body before delivering events.
56
+
- MCP: localhost auth-DoS surface closed. The rate limiter now keys on `(client_address, principal_fingerprint)` so failed attempts from one bridge can't lock out another.
57
+
- MCP: in-app "Setup for Claude Desktop / Cursor" snippets now use the stdio command form pointing at the bundled `tablepro-mcp` binary. The previous `"url"` form was rejected by Claude Desktop entirely.
33
58
- Saved connection passwords no longer disappear after quitting and relaunching the app. The legacy-keychain migration that ran on every launch was destructive on sandboxed macOS configurations: queries without `kSecUseDataProtectionKeychain` returned items that had been written *with* the flag, and the migration's "delete legacy entry" step then removed the only copy. Removed the legacy keychain migration entirely; `KeychainHelper` now exclusively reads and writes through the Data Protection keychain on every launch.
34
59
- Tab switching: rapid Cmd+Number presses no longer leave a tail of tab transitions playing after the user releases the keys. The tab-selection setter (`NSWindowTabGroup.selectedWindow`) is now wrapped in `NSAnimationContext.runAnimationGroup` with `duration = 0`, so AppKit applies each switch synchronously without queuing a CAAnimation. Lazy-load also moved out of `windowDidBecomeKey` into `.task(id:)` view-appearance lifecycle per Apple's documentation. Note: extreme Cmd+Number bursts (e.g. holding the key for key-repeat) still incur per-switch AppKit window-focus overhead; this is platform-inherent to native NSWindow tabs and documented in `docs/architecture/tab-subsystem-rewrite.md` D2
35
60
- Oracle TIMESTAMP, TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH LOCAL TIME ZONE, INTERVAL DAY TO SECOND, INTERVAL YEAR TO MONTH, DATE, RAW, and BLOB columns now render through typed decoders instead of garbled text. Tables containing INTERVAL YEAR TO MONTH or BFILE columns no longer crash the app on row fetch. Unknown column types display `<unsupported: type>` instead of crashing (#965)
0 commit comments