diff --git a/docs/CONNECTOR-ONBOARDING.md b/docs/CONNECTOR-ONBOARDING.md index 196191a5e..f6dd922c6 100644 --- a/docs/CONNECTOR-ONBOARDING.md +++ b/docs/CONNECTOR-ONBOARDING.md @@ -74,7 +74,9 @@ is the validation. whose live metadata said so on 2026-09-10 — GitHub (measured first: a secretless exchange answers `incorrect_client_credentials`, #1006), Slack, HubSpot, Zoom, Box, Asana, Docusign, Front, Wrike, Xero, AlloyDB and the - eight Google Workspace servers. Left off `amazon-ads` and `doordash`, whose + eight Google Workspace servers — and Sage Intacct, whose metadata the + 2026-09-14 catalog audit read the same way (no registration endpoint, no + `none`). Left off `amazon-ads` and `doordash`, whose metadata lists `none`, and off five of the six `tenant` entries, whose URL cannot be probed without a tenant. Azure DevOps is the sixth: its protected-resource metadata is organization-agnostic and names Microsoft @@ -157,6 +159,15 @@ Face — and a test caps it at 8–20 entries, never community provenance. The same pass audited the directory for dead/low-quality listings and added newly-verified official endpoints (see that PR in the repository history). +**Catalog corrections do not rewrite saved connections**: fleet builds the +remote MCP mount from the saved row (`srv.URL`, via `connFor` in +`internal/remotemcp/resolver.go`). The directory UI marks an entry "added" +by name (`web/src/app/settings/connections/page.tsx`), so a user who connected +an entry before a catalog fix (such as Cartesia, Octagon, or Globalping) keeps +dialing the old URL and auth after upgrading, and cannot pick the corrected +entry without removing the connection first. To adopt a catalog correction, +the operator or user must Remove the connector and re-add it. + ## Deviations / honest scope - **`auth: tenant` semantics narrowed**: it now means "your URL + OAuth". The diff --git a/docs/HOSTED-CONNECTORS-RUNBOOK.md b/docs/HOSTED-CONNECTORS-RUNBOOK.md new file mode 100644 index 000000000..ba1264e47 --- /dev/null +++ b/docs/HOSTED-CONNECTORS-RUNBOOK.md @@ -0,0 +1,182 @@ +# Hosted MCP connectors — operator runbook + +How to connect a hosted (official, per-user OAuth) MCP server to fleet, what +each vendor needs before the Connect button works, how to tell a fleet +problem from a vendor problem, and what to do when a connection stops. Every +statement here was measured against the vendor in the #1006 OAuth pack; the +per-vendor results are in [`MCP-CATALOG-STATUS.md`](MCP-CATALOG-STATUS.md). +The design behind the mechanics is in [ADR-0009](adr/0009-per-user-remote-mcp-oauth.md) +(per-user OAuth), [ADR-0050](adr/0050-remote-mcp-seats.md) (seats), +[`REMOTE-MCP-MULTI-LOGIN.md`](REMOTE-MCP-MULTI-LOGIN.md) and +[`CONNECTION-SHARING.md`](CONNECTION-SHARING.md); the directory itself in +[`MCP-CATALOG.md`](MCP-CATALOG.md) and [`CONNECTOR-ONBOARDING.md`](CONNECTOR-ONBOARDING.md). + +## Before the first connection + +- **The callback URL is derived, not configured.** fleet advertises + `FLEET_PUBLIC_BASE_URL` + `/api/oauth/mcp/callback` and logs it at boot + (`remote MCP OAuth: ENABLED … redirect …`). `FLEET_PUBLIC_BASE_URL` must be + the browser's origin for the **web tier**, not the chat-server port. Every + vendor that asks for a redirect URI gets exactly that string. +- **Some vendors refuse plain-http callbacks.** Slack requires HTTPS. Microsoft + Entra allows `http://` only for `localhost` (not `127.0.0.1`) and ignores the + port there. Single-box installs sit behind Caddy with TLS by default. A chart + deployment reaches the web tier however the operator exposes it — the chart's + optional Ingress (disabled by default, `ingress.tls` empty) or something in + front of it — and whatever the path, the public callback URL must be HTTPS or + HTTPS-only vendors (Slack) reject it; a local dev rig needs a tunnel for Slack. +- **`FLEET_MCP_OAUTH_ENCRYPTION_KEY` is the connection.** Tokens, client + secrets and registration tokens are sealed with it, bound to (owner, URL). + Lose or rotate it and every hosted connection must be reconnected by its + owner. Keep `FLEET_MCP_OAUTH_ENCRYPTION_KEY` in a separate access-controlled + secret store, never in the same backup set as the database; both artifacts + are required for recovery, so document where each lives. +- **Three shapes of Connect.** *One-click* — the vendor registers fleet as a + client itself (Notion, Linear, Stripe, Grafana Cloud, Uptime Robot, Plaid, + Cartesia, Globalping, …): the user clicks Connect and signs in. *Bring your + own client* — the vendor has no self-registration: an admin creates an OAuth + app at the vendor with fleet's callback URL and users paste the client ID + and, where the directory says **required**, the secret (GitHub, Slack, the + Google Workspace servers, Azure DevOps, Sage Intacct, …). *Tenant* — the + URL carries the customer's org or host; the form asks for it, then one of + the two flows above follows. +- **Why both a client secret and a login.** The secret identifies fleet, the + application, to the vendor; the login identifies the user whose data the + tools will act on. Vendors without self-registration need the first done by + hand; the second is always per user. + +## Per-vendor notes (measured) + +- **GitHub** — OAuth App (not a GitHub App) with fleet's callback as the + Authorization callback URL; the secret is mandatory — without it GitHub + answers `incorrect_client_credentials` with HTTP 200. Tokens live 8 hours + with a refresh token; `bad_refresh_token` means the user must reconnect. + "Revoke all user tokens" on the app is noticed at the next mount (401 → + *Reconnect needed*). No revocation endpoint: sign-out clears fleet's copy. +- **Google Workspace (Drive, Gmail, …)** — a Google Cloud OAuth client (Web + application) with fleet's callback; secret required. Enable the Workspace + MCP APIs on the project. fleet asks for `access_type=offline` and + `prompt=consent`, without which Google issues a one-hour token and no + refresh token. **The Workspace MCP servers are a Developer Preview**: a + personal Gmail account connects but every tool call answers + `The caller does not have permission`. Test with a Workspace account that is + enrolled. +- **Slack** — create the app from a manifest or by hand; under *Features → + Agents & AI Apps* turn on **Model Context Protocol** (without it the server + answers `App is not enabled for Slack MCP server access`); redirect URL must + be HTTPS; secret required. The user token has no expiry and no refresh + token; no revocation endpoint. The vendor's metadata names its bare origin + as the resource while serving MCP at `/mcp` — fleet keeps the typed URL. +- **Notion, Linear** — one click. Linear publishes a revocation endpoint, so + sign-out revokes at the vendor; Notion does not. +- **Azure DevOps** — the org must be connected to a Microsoft Entra tenant + (a standalone personal-account org is refused by Microsoft). In that + tenant: confirm the *Azure DevOps MCP* enterprise application exists (or + `az ad sp create --id 2a72489c-aab2-4b65-b93a-a91edccf33b8`), create an app + registration on the **Web** platform with fleet's callback, add a client + secret, add the *Azure DevOps MCP* delegated permission and **grant admin + consent**. Sign in with a **work account native to the tenant**: a personal + Microsoft account, even one made a tenant Member during Azure sign-up, loops + at Microsoft's account picker on the `organizations` endpoint fleet is + directed to. That user must have opened `dev.azure.com/` once, or every + tool call answers `Identity … has not been materialized`. Tokens live about + 70 minutes with a rotating refresh token; no revocation endpoint. +- **Stripe** — one click. Every API tool needs `stripe_context` (the account + id) and `livemode`; the model gets them from `list_available_accounts_or_orgs` + first. Calls made without them answer HTTP 422. +- **Grafana Cloud, Uptime Robot** — one click. Uptime Robot's server returns a + client secret at registration although fleet asked to be a public client; + fleet stores and uses it. +- **Plaid, Intercom** — no protected-resource metadata; connect worked on the + 2026-09-14 build. `main` currently refuses them at Add (their MCP path + answers 401 under every sub-path, which the discovery code reads as a + server failure). Known and deliberately not fixed at the time of writing. +- **Square, Smartlead** — SSE-only endpoints. fleet's hosted-connector + transport is streamable HTTP; these cannot connect today. + +## Running with connections + +- **Chat.** A connection mounts only when it is enabled in the conversation's + Tools picker. The web UI pre-enables the default set; an API client must + send `enabled_optional` naming the connection, or nothing hosted mounts and + the model reports "no MCP tools". +- **Scheduled tasks** mount every connected connection of the task's owner on + its default seat, subject to the overlay ceiling. Only connections enabled for + that user mount; a disabled connection (own or shared seat alike) is omitted + silently with no notice to the model. The overlay caps mounting at + `maxOverlayServers = 8` connected servers per user (`maxOverlayServers` in + `internal/agent/remote_mcp_overlay.go`), applied uniformly to chat, + scheduled runs, and the broker. Selection order follows connection list order + (`internal/remotemcp/resolver.go`'s `ConnectedServersForUser`: the owner's + own connections newest-first, then the ones shared with them); servers past the + 8-server cap are skipped and logged. `mcp_selection` pins choose which seat + account mounts for a connector, but pins do **not** bypass the 8-server cap + or reorder servers ahead of it. A pin to a seat that is not connected skips + that connector and tells the model; a pin to a server the owner never + connected dead-letters the task without a model call. +- **Expired tokens refresh headlessly** in both chat and scheduled runs, + under a row lock. Network errors and 5xx stay transient (the refresh path in + `internal/remotemcp/service.go`): the connection is skipped for that run, + remains connected, and is retried on the next call. Only terminal OAuth + errors (e.g. `invalid_grant`) mark the connection *Reconnect needed* + (`needs_reauth`). In either case, the run completes without the skipped + connector. +- **More than 128 tools (by default)** switches the run to deferred mode: + `disclosureThreshold()` defaults to 128 tools and honours both the + `FLEET_TOOL_DISCLOSURE_THRESHOLD` environment variable and the live + `tool_disclosure_threshold` admin setting. Above that threshold, the model + finds tools with `tool_search`, reads their schema with `tool_describe` + (which lists the required arguments) and calls them with `tool_call`, which + refuses a call missing a required argument and names it. +- **Seats and sharing.** One connection name can hold several logins; each is + a seat with one owner-chosen default. Sharing is per seat, the grantee never + sees the token, tool calls act as the owner at the vendor, and a revoked + share takes effect on the grantee's next turn. Share only what you would + hand over: a grantee gets the seat's full tool surface. + +## When something fails + +Read `fleet.log` first. The lines that matter: + +- `remote-mcp: skipping server "" for — token unavailable` — + refresh failed. For terminal OAuth failures (e.g. `invalid_grant`), the row is + marked *Reconnect needed* (`needs_reauth`); have the owner click Connect. For + transient network errors or vendor 5xx, the same line is logged but the + connection stays connected and the next call retries automatically. +- `remote-mcp: skipping server "" … — failed to connect: … HTTP 404` — + the vendor rejected the tools handshake; usually a wrong URL (check the + vendor's documented MCP path), not a credential problem, although the + model is currently told the connector "needs re-authorization" either way. +- `mcpbroker: tool call failed (masked to the caller): … HTTP 422 …` — the + vendor refused the tool arguments. The model sees only + `credential-owner call failed`; the real text is on this line. +- `scheduled task : wired N remote MCP server(s) for ` / + `skipped remote MCP server(s) needing re-auth or a missing pinned seat` — + what a scheduled run mounted and what it could not. +- `Fantasy tools registered: … N MCP tools DEFERRED` — the run is in deferred + mode. + +At **Add** time, the error toast names every metadata location fleet tried. +`fetch protected-resource metadata … 404` on every location means the vendor +publishes none; `authorization-server issuer mismatch` means the vendor's +document names another issuer and neither that issuer nor the vendor's host +vouched for its endpoints; `client secret required` means the vendor accepts +no public clients and the form needs the secret. + +At **sign-in**, errors starting with `AADSTS` come from Microsoft: +`50011` a redirect URI that does not match the app registration, `65001` +admin consent not granted, `90100` a stale sign-in page (start again in a +private window, or complete the new user's first sign-in at +myaccount.microsoft.com first). + +## Adding a vendor to the directory + +The catalog rules are in [`MCP-CATALOG.md`](MCP-CATALOG.md). Before listing a +hosted server, run discovery against it (the Connect button does exactly +that) and record: does it publish protected-resource metadata, does its +authorization server allow public clients (`none` in +`token_endpoint_auth_methods_supported`), and does it self-register. For +OAuth entries, verify that the unauthenticated `initialize` POST answers with +401 and protected-resource metadata rather than 404; `open` and `api_key` +entries must complete `initialize` (200) and `tools/list`. A URL that +discovers but 404s tool calls is the most common catalog error. diff --git a/docs/MCP-CATALOG-STATUS.md b/docs/MCP-CATALOG-STATUS.md new file mode 100644 index 000000000..52d86d968 --- /dev/null +++ b/docs/MCP-CATALOG-STATUS.md @@ -0,0 +1,341 @@ +# Hosted MCP connector status — the #1006 OAuth pack and the catalog audit + +The record for #986 ("test official MCPs and improve the catalog") and its +child #1006 ("test the OAuth flow with official MCPs"). Two kinds of evidence +are kept apart here: **live runs** — a real account, the browser consent +screen, a real tool call from a chat and from a scheduled task, a forced +token refresh, a sign-out — and **discovery probes**, which run fleet's own +`mcpoauth.Discover` and add-time guards against a vendor's published metadata +without logging in. A probe proves a server can be *added*; only a live run +proves it *works*. Dates are when the row was last verified; a row is not +re-verified by later releases. + +Everything below was run against a local rig: one fleet process with the +Postgres pair, the web tier on `http://localhost:3200`, real OpenRouter +models, real vendor accounts. Vendors that need an HTTPS callback (Slack) +were reached through a temporary Cloudflare quick tunnel. + +## Live runs (the manual OAuth pack) + +Legend — ✓ passed · ✗ failed · — not exercised · n/a the vendor has no such +thing · **fixed by** names the fleet PR that had to land first. + +| connector | auth shape | add | consent + token | tools list | tool call (chat) | scheduled task | refresh | sign-out | seats / sharing | verdict | last verified | +|---|---|---|---|---|---|---|---|---|---|---|---| +| GitHub | manual client, **secret required** | ✓ | ✓ | ✓ | ✓ `get_me` | ✓ | ✓ natural + forced (8 h token; dead refresh → `needs_reauth`) | local only (no revocation endpoint); vendor "Revoke all" → `needs_reauth` on the next 401 | 2 seats ✓ | **PASS** — fixed by #1405 #1446 #1449 #1454 #1464 #1465 #1467 | 2026-09-10 | +| Notion | dynamic registration, public client | ✓ | ✓ | ✓ | ✓ | ✓ pinned to the `test` seat | ✓ natural (headless in the scheduled run) | — | 2 seats ✓; share to a second user ✓, revoke ✓ | **PASS** | 2026-09-10 / sharing 2026-09-14 | +| Linear | dynamic registration, public client | ✓ | ✓ | ✓ | ✓ direct and deferred | ✓ | ✓ natural | ✓ real revocation endpoint; reconnect keeps the client id | — | **PASS** — fixed by #1467 (prompt roster) | 2026-09-09 | +| Slack | manual client, **secret required**, HTTPS callback | ✓ | ✓ | ✓ | ✓ deferred | ✓ `search_channels` | n/a — token has no expiry and no refresh token (rotation off) | local only (no revocation endpoint) | — | **PASS** with caveats — fixed by #1471 #1472; app needs the *Model Context Protocol* toggle | 2026-09-10 | +| Google Drive | manual client (GCP), **secret required** | ✓ | ✓ | ✓ | ✗ `The caller does not have permission` | mount + refresh ✓, call ✗ | ✓ forced (1 h token; needs `access_type=offline` + `prompt=consent`) | — | — | **BLOCKED by vendor** — Workspace MCP is a Developer Preview; a personal Gmail account is not eligible. fleet side fixed by #1465 | 2026-09-10 | +| Azure DevOps | tenant URL, manual Entra app, **secret required** | ✓ | ✓ tenant-native user | ✓ | ✓ `core_list_projects`, `repo_repository` | ✓ | ✓ forced (71 min token, rotated refresh token) | local only (no revocation endpoint) | — | **PASS** — fixed by #1481 | 2026-09-13 | +| Stripe | dynamic registration, public client | ✓ | ✓ | ✓ | ✓ four `stripe_api_read` calls in deferred mode | — | — | — | — | **PASS** (connect + tools) — fixed by #1482 (metadata location) and #1483 (required arguments in deferred mode) | 2026-09-14 | +| Grafana Cloud | dynamic registration, public client | ✓ | ✓ | ✓ | ✓ `list_users_by_org`, `list_datasources` | — | — | — | — | **PASS** (connect + tools) — fixed by #1482 | 2026-09-14 | +| Uptime Robot | dynamic registration (vendor returned a secret) | ✓ | ✓ | ✓ | ✓ `list-monitors`, `get-monitor-stats` | — | — | — | — | **PASS** (connect + tools) — fixed by #1485 (pointer only on POST) | 2026-09-14 | +| Plaid | no protected-resource metadata (origin is the AS) | ✓ | ✓ | ✓ | ✓ `list_teams` | — | — | — | — | **PASS on the 2026-09-14 rig build**; on `main` after #1485's final form discovery is refused (the vendor answers 401 under its MCP path). Not fixed — skipped by decision | 2026-09-14 | +| Cartesia | dynamic registration | ✓ (wrong URL) | ✓ | ✗ 404 | — | — | — | — | — | **catalog URL wrong**, corrected in #1501 (`/mcp`, from #1495); not re-run since | 2026-09-14 | +| Intercom | no protected-resource metadata | probe ✓ | — | — | — | — | — | — | — | discovery only; same `main` refusal as Plaid | 2026-09-14 | + +Cross-cutting checks, all live: + +- **Seats** (#988): two GitHub accounts and two Notion accounts on one user; a + scheduled task pinned to a non-default seat mounts that seat and leaves the + default untouched; a pin to a seat that does not exist skips the connector + with a notice; a pin to a server the owner never connected dead-letters the + task before any model spend. +- **Sharing**: a second rig user given one Notion seat sees only that seat, can + call its tools from chat and from a scheduled task, and cannot manage it + (every owner action answers 404); revocation takes effect on the grantee's + next turn / next scheduled run (access is checked at mount time in + `ConnectedServersForUser`, so a run already holding the mount finishes); + grantees cannot see or manage the credential, but a grantee's run can trigger + the broker-managed refresh of the owner's token row (and a 401 at mount sends + the owner's row to `needs_reauth`). +- **Headless refresh**: expired GitHub, Google Drive, Notion and Azure DevOps + tokens were refreshed by scheduled runs with no browser. +- **Deferred mode** (more than 128 tools): tool calls through + `tool_search`/`tool_describe`/`tool_call` for GitHub, Notion, Slack, Linear, + Stripe, Grafana Cloud, Uptime Robot. + +## Fleet changes the pack produced + +| PR | what it fixed | found by | +|---|---|---| +| #1405 | manual client without a secret sailed into a doomed exchange; `client_secret: required` on GitHub | GitHub | +| #1446 | system prompt claimed "no MCP tools connected" while hosted tools were mounted | GitHub | +| #1449 | connect failures were skipped silently; reason now logged (redacted) | GitHub | +| #1454 / #1464 | a vendor 401 at mount now marks the row `needs_reauth`; non-2xx bodies surfaced | GitHub revoke-all | +| #1465 | Google offline access; GitHub's 200-status OAuth errors; expired token without a refresh token marked | Google, GitHub | +| #1467 | live tool registry in the prompt built from the real roster | Linear | +| #1471 | Slack's resource indicator is its origin while the endpoint is `/mcp` | Slack | +| #1472 | `client_secret: required` on every manual entry whose metadata allows no public client | catalog sweep | +| #1481 | Entra's templated issuer; `offline_access` for Entra | Azure DevOps | +| #1482 | authorization-server metadata at RFC 8414's path-inserted location | audit (13 vendors) | +| #1483 | `tool_describe` shows required arguments; `tool_call` refuses a call missing one | Stripe | +| #1485 | servers with no protected-resource metadata; pointer only on POST | audit (Plaid, Uptime Robot, …) | +| #1488 | proxied authorization-server documents; registration retry as a confidential client; Auth0 `offline_access` | audit (DocuSign, ZoomInfo, Sprout Social, OVHcloud, Chargebee, Checkly) | +| #1495 | seven catalog entries with a wrong URL, auth type or docs link | audit | + +## The catalog audit (2026-09-13, re-run 2026-09-14) + +All 231 official entries with `auth: oauth`, `tenant` or `open` were probed +with fleet's own discovery. The per-entry appendix below is the re-run on the +code in `main` plus #1488 and #1495. Findings and their disposition: + +| id | finding | disposition | +|---|---|---| +| F1 | metadata only at RFC 8414's path-inserted location (13 vendors) | fixed, #1482 merged | +| F2 | no protected-resource metadata; origin is the AS | fixed, #1485 merged; `main`'s final form refuses a vendor that answers 401 under its MCP path (Plaid, Intercom) — **skipped by decision** | +| F3 | metadata pointer only on the POST reply | fixed, #1485 merged | +| F4 | document names another issuer (copy / proxy / hybrid) | fixed, merged (#1488) | +| F5 | registration always asks for a public client | two vendors accepted it anyway; retry on refusal in #1488 | +| F6 | `offline_access` not requested where the vendor needs it | Entra (#1481) and Auth0 (#1488); IdentityServer, Keycloak, Ory unverified | +| F7 | `tool_describe` hid the required-argument list | fixed, #1483 merged | +| F8 | the broker masks a vendor's 4xx argument error as "credential-owner call failed" | open, not started | +| F9 | `tool_search` ranks other connectors above the one named in the query | observation | +| F10 | a connect failure is announced to the model as "needs re-authorization" | open, not started | +| F11 | fleet has no legacy HTTP+SSE transport; Square and Smartlead document SSE-only endpoints | **skipped by decision**; entries left as they are | +| C1–C9 | seven catalog data errors (Expensify, Cartesia, Octagon, Globalping, Zerodha Kite (its `login` tool session is per-turn only; no scheduled-run auth), Sage Intacct, OpenRouter); Square, Smartlead untouched | landed in #1501 (superseded #1495) | +| F12 | Bugsnag's 401 points at its metadata over plain `http://`; the vendor redirects to https, fleet's client refuses redirects, and since #1485 a failed advertised pointer is fatal (it fell through to the well-known locations before) | open, not started — a same-host `http`→`https` upgrade of the pointer would cover it | +| F13 | Saved connections retain their original URL and auth across catalog corrections until removed and re-added. | Open — reconciliation of saved rows to updated definitions is deferred, requiring a manual re-add for Cartesia, Octagon, and Globalping. | +| V1 | 25 entries publish no scopes anywhere | live add needed per vendor | +| V2 | GoCardless, Adobe, Square, Wrike answer 403 to every unauthenticated request from the audit network | re-probe from another network before calling them broken | +| V3 | 22 tenant entries have a placeholder in the hostname and cannot be probed | expected | + + +## Appendix — every official OAuth, tenant and open entry, probed 2026-09-14 + +Probe = fleet's `mcpoauth.Discover` plus the add-time guards, run against the catalog as of #1495 with the code in `main` plus #1488. "discovery ✓" means Connect would reach the vendor's consent screen; it says nothing about tool calls. A tenant entry whose hostname carries a `{placeholder}` cannot be probed without a customer's value; tenant hostnames in the table (e.g. ``) are placeholders for the probe tenant. Counts — discovery ✓: 177 · tenant: 31 · open ✓: 12 · discovery ✗: 7 · open: 3 · not probeable from the audit box: 1. + +| entry | auth | probe verdict | client shape | issuer | notes | +|---|---|---|---|---|---| +| adobe-creativity | oauth | discovery ✗ — 403 to every unauthenticated request from the audit network (V2) | — | | | +| ahrefs | oauth | discovery ✓ | self-registering, secret | https://api.ahrefs.com/ | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| airbyte | oauth | discovery ✓ | self-registering, secret | https://mcp.airbyte.ai/ | AS lists no `none`; fleet asks `none`, retries confidential (#1488); no scopes published (V1) | +| airtable | oauth | discovery ✓ | self-registering, public client ok | https://airtable.com/oauth2/v1 | | +| airwallex | oauth | discovery ✓ | self-registering, secret | https://mcp.airwallex.com/mcp | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| aiven | oauth | discovery ✓ | self-registering, public client ok | https://api.aiven.io | | +| alchemy | oauth | discovery ✓ | self-registering, secret | https://auth.alchemy.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488); no scopes published (V1) | +| algolia | oauth | discovery ✓ | self-registering, public client ok | https://dashboard.algolia.com | | +| alloydb | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| alpha-vantage | oauth | discovery ✓ | self-registering, public client ok | https://mcp.alphavantage.co | | +| amazon-ads | oauth | discovery ✓ | manual client, public client ok | https://lwa.amazon.com | | +| amplitude | oauth | discovery ✓ | self-registering, public client ok | https://mcp.amplitude.com | advertises `offline_access`; refresh unverified | +| apify | oauth | discovery ✓ | self-registering, public client ok | https://console-backend.apify.com | | +| apollo-io | oauth | discovery ✓ | self-registering, public client ok | https://mcp.apollo.io | | +| asana | oauth | discovery ✓ | manual client, secret | https://app.asana.com | | +| atlassian | oauth | discovery ✓ | self-registering, public client ok | https://auth.atlassian.com/VCeDsk8ZHncYF1g234fKtc4lNipbBhu3 | | +| attio | oauth | discovery ✓ | self-registering, public client ok | https://app.attio.com | | +| audioscrape | oauth | discovery ✓ | self-registering, public client ok | https://mcp.audioscrape.com | | +| avalara-avatax | oauth | discovery ✓ | self-registering, secret | https://identity.avalara.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488); advertises `offline_access`; refresh unverified | +| aws-knowledge | open | open ✓ (initialize 200) | n/a | | | +| aws-mcp | oauth | discovery ✓ | self-registering, public client ok | https://us-east-1.oauth.signin.aws | no scopes published (V1) | +| axiom | oauth | discovery ✓ | self-registering, public client ok | https://authorization.axiom.co | | +| azure-devops | tenant | discovery ✓ | manual client, secret | https://login.microsoftonline.com/organizations/v2.0 | | +| better-stack | oauth | discovery ✓ | self-registering, public client ok | https://betterstack.com | | +| black-forest-labs | oauth | discovery ✓ | self-registering, public client ok | https://uhjidycotobjggwyjdww.supabase.co/auth/v1 | advertises `offline_access`; refresh unverified | +| box | oauth | discovery ✓ | manual client, secret | https://api.box.com | no scopes published (V1) | +| brex | oauth | discovery ✓ | self-registering, public client ok | https://api.brex.com | | +| bright-data | tenant | discovery ✓ | self-registering, public client ok | https://brightdata.com | | +| browserstack | oauth | discovery ✓ | self-registering, public client ok | https://mcp.browserstack.com/ | | +| bugsnag | oauth | discovery ✗ — fetch protected-resource metadata the server advertised at http://bugsnag.mcp.smartbear.co | — | | | +| buildkite | oauth | discovery ✓ | self-registering, public client ok | https://mcp.buildkite.com | | +| cal-com | oauth | discovery ✓ | self-registering, public client ok | https://mcp.cal.com | no scopes published (V1) | +| calendly | oauth | discovery ✓ | self-registering, public client ok | https://calendly.com/ | | +| canva | oauth | discovery ✓ | self-registering, public client ok | https://mcp.canva.com | | +| cartesia | oauth | discovery ✓ | self-registering, secret | https://mcp.cartesia.ai/ | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| chargebee | tenant | discovery ✓ | manual client, public client ok | https://app.chargebee.com | | +| checkly | oauth | discovery ✓ | self-registering, public client ok | https://auth.checklyhq.com/ | | +| checkout-com | oauth | discovery ✓ | self-registering, secret | https://access.mcp.checkout.com/payment-operations | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| chromatic | tenant | tenant — not probeable without a real tenant value | — | | | +| chronosphere | tenant | tenant — not probeable without a real tenant value | — | | | +| clickhouse | oauth | discovery ✓ | self-registering, public client ok | https://mcp.clickhouse.cloud | | +| clickup | oauth | discovery ✓ | self-registering, public client ok | https://mcp.clickup.com | | +| close | oauth | discovery ✓ | self-registering, public client ok | https://api.close.com | | +| cloudflare | oauth | discovery ✓ | self-registering, public client ok | https://mcp.cloudflare.com | no scopes published (V1) | +| cloudflare-docs | open | open ✓ (initialize 200) | n/a | | | +| cloudinary | oauth | discovery ✓ | self-registering, public client ok | https://asset-management.mcp.cloudinary.com | | +| cockroachdb | oauth | discovery ✓ | self-registering, public client ok | https://cockroachlabs.cloud/mcp | | +| coingecko | open | open ✓ (initialize 200) | n/a | | | +| contentful | oauth | discovery ✓ | self-registering, public client ok | https://mcp.contentful.com | no scopes published (V1) | +| context7 | open | open ✓ (initialize 200) | n/a | | | +| coralogix | tenant | tenant — not probeable without a real tenant value | — | | | +| craft | oauth | discovery ✓ | self-registering, public client ok | https://mcp.craft.do/my/auth | no scopes published (V1) | +| cube | tenant | tenant — not probeable without a real tenant value | — | | | +| customer-io | oauth | discovery ✓ | self-registering, public client ok | https://mcp.customer.io | | +| databricks | tenant | tenant — not probeable without a real tenant value | — | | | +| datadog | oauth | discovery ✓ | self-registering, public client ok | https://mcp.datadoghq.com/v1/mcp | | +| dbt | tenant | tenant — not probeable without a real tenant value | — | | | +| deepwiki | open | open ✓ (initialize 200) | n/a | | | +| digitalocean | oauth | discovery ✓ | self-registering, public client ok | https://cloud.digitalocean.com | no scopes published (V1) | +| docusign | oauth | discovery ✓ | manual client, secret | https://account.docusign.com | | +| doordash | oauth | discovery ✓ | manual client, public client ok | https://identity.doordash.com | | +| dropbox | oauth | discovery ✓ | self-registering, public client ok | https://www.dropbox.com | | +| dune | oauth | discovery ✓ | self-registering, public client ok | https://dune.com/oauth/mcp | | +| dynatrace | tenant | tenant — not probeable without a real tenant value | — | | | +| egnyte | oauth | discovery ✓ | self-registering, secret | https://mcp-oauth.egnyte.com/egnyte-connect | AS lists no `none`; fleet asks `none`, retries confidential (#1488); no scopes published (V1) | +| elastic-agent-builder | tenant | tenant — not probeable without a real tenant value | — | | | +| expensify | oauth | discovery ✓ | self-registering, public client ok | https://www.expensify.com | | +| expo | oauth | discovery ✓ | self-registering, public client ok | https://mcp.expo.dev | | +| fellow | oauth | discovery ✓ | self-registering, public client ok | https://fellow.app | | +| fibery | oauth | discovery ✓ | self-registering, public client ok | https://mcp.fibery.io/ | | +| figma | oauth | discovery ✓ | self-registering, secret | https://api.figma.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| financial-datasets | oauth | discovery ✓ | self-registering, public client ok | https://mcp.financialdatasets.ai | | +| freshdesk | tenant | tenant — not probeable without a real tenant value | — | | | +| front | oauth | discovery ✓ | manual client, secret | https://app.frontapp.com | | +| gamma | oauth | discovery ✓ | self-registering, public client ok | https://auth.gamma.app | | +| github | oauth | discovery ✓ | manual client, secret | https://github.com/login/oauth | | +| gitlab | oauth | discovery ✓ | self-registering, secret | https://gitlab.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| globalping | oauth | discovery ✓ | self-registering, public client ok | https://mcp.globalping.dev | | +| gocardless | oauth | discovery ✗ — 403 to every unauthenticated request from the audit network (V2) | — | | | +| google-calendar | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| google-chat | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| google-cloud | tenant | tenant — not probeable without a real tenant value | — | | | +| google-docs | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| google-drive | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| google-gemini-agent-platform | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| google-gmail | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| google-people | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| google-sheets | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| google-slides | oauth | discovery ✓ | manual client, secret | https://accounts.google.com | | +| gorgias | oauth | discovery ✓ | self-registering, public client ok | https://mcp.gorgias.com/ | | +| grafana-cloud | oauth | discovery ✓ | self-registering, public client ok | https://mcp.grafana.com/mcp | | +| grain | oauth | discovery ✓ | self-registering, public client ok | https://api.grain.com | no scopes published (V1) | +| gram | tenant | tenant — not probeable without a real tenant value | — | | | +| granola | oauth | discovery ✓ | self-registering, public client ok | https://mcp-auth.granola.ai | advertises `offline_access`; refresh unverified | +| guru | oauth | discovery ✓ | self-registering, public client ok | https://mcp.api.getguru.com | | +| harness | oauth | discovery ✓ | self-registering, secret | https://id.harness.io/idp/realms/HarnessIDP | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| heroku | oauth | discovery ✓ | self-registering, secret | https://mcp.heroku.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| hex | oauth | discovery ✓ | self-registering, public client ok | https://auth.app.hex.tech | | +| heygen | oauth | discovery ✓ | self-registering, public client ok | https://api2.heygen.com | | +| honeycomb | oauth | discovery ✓ | self-registering, public client ok | https://ui.honeycomb.io | | +| hootsuite | oauth | discovery ✓ | self-registering, public client ok | https://platform.hootsuite.com | | +| hootsuite-lumen | oauth | discovery ✓ | self-registering, public client ok | https://app.talkwalker.com/app/ | | +| hootsuite-nest | oauth | discovery ✓ | self-registering, public client ok | https://platform.hootsuite.com | | +| hubspot | oauth | discovery ✓ | manual client, secret | https://mcp.hubspot.com | no scopes published (V1) | +| hugging-face | oauth | discovery ✓ | self-registering, secret | https://huggingface.co | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| ideogram | oauth | discovery ✓ | self-registering, secret | https://mcp.ideogram.ai/ | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| incident-io | oauth | discovery ✓ | self-registering, public client ok | https://mcp.incident.io/mcp | no scopes published (V1) | +| intercom | oauth | discovery ✗ — 401 at a metadata location; skipped by decision | — | | | +| jetbrains-youtrack | tenant | tenant — not probeable without a real tenant value | — | | | +| jotform | oauth | discovery ✓ | self-registering, public client ok | https://oauth2.jotform.com | | +| kiwi-flights | open | open ✓ (initialize 200) | n/a | | | +| klaviyo | oauth | discovery ✓ | self-registering, public client ok | https://mcp.klaviyo.com | no scopes published (V1) | +| lambdatest | oauth | discovery ✓ | self-registering, public client ok | https://auth.lambdatest.com | no scopes published (V1) | +| langsmith | oauth | discovery ✓ | self-registering, public client ok | https://api.smith.langchain.com | no scopes published (V1) | +| lemlist | oauth | discovery ✓ | self-registering, public client ok | https://app.lemlist.com | | +| linear | oauth | discovery ✓ | self-registering, public client ok | https://mcp.linear.app | | +| looker | tenant | tenant — not probeable without a real tenant value | — | | | +| lusha | oauth | discovery ✓ | self-registering, secret | https://auth.lusha.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| magnific-freepik | oauth | discovery ✓ | self-registering, public client ok | https://auth.magnific.com/realms/mcp | advertises `offline_access`; refresh unverified | +| mailerlite | oauth | discovery ✓ | self-registering, public client ok | https://mcp.mailerlite.com | no scopes published (V1) | +| mapbox | oauth | discovery ✓ | self-registering, public client ok | https://mcp.mapbox.com | | +| mercado-libre | oauth | discovery ✓ | self-registering, public client ok | https://mcp.mercadolibre.com/mcp | | +| mercado-pago | oauth | discovery ✓ | self-registering, public client ok | https://mcp.mercadopago.com/mcp | | +| mercury | oauth | discovery ✓ | self-registering, public client ok | https://mcp.mercury.com/ | | +| messari | oauth | discovery ✓ | self-registering, public client ok | https://mcp.messari.io | no scopes published (V1) | +| meta-ads | oauth | discovery ✓ | self-registering, public client ok | https://www.facebook.com/ads | | +| metabase | tenant | tenant — not probeable without a real tenant value | — | | | +| microsoft-dataverse | tenant | tenant — not probeable without a real tenant value | — | | | +| microsoft-learn | open | open ✓ (initialize 200) | n/a | | | +| microsoft-workiq-calendar | tenant | tenant — not probeable without a real tenant value | — | | | +| microsoft-workiq-mail | tenant | tenant — not probeable without a real tenant value | — | | | +| microsoft-workiq-sharepoint | tenant | tenant — not probeable without a real tenant value | — | | | +| microsoft-workiq-teams | tenant | tenant — not probeable without a real tenant value | — | | | +| miro | oauth | discovery ✓ | self-registering, secret | https://mcp.miro.com/ | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| mixpanel | oauth | discovery ✓ | self-registering, public client ok | https://mcp.mixpanel.com/mcp | | +| monday | oauth | discovery ✓ | self-registering, secret | https://auth.monday.com/mcp | AS lists no `none`; fleet asks `none`, retries confidential (#1488); no scopes published (V1) | +| motherduck | oauth | discovery ✓ | self-registering, public client ok | https://mcp-auth.motherduck.com | | +| mux | oauth | discovery ✓ | self-registering, public client ok | https://auth.mux.com | | +| neon | oauth | discovery ✓ | self-registering, public client ok | https://mcp.neon.tech | | +| netlify | oauth | discovery ✓ | self-registering, public client ok | https://netlify-mcp.netlify.app/ | | +| netsuite | tenant | tenant — not probeable without a real tenant value | — | | | +| new-relic | oauth | discovery ✓ | self-registering, public client ok | https://oauth2.service.newrelic.com | | +| notion | oauth | discovery ✓ | self-registering, public client ok | https://mcp.notion.com | | +| octagon | oauth | discovery ✓ | self-registering, public client ok | https://login.octagonai.co | | +| omni | tenant | tenant — not probeable without a real tenant value | — | | | +| openai-developer-docs | open | open ✓ (initialize 200) | n/a | | | +| openrouter | oauth | discovery ✓ | self-registering, public client ok | https://mcp.openrouter.ai | no scopes published (V1) | +| oracle-autonomous-db | tenant | tenant — not probeable without a real tenant value | — | | | +| orca-security | oauth | discovery ✓ | self-registering, public client ok | https://auth.orcasecurity.io | no scopes published (V1) | +| otter-ai | oauth | discovery ✓ | self-registering, public client ok | https://otter.ai | | +| outreach | oauth | discovery ✓ | self-registering, secret | https://api.outreach.io | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| ovhcloud | oauth | discovery ✓ | self-registering, secret | https://mcp.eu.ovhcloud.com/oauth-proxy | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| parallel-search | open | open ✓ (initialize 200) | n/a | | | +| paypal | oauth | discovery ✓ | self-registering, public client ok | https://mcp.paypal.com | | +| pika | oauth | discovery ✓ | self-registering, public client ok | https://ecyvlzfbufloietjsmtj.supabase.co/auth/v1 | | +| pinecone-assistant | tenant | tenant — not probeable without a real tenant value | — | | | +| pipedrive | oauth | discovery ✓ | self-registering, secret | https://oauth.pipedrive.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| plaid | oauth | discovery ✗ — 401 at a metadata location; skipped by decision | — | | | +| plain | oauth | discovery ✓ | self-registering, public client ok | https://signin.auth.plain.com | | +| plane | oauth | discovery ✓ | self-registering, secret | https://mcp.plane.so/http | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| planetscale | oauth | discovery ✓ | self-registering, secret | https://api.planetscale.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| polar | oauth | discovery ✓ | self-registering, public client ok | https://api.polar.sh | | +| posthog | oauth | discovery ✓ | self-registering, public client ok | https://oauth.posthog.com | | +| postman | oauth | discovery ✓ | self-registering, public client ok | https://mcp.postman.com | no scopes published (V1) | +| preset | oauth | discovery ✓ | self-registering, public client ok | https://api.superset.sh | | +| prisma | oauth | discovery ✓ | self-registering, public client ok | https://auth.prisma.io | | +| pulumi | oauth | discovery ✓ | self-registering, public client ok | https://mcp.ai.pulumi.com | | +| pydantic-logfire | oauth | discovery ✓ | self-registering, public client ok | https://logfire-us.pydantic.dev | | +| pylon | oauth | discovery ✓ | self-registering, public client ok | https://o.auth.usepylon.com | | +| railway | oauth | discovery ✓ | self-registering, public client ok | https://backboard.railway.com | | +| ramp | oauth | discovery ✓ | self-registering, public client ok | https://mcp.ramp.com | | +| read-ai | oauth | discovery ✓ | self-registering, public client ok | https://authn.read.ai/ | | +| recraft | oauth | discovery ✓ | self-registering, public client ok | https://mcp.recraft.ai | | +| render | oauth | not probeable from the audit box (its resolver maps the host to 127.0.0.1; public DNS is fine) | — | | | +| replicate | oauth | discovery ✓ | self-registering, public client ok | https://mcp.replicate.com | no scopes published (V1) | +| replit | oauth | discovery ✓ | self-registering, public client ok | https://replit.com/oidc | | +| rootly | oauth | discovery ✓ | self-registering, public client ok | https://rootly.com | | +| runpod | oauth | discovery ✓ | self-registering, public client ok | https://mcp.getrunpod.io | no scopes published (V1) | +| runway | oauth | discovery ✓ | self-registering, public client ok | https://mcp.runwayml.com | | +| sage-intacct | oauth | discovery ✓ | manual client, secret | https://mcp.intacct.com | | +| salesforce | tenant | discovery ✓ | self-registering, secret | https://login.salesforce.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488); advertises `offline_access`; refresh unverified | +| sanity | oauth | discovery ✓ | self-registering, public client ok | https://mcp.sanity.io | | +| scrapfly | open | open — key in the URL; probe used a bogus key (answered 401) | n/a | | | +| scrapingbee | tenant | tenant — not probeable without a real tenant value | — | | | +| semaphore | oauth | discovery ✓ | self-registering, public client ok | https://mcp.semaphoreci.com/mcp/oauth | | +| semgrep | oauth | discovery ✓ | self-registering, public client ok | https://login.semgrep.dev | | +| semrush | oauth | discovery ✓ | self-registering, public client ok | https://oauth.semrush.com | advertises `offline_access`; refresh unverified | +| sentry | oauth | discovery ✓ | self-registering, public client ok | https://mcp.sentry.dev | | +| servicenow | tenant | tenant — not probeable without a real tenant value | — | | | +| shopify-storefront | tenant | tenant — not probeable without a real tenant value | — | | | +| shortcut | oauth | discovery ✓ | self-registering, public client ok | https://api.app.shortcut.com | | +| signoz | tenant | tenant — not probeable without a real tenant value | — | | | +| slack | oauth | discovery ✓ | manual client, secret | https://mcp.slack.com | | +| slite | oauth | discovery ✓ | self-registering, public client ok | https://slite.com/api/mcp/oauth | | +| smartlead | open | open — key in the URL; probe used a bogus key (answered 404) | n/a | | | +| snowflake | tenant | tenant — not probeable without a real tenant value | — | | | +| socket | oauth | discovery ✓ | self-registering, public client ok | https://api.socket.dev | | +| sourcegraph | tenant | tenant — not probeable without a real tenant value | — | | | +| spacelift | tenant | discovery ✓ | self-registering, public client ok | https://.app.spacelift.io | | +| sprout-social | oauth | discovery ✓ | self-registering, secret | https://identity.sproutsocial.com/oauth2/84e39c75-d770-45d9- | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| square | oauth | discovery ✗ — 403 to every unauthenticated request from the audit network (V2) | — | | | +| stainless | tenant | tenant — not probeable without a real tenant value | — | | | +| stripe | oauth | discovery ✓ | self-registering, public client ok | https://access.stripe.com/mcp | | +| supabase | oauth | discovery ✓ | self-registering, secret | https://api.supabase.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| superhuman-mail | oauth | discovery ✓ | self-registering, public client ok | https://mcp.auth.mail.superhuman.com | | +| surveymonkey | oauth | discovery ✓ | self-registering, secret | https://mcp.surveymonkey.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| teamwork | oauth | discovery ✓ | self-registering, public client ok | https://teamwork.com | | +| thirdweb | open | open — key in the URL; probe used a bogus key (answered 401) | n/a | | | +| ticktick | oauth | discovery ✓ | self-registering, public client ok | https://ticktick.com | | +| tigris | oauth | discovery ✓ | self-registering, public client ok | https://mcp.storage.dev | | +| tinybird | tenant | tenant — not probeable without a real tenant value | — | | | +| todoist | oauth | discovery ✓ | self-registering, public client ok | https://todoist.com | | +| trello | oauth | discovery ✓ | self-registering, public client ok | https://auth.atlassian.com/VCeDsk8ZHncYF1g234fKtc4lNipbBhu3 | | +| twelve-data | oauth | discovery ✓ | self-registering, secret | https://mcp.twelvedata.com/ | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| twilio | open | open ✓ (initialize 200) | n/a | | | +| typeform | oauth | discovery ✓ | self-registering, public client ok | https://api.typeform.com | advertises `offline_access`; refresh unverified | +| upstox | oauth | discovery ✓ | self-registering, public client ok | https://mcp.upstox.com | no scopes published (V1) | +| uptime-robot | oauth | discovery ✓ | self-registering, secret | https://mcp.uptimerobot.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488) | +| val-town | oauth | discovery ✓ | self-registering, public client ok | https://www.val.town/oauth | | +| vercel | oauth | discovery ✓ | self-registering, secret | https://vercel.com | AS lists no `none`; fleet asks `none`, retries confidential (#1488); advertises `offline_access`; refresh unverified | +| vimeo | oauth | discovery ✓ | self-registering, public client ok | https://mcp.vimeo.com/ | | +| webflow | oauth | discovery ✓ | self-registering, public client ok | https://mcp.webflow.com | no scopes published (V1) | +| wix | oauth | discovery ✓ | self-registering, public client ok | https://mcp.wix.com | | +| wiz | oauth | discovery ✓ | self-registering, public client ok | https://mcp.app.wiz.io | | +| wordpress-com | oauth | discovery ✓ | self-registering, public client ok | https://public-api.wordpress.com | | +| wrike | oauth | discovery ✗ — 403 to every unauthenticated request from the audit network (V2) | — | | | +| x-docs | open | open ✓ (initialize 200) | n/a | | | +| xero | oauth | discovery ✓ | manual client, secret | https://identity.xero.com | | +| zerodha-kite | open | open ✓ (initialize 200) | n/a | | `login` tool session lives only within the calling turn; does not persist across turns and cannot authenticate in scheduled runs | +| zoom | oauth | discovery ✓ | manual client, secret | https://zoom.us | | +| zoominfo | oauth | discovery ✓ | self-registering, secret | https://okta-login.zoominfo.com/oauth2/default | AS lists no `none`; fleet asks `none`, retries confidential (#1488); docs_url 404 | diff --git a/docs/README.md b/docs/README.md index 8a66a63bd..246d336aa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -124,6 +124,11 @@ there) and [`../.agents/skills/steward/SKILL.md`](../.agents/skills/steward/SKIL what a proxied issuer must prove, tenant scoping, dynamic client registration, the `offline_access` clause): [`docs/MCP-OAUTH-DISCOVERY.md`](MCP-OAUTH-DISCOVERY.md) +- **Connecting hosted (official, per-user OAuth) MCP servers** — what each + vendor needs before Connect works, how to read a failure, and which vendors + were verified live: [`docs/HOSTED-CONNECTORS-RUNBOOK.md`](HOSTED-CONNECTORS-RUNBOOK.md) + + [`docs/MCP-CATALOG-STATUS.md`](MCP-CATALOG-STATUS.md) (the #1006 status + table and the per-entry catalog audit) - **Bundle-managed SES/S3 email-report infrastructure:** use the external canonical [new-client email-report runbook](https://github.com/ElcanoTek/ses-s3-setup/blob/main/docs/NEW-CLIENT-EMAIL-SETUP.md); keep client-specific inventory in the external client bundle. @@ -212,6 +217,7 @@ above fails otherwise. - [`generating-demo-gif.md`](generating-demo-gif.md) — Generating the demo GIFs (TUI + web) - [`GUARDRAILS.md`](GUARDRAILS.md) — Prompt-injection guardrails (#702) - [`HOOKS.md`](HOOKS.md) — Governed lifecycle hooks (#788) +- [`HOSTED-CONNECTORS-RUNBOOK.md`](HOSTED-CONNECTORS-RUNBOOK.md) — Hosted MCP connectors — operator runbook - [`implementation-plans-enhancements.md`](implementation-plans-enhancements.md) — Implementation plan: #984 — Fleet ↔ Buzz bridge - [`INPUT-QUEUE.md`](INPUT-QUEUE.md) — Input queue & mid-turn steering (#785) - [`keyboard-shortcuts.md`](keyboard-shortcuts.md) — Keyboard shortcuts @@ -223,6 +229,7 @@ above fails otherwise. - [`MAINTENANCE.md`](MAINTENANCE.md) — Reclamation, disk backpressure, and stuck-task backstops - [`MCP-BROKER-SCOPES.md`](MCP-BROKER-SCOPES.md) — MCP broker scoped sessions - [`MCP-BUNDLE-ENV.md`](MCP-BUNDLE-ENV.md) — MCP bundle env contract: `${FLEET_WORKSPACE}`, `MCP_VARIANT_CLIENT`, `identity_env`, interactive critical-tool staging +- [`MCP-CATALOG-STATUS.md`](MCP-CATALOG-STATUS.md) — Hosted MCP connector status — the #1006 OAuth pack and the catalog audit - [`MCP-CATALOG.md`](MCP-CATALOG.md) — The MCP connector directory — trust classes, built-in catalog, provenance - [`MCP-OAUTH-DISCOVERY.md`](MCP-OAUTH-DISCOVERY.md) — MCP OAuth discovery — the chain, proxied issuers, tenant scoping, registration, refresh scopes - [`MCP-RELOAD.md`](MCP-RELOAD.md) — MCP server hot-reload (#218) @@ -265,6 +272,7 @@ above fails otherwise. - [`SUBAGENTS.md`](SUBAGENTS.md) — Sub-agents: default-on, parent decides, typed children (#1043) - [`TASK-SCHEDULE-UX.md`](TASK-SCHEDULE-UX.md) — Create Task schedule controls - [`TASK-SERIALIZATION.md`](TASK-SERIALIZATION.md) — Task serialization — opaque `serialization_key` mutual exclusion (#709) +- [`TASK-TAGS.md`](TASK-TAGS.md) — Task tags on the board — chips, the tag filter, and the catalogue TTL - [`TASK-TITLES.md`](TASK-TITLES.md) — Task titles - [`TEAM-SHARING.md`](TEAM-SHARING.md) — Sharing work inside a project — team-shared chats and team learnings - [`TESTING.md`](TESTING.md) — Testing fleet diff --git a/docs/TASK-TAGS.md b/docs/TASK-TAGS.md new file mode 100644 index 000000000..10792762d --- /dev/null +++ b/docs/TASK-TAGS.md @@ -0,0 +1,95 @@ +# Task tags on the board + +What shipped when tags stopped being write-only, what deviated, and what was +deliberately left out. The user-facing description lives in the Operations +Center guide ("Finding things"); this note is for whoever changes the code. + +## The gap + +Tags (#212) were storable and queryable but invisible. The create form accepted +them, `models.Task` carried them, `TaskFilter.Tags` filtered on them, +`GET /tasks?tag=a&tag=b` narrowed to tasks carrying **both**, and +`GET /tasks/tags` returned the whole catalogue with per-tag counts — and no +surface in the web app ever rendered a tag again. So the one thing a tag is +for, finding the rest of its group, could not be done from the UI at all. + +The gap was found while writing the user guide, which is worth recording: the +guide had to describe tags as "stored metadata rather than a control on that +screen", and a sentence that awkward is usually a defect wearing prose. + +## What shipped + +- **Chips.** A task's tags render on its table row and its phone card, coloured + from the same hashed palette (`shared/lib/labelColors`) as the chat + conversation labels, so one tag reads the same everywhere it appears. +- **Every chip is a control.** Clicking one adds that tag to the board's + filter; clicking a selected one removes it. Tags AND server-side, so each + addition narrows and each removal widens. +- **A Tags group in the filter bar** — a select that *adds* a tag, plus a + removable chip per selected tag. The select never holds a value: the board is + filtered by every chip beside it, not by the last one chosen, and a select + reading `ops` while `ops + urgent` were applied would misstate the board. +- **Tags count as an active filter**, so **Clear filters** appears and clears + them. Without that the only way back to the full board was a page reload. +- **`/api/orchestrator/tasks/tags`**, a thin proxy to the existing catalogue + endpoint. The static `tags` segment wins over the sibling `[taskId]` route, + so it does not shadow `GET /tasks/{id}` — the same ordering `cmd/fleet/main.go` + spells out explicitly for the Go router. `/tasks/tags` requires `view_tasks` + and is scoped by the same #1082 own-rows rule as the task list, verified by + `TestTagCatalogueScope` (storage) and `TestTagCatalogueAuthz` (handlers). + +Two things underneath had to change: + +- **`passThroughQuery` forwards every value of a repeated parameter.** It read + only the first, which is right for every single-valued filter and wrong for + `tag`: dropping the second of `?tag=a&tag=b` *widens* the result instead of + narrowing it — the one direction a filter must never fail in. Single-valued + parameters behave exactly as before. +- **The phone card's box moved from its ` + ))} + + ) : null} + + + ) : null}