Skip to main content

mcp-connections

Work with MCP servers — connect one, add an entry to the server catalog, debug "the agent cannot see my MCP tools" or a failing OAuth flow, or change how connections are probed, prefixed or filtered. Use when the ask involves an external SaaS tool (GitHub, Linear, Notion, Slack, Stripe, Postgres…) and before writing a capability that would just be an API client for one.

Aller à l'installation

Informations de source

Dépôt
vstorm-co/agenticos
Dernière activité de la source
5 septembre 2026 à 11:08
Langue détectée de SKILL.md
anglais
Étoiles
37
Forks
7

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
mcp-connections
description
Work with MCP servers — connect one, add an entry to the server catalog, debug "the agent cannot see my MCP tools" or a failing OAuth flow, or change how connections are probed, prefixed or filtered. Use when the ask involves an external SaaS tool (GitHub, Linear, Notion, Slack, Stripe, Postgres…) and before writing a capability that would just be an API client for one.
# MCP connections **Read `docs/mcp.md`** for the model and `docs/howto/add-mcp-server.md` for adding a catalog entry. Code: `app/agents/mcp.py`, `app/agents/mcp_oauth.py`, `app/services/mcp_connection.py`, `app/services/mcp_catalog.py`. ## Reach for this before writing a capability If the ask is "let the agent read our Linear issues", the answer is usually a connection, not code. A capability is right for something the platform must *guarantee* — a budget guard, a sandbox, retrieval that cites sources. MCP is right for the forty SaaS products a company happens to use. Adding a catalog entry is one object in `app/core/catalog/mcp_servers.json`. No code. And nothing needs to be in the catalog for a URL to work — the *Custom server* entry introspects any reachable server. ## Personal vs organization Two kinds, differing in exactly two places and both are the point: | | Personal | Organization | |---|---|---| | Reached by | Its owner, through a binding to *each person's own account* | Any agent whose spec names it | | Gate | Owner only | `mcp:manage` | | Credential sealed to | The **member** | The **organization** | | A spec may name it | **No** - a spec names the *service* (`account: personal`, `catalog_key`) and the owner's connection is found at run time | Yes, `account: organization`, `connection_id` | A spec binds a service one of two ways (`McpServerRef` in `app/agents/spec.py`). `OrgMcpServerRef` is the organization's connection, answering for everybody. `PersonalMcpServerRef` names a catalog key: `build_toolsets_for_agent` looks up the *sender's* own connection by that key on every turn - the message's author, never the thread's - and where there is none (an API key, the widget, a schedule, an unlinked chat sender) reports the gap instead of skipping the server, and `_with_personal_service_gaps` in the runner tells the model what is missing and where the person connects it. A member's connection is never named by id: a published agent that reached different tools depending on who built it could not be reviewed. A personal connection is sealed to the member rather than an organization because it has none, and its owner may belong to several: binding it to whichever was active would make the token unreadable the moment they switched. ## The four behaviours that surprise people **A dead server is skipped, not raised.** Each server gets a 3-second `tools/list` probe before the turn; failures log a warning and the turn proceeds without those tools. Pydantic AI enters every toolset when a run starts, so raising would let one expired token abort every agent that names the connection. The trade is real: a skipped server means the model answers *without* the tools. `/test` and `last_status` are how you find out. **Tools are prefixed with the connection name.** `github-work` → `github_work_*`. Two servers exposing the same tool name make Pydantic AI raise on duplicates, which aborts the turn. **Two connections reducing to the same prefix are deduplicated**, first one wins, with a warning naming the loser. Deployment-managed servers are ordered first. **An allowlist filters before prefixing**, so it compares unprefixed names. ## OAuth Five steps in `mcp_oauth.py`: discover (RFC 9728 → RFC 8414) → dynamic client registration (RFC 7591) → consent URL (PKCE + state + RFC 8707 resource indicator) → exchange → refresh. Split across two HTTP requests because this is a web app, not a CLI. **Every URL in that flow is SSRF-checked and pinned**, not just the one somebody typed — discovery means the remote server picks most of the addresses we call. The flow's only client is `mcp_oauth._client()`, a `PinnedAsyncClient` (`app/core/pinned_http.py`): it checks each request's URL with `sanitize.resolve_pinned_url` and then dials **that address**, with the original host in `Host` and in TLS SNI. Nothing resolves the name twice, so there is no window to rebind (#860). Do not add a request path that builds a plain `httpx.AsyncClient` — `_send` takes a `PinnedAsyncClient` so that mistake does not type-check. A refused hop crosses `httpx` out of the transport as a `UrlRefusedError`, and `_send` answers it with a fixed `OAuthError` sentence. Catch the narrow type, never `ValueError`: only the narrow one is a refusal written here, and every other `ValueError` reported as "this server pointed us at a blocked address" is a confident lie about whose fault a failure was (#861). Behind `HTTP_PROXY`/`HTTPS_PROXY` the proxy does the connecting, so there the pinned address is what the proxy is *asked* to reach. Honoured deliberately — a proxy-only deployment would otherwise lose OAuth entirely, and the proxy is its own egress control. Do not "fix" that by naming a transport on the client: an explicit transport turns httpx's environment-proxy mounting off, which is how it was broken once already. Two things the pin does not cover, and neither is an oversight. The **consent URL** is checked with `validate_mcp_url` and then handed to a browser that resolves it itself. The **connection's own URL** is checked at save and resolved again at run time — a point-in-time check (#840), bearable only because an operator typed it. A URL a *model* chose belongs in neither; it needs `safe_download`. An organization's OAuth connection is still the consenting person's grant at the provider. Revoking their access there breaks the organization's server. ## MCP tools are not gated `approval_required_tools` iterates `spec.capabilities` only. MCP tools are discovered at run time, so nothing declared them and nothing approves them. Say so plainly when reviewing a change that adds a connection to a published agent — do not imply a gate exists. Cost is the same story: what a server does on its own side is outside this platform's budget. Only model tokens are counted. ## Routes ``` GET /api/v1/me/mcp-connections personal GET /api/v1/mcp-connections organization (mcp:manage) POST /api/v1/mcp-connections/{id}/test probe, list tools, persist last_status POST /api/v1/mcp-connections/oauth/start declared above /{id} — that route parses its segment as a UUID and would 422 ``` `PATCH` with `auth_token: ""` clears a stored credential. ## Test `frontend/e2e/mcp-servers.spec.ts` covers the UI. For the backend, the interesting assertions are the refusals: a personal connection is not bindable by a spec, a connection from another organization is unreachable, and a prefix collision drops the second server rather than aborting the turn.
Voir sur GitHub