12 KiB
Connecting any remote MCP server
Paperclip can connect a standards-compliant remote HTTP MCP server without a
Paperclip code change. A curated AppDefinition is a convenience layer —
branding, tailored fields, scoped defaults, support copy — not a prerequisite.
This is the documented baseline for connecting anything. Read Connection authoring runbook when you want to add the branded convenience layer on top for a vendor Paperclip should promote.
Accepted in the generic remote MCP plan, implemented in PAP-17087.
The two routes
| Route | Where | Use it when |
|---|---|---|
| Guided URL | Apps → Connect an app → Connect your own MCP server | You have the server's address. Paperclip probes it and walks you through whatever it needs. |
| Paste a config | Advanced → Paste a config | A README gave you an mcpServers snippet, or the server needs headers with names Paperclip could not guess. |
Both routes normalize through the same backend contract, so auth discovery, secret handling, catalog refresh and review cannot diverge between them.
Advanced → Run your own is a separate, higher-trust path for local stdio commands and is deliberately not covered here.
Don't know the address or the headers? The question-mark control beside
Paste a config gives you a request you can hand to an agent: it asks the
agent to consult the vendor's current documentation and reply with one
paste-ready mcpServers JSON object using credential placeholders, plus notes
on how to obtain each credential. Paste only the JSON block back into Paperclip;
Paperclip reads the header names from it and asks you for the values, which it
stores as Paperclip secrets.
What the guided URL flow does
After you paste an address and press Check link, Paperclip probes the endpoint and branches:
| Endpoint says | You get |
|---|---|
| Nothing needed | Discovered actions, straight to review. |
| Needs authorization, and publishes discoverable OAuth metadata | Sign in to continue — a browser sign-in at the provider. |
| Needs authorization, but no discoverable sign-in | A prompt to add the key or headers its docs list, under Advanced authentication. |
| Needs a client you registered yourself | A prompt for a client ID and secret. The draft connection is kept — you don't start over. |
| Not a valid address / private network / unreachable | The specific problem and which field to change. |
An unknown server is labelled Unverified server with its host shown, at every step through review, access and install. Reads are enabled for review; state-changing actions start off; newly discovered actions are quarantined until reviewed. That is the same treatment a curated connection gets.
Advanced authentication
Collapsed by default. Open it when the server's docs are specific:
- No sign-in needed — the server is open to anyone with the address.
- Key or token — sent as an
Authorizationheader. - Custom headers — for servers that name their own headers.
- Browser sign-in — optionally with a client ID and secret you registered yourself, for providers that require preregistration.
Every value you enter becomes a Paperclip secret. Values are write-only: they never appear in stored config JSON, logs, activity details, API responses after write, or UI readback. Only header names are shown in review and diagnostics.
Paperclip refuses to send header names it manages or that belong to the
transport — Host, Cookie, Content-Length, Transfer-Encoding,
hop-by-hop headers, and anything under Proxy-* or Sec-* — and rejects
values containing line breaks or control characters. This is enforced in shared
code (packages/shared/src/mcp-remote-headers.ts), checked at the API boundary,
and re-checked in the service immediately before the header is projected onto a
real request.
How sign-in gets a client
You never choose this; Paperclip resolves it and the wizard shows none of it. Recorded here for security review and diagnostics. In preference order:
- Deployment-preconfigured client.
PAPERCLIP_TOOL_OAUTH_<PROVIDER>_CLIENT_ID/_SECRET, or the unsuffixedPAPERCLIP_TOOL_OAUTH_CLIENT_ID/_SECRET. Always wins when set. - Client ID Metadata Document (CIMD). When the authorization server
advertises
client_id_metadata_document_supported, Paperclip presents the URL of its own published metadata document as theclient_id. Nothing is registered. Requires a public HTTPS base URL (PAPERCLIP_PUBLIC_URL): the authorization server has to fetch that document server-to-server, so loopback and plain-HTTP deployments fall through to the next tier. The document is served unauthenticated at/api/tools/oauth/client-metadataand contains only this deployment's callback and the grant/response/auth methods Paperclip uses — no company, connection or secret data. - Dynamic client registration (RFC 7591). When the authorization server
advertises a
registration_endpoint. Paperclip registers a public client (token_endpoint_auth_method: none,application_type: web, PKCE S256). - Manual preregistered client. The client ID and secret you paste under Advanced authentication → Browser sign-in.
A generic connection may register (tiers 2 and 3) only after validated protected-resource and authorization-server discovery actually produced a metadata document, and only on an explicit operator connect action. An endpoint that merely returns a 401 does not earn a registration.
Client binding
Client material is bound to the authorization-server issuer, the MCP resource URL, the callback URI, and the company. If any of those change:
- a Paperclip-minted client (CIMD or DCR) is re-registered;
- a client you supplied yourself is not — Paperclip stops and asks you to re-enter it, because it cannot register on your behalf in a console it does not control.
Credentials are never reused across issuers or across companies.
Endpoint addresses are validated before they are used
Every OAuth endpoint address is chosen by the remote server — in discovered
metadata, in a WWW-Authenticate hint, in a pasted config, or in a gallery
default — and the authorization endpoint additionally becomes a top-level browser
navigation. All of them are parsed by one shared validator
(checkOAuthEndpointUrl in @paperclipai/shared) and must be:
https:. Plainhttp:is refused, except for a loopback host under the local-development policy (the same policy that allows private remote endpoints), and except for this deployment's own origin.- Free of embedded credentials.
https://accounts.google.com@evil.test/…reads as the wrong site to a human, so Paperclip refuses it. - Free of a fragment, and a well-formed absolute URL.
javascript:, data:, file: and friends are therefore refused before they can
reach window.location. The board applies the same validator to the address it
receives, so an unsafe value cannot pass the API boundary and then execute at the
navigation boundary. A refusal is reported as
oauth_<kind>_endpoint_rejected (422) and the unsafe value is never persisted on
the connection.
An address that passes is still only an address: a valid HTTPS authorization page can be a phishing page. The redirect screen names the host you are being sent to, and the Unverified server label stays visible for an endpoint with no curated definition.
Protocol conformance
- RFC 8707
resourceon authorization, token, and refresh requests, naming the canonical MCP endpoint (origin + path, no query or fragment), so the authorization server can audience-restrict the token to that server. - RFC 9728 protected-resource discovery, path-aware first
(
/.well-known/oauth-protected-resource<path>) then origin. - RFC 8414 authorization-server discovery for issuers with a path, in the
spec's insertion form (
/.well-known/oauth-authorization-server<path>) and the widely deployed OIDC suffix form (<path>/.well-known/...). A metadata document whoseissuerdisagrees with the issuer used to build the discovery URL is discarded. - RFC 9207
issvalidated against the persisted expected issuer when the authorization server returns it. A mismatch refuses the code rather than exchanging it. An absentissis tolerated — it is optional and widely omitted. - PKCE S256, exact redirect/state binding, and SSRF/private-network and redirect limits are unchanged from the curated path.
The discovered auth kind, issuer, and resource are persisted on the connection,
so refresh, reconnect, revoke and diagnostics all use the generic path instead of
falling back to authKind: none semantics.
Curated definitions remain optional
A curated definition matching a pasted endpoint is offered as a branded shortcut beside the generic form — never instead of it. A definition adds labels, logos, field validation, scoped defaults and support copy. It does not unlock a separate execution capability, and it must not create a second connection or change ownership.
The bespoke PostHog connection is the worked example: it is the polished route for most users, and PostHog is also connectable generically through this page with either a key or browser sign-in.
Verifying
Deterministic coverage lives in
server/src/__tests__/generic-mcp-connection.test.ts, which stands up an
in-process MCP server plus authorization server. It needs no network and no
vendor credentials, and every case connects by URL without naming a gallery app.
A credentialed vendor smoke (for example live PostHog OAuth) may be recorded by
QA but is not required for deterministic verification.
Opt-in generic Notion live smoke
pnpm smoke:notion-generic-live exercises the generic Advanced → Paste a
config route against https://mcp.notion.com/mcp. It is intentionally outside
the normal unit, browser, and CI-required suites. Run it only against an
already-running, browser-reachable HTTPS Paperclip instance with these bindings
provided by the execution environment:
PAPERCLIP_E2E_BASE_URL,PAPERCLIP_E2E_EMAIL, andPAPERCLIP_DEV_LOGIN_PASSWORDfor the target instance;PAPERCLIP_API_URL,PAPERCLIP_API_KEY,PAPERCLIP_RUN_ID, andPAPERCLIP_TASK_IDfor the control plane;- the approved on-demand secret binding
access.notion_generic_flow_test_account, delivered by the agent secret API under its normalized keygeneric-flow-test-account, for the existing Notion test account.
If that account requires an emailed one-time code, also set AGENTMAIL_API_KEY
and NOTION_AGENTMAIL_INBOX_ID for the already-configured forwarding inbox. The
smoke loads the AgentMail SDK only after Notion presents the code challenge,
accepts only a fresh authenticated Notion message, fills the code once in
memory, and never records the message, address, or code.
Check the URL, health endpoint, and binding metadata without retrieving the credential value or opening a browser:
pnpm smoke:notion-generic-live -- --dry-run
The live command retrieves the credential only after the safe preflight and
Paperclip login succeed. It disables trace, video, and HAR capture, takes only
post-callback screenshots, enables and invokes only notion-get-self, proves
notion-create-pages remains locally denied, and removes its uniquely named
connection in a finally cleanup. Its summary.json and PNG files contain
sanitized IDs, decisions, outcomes, and endpoint origins/paths only; they
default to PAPERCLIP_RUN_SCRATCH_DIR, or to NOTION_EVIDENCE_DIR when set.
Run the credential-free harness checks with:
node --test scripts/smoke/notion-generic-live.test.mjs