V1 Catalog MCP Servers (preview)#

The V1 agent runtime consumes MCP servers as catalog-authored resources rather than per-agent tool configurations. This page is the operator reference for authoring those resources.

Note

This is the V1 surface. The legacy V0 MCP Server Tools configuration (see MCP Server Tools) is unaffected and continues to work for agents that don’t run on V1.

Authoring a server#

Each MCP server is a MCP resource in the project’s genai_config workspace. Authentication is the auth block, the same discriminated union every other host resource carries: omit it for an open server, otherwise type selects one of five shapes.

Credential material never appears in the resource. Every variant that needs one carries a secret handle the deployment’s secret store resolves at request time (EnvSecretResolver reads SQ_<HANDLE_UPPER> by convention; other resolvers are pluggable).

no auth block

Server is open or trusts the cluster network. No credentials.

kind: MCP
schema: v1
metadata:
  id: open-search
title: Open Search
url: https://mcp.example.com/sse
transport: sse
requires_approval: true
bearer

Single shared bearer token, sent as Authorization: Bearer.

kind: MCP
schema: v1
metadata:
  id: company-kb
title: Company KB
url: https://mcp.example.com/kb
transport: streamable_http
auth:
  type: bearer
  secret: company_kb_token
requires_approval: false
api_key

Shared key in the header the server names. Omit scheme to send the key bare, which is what most custom headers expect; set it to send <scheme> <key>. header: Authorization is rejected at publish time without a scheme, because RFC 7235 wants an auth-scheme before the credentials there — use bearer for Authorization: Bearer.

kind: MCP
schema: v1
metadata:
  id: ticketing
title: Ticketing
url: https://mcp.example.com/tickets
transport: streamable_http
auth:
  type: api_key
  secret: ticketing_api_key
  header: X-API-Key
basic

HTTP basic auth. The handle resolves to a username and password pair (SQ_<HANDLE_UPPER>_USERNAME and SQ_<HANDLE_UPPER>_PASSWORD under EnvSecretResolver).

kind: MCP
schema: v1
metadata:
  id: internal-api
title: Internal API
url: https://mcp.example.com/internal
transport: streamable_http
auth:
  type: basic
  secret: internal_api
oauth

Each end user authorises the server out-of-band through OAuth 2.1 with PKCE. The platform stores per-user access tokens Fernet-encrypted at rest, keyed on (tenant, project_id, user, server_id) — since server_id is project-local, the credential scope is too (two projects authoring id="linear" against different upstreams are independent; each prompts the user once).

With no secret, the platform registers an OAuth client dynamically (RFC 7591) the first time anyone authorizes, and every later user authorizes against the same client. If users reach your Squirro instance under more than one address, each callback URL gets a client of its own, and connections made through one address keep working when another is used.

The authorize request asks for the scopes you author. With none authored, it asks for the scope the provider granted at registration, and failing that, for the scope the MCP server advertises. If neither names one, a server that publishes OAuth protected-resource metadata (RFC 9728) receives no scope parameter, and any other server receives openid profile email.

kind: MCP
schema: v1
metadata:
  id: linear
title: Linear
summary: Linear issue tracker
url: https://mcp.linear.app/sse
transport: sse
auth:
  type: oauth
requires_approval: true
tool_approval_overrides:
  search_issues: false  # opt this one tool out of approval

A provider that does not support dynamic registration — Google is the common case — needs a pre-registered client. secret then names a handle resolving to the client id and secret pair (SQ_<HANDLE_UPPER>_USERNAME and SQ_<HANDLE_UPPER>_PASSWORD), and scopes becomes required: there is no registration to report one, so an empty list would request a minimal default and every tool call would fail on permissions.

kind: MCP
schema: v1
metadata:
  id: gmail
title: Gmail
url: https://mcp.example.com/gmail
transport: streamable_http
auth:
  type: oauth
  secret: google_mcp_client
  scopes:
    - https://www.googleapis.com/auth/gmail.readonly
requires_approval: true

Register the redirect URI you pin in SQ_GENAI_V1_EXTERNAL_MCP_REDIRECT_URI with the provider when you create the client; see User OAuth flow below.

client_credentials

The connector runs as a service account: one machine identity shared by everyone in the project, rather than each end user signing in. Use it when the upstream should see the integration rather than the person driving it — a shared integration account, an internal service, or a provider that only issues machine tokens.

secret is required and names the same handle shape a pre-registered OAuth client uses: the client id and secret pair (SQ_<HANDLE_UPPER>_USERNAME and SQ_<HANDLE_UPPER>_PASSWORD). There is no dynamic-registration fallback, because registration exists to bind a client to a user’s consent and this grant has no user.

kind: MCP
schema: v1
metadata:
  id: ticketing
title: Ticketing
url: https://mcp.example.com/ticketing
transport: streamable_http
auth:
  type: client_credentials
  secret: ticketing_service_account
  scopes:
    - tickets.read

The platform mints the token when it opens the connection and sends it as Authorization: Bearer. Nobody is asked to sign in, so the connector is listed as ready for every user of the project from the moment you publish it.

scopes is optional here. Providers that scope by audience need none, and an empty list sends no scope parameter at all rather than substituting a default that would narrow the grant.

Important

Connectors that use oauth or client_credentials need an authorization server and token endpoint served over https. Tokens and client credentials are never sent to one on plain http, unless it runs on the same machine as your Squirro instance (localhost). A connector configured that way cannot sign users in or obtain a token, and is left out of conversations until its authorization server is served over https.

Note

auth_mode and bearer_handle are the shape this page described before the auth block existed. They still parse and are still honoured, so a published project keeps working, but an authored auth block wins over them and the mcp_deprecated lint nudges you across. auth_mode: static_bearer with bearer_handle: h becomes auth: {type: bearer, secret: h}; auth_mode: per_user_oauth becomes auth: {type: oauth}.

Activating the surface#

The capability toggle capability.mcp.enabled gates the entire MCP surface. The default is auto — which resolves to True when at least one MCP resource is authored, False otherwise.

Explicit false (compliance lock) hides every server even when resources are present. Explicit true requires at least one authored resource (publish-time validator).

Approval policy#

Each server declares a default requires_approval (default true — fail-safe). Individual tools can opt out via tool_approval_overrides keyed on the upstream tool name (as the server’s tools/list advertises it, not the namespaced mcp_<id>__<tool> form). Precedence at dispatch time:

  1. Catalog tool_approval_overrides[upstream]

  2. Server-advertised _meta.requires_approval on the tool (where the server uses Squirro’s MCP convention)

  3. Server-level requires_approval

  4. Hard default True

Approval gating runs as one HITL prompt per turn carrying every pending tool call; the user approves / edits / rejects each one in one resume action.

User OAuth flow#

For servers with an oauth auth block:

  1. The catalog endpoint (GET /v1/projects/{pid}/catalog) advertises the server with requires_oauth: true, authenticated: false, and authorize_url: /v1/external_mcp/authorize?server_id=<id>.

  2. The FE redirects the user’s browser to authorize_url.

  3. The platform locates the authorization server, through the MCP server’s protected-resource metadata (RFC 9728) when the server publishes it. It then resolves the pre-registered client if the resource names one, or registers a client dynamically if not, runs PKCE, and redirects to the IdP. When the MCP server publishes protected-resource metadata, the platform requests tokens for that server specifically, using the RFC 8707 resource parameter. If the platform finds no authorization server metadata that it can verify, the authorize request fails with HTTP 502.

  4. The IdP redirects back to /v1/external_mcp/callback; the platform exchanges the code for tokens and stores them Fernet-encrypted under (tenant, project_id, user, server_id).

  5. The next catalog poll shows the server as authenticated: true. The agent can now dispatch tools through it.

Disconnecting an integration#

The FE drives disconnect through DELETE /v1/external_mcp/secrets/{project_id}/{server_id}. The endpoint targets the calling user’s own row keyed (tenant, project_id, user, server_id); there is no admin override that disconnects another user’s integration. project-read is the floor — a read-only project member can still disconnect their own integration.

The endpoint is local-delete only: it does not call the IdP’s token-revoke endpoint. The grant on the upstream lingers until the user revokes it from their IdP account, and tokens expire naturally. This matches the per-project scope (revoking at the IdP would invalidate any sibling project issued via the same client_id). Returns 204 on success, 404 when no row exists for the target.

The redirect URI the IdP echoes must be pinned in production via SQ_GENAI_V1_EXTERNAL_MCP_REDIRECT_URI. The value must exactly match the URL registered with the IdP (most IdPs reject token exchanges with mismatched redirect URIs). Without the env var, the platform falls back to deriving the URI from the incoming request’s X-Forwarded-Host / X-Forwarded-Proto headers — convenient for local dev behind an honest reverse proxy, but a security risk in any environment where those headers can be spoofed. The platform logs a warning whenever the fallback path is used.

Changing SQ_GENAI_V1_EXTERNAL_MCP_REDIRECT_URI does not disconnect anyone. The next user to authorize through the new URL gets a newly registered client, and connections made through the old URL keep renewing against the client they were issued to.

On Squirro Self-Service, connectors whose auth block names a client secret can share one redirect URI across all instances, so you register a single URL with each provider instead of the callback of every instance. Those connectors then send https://start.squirro.com/oauth2_middleman to the provider, and the sign-in returns to the instance it started from. Connectors that register their own client keep using the callback of the instance. To set up the shared redirect URI on an instance:

  1. Register https://start.squirro.com/oauth2_middleman as a redirect URI with each provider, for example in the Google OAuth client or the Salesforce External Client App. Keep the callback of the instance registered as well until every instance uses the shared URI.

  2. Add SQ_GENAI_V1_EXTERNAL_MCP_OAUTH_MIDDLEMAN_URL=https://start.squirro.com/oauth2_middleman to the docker-override.env file. For the step-by-step procedure, see the genai.ini page.

  3. Restart the GenAI service with sudo systemctl restart sqgenaid.

Register the URI before you set the variable. Otherwise, the provider rejects new connections with a redirect URI mismatch. Connections that already exist keep working and do not need to be authorized again. The variable applies to every connector with a client secret on the instance, so if a customer brings their own provider app on a Squirro Self-Service instance, that app must also register the shared URI. Otherwise, leave the variable unset on that instance. Leave it unset on deployments that are not Squirro Self-Service instances, such as on-premises installations, because the shared URI only returns sign-ins to Squirro Self-Service instances.

Tokens are renewed once, at the start of a turn. A token due to expire within five minutes is refreshed against the stored refresh token before any tool runs, so a user whose provider issues short-lived tokens is not sent back for consent every hour. There is no background refresh job and nothing refreshes mid-flight: a token that dies during a turn fails that tool call, and is renewed at the start of the next one.

A grant the user has revoked at the provider, or one the provider has expired, cannot be renewed. That server is left out of the turn, the catalog reports it as authenticated: false with an authorize_url, and the user reconnects through the same flow. A provider that is merely unreachable is reported the same way, which means the connect button is the only thing on offer while an outage lasts, though the platform retries rather than discarding the credential and backs off for a minute between attempts.

Resetting a Registered Client#

If a provider stops accepting a client the platform registered, for example because the client was deleted at the provider, the provider shows an error on its own sign-in page and does not return the user to your Squirro instance. Signing in again does not help, because every attempt presents the same client.

A project administrator can remove the registered clients for one server with DELETE /v1/external_mcp/clients/{project_id}/{server_id}. The next sign-in registers a new client. Each user of that server keeps their current token until it expires and then signs in again. The endpoint does not affect a pre-registered client named by secret, which you rotate in the secret store instead. It returns 204 on success and 404 when the server has no registered client.

Going further#

  • Plugin authors writing tools exposed through /studio-mcp (the internal aggregator) consult the V1 internal developer guide studio-mcp-developers-guide.md under the genai service docs.

  • Resource schemas + the picker payload shape are documented in the genai_config reference under the V1 MCP and StudioMcpConfig kinds.