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
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,28 @@ SEATABLE_MOCK=
# Server mode: selfhosted (default) or managed (multi-tenant HTTP with per-client auth)
SEATABLE_MODE=selfhosted

# Required in managed mode (min. 32 chars). Seals the OAuth access/refresh tokens and
# client registrations the server issues, so the raw SeaTable API token is never handed
# to a client. Must stay stable across restarts: generate with `openssl rand -hex 32`.
# SEATABLE_TOKEN_SECRET=

# Multi-base mode (selfhosted only): serve multiple bases from one process.
# JSON array with base_name and api_token. Use instead of SEATABLE_API_TOKEN.
# SEATABLE_BASES='[{"base_name":"CRM","api_token":"token_abc"},{"base_name":"Projects","api_token":"token_def"}]'

# CORS: comma-separated list of allowed origins (HTTP mode only). If empty, CORS is disabled.
# CORS_ALLOWED_ORIGINS=https://cloud.seatable.io,https://seatable-demo.de,http://localhost:3000

# OAuth (managed mode only): comma-separated hosts whose https callbacks are shown
# without the "unknown destination" confirmation step. Loopback addresses and
# private-use app schemes (cursor://, vscode://, ...) never need an entry — the
# code stays on the user's machine. Unknown https hosts still work; the user just
# has to confirm the destination first. Unset = the built-in list of hosted MCP
# clients. A single '*' disables the confirmation entirely (not recommended).
# SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS=claude.ai,claude.com,chatgpt.com

# Lifetime of an issued OAuth access token, in seconds (default 3600, range 30..2592000).
# Lower it to narrow the window after a SeaTable token is revoked; raise it if real
# clients renew badly and would otherwise re-prompt their users. Useful for testing:
# set it to 120 and you see within minutes whether a client refreshes silently.
# SEATABLE_ACCESS_TOKEN_TTL=3600
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Required: `SEATABLE_SERVER_URL`

Auth (one required in selfhosted): `SEATABLE_API_TOKEN` (single-base) or `SEATABLE_BASES` (multi-base, JSON array `'[{"base_name":"CRM","api_token":"..."}]'`)

Required in managed mode: `SEATABLE_TOKEN_SECRET` (min. 32 chars, stable across restarts) — seals issued OAuth tokens and client registrations.

Optional: `SEATABLE_MODE` (`selfhosted`|`managed`, default `selfhosted`), `SEATABLE_MOCK=true` (offline mock), `SEATABLE_ENABLE_DEBUG_TOOLS=1` (enables `echo_args` tool)

Copy `.env.example` to `.env` for local development.
Expand All @@ -46,7 +48,15 @@ Copy `.env.example` to `.env` for local development.
### Modes

- **Selfhosted** (default): Single API token from env, one client per process. Supports multi-base via `SEATABLE_BASES`.
- **Managed** (`SEATABLE_MODE=managed`): HTTP-only, each client authenticates with their own Bearer token. Token validated against SeaTable (`src/auth/tokenValidator.ts`) with positive (5 min) / negative (1 min) cache. Rate limiting via `src/ratelimit/` (per-token, per-IP, global, concurrent connections).
- **Managed** (`SEATABLE_MODE=managed`): HTTP-only, each client authenticates with their own Bearer token **on every request** — the `mcp-session-id` header is a routing value, never a credential, and a request must resolve to the identity that created the session. Token validated against SeaTable (`src/auth/tokenValidator.ts`) with positive (1 min) / negative (1 min) cache. Rate limiting via `src/ratelimit/` (per-token, per-IP, global, concurrent connections).

### OAuth (managed mode)

`src/auth/oauthProvider.ts` bridges SeaTable API tokens into an OAuth 2.0 authorization code flow. Client registrations and issued tokens are **stateless sealed envelopes** (`src/auth/tokenCipher.ts`, AES-256-GCM keyed from `SEATABLE_TOKEN_SECRET`), so no server-side store is needed and they survive restarts. The `client_id` carries the client's registered `redirect_uris`; `/authorize` rejects anything it cannot open. PKCE `S256` is mandatory and every code is bound to client + exact callback + challenge. The raw SeaTable API token is never returned — `resolveAccessToken()` unseals it server-side.

Callback policy is in `isPermittedRedirectUri()` (may it be used at all) and `isTrustedRedirectUri()` (may it skip the confirmation step). Unknown remote https destinations are allowed but meet an acknowledgement page; the acknowledgement is read from the form body only and requires `Sec-Fetch-Site: same-origin`, so neither the entry link nor a foreign auto-submit can skip it. `SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS` only removes that friction.

Adversarial coverage lives in `tests/oauthProvider.security.spec.ts`, `tests/oauthRedirectPolicy.spec.ts`, `tests/oauthRateLimit.spec.ts` and `tests/managedSessionAuth.spec.ts`; they encode attacker behaviour, not honest-client mistakes. `tests/oauthObservability.spec.ts` pins the audit fields (`flow`, `ip`, `callback`, `clientName`) that the July–August 2026 log analysis found missing.

### Tool Registration Pattern

Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,33 @@ For hosting an MCP endpoint where each client authenticates with their own SeaTa
```bash
SEATABLE_MODE=managed \
SEATABLE_SERVER_URL=https://your-seatable-server.com \
SEATABLE_TOKEN_SECRET=$(openssl rand -hex 32) \
PORT=3000 npx -y @seatable/mcp-seatable --sse
```

Clients pass their API token via `Authorization: Bearer <token>` on session initialization. The server validates the token against SeaTable and applies rate limits (60 req/min per token, 120/min per IP, 20 concurrent connections per token).
`SEATABLE_TOKEN_SECRET` is **required** in managed mode. It seals the OAuth tokens the server issues, so the underlying SeaTable API token never has to be handed to a client. Keep it stable across restarts — changing it invalidates every issued access and refresh token and forces all clients to re-authorize.

**OAuth support:** Managed mode also exposes OAuth 2.0 endpoints (`/authorize` and `/token`), enabling OAuth-compatible clients like ChatGPT to connect. During the OAuth flow, the user enters their SeaTable API token, which is then used as the access token — no external OAuth provider required.
Clients pass their credential via `Authorization: Bearer <token>` — on session initialization **and on every subsequent request**, including `GET` and `DELETE`. The `mcp-session-id` header is a routing value only; it is never accepted on its own. Each request is re-validated and must resolve to the same identity that created the session, otherwise the server answers `401` (missing/invalid credential) or `403` (valid credential, wrong session). Rate limits apply as before (60 req/min per token, 120/min per IP, 20 concurrent connections per token).

**OAuth support:** Managed mode also exposes OAuth 2.0 endpoints (`/authorize` and `/token`), enabling OAuth-compatible clients like ChatGPT to connect — no external OAuth provider required. During the flow the user enters their SeaTable API token; the server seals it into its own short-lived access token (1 h) and a rotating refresh token (14 d). The raw SeaTable API token is never returned to a client.

Clients must register at `/register` first: the returned `client_id` carries the client's name and its `redirect_uris`, and the server accepts a callback only if it is one the client registered (loopback callbacks may vary the port, per RFC 8252). PKCE with `S256` is mandatory, and every authorization code is bound to the client, the exact callback and the challenge.

**Where a code may be delivered.** With open dynamic registration, "registered client" is not a trust statement — anyone can register. What matters is whether the code leaves the user's machine:

| Callback | Behaviour |
|---|---|
| Loopback (`http://127.0.0.1:…`, `localhost`, `[::1]`) | allowed, no extra step — the code stays on the user's machine |
| Private-use scheme (`cursor://`, `vscode://`, `com.example.app:/…`) | allowed, no extra step — handed to a local application |
| `https` on a host in `SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS` | allowed, no extra step |
| `https` on any other host | allowed **after** the user confirms the destination on a separate page |
| Remote plaintext `http`, `javascript:`, `data:`, `file:`, `blob:` | rejected |

The confirmation cannot be skipped from the entry link: it is read from the form body only, and a POST auto-submitted by a foreign page is refused via `Sec-Fetch-Site`. The trusted-host list therefore removes friction — it is not a gate, and leaving it unset breaks nothing.

The consent screen leads with the destination the authorization will be sent to. The application's name is shown as **self-reported**, because with open registration it is chosen by whoever registered the client and cannot be verified.

The OAuth endpoints are rate limited per IP (30/min overall, 10/min for token submissions), so `/authorize` cannot be used as an unthrottled oracle for testing SeaTable API tokens.

OAuth endpoints follow the MCP specification (RFC 8414 metadata discovery, PKCE, dynamic client registration):

Expand Down Expand Up @@ -207,6 +228,8 @@ Authentication (one of these is required in selfhosted mode):
Optional:

- `SEATABLE_MODE` — `selfhosted` (default) or `managed` (multi-tenant HTTP with per-client auth)
- `SEATABLE_TOKEN_SECRET` — **required in managed mode**, min. 32 chars. Seals issued OAuth tokens and client registrations; must be stable across restarts (`openssl rand -hex 32`)
- `SEATABLE_ACCESS_TOKEN_TTL` — lifetime of an issued access token in seconds (default `3600`, range `30`–`2592000`). Lower narrows the window after a SeaTable token is revoked; higher spares users a re-prompt if their client renews badly. The refresh token is never issued shorter-lived than the access token.
- `SEATABLE_MOCK=true` — Enable mock mode for offline testing
- `CORS_ALLOWED_ORIGINS` — Comma-separated list of allowed origins for CORS (HTTP mode only, disabled if unset)
- `METRICS_PORT` — Prometheus metrics port (default: `9090`, HTTP mode only)
Expand Down
Loading
Loading