Skip to main content

Pairing

Pairing is how an extension gets a TablePro token without the user copying and pasting one. The user runs a Pair with TablePro command in the extension, picks scopes and connections inside TablePro, and the extension receives a token over a Raycast deep link callback. The flow is PKCE-flavored: the extension generates a verifier, hashes it into a challenge, and the token is only released after the verifier is presented. This prevents another app on the same machine from intercepting the redirect and stealing the token.

Sequence

Step by step

1. Extension generates a verifier and challenge

The verifier is 32 random bytes, base64url-encoded. Keep it in memory until the exchange step. Do not log it.
See the URL scheme reference for parameters.

3. TablePro shows the approval sheet

The user sees:
  • The client name from the request.
  • A scopes radio (defaults to the requested scope, downgradeable).
  • A connections multi-select (defaults to all unless connection-ids was provided).
  • An expiry picker (defaults to never).
The user can change any of these before approving. The query parameters are a request, not a grant.

4. TablePro generates a token and a one-time code

On approval, TablePro calls MCPTokenStore.generate(...) to mint a token, then stores a pending exchange:
The plaintext token is held in memory only. The token store keeps the hashed form on disk (SHA-256 + salt).

5. TablePro redirects with the code

TablePro opens the redirect URL with NSWorkspace.shared.open(...). The encoding depends on the redirect scheme:
  • raycast://...: TablePro appends ?context={"code":"<uuid>"} (URL-encoded JSON). Raycast parses context and passes it to the receiving command as LaunchProps.launchContext. This matches Raycast’s documented launch-context convention.
  • Anything else (http://127.0.0.1:<port>/callback, custom schemes): TablePro appends ?code=<uuid> as a flat query parameter. Standard OAuth-callback shape.

6. Extension exchanges the code

The extension reads the MCP port from ~/Library/Application Support/TablePro/mcp-handshake.json, then:
The exchange endpoint requires no bearer auth. The single-use code is the auth.

7. TablePro validates and returns the token

Server-side check:
If equal, return the plaintext token and delete the pending exchange. If the code has expired (5 minutes) or the verifier does not match, return 403.

8. Extension stores the token

For preferences-backed storage, use updateCommandMetadata or write to the password preference. Tokens stored in Raycast preferences live in the macOS Keychain.

Security properties

Errors

A failed exchange is recorded in the activity log under the auth category with outcome denied.

Denied approvals

If the user clicks Deny on the approval sheet, TablePro opens the redirect URL with two extra parameters so the extension can show a clear error and stop spinning:
  • error=denied
  • error_description=user_denied
For raycast://... redirects these are wrapped inside the standard context JSON payload ({"error":"denied","error_description":"user_denied"}); for any other scheme they are appended as flat query parameters. Extensions should treat the presence of an error parameter on the callback as terminal and surface the description to the user.

Implementing pairing in another extension

The flow is not Raycast-specific. Cursor, Claude Desktop, or any custom client can use it. Requirements:
  1. Generate a verifier and challenge.
  2. Open tablepro://integrations/pair?... with a deep link callback URL the OS can route back to the extension.
  3. Read the MCP port from the handshake file.
  4. POST { code, code_verifier } to /v1/integrations/exchange.
  5. Store the returned token in OS Keychain.
If the extension cannot register a custom URL scheme, open a localhost HTTP server on a chosen port and pass http://127.0.0.1:<port>/callback as the redirect.