feat: External API integration with MCP tools and Raycast support#925
feat: External API integration with MCP tools and Raycast support#925
Conversation
Renames the existing mcp-server target's product to tablepro-mcp and moves source from TablePro/MCPBridge to TablePro/CLI per plan. Adds auto-launch of TablePro via tablepro://integrations/start-mcp with a 10-second poll for the handshake file, plus stale-PID detection that relaunches the host. Bridges SSE responses by emitting each data: line as a separate JSON-RPC message on stdout. Adds an explicit TablePro -> mcp-server target dependency so the helper binary always builds before the app embed step. Sets PRODUCT_BUNDLE_IDENTIFIER=com.TablePro.tablepro-mcp. Verification: xcodebuild -scheme mcp-server -configuration Debug build succeeds; tablepro-mcp ends up at TablePro.app/Contents/MacOS/tablepro-mcp, executable, codesigned, hardened runtime on. swiftlint --strict clean on TablePro/CLI/. build-release.sh now fails fast if the embedded helper is missing from the release bundle. The internal Xcode target name stays "mcp-server" to keep the pbxproj diff minimal; only the product name, bundle id, and source path change.
Move the full parameter list (core, SSH, SSL, plugin-specific af_* fields) from features/deep-links into external-api/url-scheme. The url-scheme page previously punted to deep-links via 'See Deep Links for the full parameter list', which becomes a dangling link once deep-links is removed.
…-api The previous page duplicated the External API tool catalog, token model, and stdio bridge config in less detail. It also hardcoded a stale default port (23508) and the internal Xcode target name (mcp-server) instead of the shipped binary name (tablepro-mcp). Replace with a focused page that covers the in-app Settings > MCP UI: enable toggle, lazy-start, remote access, TLS, audit log. Link to the canonical external-api pages for protocol details, the tool catalog, tokens, pairing, and client setup. Section anchors #enabling-the-server and #remote-access are preserved so existing cross-references from customization/settings.mdx and external-api/cursor-claude.mdx still resolve.
The page documented the pre-0.37 'tablepro://connect/<name>' syntax, which contradicts external-api/url-scheme.mdx (UUID-keyed paths only). Database URL schemes (mysql://, postgresql://, etc.) are already canonical in databases/connection-urls.mdx, and the tablepro://import parameter table now lives in external-api/url-scheme.mdx. - Delete docs/features/deep-links.mdx - Update features/overview.mdx Workflow card to point to external-api/url-scheme - Remove features/deep-links from docs.json Workflow group
…o path The MCP subsystem wrote its handshake, tokens, and audit DB under ~/Library/Application Support/com.TablePro/, while every other on-disk file (connections.json, TabState/, LastQuery/, known_hosts, themes) lives under ~/Library/Application Support/TablePro/. Bundle-id paths are the sandboxed-app convention; TablePro is non-sandboxed and should use the friendly app-name directory throughout. Updates the three MCP path constructions, the bundled tablepro-mcp bridge, the Raycast extension, and every docs reference. The com.TablePro identifier is preserved in OSLog subsystems, Keychain, UserDefaults keys, the bundle id, AppGroup ids, and NSUserActivity types since those are not file paths. No migration code or compat shim per CLAUDE.md "no backward-compat hacks". After upgrading, users must re-pair Raycast, Cursor, Claude Desktop, and any other external MCP clients, and may delete the stale directory with `rm -rf ~/Library/Application Support/com.TablePro`.
Also log previously swallowed best-effort failures from updateCommandMetadata and the post-pair redirect so they surface in dev. The setInterval polling in WaitingView remains, since LocalStorage cannot signal cross-command writes.
…tead of new native window
…ies (#958) * fix(mcp): app-modal auth alert, strip trailing semicolons, decode + as space * refactor(ui): centralize cross-process alerts in AlertHelper * refactor(concurrency): introduce OnceTask, migrate MCP dedup sites * refactor(mcp): replace router switch with route registry * refactor(schema): single SchemaService for sidebar state and table list (WIP) * refactor(mcp): MCPAuthPolicy gate, typed session/pairing lifecycles (WIP) * refactor(lifecycle): typed launch coordinator, grammar parser, tab router (WIP) * fix(refactor): post-merge build fixes for Wave 2 integration - Restore PairingRequest/PairingExchange types deleted with DeeplinkHandler - Add reason: label to AuthDecision.denied call sites in MCPAuthPolicy - Convert PairingApproval.allowedConnectionIds to ConnectionAccess - Mark TransientConnectionFactory @mainactor for ConnectionURLParser access - Rename AppLaunchCoordinator.didFinishLaunching property to hasFinishedLaunching - Drop tables binding from MainContentView preview (SchemaService now owns it) * refactor(welcome): imperative NSWindow factory replaces SwiftUI Scene auto-open Welcome window was the first SwiftUI Scene declared, so SwiftUI auto-opened it on every cold launch before AppLaunchCoordinator could check for pending intents. The coordinator orderOut'd the window after URL events arrived, but SwiftUI had already painted the welcome frame. Move Welcome to imperative NSWindow + NSHostingController pattern, matching how main windows are already created. AppLaunchCoordinator becomes the only place that decides whether welcome appears. - Add WelcomeWindowFactory with openOrFront / close / orderOut - Drop Window("Welcome to TablePro", id: "welcome") Scene from TableProApp - Drop openWelcomeWindow Notification.Name (no remaining posters) - Drop AppDelegate.configureWelcomeWindowStyle (factory owns style) - Drop AppDelegate.openWelcomeWindow private helper - Replace 7 openWindow/closeWindows(id: "welcome") sites with factory calls - Ignore .claude/worktrees, .profraw, scratch reference repos * chore(refactor): drop dead notifications, wire SQL file drain After the Wave-2 lifecycle refactor, several Notification.Name declarations have no posters or no listeners. Delete them and rewire the only path that still needed cross-component messaging. - Drop .openWelcomeWindow (factory replaced it) - Drop .openMainWindow (no posters; WindowManager.openTab is direct) - Drop .switchSchemaFromURL (TabRouter calls coordinator directly) - Drop .applyURLFilter (method on coordinator, not a notification) - Drop .connectionShareFileOpened, .deeplinkImportRequested (WelcomeRouter pendingImport replaces them) - Drop OpenWindowHandler.onReceive(.openMainWindow) (dead listener) - Wire WelcomeRouter pendingSQLFiles to .databaseDidConnect → posts .openSQLFiles for drain * refactor(connection-form): imperative NSWindow factory replaces SwiftUI Scene The connection-form WindowGroup was the only remaining declared Scene besides Settings, so SwiftUI auto-restored it on cold launch and produced the same flash bug the welcome refactor fixed. Apply the same imperative factory pattern to make the entire user-facing window lifecycle imperative and AppLaunchCoordinator-driven. - Add ConnectionFormWindowFactory (imperative open / close, per-id NSWindow) - Drop WindowGroup(id: "connection-form", for: UUID?.self) Scene - Replace 7 openWindow / closeWindows(id: "connection-form") sites - Replace OpenWindowHandler with SettingsNotificationBridge inside Settings (only forward .openSettingsWindow to SwiftUI's openSettings action) - Drop WindowOpener helper (no consumers after factory migration) - Drop @Environment(\\.openWindow) from ConnectionFormView and WelcomeWindowView - Drop WelcomeViewModel.setUp(openWindow:) signature; setUp() takes no args After this commit Settings is the only declared Scene, which is special: SwiftUI never auto-opens Settings on launch. * fix(tabrouter): focus existing table tab when deeplink targets already-open table Native macOS pattern: opening the same target twice focuses the existing window/tab, it does not create duplicates (Finder folders, Xcode files). Before opening a new native window tab from a deeplink, scan all active coordinators for a tab matching (connectionId, tabType: .table, tableName, optional database, optional schema). If found, select that tab and bring its window to front; skip openTab. If not found, fall through to creating a new tab as before. * fix(switcher): route through TabRouter so .restoreOrDefault intent applies Connection switcher built EditorTabPayload(connectionId:) directly with the type defaults (tabType: .query, intent: .openContent). That always forced a fresh empty query tab instead of restoring the user's last tabs or opening the connection's default tab. Single path: both switchToSession and connectToSaved now call TabRouter.route(.openConnection(id)). TabRouter focuses an existing window for the connection if any; otherwise creates a new window with intent: .restoreOrDefault and ensures the connection is live. Drop the manual NSWindow.tabbingMode = .disallowed workaround — per- connection tabbingIdentifier in WindowManager.openTab already prevents cross-connection tab merging when groupAllConnectionTabs is off. Drop the now-unused isConnecting per-row spinner state — popover dismisses synchronously now, so the spinner had no chance to render. * Update .gitignore * refactor(review): address PR #958 reviewer findings - MCPAuthPolicy.promptApproval: defer group.cancelAll() so timeout task cannot leak when the dialog task throws on a future code change - TabRouter.focusExistingTableTab: tighten matching so a deeplink that specifies a database does not match a tab whose databaseName is nil; similarly for schema - AlertHelper.swift: strip /// doc comments and inline // comments per CLAUDE.md "no comments" rule (function names are self-descriptive) - AppDelegate: drop configureConnectionFormWindowStyle, configuredWindows tracking, windowDidBecomeKey observer — ConnectionFormWindowFactory applies style at creation time, no post-hoc restyle needed - MCPConnectionBridge: drop unused connectDedup property and route connect/connectIfNeeded through DatabaseManager.ensureConnected so dedup lives in one place (manager-level OnceTask) - Delete dead MCPPendingPairing/MCPPairingRegistry — pairing service uses PairingExchangeStore + NSLock; the actor-based duplicate from Wave 2B was never wired up * fix(setup): always create default tab when restoreOrDefault has no persisted state handleRestoreOrDefault left the new window with zero tabs when restoreFromDisk returned an empty result (first-time connection, persistence cleared, etc.). Tab manager being empty causes updateWindowTitleAndFileState's fallback branch to use connection.name as the window title, so the user sees an empty "<Connection>" tab next to any subsequent tab opened from a deeplink or Cmd+T. Hoist the "add default empty query tab" logic to a helper and run it in both paths: when other windows already exist for the connection, and when restoreFromDisk returns empty. Window now always has at least one tab. * fix(tabs): number new query tabs across all windows for the connection QueryTabManager.addTab defaulted the new tab title to nextQueryTitle computed against this manager's local tabs. Each window has its own tab manager, so a fresh window numbering its first tab always returned "Query 1" — even if a sibling window already had Query 1, Query 2. Inject globalTabsProvider into QueryTabManager. SessionStateFactory supplies a closure that returns MainContentCoordinator.allTabs(for: connectionId), so the title generator sees every existing query tab in the connection's tab group, not just this window's. The closure is lazy (called only when title is computed) so manager init stays decoupled from the registry's MainActor isolation at use time. The default empty closure keeps the type usable in tests and preserves a sensible per-window fallback when no provider is wired. * fix(setup): empty workspace as default for new connection (no auto query tab) Reverts the addDefaultEmptyTab fallback in handleRestoreOrDefault. Opening a fresh connection no longer creates a default "Query 1" tab — the window shows the existing emptyStateView placeholder ("No tabs open" + Cmd+T / click-a-table hint) instead. Native macOS pattern: opening a workspace shouldn't pre-fill content. The user explicitly creates content via Cmd+T, sidebar table click, Raycast deeplink, etc. The empty-tabs path is a supported state with proper UI. * refactor(tab-router): add openQuery dedup, route openTable through fast-path, drop fragile string match, fix openDatabaseURL double-open * refactor(polish): fix safeMode dialog window race, strip Sessions comments, type session-term reason, dedup preview tab, fix Cmd+1..9 guard, fix denial string format * refactor(routing): unify connection-open paths through TabRouter * refactor(persistence): track save Task, clear empty-payload files, escalate save errors * refactor(state): cross-window correctness + lifecycle leak fixes * refactor(round2-review): fix 9 PR #958 review findings Critical: - MCPServer.stop() runs cleanup handlers before clearing sessions so future handlers can read session data High: - TabRouter.focusExistingTableTab assigns tabManager.selectedTabId directly instead of delegating to coordinator.openTableTab — the latter has its own create-new-tab fast-path that produced duplicate tabs - SessionStateFactory tracks pending-expiration tasks per payload and cancels on consume; eliminates 5s dormant Task per successful window open - OnceTask.execute uses a generation counter so the defer cleanup only removes the entry when it still matches the caller's task; cancel + re-execute no longer evicts a sibling caller's in-flight task - SchemaService.load now joins the in-flight load when state is .loading instead of returning empty-handed; callers can await completion Medium: - Remove unused TabPersistenceCoordinator.saveNow(persistedTabs:) overload that lacked an empty-array guard - TabRouter.previewForSQL uses NSString.substring(to:) for O(1) head slice consistent with the project's NSString convention - WelcomeViewModel.awaitWelcomeRouterChange wraps the continuation in a ContinuationBox + withTaskCancellationHandler so deinit cancellation resolves cleanly instead of leaving a dormant Task Low: - Strip explanatory /// and inline comments from WindowManager, SessionStateFactory, TabDiskActor, TabPersistenceCoordinator, MCPServer per CLAUDE.md "no comments" rule
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f162f287f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| try await authPolicy.resolveAndAuthorize( | ||
| token: MCPToolHandler.anonymousFullAccessToken, | ||
| tool: "describe_table", |
There was a problem hiding this comment.
Enforce caller token when authorizing resource reads
resources/read is authorized with MCPToolHandler.anonymousFullAccessToken, so token scope and per-connection allowlists are bypassed for schema/history resources. In authenticated deployments, a readOnly or connection-limited token can still read resources it should not access because authorization is evaluated as full-access anonymous instead of the request’s bearer token.
Useful? React with 👍 / 👎.
| "switch_database", "switch_schema", "export_data" | ||
| ] | ||
| static let requiresFullAccess: Set<String> = ["confirm_destructive_operation"] | ||
| static let requiresReadWrite: Set<String> = ["switch_database", "switch_schema", "export_data"] |
There was a problem hiding this comment.
Require readWrite scope for UI-mutating integration tools
The permission map does not include UI-mutating tools (open_connection_window, open_table_tab, focus_query_tab) in requiresReadWrite, so they fall through to the default readOnly requirement. Because these handlers open/focus windows and tabs, readOnly tokens can still mutate app state, which violates the intended scope separation and weakens token least-privilege behavior.
Useful? React with 👍 / 👎.
…pairing pending cap
…oid Settings toolbar bleed
… toolbar, friendlier time and token labels
…ith explicit spacers
Summary
Ships TablePro's first-class External API for Raycast, Cursor, Claude Desktop, and other MCP-compatible clients. Three integration channels with clearly separated responsibilities:
tablepro://URL scheme for navigation actions (open connection, table, query)49 commits across Swift (foundation, pairing, audit, tools, CLI binary, settings rename), Mintlify docs (full External API section + cross-references through the existing docs site), and unit tests (export injection regression).
What's new
Swift
tablepro://URL scheme; newintegrations/pair,integrations/start-mcp,connect/<uuid>/query?...&token=actionsblocked/readOnly/readWrite, defaults toreadOnly)MCPPairingServicewith PKCE exchange + native approval sheetmcp-audit.db, 90-day retention) + Activity Log view in Settingslist_recent_tabs,search_query_history,open_connection_window,open_table_tab,focus_query_tabtablepro-mcpstdio CLI binary embedded in the app bundle (enables Cursor / Claude Desktop / Claude Code without an extension)~/Library/Application Support/TablePro/(matches macOS convention for non-sandboxed apps)Security hardening (post-review)
export_datatable names validated against^[A-Za-z0-9_][A-Za-z0-9_.]*$and dialect-quoted; synthesized queries now route throughcheckQueryPermissionlike the explicit query branchlist_connectionsfiltered by the token'sallowedConnectionIds; empty Set means no accessfocus_query_tabrejects tabs without a connection (no auth bypass via nil-connection scratch tabs)open_connection_window,open_table_tab,focus_query_tabonly requirereadOnly(was blocking the default Raycast user sincereadOnlyis the connection default)export_dataoutput_pathrestricted to~/Downloads/; symlinks and..resolved before the bounds checkfocus_query_tab(no window-focus-then-deny TOCTOU)Docs
docs/external-api/section: URL scheme reference, MCP tools catalog, MCP resources, pairing sequence, tokens model, MCP clients (9 client config examples), versioning policyfeatures/deep-links.mdxfeatures/mcp.mdxfrom 307 lines to 81 as an orientation page that links intodocs/external-api/Tests
MCPToolHandlerExportTests(new, 7 cases): regression tests forexport_datatable-name injection (;DROP TABLE, backtick escape, leading dot, empty), schema-qualified-name acceptance, sandbox enforcement onoutput_pathNote on the Raycast extension
The Raycast extension that consumes this API is shipped separately to the official Raycast Store via a PR to github.com/raycast/extensions, per Raycast's distribution model (every Store extension lives in that monorepo). That follow-up PR is staged locally and will go up after this lands and the next TablePro version ships, so the extension can reference shipped behavior.
Breaking changes
tablepro://connect/<name>/...deep links removed. Replace with UUID-keyed paths via Copy Connection Deep Link in the sidebar context menu. User-saved bookmarks must be regenerated.~/Library/Application Support/com.TablePro/to~/Library/Application Support/TablePro/. Existing tokens, audit log, and handshake files are not migrated. Re-pair Raycast / Cursor / Claude Desktop / any external clients after upgrading. Optionally delete the old directory:rm -rf ~/Library/Application Support/com.TablePro/Architecture decisions
MCPTokenStore)readOnlybelt-and-suspenders against accidental mutations from a token withfullAccessscopeTest plan
xcodebuild ... buildsucceeds cleanopen "tablepro://connect/<uuid>"opens the connection; old name-keyed paths fail gracefullyopen_table_tabworks from Raycast on a defaultreadOnlyconnection (regression check for the readWrite-gate fix)readOnlyconnection rejects mutating SQL with 403export_datarejects table names with SQL metacharacters; only writes under~/Downloads/tablepro-mcpinto Claude Desktop'smcp-config.json, restart,@tableprolists toolsRelated
mainas of c9a17b1)