feat(skills-catalog): ship connector authoring and connection skills

Adds two optional catalog skills under software-development, continuing
PAP-18408 and PAP-18464:

- author-connector-definition — mechanical authoring of one Paperclip
  catalog connector definition, from provider MCP probe through catalog
  tests.
- connect-agent-tools — getting an agent working against an outside tool,
  self-hosted first, with a per-deployment support matrix.

Both derive trustLevel markdown_only: no scripts/ or assets/ entries, so
no audit allowlist change is required. Frontmatter descriptions are 291
and 293 characters, under the 300-character shipped-catalog cap.

Self-hosted is the primary documented path. Cloud rows in the support
matrix are labelled untested or unsupported and are not inferred from
self-hosted results.

Verified in this worktree at origin/master b70641f23f:
  build:manifest  Wrote generated/catalog.json with 19 catalog skills.
  validate        Catalog manifest is valid with 19 catalog skills.
  test            5 files / 20 tests passed
  typecheck       clean

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Gardener committed 2026-09-19 16:59:17 +00:00
1 parent b70641f23f
commit 0fe055cc8f
9 files changed
+1936 -1

No files matched your search

@@ -0,0 +1,380 @@
---
name: author-connector-definition
description: >
Author one Paperclip catalog connector definition: probe the provider MCP
metadata, fix the five connection axes, add official branding, edit the
generator source, regenerate, and get the catalog tests green with evidence.
Mechanical authoring only; not research approval, not PR submission.
key: paperclipai/optional/software-development/author-connector-definition
recommendedForRoles:
- engineer
tags:
- mcp
- connectors
- app-definition
- catalog
- codegen
requires:
- git
- curl
- node
- pnpm
---
# Author Connector Definition
Turn a provider name into a reviewable catalog-connector change in the Paperclip
App, with executed evidence for every claim.
This skill owns the **mechanics** of one definition: the generator contract, the
slug allowlist, branding validation, the exact-count test assertions, and the
offline verification loop. It does not own the process around them.
**A catalog entry is a convenience layer, not a prerequisite.** The runbook is
explicit: an operator can connect any standards-compliant remote HTTP MCP server
from **Connect your own MCP server** or **Paste a config** with no Paperclip
code change at all, including servers that need browser sign-in
(`CONNECTOR-PLAYBOOK.md:23-26`). Check that the requester actually needs a
catalog entry before spending a single edit here. `connect-agent-tools` owns
that decision.
**Self-hosted first.** Everything this skill produces is authored and verified
against an App checkout and an instance you run yourself. Phases 8 and 9 of the
runbook are satisfied by an isolated self-hosted instance; no Paperclip Cloud
account is required to author, validate, or submit a connector. Where a method
is genuinely Cloud-gated — the curated `platform_shared` Paperclip-managed OAuth
profile is the one current case — record it as `unsupported` for self-hosted
rather than treating Cloud as the baseline.
## Use This When
Trigger on a request that names a provider and asks for a catalog connector,
gallery card, or `AppDefinition` — for example "add a Neon connector", "put
Fireflies in the Connectors catalog", "author the app definition for X".
Also trigger when an existing definition needs an axis change: a new method, a
different `ownershipModes` set, a widened `scopesHint`, a corrected endpoint, or
a visibility change.
## Do Not Use This When
Hand the request to the right owner instead:
| Request | Owner |
| --- | --- |
| "Connect this MCP server for our company" | No code change. The operator uses **Connect your own MCP server** or **Advanced → Paste a config**. Say so and stop. |
| "My agent needs to use X" / "will this work self-hosted?" | `connect-agent-tools`. It owns the deployment decision, the smallest-path ladder, and the live proof. Come back here only if a catalog entry is genuinely the requirement. |
| "Research whether we should integrate X" | The requester's research process. Inside Paperclip Content, `prepare-mcp-integration` owns the research package and its human gate — but the runbook is now self-contained, so an external contributor does **not** need it. |
| "Open the connector PR" | Runbook Phase 12 plus the standard PR-preparation skill. This skill stops before any PR. |
| "The provider has no MCP server" | Out of scope. A manifest cannot wrap an arbitrary REST API, run an arbitrary local command, or register an OAuth client in a console Paperclip does not control. Report the boundary. |
| "Ship it" / "install it" / "release it" | Not authorized here. See **Authorization boundaries**. |
PR #13675 made the runbook self-contained so that "contributors can implement a
connector without access to an internal issue tracker", and removed the separate
private validation issue ("No separate private validation issue is required",
`CONNECTOR-PLAYBOOK.md:1443`). Treat `prepare-mcp-integration` as an internal
convenience for people working inside Paperclip Content, not as a gate a public
contributor must pass. When it does apply, that skill drives and calls this one
for the definition itself.
## Required Inputs
Collect these before step 1. Ask only for what you cannot determine safely.
1. **Provider name**, and the slug you intend to use (stable, lowercase,
kebab-case).
2. **App repository checkout and target branch.** Record the exact commit you
read the contract from; reread it before you edit.
3. **Who decided this provider should be in the catalog**, and on what basis.
A public contributor's own reasoning, recorded in the PR, is sufficient —
the runbook no longer requires a private research approval. Inside Paperclip
Content, that decision is `prepare-mcp-integration`'s gate; if it applies and
has not been taken, stop and route there.
4. **Intended visibility**: store-visible, or connectable-but-hidden.
5. **Location of the ingestion corpus** (`paperclip-content`), or the value for
`PAPERCLIP_CONTENT_TEMPLATES`. This corpus is in a non-public repository; the
generator refuses to run without it. If you do not have it, this skill's
path is closed to you — say so plainly rather than editing the guard, and
note that connecting the server directly (`connect-agent-tools` path 2) is a
different thing, not a lesser substitute.
6. **The deployment you will validate on.** An isolated self-hosted instance is
the expected answer and is sufficient.
Refuse to guess an endpoint, a scope set, or a risk tier. Each one is a
reviewable claim and must come from the provider's own metadata or documentation.
## Scope Boundaries
- **One provider per run.** Do not bundle two providers into one change.
- **Do not edit the generated JSON as a source of truth.** The durable source is
the generator script — with nine documented exceptions. See
`references/catalog-contract.md`.
- **Do not change global defaults.** Risk classification, the recommended access
policy, and shared validators are not provider-local knobs. If the provider
needs one changed, stop and say so.
- **Do not touch unrelated providers.** Generated output renumbers on insertion;
that is expected. Any *semantic* change to another provider is a mistake.
- **Do not add server or UI code** until you have shown that the manifest cannot
express the provider.
## Authorization Boundaries
- **Secrets stay in approved flows.** Never place a credential in the generator
script, a definition, connection config, application metadata, a committed
fixture, a comment, a screenshot, a trace, a HAR file, or a report. If you are
handed a credential, propose it as a Paperclip secret immediately and never
echo it. Assert absence in a test rather than merely not printing it.
- **No external writes during research.** Unauthenticated metadata reads are
allowed. Dynamic client registration, consent, account mutation, and any write
tool call are not research — they need explicit authorization.
- **No credential grants.** Do not create, share, or widen a connection grant.
- **No installs or releases.** Pushing, opening a PR, merging, deploying,
installing a skill, or making a provider store-visible in a shipped build are
all release-stage actions that require explicit human authorization. This
skill ends at a verified local change.
- **No roadmap claims.** Do not write "coming soon" or promise a future method.
## Step 1 — Discover The Current Contract
Read from the recorded commit, not from memory and not from a stale worktree:
- `doc/connections/CONNECTOR-PLAYBOOK.md` — the authoring runbook. Read its
**five axes**, **reuse-path classification** (MCP-direct / OpenAPI-shim /
vendor-deep-wrapper), **risk tiers S1–S4**, and the **production validation**
evidence matrix in Step 9. Since PR #13675 that matrix lives in the runbook
itself, and it is the list your evidence must answer.
- `doc/connections/GENERIC-REMOTE-MCP.md` — the no-code baseline, so you can
tell the requester when they do not need you. It also documents the OAuth
client-resolution tiers and which of them need a public HTTPS origin.
- `doc/connections/CHAT-CONNECTOR-UX.md` — **only if the provider is a chat or
email provider.** It owns step navigation, the step-owned footer, credential
instructions, provider handoffs, personal identity linking, the optional
message test, and management states. Do not apply Slack's steps or credential
types to a provider that does not work that way, and do not skip it when they
apply. `connect-agent-tools/references/chat-and-email.md` is the short routing
layer over it.
- `doc/connections/README.md` — the identity/connections boundary. Every
connector is a plane P2 resource credential, never a sign-in authenticator.
Sign-in tokens are never reused as resource tokens; `id.paperclip.ing` never
stores resource tokens.
- `scripts/ingest-app-definitions.mjs` — the generator. Read its corpus guard,
its `brandingFor`, its tuple mapper, and its `validateApp`.
- `packages/shared/src/types/app-definition.ts` — the field contract.
- `packages/shared/src/app-definitions.ts` — the slug allowlists and the
recommended access defaults.
- `packages/shared/src/app-definitions.test.ts` — the assertions your change
must satisfy.
Then read `references/catalog-contract.md` in this skill for the file-by-file
map and the assertions that carry exact counts. Treat that reference as a
starting index, not as a substitute: if it disagrees with the commit you read,
the commit wins, and you record the drift in your report.
## Step 2 — Probe The Provider, Read-Only
First establish where the MCP server runs and which process connects to it.
For a desktop server, loopback means the connector runtime's machine, not the
user's browser or a cloud worker. Record desktop/open-document prerequisites;
do not invent hosted OAuth for a same-machine, unauthenticated server. If the
current transport or egress policy cannot support it, report the runtime gap.
For OAuth providers, these unauthenticated requests inspect the transport and
auth axes. Skip OAuth discovery for a provider that does not use OAuth:
```sh
curl -s -i -X POST "$SERVER_URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
# Read resource_metadata out of the WWW-Authenticate challenge, then:
curl -s "$RESOURCE_METADATA_URL" # RFC 9728 -> authorization_servers
curl -s "$ISSUER/.well-known/oauth-authorization-server" # RFC 8414
```
Record, with the date you read it: the endpoint and its trailing-slash
behaviour; the unauthenticated status and challenge; the protected-resource and
authorization-server metadata URLs; the exact issuer; authorize, token,
registration and revocation endpoints; whether `token_endpoint_auth_method:
none` and PKCE `S256` are advertised; the provider's full `scopes_supported` and
the narrower set you will request; documented prerequisites (plan, admin
consent, preview enrolment, region, tenant identifier); and the revocation
procedure.
Discard an authorization-server document whose `issuer` disagrees with the
issuer used to build the URL.
Mark as **unprobed** anything you could not establish without registering or
authenticating — the redirect-URI constraint usually falls here, because probing
it properly means attempting registration. An unprobed constraint is omitted
from the definition and reported as a gap, never guessed.
## Step 3 — Fix The Five Axes In Writing
Record all five before editing anything: transport, authentication, OAuth client
ownership, credential source, grant identity. Then record the **reuse path** the
runbook asks for — MCP-direct, OpenAPI-shim, or vendor-deep-wrapper — with the
reason a lighter path is or is not enough. A provider name alone is not a
classification: a read-only method and a method that sends messages or changes
infrastructure can carry different risks and belong in different rows.
Then record the two judgements that reviewers will challenge:
- **Risk tier** (`S1`–`S4`), and why. Classify `S4` when the normal catalog
includes payments, external sends, refunds, production deployment, deletion,
or tenant-wide administration.
- **Scope allowlist**, and what it costs. A narrow allowlist is the right
default; state plainly which tools will fail at call time because of it.
If discovery worked, ship **only** `serverUrl` in `defaults`. A complete
`authorizationEndpoint` + `tokenEndpoint` pair is authoritative and suppresses
discovery permanently, including endpoints a previous discovery persisted. Ship
that pair only for a provider that publishes no usable metadata, and say in your
report that you now own keeping it current.
## Step 4 — Branding Before Generation
Generation fails closed without a branding row, so do this first. Obtain the
official mark from the provider's brand kit, product site, or official
repository; prefer SVG; never use a favicon proxy, a scraped icon, or an
imitation. Add the asset and its manifest row, then run the two brand checks in
`references/catalog-contract.md`.
If you cannot obtain an official mark, stop and report it. A placeholder is
acceptable only in an explicitly labelled local fixture and must never be
proposed for release.
## Step 5 — Author, Register, Generate
1. Add the provider to the generator source. Prefer the existing tuple shape and
the existing `method()` helper over a hand-built object.
2. Register the slug so the definition is reachable. A definition that generates
cleanly but is absent from the slug allowlist is invisible in the product —
this is the most common silent failure.
3. Regenerate. Read the whole diff. Expect positional renumbering in the
generated registry; reject any semantic change to another provider.
## Step 6 — Verify Offline, Then Report Honestly
Run the deterministic ladder in `references/catalog-contract.md`. When you must
verify without mutating the checkout, build the isolated harness described in
`references/offline-verification.md` — it runs the real generator, the real
brand validators, and the real catalog tests against a copy, and includes a
fidelity check that proves the harness reproduces the checked-in output
byte-for-byte before you trust any result from it.
Then split your evidence into three explicit buckets and never blur them:
- **Executed** — the exact command and its real output.
- **Source-inspected** — read and reasoned about, not run. Server, UI and
browser suites usually land here when you have no instance.
- **Not done** — including every account-bound step. A green catalog test is not
provider validation, and a mocked OAuth test is not a live connect.
Then answer the runbook's nine production-validation scenarios — setup and
consent, authentication, catalog and configuration, allowed execution, denied
execution, runtime delivery, refresh and recovery, revoke and reconnect,
activity and secret handling (`CONNECTOR-PLAYBOOK.md:1445-1455`) — with
**pass / fail / not run / not applicable** and a reason for the last two, plus
environment, method key, commit, and accessible redacted evidence. Offline
authoring legitimately produces a column of "not run"; what it must never
produce is a blank or an optimistic one.
Report those results per deployment, self-hosted first, using the four labels
in `connect-agent-tools/references/deployment-support-matrix.md`. A pass on a
self-hosted instance makes that row `verified` for self-hosted and leaves Cloud
`untested`; the reverse is equally true.
## Optional Live Acceptance Handoff
Offline authoring does not certify the connector for production. When the user
separately authorizes live testing, read
[Live acceptance](references/live-acceptance.md). Otherwise hand that checklist
to the tester and mark it not executed. The reference does not authorize grants,
provider writes, runtime changes, deployment, or installation.
Keep four results separate: static validation, board Test calls, a real agent
run through the managed connector, and verified provider-side output. Only the
last two establish that an agent can complete the tested workflow.
## Stop Conditions
Stop and report rather than working around any of these:
- An applicable research gate has not been taken, or does not cover this
provider.
- The ingestion corpus is unavailable, so the generator cannot run.
- The provider has no compatible MCP server, or needs a transport with no execution
path.
- The only viable method is Cloud-gated (`platform_shared` Paperclip-managed
OAuth). Record it as `unsupported` for self-hosted and hand it to product
engineering; do not imply a self-hosted equivalent.
- The provider requires a registration or approval that a customer cannot
complete. Retain the research with an unavailable reason and expose no connect
action.
- No official artwork is obtainable.
- The manifest cannot express the provider without new shared runtime code.
- A test assertion would have to be weakened rather than updated.
- A raw credential appears anywhere. Treat it as an incident: revoke and rotate,
remove the evidence, trace every response, log and audit path, and add a
canary regression test.
- The change would need a global default, a risk classification, or a shared
validator to move.
## Output Contract
Deliver exactly this, and nothing that implies more:
1. **The change** — generator source edit, branding asset and manifest row, slug
registration, regenerated output, and the one test-count update, as a diff or
a working tree. No commits pushed, no PR.
2. **A source map** — every file you changed and every claim you relied on, with
the commit and the file:line evidence for each.
3. **A provider evidence record** — endpoint, metadata URLs, issuer, advertised
registration and PKCE support, scopes considered versus requested, risk tier
with reasoning, and the date you read each one. No tool inventory you did not
observe. No credentials.
4. **Verification results** — executed, source-inspected and not-done, each with
real command output for the executed set, plus the runbook's nine
production-validation scenarios marked pass / fail / not run / not
applicable.
5. **A deployment support matrix** — self-hosted same-machine, self-hosted
server/VPS, and Cloud, each capability labelled `verified`, `untested`,
`unsupported` or `deferred` with its evidence or named follow-up owner. Use
`connect-agent-tools/references/deployment-support-matrix.md` as the shape.
6. **Remaining gaps** — unprobed constraints, the account-bound lifecycle, and
anything a reviewer must authorize before release.
7. **An explicit statement** of what did not happen: no push, no PR, no deploy,
no install, no credential grant, no external write.
## Verification Checklist
Tick each item only with evidence, and mark the ones you could not run:
- [ ] Endpoint and auth mode come from the provider's own metadata, dated.
- [ ] Issuer in the authorization-server document matches the issuer used to
fetch it.
- [ ] `defaults` ships only `serverUrl` for a discovery-capable provider, or the
hard-coded pair is justified and owned.
- [ ] `scopesHint` is the reviewed minimum, not the discovered set.
- [ ] Risk tier is justified against the provider's actual mutation surface.
- [ ] `ownershipModes` reflects what the provider advertises; `dcr` omitted for
a provider Paperclip must not auto-register.
- [ ] Official artwork present, manifest row added, both brand checks pass.
- [ ] Slug registered so the definition is connectable at the intended
visibility.
- [ ] Generator source and generated output are consistent in one change.
- [ ] Generated diff contains no semantic change to another provider.
- [ ] Catalog tests pass, with count assertions updated rather than weakened.
- [ ] Required, non-advanced fields are the minimum, and any addition to the
default-path allowlist is justified.
- [ ] No credential value appears in any file, output, or report.
- [ ] Reuse path (MCP-direct / OpenAPI-shim / vendor-deep-wrapper) is recorded
with the reason a lighter path is or is not enough.
- [ ] For a chat or email provider, `CHAT-CONNECTOR-UX.md` was read and applied;
for every other provider it was correctly not applied.
- [ ] All nine production-validation scenarios are marked pass / fail / not run
/ not applicable, with reasons for the last two.
- [ ] The deployment support matrix is filled in, self-hosted first, with the
four labels and no Cloud row claimed as `verified` without Cloud evidence.
- [ ] Account-bound lifecycle recorded as outstanding if it was not run.
- [ ] Report states executed versus source-inspected versus not done.
@@ -0,0 +1,258 @@
# Catalog authoring contract
Index of the files and assertions a catalog-connector change has to satisfy.
**Verified against Paperclip App commit `e558f25e` (15 September 2026), by
re-reading each cited file and by running the command ladder below through the
isolated harness.** Previously verified at `728f7185` (14 September 2026); every
claim below survived that move, but several line numbers did not, so this
revision cites symbols and landmarks you can grep for and keeps line numbers
only where they are load-bearing. If the commit you read disagrees, the commit
wins — and record the drift in your report so this file gets corrected.
## Files a minimal store-visible connector touches
| File | Role | Editable? |
| --- | --- | --- |
| `scripts/ingest-app-definitions.mjs` | Human-authored provider source. The `apps` array starts at line 180; the tuple mapper follows it (grep for `schemaVersion: 1,` inside the `.map((`, around line 795). | Yes — this is the source. |
| `ui/public/brands/apps/<slug>.svg` | Official mark. | Yes. |
| `ui/public/brands/apps/manifest.json` | Branding provenance: slug, provider name, `catalogVisible`, `localAsset`, optional `darkAsset`, optional `aliases`. | Yes. |
| `packages/shared/src/app-definitions.ts` | `CONNECTABLE_APP_SLUGS` (line 6) and `APP_STORE_HIDDEN_SLUGS` (line 44). | Yes. |
| `packages/shared/src/types/app-definition.ts` | The field contract the generator output has to satisfy. | Read-only for authoring. |
| `packages/shared/src/app-definitions/<slug>.json` | Generated definition. | **Generated** — see the exception below. |
| `packages/shared/src/app-definitions.generated.ts` | Generated positional registry. | **Generated.** |
| `packages/shared/src/app-definitions.ingestion-report.json` | Generated review report. | **Generated.** |
| `packages/shared/src/app-definitions.test.ts` | Catalog assertions, including exact counts. | Yes — update, never weaken. |
| `packages/shared/src/self-serve-mcp-research.json` | Dated research ledger. Membership implies connectability. | Only for that programme. |
The runbook's "shortest valid implementation" list omits
`packages/shared/src/app-definitions.ts` and the ingestion report. Both are
required for a store-visible provider.
## The generated-file exception
`scripts/ingest-app-definitions.mjs:1379-1396` (`reviewedGoogleSlugs`) reads nine
definitions back from the output directory and re-emits them verbatim:
```js
for (const slug of reviewedGoogleSlugs) {
const existingIndex = apps.findIndex((app) => app.slug === slug);
if (existingIndex >= 0) apps.splice(existingIndex, 1);
apps.push(JSON.parse(fs.readFileSync(path.join(out, `${slug}.json`), "utf8")));
}
```
For `gmail` and the eight `google-*` slugs the JSON file **is** the maintained
source. For every other provider it is generated output and editing it is a
change the next run reverts.
## Generator preconditions
- **Corpus.** `scripts/ingest-app-definitions.mjs:4-9` resolves
`PAPERCLIP_CONTENT_TEMPLATES`, defaulting to
`../../paperclip-content/research/connections/vercel/templates`. Line 1541-1542
throws `Expected 99 captures, found N` unless exactly 99 `.md` files
(excluding `INDEX.md`) are present. A new provider needs no capture of its
own; the corpus still has to be complete. The corpus lives in the
non-public `paperclip-content` repository: if you do not have it, this whole
path is closed to you and you report that rather than stubbing the guard out.
- **Branding.** `brandingFor` (declared line 20, throws line 29) throws
`<slug>: missing local branding provenance` unless the slug has a manifest row
(only `oauth-generic` and `api-key-generic` are exempt). Branding precedes
generation.
- **Method invariants** (`validateApp`): `schemaVersion === 1`; non-empty
`slug`, `name`, `methods`; an `api_key` tool method requires `keyPlacement`;
an `oauth` method requires a non-empty `ownershipModes`; a required
non-checkbox tenant/extension/credential field requires a `placeholder`.
## Tuple shape
`[slug, name, description, category, domain, urlPatterns, method|methods, extra?]`
- `category` is one of `ai`, `analytics`, `commerce`, `communication`,
`content`, `data`, `developer`, `productivity`, `other`.
- The 5th element (`domain`) is discarded by the mapper. `docsUrl` must go in
`extra`, or it is only backfilled for providers that also appear in the
research ledger (`ingest-app-definitions.mjs:1315`).
- `method(key, transport, auth, defaults, riskTier, guidanceMd, extra)` fills
`ownershipModes` as `["customer", "dcr"]` for `oauth` and `["customer"]`
otherwise, plus a default `whenToUse`.
- `featured` is a hard-coded six-slug list in the mapper. Do not add to it as
part of authoring a new provider.
## Visibility chain
```txt
APP_DEFINITIONS (generated, all providers)
└─ filtered by CONNECTABLE_APP_SLUGS ─▶ CONNECTABLE_APP_DEFINITIONS
└─ minus APP_STORE_HIDDEN_SLUGS ─▶ APP_STORE_DEFINITIONS ─▶ gallery API
```
`GET /api/companies/:companyId/tools/apps` returns `APP_STORE_DEFINITIONS`
directly (`server/src/routes/tool-access.ts:831`). There is no company-scoped
definition store, and `connectToolAppSchema` accepts a `galleryKey` or a `link`
and nothing else (`packages/shared/src/validators/tool-access.ts:472-474`). A
catalog connector is therefore always a shared-source change.
## Assertions with exact counts or sets
These fail on any addition. Update them deliberately.
| Assertion | Location | What breaks |
| --- | --- | --- |
| `expect(APP_STORE_DEFINITIONS).toHaveLength(46)` | `app-definitions.test.ts:689`, in `it("withholds unverified and reserved providers …")` | Any new store-visible provider. Bump the count. |
| `catalogVisible` manifest set must equal the store-visible definition set, and asset paths must equal `branding` values | same test, from line 716 (`manifest.providers.filter(… catalogVisible)`) | A manifest row without a definition, or the reverse. Also enforces PNG ≥ 128×128 and rejects script/`foreignObject`/event handlers in SVG. |
| Required non-advanced tenant/extension fields enumerated in a short allowlist | `app-definitions.test.ts` around line 905 (`field.required && field.advanced !== true && !field.hidden`) | Any visible required field on the default path. Prefer making the field optional or advanced with a default. |
| `expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(43)` | `app-definitions.test.ts:280` | Adding a provider to the research ledger. |
| Ledger entry count with a fixed `verifiedAt` (`"2026-08-26"` at this commit) | `app-definitions.test.ts:434-435` | Same. Also requires HTTPS `docsUrl`/`serverUrl`, a non-empty `authMode`, a prerequisite longer than 10 characters, and a valid tier. |
| `APP_STORE_HIDDEN_SLUGS` exact sorted list | `app-definitions.test.ts:666-688` | Hiding a provider. Hidden slugs must still be connectable. |
| Method and field invariants across the whole catalog | `it("enforces method and field invariants")`, `app-definitions.test.ts:961` | A missing `keyPlacement`, empty `ownershipModes`, or a required credential field with no placeholder. |
The provider-slug membership check above those uses `arrayContaining`, so an
addition does not break it. Confirmed at `e558f25e`: adding one store-visible
provider failed exactly one assertion, the count at line 689.
## Network and deployment guard
A private, loopback or reserved MCP address is accepted only when the deployment
is *not* both authenticated and publicly exposed; link-local egress is denied in
every mode.
```ts
// server/src/services/tool-access.ts:3175-3180 (and tool-gateway.ts:3132-3137)
function allowPrivateRemoteEndpoints() {
return (
options.deploymentMode !== "authenticated" ||
options.deploymentExposure !== "public"
);
}
```
`DEPLOYMENT_MODES` is `["local_trusted", "authenticated"]` and
`DEPLOYMENT_EXPOSURES` is `["private", "public"]`
(`packages/shared/src/constants.ts:4-8`). The check runs inside
`guardedRemoteHttpFetch` at dial time rather than as a standalone pre-flight,
which is what closes the DNS-rebinding window — so a same-machine desktop MCP
endpoint is a local-deployment capability, not a configuration flag to widen.
Report it as an unsupported deployment shape instead.
## Access defaults
```ts
// packages/shared/src/app-definitions.ts:245-257
export function recommendedDefaultsForApp(app, methodKey) {
void app;
void methodKey;
return { access: "all_agents", askFirstRiskLevels: [] };
}
```
Uniform and open for every provider, method and tier: every discovered action is
enabled and every active action defaults to **Allowed**, including `write` and
`destructive`. This is an opt-in restriction model — finishing a connection is a
configure-authorized, audited operation and **Ask first** stays available
afterwards. Do not compensate by misclassifying a tool, and do not change this
function in a provider change.
Changed-action quarantine activates when a *connection* sets
`quarantineNewEntries`. It is not an `AppDefinition` field; declaring it in a
manifest does nothing.
## Transport boundaries
| Transport | Manifest-only? | Boundary |
| --- | --- | --- |
| `mcp_remote` | Yes | First-class: discovery, health, catalog, gateway, OAuth, credential projection. |
| `local_stdio` | Only via a registered template | Reported unsupported outside `local_trusted` mode or a configured trusted runtime host (`server/src/services/tool-access.ts:4788-4789`). Never put a bare command in a definition. |
| `rest_api` | No | Not exposed through the connected MCP gateway. Needs an execution adapter first. |
`api_key` is an authentication mode, not a transport. Most API-key catalog
entries authenticate a remote MCP server.
Header credentials and secret-bearing generated URLs have the complete generic
runtime path. The schema also accepts `query`, `body_json` and `env` placements,
but schema acceptance is not proof the gateway projects them — trace the
invocation path and add an end-to-end fixture before shipping one.
## OAuth endpoint precedence
1. A **complete** `authorizationEndpoint` + `tokenEndpoint` pair in the method's
`defaults` is used unconditionally. Discovery never runs, and endpoints
stored on the connection and `401` challenge hints are not consulted at all.
2. Otherwise, for `mcp_remote`, endpoints already stored on the connection (then
the challenge hints, field by field) are used if they form a complete pair.
3. Only then does the RFC 9728 → RFC 8414 discovery chain run.
So a complete manifest pair is authoritative and outlives its own accuracy.
Ship `serverUrl` alone for a discovery-capable provider.
Client resolution order, independent of the above: deployment-preconfigured
client, Client ID Metadata Document (needs a public HTTPS base URL), RFC 7591
dynamic registration, then an operator-supplied client. For a curated method,
`ownershipModes` gates only the curated path — omit `dcr` for a provider
Paperclip must not auto-register.
## Command ladder
Deterministic, no vendor account:
```sh
node scripts/check-app-brand-assets.mjs
node --test scripts/app-brand-validation.test.mjs
pnpm connections:ingest-app-definitions # honours PAPERCLIP_CONTENT_TEMPLATES
pnpm exec vitest run \
packages/shared/src/app-definitions.test.ts \
packages/shared/src/app-definitions-url.test.ts
pnpm --filter @paperclipai/shared typecheck
```
Wider, when the change reaches server or UI code:
```sh
pnpm exec vitest run \
server/src/__tests__/tool-access-service.test.ts \
server/src/__tests__/generic-mcp-connection.test.ts \
server/src/__tests__/tool-connection-removal.test.ts \
ui/src/pages/apps/AppsConnect.test.tsx \
ui/src/pages/apps/Browse.test.tsx
pnpm check:token-gates
pnpm -r typecheck
```
`server/src/__tests__/generic-mcp-connection.test.ts` stands up an in-process
MCP server and authorization server, so the OAuth path is exercisable with no
network and no credentials. Extend it with a fixture rather than reaching for a
real account.
Diff review before handing off:
```sh
git diff --check
git status --short
git diff -- scripts/ingest-app-definitions.mjs \
packages/shared/src/app-definitions \
packages/shared/src/app-definitions.generated.ts \
ui/public/brands/apps
```
`app-definitions.generated.ts` uses positional imports (`a0`, `a1`, …), so
inserting one provider renumbers every later import — roughly 50 diff lines for
a one-provider change. That is correct output, not churn to fix. Moving the
tuple to the end of the tuple list does not avoid it, because several providers
are appended to `apps` after that list.
## Account-bound lifecycle (not deterministic)
Required once per exposed method before a provider is store-ready, and outside
this skill's authority without explicit approval: preflight on public metadata
only; connect; catalog listing compared against reviewed expectations; one
narrow read; write-classification check; one call through an actual run-scoped
gateway; refresh and reconnect; revoke and confirm calls fail closed; reconnect
after removal; and a credential scan across responses, logs, activity, audit and
every evidence artifact.
Record only provider and method key, date, environment, endpoint origin and
path, non-sensitive connection ID, tool names and counts, policy result,
redacted outcome codes, and revoke/reconnect outcome. Never record token values,
secret-bearing URLs, authorization codes, provider session detail, personal
email, tenant content, HAR files, or pre-callback screenshots.
@@ -0,0 +1,99 @@
# Live connector acceptance
Use after offline authoring, only for a separately authorized test. Test one
bounded workflow in an isolated instance and a disposable provider resource.
Do not interpret a request to author a definition as permission to run writes.
## Preflight
- Record source commit, dirty changes, instance URL, process workspace, isolated
database, provider endpoint, connection, agent, and required tools.
- Establish the network path: hosted HTTP, same-machine desktop HTTP, or a
supported local process. A laptop's loopback endpoint is not reachable as
loopback from a cloud runtime. Never weaken shared egress rules to pass a test.
- Check provider prerequisites such as a running desktop app and open document.
Capture the effective tool inventory and permissions for the actual test agent;
a catalog card or connected badge does not prove either.
- Review instances may suppress execution. Inspect the current worktree run
guard (historically `enableWorktreeRunExecution`) before diagnosing a queued
task as a provider failure. Enabling it is a separate, isolated-instance
action; keep unrelated agent schedules off and record any change for restoration.
- Use existing narrowly authorized permissions. Ask the operator for missing
grants; do not broaden global defaults or downgrade provider risk annotations.
## Prove the actual path
| Evidence | What it proves | What it does not prove |
| --- | --- | --- |
| Generator, schema and unit tests | Definition fits tested contracts | Provider connectivity |
| Board Test call | That test endpoint can execute the action | Agent discovery or execution |
| Fresh agent run with matching gateway audit | Agent used the managed connector | Correct final output |
| Readback and provider-side inspection | Expected result exists and is usable | Untested lifecycle or providers |
Start a real, bounded agent task. Do not manufacture heartbeat rows or count a
synthetic session as agent execution. Record terminal run state and correlate
connection/tool invocation events with that exact run ID.
A direct provider plugin, raw API call, browser action, or unrelated MCP server
can help diagnosis but cannot count as managed-connector acceptance. Inspect
actual tool namespaces and gateway events, not just the assistant's claim.
If an alternate plugin contaminates the test, exclude that run. With appropriate
authorization, isolate it in the test profile, retry a fresh session, and restore
the profile afterward. Do not disable the user's global plugins.
## Verify the result, not just success responses
For an authorized write, use an explicitly identified disposable resource. Save
the returned resource ID and pass it to subsequent calls rather than relying on
the active document. Read back expected content and counts. For visual output,
inspect a rendered screenshot for actual content, missing assets and layout.
A successful screenshot call can precede rendering. Use a bounded read-only
wait/recheck; a blank image is not acceptance. Stop after the test's declared
timeout and report the gap. Before retrying any timed-out write, reconcile IDs
and provider state to avoid duplicates. A local checkpoint is not an atomic
transaction with the provider. Never delete existing user content as cleanup.
## Diagnose and test failures proportionally
Distinguish missing tool discovery, denied permission, provider error, transport
timeout, and artifact-upload failure. A different profile ID alone does not
establish stale configuration. Preserve the error and trace for the failed run;
a later success does not prove its historical root cause.
Exercise applicable negative/lifecycle cases in the authorized sandbox:
unknown tool, denied action, missing desktop/document, disconnect/reconnect,
and OAuth refresh/revocation where supported. Mark inapplicable cases with a
reason and untested cases as pending. Security-policy or shared-runtime changes
need their own review; a local happy path does not clear those gates.
## Paper Desktop example: lessons from a local test
The September 2026 test used a same-machine HTTP MCP endpoint and an explicit
new file. The board harness imported three images; a separate real agent run
performed a bounded canvas write, readback and screenshot through the managed
gateway. Keep those two proofs separate. An earlier run through the direct
Paper plugin was excluded. An initially blank screenshot needed a later read.
For a new Paper test, recheck the official MCP docs and current tool schemas.
Confirm desktop state, explicit file targeting, and that the provider process
can read imported assets. Do not copy personal paths or file IDs. These findings
do not certify cloud execution, other providers, or OAuth lifecycle behavior.
## Acceptance receipt
Report these fields with secret-free evidence links:
- Source commit and local changes; runtime/provider versions where available.
- Instance, runtime placement and isolation boundary.
- Approved workflow, resource scope and effective permissions.
- Static checks, board calls, real run ID and correlated gateway invocations.
- Expected versus observed readback; rendered evidence when relevant.
- Negative/lifecycle checks: passed, failed, pending or not applicable.
- Temporary configuration changes and restoration result.
- Remaining release/security gates and what was not tested.
Provide a reproducible test with configurable IDs and documented prerequisites,
not personal credentials or dependence on an undisclosed private corpus. Claim
only the workflow and environment actually tested. Keep local acceptance,
PR-ready, merged and deployed as separate states.
@@ -0,0 +1,229 @@
# Offline verification harness
Run the real catalog generator, the real brand validators, and the real catalog
tests without mutating the App checkout. Use this when you must verify before
you have permission to write to the repository, when another task owns the
working tree, or when you want to prove a failure is attributable to your change
and not to the environment.
The recipe below builds it. It exports the needed files with `git archive` and
`git show` at a pinned commit and symlinks `node_modules` read-only, so the
checkout is untouched.
This used to ship as `scripts/make-harness.sh` inside the skill. It is inlined
here on purpose: a skill package containing anything under `scripts/` derives
the `scripts_executables` trust level (`deriveTrustLevel` in
`packages/skills-catalog/src/catalog-builder.ts:763-767`), and the shipped
catalog pins that set to exactly one key
(`packages/skills-catalog/src/shipped-catalog.test.ts:132-136`). Carrying the
recipe as documentation keeps this package `markdown_only` and installable
without an audit-allowlist change. Save it to a file yourself and `chmod +x` it
if you prefer to run it as a script.
## Build it
Save as `make-harness.sh` outside the App checkout, then:
```sh
./make-harness.sh <app-repo> <commit-ish> <out-dir> <corpus-dir>
```
<details>
<summary><code>make-harness.sh</code></summary>
```bash
#!/usr/bin/env bash
# Build an isolated harness that runs the real catalog generator, the real brand
# validators, and the real catalog tests against a copy of a pinned commit.
#
# Nothing in the App checkout is modified: files are exported with `git archive`
# and `git show`, and node_modules is symlinked read-only.
#
# Usage:
# make-harness.sh <app-repo> <commit-ish> <out-dir> <corpus-dir>
set -euo pipefail
APP_REPO=${1:?app repo path}
COMMIT=${2:?commit-ish}
OUT=${3:?output dir}
CORPUS=${4:?ingestion corpus dir}
[ -d "$CORPUS" ] || { echo "corpus not found: $CORPUS" >&2; exit 1; }
[ -d "$APP_REPO/node_modules" ] || { echo "install dependencies in $APP_REPO first" >&2; exit 1; }
SHA=$(git -C "$APP_REPO" rev-parse "$COMMIT")
rm -rf "$OUT"; mkdir -p "$OUT/gen/scripts" "$OUT/vt/packages/shared" "$OUT/vt/ui/public/brands"
echo "$SHA" > "$OUT/PINNED_COMMIT"
# --- generator harness -------------------------------------------------------
for f in scripts/ingest-app-definitions.mjs \
scripts/app-brand-validation.mjs \
scripts/check-app-brand-assets.mjs \
scripts/app-brand-validation.test.mjs; do
git -C "$APP_REPO" show "$SHA:$f" > "$OUT/gen/$f"
done
git -C "$APP_REPO" archive "$SHA" ui/public/brands/apps | tar -x -C "$OUT/gen"
git -C "$APP_REPO" archive "$SHA" \
packages/shared/src/app-definitions \
packages/shared/src/app-definitions.generated.ts \
packages/shared/src/app-definitions.ingestion-report.json \
packages/shared/src/app-definitions.ts \
packages/shared/src/self-serve-mcp-research.json | tar -x -C "$OUT/gen"
# Pristine copies, so fidelity and change can be told apart.
cp -r "$OUT/gen/packages/shared/src/app-definitions" "$OUT/gen/.baseline-definitions"
cp "$OUT/gen/packages/shared/src/app-definitions.generated.ts" "$OUT/gen/.baseline-generated.ts"
cp "$OUT/gen/packages/shared/src/app-definitions.ingestion-report.json" "$OUT/gen/.baseline-report.json"
cat > "$OUT/gen/verify-fidelity.sh" <<'FIDELITY'
#!/usr/bin/env bash
# Regenerate from the pristine source and prove the harness reproduces the
# checked-in output exactly. Run this BEFORE applying your own change.
set -euo pipefail
: "${PAPERCLIP_CONTENT_TEMPLATES:?point this at the ingestion corpus}"
node scripts/ingest-app-definitions.mjs
diff -rq .baseline-definitions packages/shared/src/app-definitions
diff -q .baseline-generated.ts packages/shared/src/app-definitions.generated.ts
diff -q .baseline-report.json packages/shared/src/app-definitions.ingestion-report.json
echo "fidelity OK: harness reproduces the checked-in output byte-for-byte"
FIDELITY
chmod +x "$OUT/gen/verify-fidelity.sh"
# --- vitest harness ----------------------------------------------------------
git -C "$APP_REPO" archive "$SHA" \
packages/shared/src packages/shared/package.json packages/shared/tsconfig.json | tar -x -C "$OUT/vt"
git -C "$APP_REPO" show "$SHA:tsconfig.base.json" > "$OUT/vt/tsconfig.base.json"
ln -s "$(cd "$APP_REPO" && pwd)/node_modules" "$OUT/vt/node_modules"
ln -s "$(cd "$APP_REPO" && pwd)/packages/shared/node_modules" "$OUT/vt/packages/shared/node_modules"
cat > "$OUT/vt/sync-from-gen.sh" <<'SYNC'
#!/usr/bin/env bash
# Copy the generator harness's current output into the vitest harness, so the
# tests run against exactly what you generated.
set -euo pipefail
GEN=${1:?path to the gen harness}
rm -rf ui/public/brands/apps && mkdir -p ui/public/brands
cp -r "$GEN/ui/public/brands/apps" ui/public/brands/apps
cp "$GEN/packages/shared/src/app-definitions.ts" packages/shared/src/app-definitions.ts
cp "$GEN/packages/shared/src/app-definitions.generated.ts" packages/shared/src/app-definitions.generated.ts
cp "$GEN/packages/shared/src/app-definitions/"*.json packages/shared/src/app-definitions/
echo "synced from $GEN"
SYNC
chmod +x "$OUT/vt/sync-from-gen.sh"
echo "harness ready at $OUT (pinned $SHA)"
echo " generator: $OUT/gen"
echo " vitest: $OUT/vt/packages/shared"
```
</details>
```text
harness ready at /tmp/harness (pinned e558f25e99020b8ab762b91a8eabb0fe383f993f)
generator: /tmp/harness/gen
vitest: /tmp/harness/vt/packages/shared
```
The output above is from a run at Paperclip App commit `e558f25e` on
15 September 2026, Node v24.20.0. The harness was previously exercised at
`728f7185`; the recipe did not need changing between the two.
Two roots, because they have different working directories:
- `gen/` — the generator runs from a repo-shaped root (`process.cwd()` is the
root it reads `ui/public/brands/apps/manifest.json` from and writes
`packages/shared/src/app-definitions/` into).
- `vt/` — the vitest root. `app-definitions.test.ts` resolves `../../../ui/public`
relative to its own file, so `vt/ui/public/brands/apps` has to exist.
## Prove the harness first
Never trust a harness you have not checked. Regenerate from the pristine source
and confirm the output matches the checked-in files byte-for-byte:
```sh
cd <out-dir>/gen
PAPERCLIP_CONTENT_TEMPLATES=<corpus-dir> ./verify-fidelity.sh
```
```text
Parsed 99 captures and 179 states; emitted 70 Wave 1 definitions and flagged 63 states for review.
fidelity OK: harness reproduces the checked-in output byte-for-byte
```
The corpus is in the non-public `paperclip-content` repository. Without it the
generator refuses to run and this harness cannot be built — report that as a
blocked prerequisite rather than editing the guard.
If that diff is not empty, stop. Either the corpus is wrong or the harness is
missing an input, and any later result is meaningless.
## Run your change through it
```sh
cd <out-dir>/gen
# edit scripts/ingest-app-definitions.mjs, ui/public/brands/apps/manifest.json,
# packages/shared/src/app-definitions.ts, and add the brand asset
node scripts/check-app-brand-assets.mjs
node --test scripts/app-brand-validation.test.mjs
PAPERCLIP_CONTENT_TEMPLATES=<corpus-dir> node scripts/ingest-app-definitions.mjs
cd <out-dir>/vt && ./sync-from-gen.sh <out-dir>/gen
cd <out-dir>/vt/packages/shared
node ../../node_modules/vitest/vitest.mjs run \
src/app-definitions.test.ts src/app-definitions-url.test.ts
```
Expect the count assertion to fail first, then update it and rerun. Real output
from the Neon worked example at `e558f25e`:
```text
Validated 70 brand identities: local paths, aliases and artwork safety.
Parsed 99 captures and 179 states; emitted 71 Wave 1 definitions and flagged 63 states for review.
AssertionError: expected [ { schemaVersion: 1, …(8) }, …(46) ] to have a length of 46 but got 47
❯ src/app-definitions.test.ts:689:35
```
```text
Test Files 2 passed (2)
Tests 27 passed (27)
```
Also diff the generated registry and confirm the only changes are positional.
At this commit one inserted provider moved 125 lines of `a<N>` imports in
`app-definitions.generated.ts` and changed nothing semantic in another
provider:
```sh
diff .baseline-generated.ts packages/shared/src/app-definitions.generated.ts \
| grep '^[<>]' | grep -v 'a[0-9]' # must print nothing
```
## What this harness does not cover
Be explicit about this in your report. The harness reaches the shared package
only. It does not run:
- the server suites (`tool-access-service`, `generic-mcp-connection`,
`tool-connection-removal`) — they need the server package and its fixtures;
- the UI suites (`AppsConnect`, `Browse`) — they need the UI package;
- `pnpm check:token-gates`, `pnpm -r typecheck`, `pnpm build`, `pnpm test:e2e`;
- anything account-bound.
Source-inspecting those is a legitimate intermediate result. Reporting them as
passed is not.
Above all: a green run here proves the definition fits the tested contracts. It
is not provider validation, not an agent run, and not acceptance. See
`references/live-acceptance.md` for the gate that is.
## Notes
- `node_modules/.bin/vitest` is a shell wrapper that Node cannot execute
directly. Call `node_modules/vitest/vitest.mjs`.
- `--reporter=basic` is not a valid reporter on vitest 4. Omit it.
- The harness needs `tsconfig.base.json` at the harness root; the script exports
it, because the shared package's tsconfig extends it and the transform fails
without it.
@@ -0,0 +1,491 @@
---
name: connect-agent-tools
description: >
Get a Paperclip agent working with an outside tool, self-hosted or Cloud:
start from the workflow and its proof, establish the deployment and its real
reachability, take the smallest supported connection path, and verify a real
agent run with provider-side readback. Not for catalog authoring.
key: paperclipai/optional/software-development/connect-agent-tools
recommendedForRoles:
- engineer
- operations
tags:
- mcp
- connectors
- integrations
- connections
- verification
requires:
- curl
---
# Connect Agent Tools
Take "my agent needs to use X" to "an agent did the job through the managed
connection, and here is the artifact on X's side".
This skill owns the **decision and the proof**. It does not own catalog
mechanics; `author-connector-definition` does, and most runs never need it.
**Self-hosted Paperclip is the primary target.** Every step below works on an
instance you run yourself, on your own machine or on your own server, with no
Paperclip Cloud account, no managed Cloud OAuth client, and no Paperclip-operated
infrastructure. Cloud is covered as a third deployment shape, not as the
reference implementation. Where the two diverge, the divergence is written down
in [`references/deployment-support-matrix.md`](references/deployment-support-matrix.md)
with the guard or document behind it.
## Use This When
- An agent cannot reach a tool it needs, and nobody has established why.
- Someone asks for "a connector for X" without having checked whether X is
already connectable.
- A connection exists, shows **Connected**, and the agent still cannot do the
job.
- Someone needs to know whether an integration will work on a server or in
Paperclip Cloud, not just on their own computer.
## Do Not Use This When
| Request | Owner |
| --- | --- |
| "Add X to the Connectors catalog" and the catalog change is already approved | `author-connector-definition`. Come back here for the live proof. |
| "Research whether we should integrate X" as a company decision | The requester's research process. This skill reads provider metadata; it does not own an integration decision. |
| "Give agent Y access to the existing connection" | Operator action on the connection's grant and agent access. No new connection needed. |
| "Build the MCP server" | Product engineering. Report the gap (step 5) and stop. |
| "Ship it", "install it", "release it", "grant the credential" | Not authorized here. See **Authorization boundaries**. |
## Self-Hosted Quickstart
The smallest complete run, on an instance you host yourself. Use it as the
worked shape for step 3 path 2; it needs no Paperclip Cloud account and no
Paperclip code change.
**1. Establish the deployment.** Read `PAPERCLIP_DEPLOYMENT_MODE` and
`PAPERCLIP_DEPLOYMENT_EXPOSURE` from the instance's configuration. Unset means
`local_trusted`, and `local_trusted` forces `private`.
**2. Probe the provider read-only.** This is Neon's hosted MCP server, executed
19 September 2026:
```sh
curl -s -i -X POST "https://mcp.neon.tech/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
```
```text
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="No authorization provided", resource_metadata="https://mcp.neon.tech/.well-known/oauth-protected-resource/mcp"
```
Follow the pointer, then the issuer's authorization-server document:
```sh
curl -s "https://mcp.neon.tech/.well-known/oauth-protected-resource/mcp"
curl -s "https://mcp.neon.tech/.well-known/oauth-authorization-server"
```
```text
resource = https://mcp.neon.tech/mcp
authorization_servers = ["https://mcp.neon.tech"]
issuer = https://mcp.neon.tech
registration_endpoint = https://mcp.neon.tech/api/register
authorization_endpoint = https://mcp.neon.tech/api/authorize
token_endpoint = https://mcp.neon.tech/api/token
revocation_endpoint = https://mcp.neon.tech/api/revoke
scopes_supported = ["read", "write"]
code_challenge_methods_supported = ["S256"]
token_endpoint_auth_methods_supported = ["client_secret_post", "client_secret_basic", "none"]
client_id_metadata_document_supported = <absent>
```
**3. Read the result.** The issuer matches the host used to build the URL, so
the document is usable. A `registration_endpoint` plus `none` and `S256` means
the DCR tier applies — and DCR is instance-local, so this works identically on a
self-hosted instance and on Cloud. CIMD is not advertised, so the public-HTTPS
requirement that tier carries is irrelevant here. **No Cloud account, no managed
OAuth client, and no public origin are needed for this provider.**
**4. Connect it.** The operator uses **Connect your own MCP server** and pastes
`https://mcp.neon.tech/mcp`. No catalog entry, no code change. Start writes at
**Off**.
**5. Prove it.** Step 5 of this skill. A **Connected** badge is not the proof;
a real agent run with provider-side readback is.
What this quickstart claims, precisely. The probe output above is executed
against Neon. The **connect and consent for Neon specifically** are `untested`
here, because OAuth consent needs an authorized Neon account.
The rest of the shape is not hypothetical. The same no-code path — connect by
pasted URL, catalog discovery, effective policy, a real agent run correlated to
its run ID, provider-side readback, revoke and reconnect, credential redaction —
is **`verified` on self-hosted**, on two isolated instances, against a loopback
MCP server and against DeepWiki, a real third-party public server. Run IDs and
provider-side artifacts are in
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md).
What that buys you is confidence in the path, not in your provider: your run
still has to produce its own proof, and the whole OAuth column remains
`untested` on every deployment shape.
## Required Inputs
Collect these before step 1. Ask for what you cannot establish safely.
1. **The workflow.** What the agent should accomplish, in one sentence.
2. **The proof.** The artifact you will open on the provider's side afterwards.
If the requester cannot name one, the workflow is still being designed — say
so and stop there.
3. **Deployment shape.** Where this Paperclip runs: the same computer as the
provider, a server or VPS, or Paperclip Cloud. Ask; do not infer it from the
URL someone pasted in a browser.
4. **Provider identity** and its official documentation URL. Not a search
result, not a third-party list.
5. **Who can authorize what.** Which operator can finish a connection, and
whether any live write has been authorized. Absent that, this run ends at a
read-only proof.
## Scope Boundaries
- **One tool, one workflow per run.** Breadth after the first real success.
- **Do not weaken a guard to make a test pass.** The private-endpoint check, the
link-local denial, the transport rules and the risk annotations are the
product's boundaries, not obstacles.
- **Do not widen permissions or grants.** Use the narrow authorization you were
given and name what is missing.
- **Do not change global defaults**, shared validators or risk classification.
- **Do not add server or UI code** before showing that no supported path exists.
## Authorization Boundaries
- **Secrets stay in approved flows.** Never place a credential in a config file,
a comment, a screenshot, a trace, a HAR file, a fixture or a report. If you
are handed one, propose it as a Paperclip secret and never echo it.
- **Unauthenticated metadata reads are research.** Dynamic client registration,
consent, and any write tool call are not. They need explicit authorization.
- **No credential grants.** Do not create, share or widen a grant.
- **No installs, deploys, pushes, PRs or releases.**
- **No new provider writes or runtime changes** without their own authorization,
even when a read-only proof succeeded.
- **No roadmap claims.** Do not promise a future transport or a Cloud
capability that has not shipped.
## Step 1 — Write Down The Workflow And The Proof
Two sentences, recorded in the task before anything else:
- The job: "the on-call agent opens a database branch before a migration test".
- The proof: "the branch is visible in the provider's console, and the agent's
run shows the tool call that created it".
Everything below is in service of the second sentence. A **Connected** badge, a
catalog card and a green test suite are not it.
## Step 2 — Establish Where This Runs
Two locations, routinely conflated: where Paperclip's runtime executes the call,
and where the provider's server is. Only the runtime's view of the network
counts.
Name one of three deployment shapes. Do not infer it from a product tier, from
a URL someone pasted in a browser, or from the absence of an error — read the
instance's actual configuration.
| Shape | Typical config | Call originates from | Check |
| --- | --- | --- | --- |
| **A. Self-hosted, same machine** | `PAPERCLIP_DEPLOYMENT_MODE=local_trusted` | That computer | Can the runtime reach the service *and* the files it needs? Desktop app running, document open, correct user session. |
| **B. Self-hosted, server or VPS** | `authenticated`, plus `private` or `public` exposure | That server | Is the endpoint reachable from the server, not merely from the requester's browser? Does the instance have a public HTTPS origin, and does anything here actually need one? |
| **C. Paperclip Cloud** | hosted, `authenticated` + `public` | A hosted environment | Is the integration supported and tested from the Cloud runtime? Nothing on anyone's laptop is reachable. |
Shapes A and B are the primary path. Work them first and completely; treat C as
a separate deployment that needs its own evidence.
Self-hosted does not imply desktop-compatible. A locally launched MCP server
that only calls a vendor's hosted API is portable; one that drives an open
desktop app, local files or a private network needs those where the runtime is.
Two configuration facts decide most of what follows, both at App commit
`9335b7db10`:
- `local_trusted` **forces** exposure to `private`
(`server/src/config.ts:187-190`). A shape-A instance can never reach the
`authenticated` + `public` combination that the private-endpoint guard
refuses.
- Paperclip refuses private, loopback and reserved MCP addresses only on a
deployment that is both authenticated and publicly exposed
(`allowPrivateRemoteEndpoints` at `server/src/services/tool-access.ts:3195-3200`,
mirrored in `tool-gateway.ts:3135-3140`), and refuses link-local addresses in
every mode (`server/src/services/remote-http-endpoint-guard.ts`).
Read those at the commit in front of you and record it. Never propose a tunnel,
a proxy or an exception to get around the guard.
**An agent cannot read its own deployment's support matrix, but an operator
always can.** `GET /api/companies/:companyId/tools/runtime-health` returns the
live `supportMatrix` for the instance and requires board access: a run-scoped
agent credential gets `403 {"error":"Board access required"}`, while a board
actor on an instance they own reads it fine (both executed 19 September 2026;
route at `server/src/routes/tool-access.ts:2500-2504`). So if you are the agent,
ask the operator to run it and paste the `supportMatrix` block, or read the
configuration directly. Do not guess it, and do not report the `403` as though
the surface were unavailable — it is available to the person you are asking.
Label every capability you establish **verified**, **untested**, **unsupported**
or **deferred**, each with its reason. Fill in a copy of
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md)
rather than inventing your own labels. A local success is never Cloud evidence,
and Cloud success is never self-hosted evidence.
If the provider is a chat or email provider, read
[`references/chat-and-email.md`](references/chat-and-email.md) now — the inbound
delivery mechanism, not the deployment tier, decides whether a public HTTPS
origin is a real prerequisite.
## Step 3 — Take The Smallest Supported Path
Work down. Stop at the first path that does the job. The runbook states the
governing principle: "A catalog entry is a convenience layer, not a
prerequisite. An operator can connect any standards-compliant remote HTTP MCP
server from **Connect your own MCP server** or **Paste a config** with no
Paperclip code change at all — including servers that need browser sign-in."
(`CONNECTOR-PLAYBOOK.md:23-26`.)
1. **An existing connector.** Check the catalog and the provider pages first. If
a connector exists and the workflow still fails, this is a configuration,
grant or permission problem — diagnose that, do not build.
2. **A compatible MCP endpoint, connected by URL.** No code change. All of these
must hold: an `http`/`https` MCP endpoint; reachable from the runtime;
authentication in a shape Paperclip drives (OAuth advertised through RFC 9728
/ RFC 8414 metadata, a key presented as a header, a provider-generated
secret-bearing URL, or none); and an OAuth client Paperclip can obtain
(preconfigured, CIMD, dynamic registration, or one the operator registered).
Name which condition failed rather than improvising.
**None of those four conditions needs Paperclip Cloud.** DCR is
instance-local: each instance registers its own public client against its own
`/api/tools/oauth/callback`, and "Cloud-hosted and self-hosted instances use
the SAME path — the only per-instance difference is the hostname inside the
redirect URI" (`CONNECTOR-PLAYBOOK.md:1569-1578`). CIMD is the one tier that
needs a public HTTPS `PAPERCLIP_PUBLIC_URL`, and a deployment without one
falls through to DCR rather than failing. A self-hoster whose provider cannot
do DCR registers their own client and sets
`PAPERCLIP_TOOL_OAUTH_<PROVIDER>_CLIENT_ID` / `_SECRET`. The only genuinely
Cloud-gated path is the curated `platform_shared` Paperclip-managed OAuth
profile; if that is the only option, say so and stop rather than implying a
self-hosted equivalent exists.
3. **A catalog definition.** Only when a *reusable, branded, reviewed* entry is
the actual requirement, and only with the App repository and its ingestion
corpus available. This is a shared-source change that ends in review and a
release, not a connection anyone can use today. Hand it to
`author-connector-definition` and come back here for the proof. If the
repository or corpus is unavailable, say so plainly — path 2 is not a lesser
substitute for it, and the reverse is also true.
4. **No supported transport or network path.** Go to step 5.
Before any provider round-trip, probe read-only and record the date:
```sh
curl -s -i -X POST "$SERVER_URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
# For an OAuth challenge, follow resource_metadata, then the issuer's
# authorization-server document. Discard a document whose issuer disagrees with
# the issuer used to build the URL.
```
Record the endpoint and its trailing-slash behaviour, the unauthenticated
status and challenge, the metadata URLs, the exact issuer, whether registration
and PKCE `S256` are advertised, the provider's full `scopes_supported` and the
narrower set you will request, documented prerequisites (plan, admin consent,
desktop app, open document, region, tenant), and the revocation procedure.
Mark as **unprobed** anything that would require registering or authenticating,
and never guess it.
## Step 4 — Configure Within Authorization
Have the authorized operator finish the connection. Then, before claiming
anything:
- Start writes at **Off** on a server nobody has reviewed and promote
deliberately. Re-run **Refresh actions** after the server changes.
**Then re-check the profile, because promotion is not durable.** At App commit
`9335b7db10`, a connection created from a pasted URL is flagged
`unverifiedServer: true` and `quarantineNewEntries: false` in the same
expression (`server/src/services/tool-access.ts:12721`), so every catalog
refresh auto-allows whatever new tools the provider has started advertising —
including ones classified `destructive`. Verified by execution; see **F1** in
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md).
On a server you do not control, the tool set is provider-controlled, not
operator-controlled. Say that out loud in your report.
- **Check that the connection is installed on the agent, not merely permitted.**
Finishing the wizard with `access: {agentIds: [...]}` writes the tool-profile
binding but no install row, and readiness needs both
(`server/src/services/connection-intents.ts:241-248`). The symptom is a green
**Connected** badge and an allowed tool the agent cannot see. Verified on two
instances — **F10** in the same reference.
```sh
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/tool-connections/$CONNECTION_ID/installs"
# {"installs": []} means the agent still cannot use it, whatever the badge says.
```
- Check the *effective* tool inventory and permissions of the agent that will
actually run the workflow. A connected badge proves neither.
- Check that the instance executes tasks at all. A review or worktree instance
can suppress execution, which looks exactly like a provider failure.
- If a self-hosted agent run fails on *everything* with `401` or
`getaddrinfo ENOTFOUND`, suspect the runtime's environment before the
connector. A Paperclip server started from inside another agent's run inherits
that run's `PAPERCLIP_API_URL` / `PAPERCLIP_API_KEY` and hands them to the
agents it spawns; and on shape B, `auth.publicBaseUrl` is what spawned agents
get as their control-plane URL, so it has to resolve from the runtime's own
machine.
## Step 5 — Verify A Real Result
Four kinds of evidence, in increasing strength. Never blur them.
| Evidence | Proves | Does not prove |
| --- | --- | --- |
| Generator, schema and unit tests | The definition fits tested contracts | Anything about the provider |
| A board **Test** call | That the endpoint can execute that action *for the board* | Anything at all about the agent — see below |
| The policy simulator with `actorType: "agent"` | What the agent's policy decides | That the call would actually succeed |
| A real agent run, with gateway audit events correlated to that run ID | An agent used the managed connector | That the output is correct |
| Provider-side readback of the artifact | The expected result exists and is usable | Any other workflow, provider or deployment |
Only the last two answer the original question.
**The board Test call is labelled with an agent and does not run as one.**
`executeTestCall` builds the session with `actorType: "user"` and the board
user's ID, and agent tool profiles bind to agents. Executed 19 September 2026:
testing as an *ungranted* agent returned `HTTP 200 {"decision":"allowed"}` and
the read succeeded; testing a write the agent's policy denied also returned
`allowed` — **and created the file on the provider.** So Test produces false
passes in one direction and unintended writes in the other. Use
`POST /api/companies/{c}/tools/policy/test` with `actorType: "agent"` for
permission questions, and a real run for execution questions. Detail: **F2** in
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md).
1. Start a real, bounded agent task — the workflow from step 1. Do not
manufacture heartbeat rows and do not count a synthetic session.
2. Correlate connection and tool-invocation events with that exact run ID, and
inspect the tool namespaces actually used. An assistant's claim that it used
a tool is not evidence.
3. Read the artifact back on the provider. For visual output, inspect the
rendered image for real content — a screenshot call can return success before
rendering finishes, and a blank image is not acceptance. Use a bounded
read-only recheck, then report the gap rather than waiting indefinitely.
4. For an authorized write, target an explicitly disposable resource, save the
returned resource ID and pass it to later calls instead of relying on "the
active document". Reconcile provider state before retrying a timed-out write;
a local checkpoint is not an atomic transaction with the provider. Never
delete existing user content as cleanup.
A direct provider plugin, a raw API call, a browser action or an unrelated MCP
server reaching the same service can help you diagnose and can never count as
acceptance. If one contaminates a run, exclude the run.
Then exercise the applicable negative and lifecycle cases in the authorized
sandbox. Do not invent your own list: the runbook's **production validation**
section is the canonical one, and PR #13675 moved it into the runbook precisely
so contributors no longer need a private validation issue for it. Its nine
scenarios are setup and consent, authentication, catalog and configuration,
allowed execution, denied execution, runtime delivery, refresh and recovery,
revoke and reconnect, and activity and secret handling
(`CONNECTOR-PLAYBOOK.md:1445-1455`).
Record **pass**, **fail**, **not run** or **not applicable** per scenario with a
reason for the last two, plus environment, method key, commit, reproduction
steps, expected and actual result, and accessible redacted evidence. Then map
each result into the four deployment labels in
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md):
a scenario that passed on your self-hosted instance makes that row `verified`
**for that deployment only** and leaves the other columns `untested`.
**Run the self-hosted lifecycle first and completely.** Cloud parity is
follow-on work, and an untested Cloud column is `untested` — never `verified`
by analogy with a self-hosted pass. One happy path does not clear a security or
shared-runtime review.
When the path is path 3 and live testing has been separately authorized, the
`author-connector-definition` skill's `references/live-acceptance.md` is the
longer checklist for the same gate; use it rather than restating it.
## Stop Conditions
Stop and report rather than working around any of these:
- The requester cannot name the artifact that would prove success.
- The provider has no MCP endpoint, or needs a transport with no execution path.
- The endpoint is only reachable from somewhere the runtime is not.
- A guard would have to be widened, a tunnel opened, a global default changed,
or a risk annotation downgraded.
- The provider requires a registration or approval the customer cannot
complete.
- Path 3 is the real requirement and the App repository or its ingestion corpus
is unavailable to you.
- A live write is needed and has not been authorized.
- A raw credential appears anywhere. Treat it as an incident: revoke and rotate,
remove the evidence, trace every response, log and audit path, and add a
canary regression test.
## Output Contract
Deliver exactly this, and nothing that implies more:
1. **The workflow and the proof**, as written in step 1.
2. **A filled deployment support matrix**, in the shape of
`references/deployment-support-matrix.md`: a row per capability, a column per
deployment shape you care about, and every cell labelled `verified`,
`untested`, `unsupported` or `deferred` with the guard, provider fact,
executed command or named follow-up owner behind it, plus the commit or
document date you read it from. Self-hosted columns come first and are
expected to be the most complete.
3. **The path taken**, and why the smaller ones were ruled out.
4. **A provider evidence record** — endpoint, metadata URLs, issuer, advertised
registration and PKCE support, scopes considered versus requested,
prerequisites, and the date of each read. No credentials. No tool inventory
you did not observe.
5. **Evidence, in the four separate buckets** of step 5, with the real run ID
and correlated invocations for anything you call an agent run.
6. **Negative and lifecycle results**: passed, failed, pending, not applicable.
7. **Remaining gaps** — unprobed constraints, untested deployments, and anything
a reviewer must authorize before this is relied on.
8. **An explicit statement** of what did not happen: no grant, no install, no
deploy, no push, no PR, no unauthorized write, no configuration left changed.
## Verification Checklist
Tick each item only with evidence, and mark the ones you could not run:
- [ ] The workflow and its provider-side proof are written down.
- [ ] Runtime location and provider location are both established, not assumed.
- [ ] The deployment shape (A, B or C) is named from actual configuration, not
from a product tier.
- [ ] The reachability verdict cites the current guard or provider document,
with a date or commit.
- [ ] The support matrix is filled in, self-hosted columns first, with every
cell labelled `verified` / `untested` / `unsupported` / `deferred`.
- [ ] No step of the delivered workflow requires a Cloud account, a managed
Cloud OAuth client, or private infrastructure — or, if one does, that
step is labelled `unsupported` for self-hosted rather than assumed away.
- [ ] The smallest supported path was taken, and the rejected ones are named.
- [ ] Provider metadata was read unauthenticated and dated; nothing was
registered during research.
- [ ] Requested scopes are the reviewed minimum, with the cost stated.
- [ ] The acting agent's effective tools and permissions were inspected.
- [ ] Instance execution was confirmed before any provider was blamed.
- [ ] A real agent run exists, with audit events correlated to its run ID.
- [ ] The artifact was read back on the provider and inspected, not inferred
from a success response.
- [ ] Negative and lifecycle cases are recorded with per-case status.
- [ ] No credential value appears in any file, output or report.
- [ ] Temporary configuration changes are listed and restored.
- [ ] The report separates executed, source-inspected and not done.
@@ -0,0 +1,90 @@
# Chat and email connections
Read this **only when the provider is a chat or email provider** — Slack,
Discord, Telegram, AgentMail, and others with comparable capabilities. Most
connector work never needs it, and imposing Slack's steps on a provider that
does not work that way is one of the failure modes the guide exists to prevent.
The authority is
[`doc/connections/CHAT-CONNECTOR-UX.md`](https://github.com/paperclipai/paperclip/blob/master/doc/connections/CHAT-CONNECTOR-UX.md),
added by PR #13675 and merged at `c9e867797939fe069278c4ae660f86f7692ed866`.
Read it at the commit you are working against and record that commit. This page
is the routing layer and the self-hosted reachability rules; it is not a
substitute for the guide.
## Three things that are routinely conflated
Keep them separate in your plan, your UI, and your evidence. The guide is
explicit that they establish different things.
| Concern | Question it answers | Failure if merged |
| --- | --- | --- |
| **Resource setup** | Is the provider app/bot/mailbox created, credentialed, and delivering to this instance? | A working bot that no individual can actually address. |
| **Personal identity linking** | Which Paperclip person is this external account? | Someone inherits another person's authority by sending a message. |
| **Company membership** | Should this external person have any authority here at all? | A stranger in a shared channel acts as a member. |
New connections deny unlinked people by default; linking requires ownership
confirmation; nonmembers request access before receiving authority
(`CHAT-CONNECTOR-UX.md#apply-and-verify`). An optional message test must never
become an unexplained completion gate.
## Establish the delivery mechanism before you promise a prerequisite
This is the step that decides whether a self-hoster can use the provider at all.
Ask the provider's own documentation, not another provider's wizard:
> Whether inbound delivery needs a public HTTPS callback, an outbound socket,
> polling, or another mechanism. — `CHAT-CONNECTOR-UX.md:18-22`
Then apply the deployment matrix in
[`deployment-support-matrix.md`](./deployment-support-matrix.md):
| Mechanism | Self-hosted, same machine | Self-hosted, server/VPS | Paperclip Cloud |
| --- | --- | --- | --- |
| Public HTTPS callback | `unsupported` without a public origin | Works only with a genuinely public HTTPS origin | Cloud supplies HTTPS |
| Outbound socket | No public origin needed | No public origin needed | Supported |
| Polling | No public origin needed | No public origin needed | Supported |
Two rules the guide states and this skill enforces:
- **Having HTTPS is not being reachable.** "A private-network HTTPS URL alone is
not proof of public reachability" (`CHAT-CONNECTOR-UX.md:77-79`). A tailnet
certificate does not satisfy a provider that has to POST to you from its own
infrastructure. Verify with observed inbound traffic, not with a green
padlock.
- **Do not require a public URL for a connector that does not use public
callbacks** (`CHAT-CONNECTOR-UX.md:81-82`). Showing a public-origin
prerequisite on a socket-based provider tells a self-hoster their deployment
is unsupported when it is not.
When the provider genuinely needs a public callback and the operator is
self-hosting, say so plainly and link maintained setup documentation. Cloud
"supplies HTTPS" is a fact worth stating; it is not the only answer, and it must
not be the only answer offered (`CHAT-CONNECTOR-UX.md:82-84`).
Never propose a tunnel, a relay, or a guard exception to work around
reachability. Report the gap.
## What this changes in the skill's steps
- **Step 2 (where this runs)** additionally establishes the inbound mechanism
and, if it is a callback, whether the origin is publicly reachable from the
provider — tested, not assumed.
- **Step 4 (configure)** keeps identity linking and membership approval as
separate actions from resource setup, and does not treat the optional
conversation test as a completion gate.
- **Step 5 (verify)** adds one requirement to the real-agent-run evidence: an
incoming message *and its reply* must correlate to the same task. The runbook
names this in its runtime-delivery row —
"Chat/email also routes an incoming message and its reply to the same task."
Distinguish live events from simulated ones, and distinguish an optional
unobserved callback from a real failure.
## If you are designing the setup UI
You are past this skill's boundary. The wizard conventions — stable numbered
steps, the single step-owned footer with **Save & exit** left and the primary
action right, prerequisites first on a tinted background, help icons on
credential fields, manifests behind a modal rather than inline, resume across
back navigation and external provider visits — all live in the guide, with
`DESIGN.md` for components and tokens. Follow it there; do not re-derive it.
@@ -0,0 +1,280 @@
# Deployment support matrix — self-hosted first
Paperclip runs as open-source software you host yourself, and as Paperclip
Cloud. **The self-hosted path is the primary one in this skill.** Nothing in the
documented workflow below requires a Cloud account, a managed Cloud OAuth
client, or any Paperclip-operated infrastructure.
Fill in a copy of this matrix for your own deployment before you claim that a
connector works. Do not copy the labels from this page into a report: they
record what has been established *here*, at the commits named below, and a
deployment you have not exercised is `untested` no matter what this file says.
## The four labels
Use exactly these, and never substitute one for another:
| Label | Means | Requires |
| --- | --- | --- |
| `verified` | Exercised on this deployment and the evidence is recorded. | A command, a run ID, or a provider-side artifact — with date, deployment mode, and commit. |
| `untested` | The path exists in source and is expected to work, but nobody ran it here. | The file:line or document you read, and the reason it was not exercised. |
| `unsupported` | The product refuses it on this deployment. | The guard that refuses it, quoted or cited by file:line. |
| `deferred` | Deliberately out of scope for now. | A reason and a named owner for the follow-up. |
`untested` is not a soft `verified`. A green test suite, a **Connected** badge,
a catalog card, and a successful board **Test** call are all compatible with a
connector no agent can actually use. Source-confirmed behaviour is `untested`
until someone runs it. The board **Test** call in particular is not evidence of
an agent's permissions — see **F2** below, where it returns `allowed` for an
agent whose own policy denies the call.
## Establish your deployment first
Two independent settings decide almost everything below. Read them from your own
instance's configuration, not from a product tier name.
| Setting | Env var | Values | Default |
| --- | --- | --- | --- |
| Deployment mode | `PAPERCLIP_DEPLOYMENT_MODE` | `local_trusted`, `authenticated` | `local_trusted` |
| Exposure | `PAPERCLIP_DEPLOYMENT_EXPOSURE` | `private`, `public` | `private` |
Source: `packages/shared/src/constants.ts:4-8` and `server/src/config.ts:170-190`
at Paperclip App commit `9335b7db10`. Both are also settable as
`config.server.deploymentMode` / `config.server.exposure` in the instance's
`config.json`, which is what `config.ts` reads after the environment.
One consequence is easy to miss and worth writing down, because it is what makes
the same-machine self-hosted path so capable:
> `local_trusted` **forces** exposure to `private`. `server/src/config.ts:187-190`
> ignores `PAPERCLIP_DEPLOYMENT_EXPOSURE` when the mode is `local_trusted`.
So a same-machine self-hosted instance can never land in the
`authenticated` + `public` combination that the private-endpoint guard refuses.
Three deployment shapes follow from this. Name yours before step 3 of the skill:
| Shape | Typical config | What it is |
| --- | --- | --- |
| **A. Self-hosted, same machine** | `local_trusted` (⇒ `private`) | Paperclip on your laptop or workstation, alongside the tools it drives. |
| **B. Self-hosted, server or VPS** | `authenticated` + `private` or `public` | Paperclip on a box you control, reached over your network or the public internet. |
| **C. Paperclip Cloud** | hosted, `authenticated` + `public` | Paperclip runs in Paperclip's hosted environment. |
Shape B splits further on one axis that matters more than exposure: whether the
instance has a **public HTTPS origin**. A VPS on a tailnet with private HTTPS is
a different case from one on a public domain, and the difference decides CIMD
and inbound chat callbacks — not OAuth in general.
### Shape B has two hard prerequisites that are easy to discover the hard way
Both were hit while filling this matrix, on `authenticated` + `public`:
1. **No embedded database.** Startup refuses:
`authenticated public deployments require DATABASE_URL or
config.database.connectionString; refusing embedded PostgreSQL fallback`.
A public self-hosted instance needs an external PostgreSQL.
2. **An explicit, resolvable public base URL.** `auth.baseUrlMode` must be
explicit and `auth.publicBaseUrl` is required
(`packages/shared/src/config-schema.ts:167-187`). This value is not only
browser-facing: the runtime hands it to spawned agents as their control-plane
URL, so it must resolve **from the machine the runtime runs on**, not just
from your browser. A placeholder hostname produces `getaddrinfo ENOTFOUND` on
every MCP server and every API call in the agent's run, which reads exactly
like a broken connector.
Also on shape B: `POST /api/bootstrap/claim` returns
`404 Browser first-admin claim is not available` unless exposure is `private`,
and a merely signed-up user is not an instance admin. Claim the admin while the
instance is `private`, then restart it as `public`. Changing `auth.publicBaseUrl`
invalidates existing board sessions.
## How this matrix was filled
Executed 19 September 2026 against Paperclip App commit
`9335b7db10425277bcb84ba8137d08c498324dba` on **two isolated instances started
for the purpose** — shape A (`local_trusted`, embedded PostgreSQL, loopback
`127.0.0.1:3810`) and shape B (`authenticated`, external PostgreSQL, run as both
`private` and `public`). Providers: a disposable loopback MCP notes server
written for the test, and DeepWiki (`https://mcp.deepwiki.com/mcp`), a real
third-party public server with `auth: "none"`.
**No Cloud instance was reachable.** Every Cloud cell below is `untested` or
`unsupported`, and none of it is inferred from a self-hosted result. The same
honesty runs in reverse: a shape-A pass is not shape-B evidence.
Full reproduction, run IDs, and provider-side artifacts:
[`PAP-18465-SELF-HOSTED-VERIFICATION.md`](../../PAP-18465-SELF-HOSTED-VERIFICATION.md).
## Matrix
### Transport and reachability
| Capability | A. Self-hosted, same machine | B. Self-hosted, server/VPS | C. Paperclip Cloud | Evidence |
| --- | --- | --- | --- | --- |
| `mcp_remote` to a public provider endpoint | `untested` | **`verified`** on `authenticated` + `public` | `untested` | B: DeepWiki connected, catalog refreshed to 3 tools, `read_wiki_structure` executed through the gateway, `decision: allowed`. |
| `mcp_remote` to a loopback endpoint (`127.0.0.1`) | **`verified`** | **`verified`** on `private`; **`unsupported`** on `public` | `unsupported` | A: connected, `healthStatus ok`, 7 tools, real agent runs against it. B `public`: `HTTP 400 {"code":"remote_http_private_endpoint"}` on connect, `HTTP 502` on refresh. Guard: `allowPrivateRemoteEndpoints`, `server/src/services/tool-access.ts:3195-3200`. |
| `mcp_remote` to a link-local address | `untested` | `untested` | `untested` | `isAlwaysDeniedLinkLocalIp` in `server/src/services/remote-http-endpoint-guard.ts` denies in every mode. Source-cited only; no live call was made. |
| `local_stdio` with an approved template | `untested` — instance declares it **supported** | `untested` — instance declares it **unsupported** in both exposures | `untested` | The live `supportMatrix` differs by shape (blocks quoted in the verification report). No stdio connection was created, so the capability itself is untested everywhere. |
| `rest_api` through the connected MCP gateway | `untested` | `untested` | `untested` | Not exposed through the gateway; needs an execution adapter. Runbook "Transport support and boundaries". |
**The self-hosted advantage is real and worth stating plainly:** shape A reaches
desktop apps, local files, and private-network services that Cloud cannot reach
from a hosted runtime, and the loopback row above is the executed proof. That is
not a Cloud defect, and it is not something to engineer around with a tunnel.
Pick the shape that matches the provider.
### Authentication
| Capability | A. Same machine | B. Server/VPS | C. Cloud | Evidence |
| --- | --- | --- | --- | --- |
| `auth: "none"` remote MCP | **`verified`** | **`verified`** (both exposures) | `untested` | Connect → catalog → gateway execution on both instances. |
| API key / PAT in a header | **`verified`** | `untested` | `untested` | A: connection with `credentialRefs: [{placement: "header", key: "x-api-key", secretId}]`; the provider's request log shows the header arrived, and the value appears nowhere in Paperclip (see redaction row). |
| OAuth via dynamic client registration (RFC 7591) | `untested` | `untested` | `untested` | **DCR is instance-local.** Each instance registers its own public client against its own `/api/tools/oauth/callback`. "Cloud-hosted and self-hosted instances use the SAME path — the only per-instance difference is the hostname inside the redirect URI." `CONNECTOR-PLAYBOOK.md:1569-1578`. Consent needs an authorized provider account; not exercised. |
| OAuth via CIMD | `untested`; falls through to DCR on loopback/plain HTTP | `untested` with a public HTTPS origin; falls through without one | `untested` | CIMD "requires a public HTTPS base URL … the authorization server has to fetch that document server-to-server, so loopback and plain-HTTP deployments fall through to the next tier." `GENERIC-REMOTE-MCP.md:94-104`. Neither test instance had a public HTTPS origin. |
| OAuth with a client you registered yourself | `untested` | `untested` | `untested` | Deployment-preconfigured `PAPERCLIP_TOOL_OAUTH_<PROVIDER>_CLIENT_ID` / `_SECRET` outranks every other tier — `server/src/services/tool-access.ts:8787-8804`. Not exercised. |
| OAuth redirect from a plain-HTTP non-loopback origin | n/a — loopback is HTTP-allowed | **`unsupported`** for `https-or-loopback-http` providers | n/a — Cloud is HTTPS | Fails fast with `oauth_redirect_origin_unsupported`; configure TLS. `CONNECTOR-PLAYBOOK.md:1580-1594`. |
| Provider-generated secret-bearing URL | `untested` | `untested` | `untested` | Generic runtime path is complete. Treat the URL as a credential. |
| Paperclip-managed OAuth (`platform_shared`) | **`unsupported`** | **`unsupported`** | `untested` | Requires a reviewed Cloud connector profile and Cloud's fixed provider callback. `CONNECTOR-PLAYBOOK.md:184`. |
**No Cloud account is required for self-hosted OAuth.** The playbook is explicit
that DCR needs neither Paperclip ID nor Paperclip Connect, and that
`id.paperclip.ing` authenticates operators only and never holds resource tokens.
Only the curated `platform_shared` profile is genuinely Cloud-gated, and it is
one row of this table, not the default path. Note that the whole OAuth block is
`untested` on every shape: it is source-confirmed, not exercised.
### Lifecycle
| Capability | A. Same machine | B. Server/VPS | C. Cloud | Evidence |
| --- | --- | --- | --- | --- |
| Connect by URL, no code change | **`verified`** | **`verified`** | `untested` | `POST …/tools/apps/connect` with a pasted link, then `/finish`. No catalog entry authored; `CONNECTOR-PLAYBOOK.md:23-26` says none is needed, and none was. |
| Catalog discovery and risk classification | **`verified`** | **`verified`** | `untested` | A: `read` / `write` / `destructive` assigned correctly across 7 tools with no manual input. |
| Effective policy on the acting agent | **`verified`** | **`verified`** | `untested` | A: `allowedToolNames` moved `[] → [list_notes, read_note] → +create_note` as entries were added; an ungranted agent stayed `[]`. |
| Narrow read through the gateway | **`verified`** | **`verified`** | `untested` | A: two agent runs. B: DeepWiki `read_wiki_structure`. |
| Authorized write through the gateway, disposable resource | **`verified`** | `untested` | `untested` | A only. B's provider is read-only and no write was authorized there. |
| A real agent run correlated to its run ID | **`verified`** | **`verified`** | `untested` | A: runs `af9afa19…`, `135b2654…`. B: run `f027aa16…`, `allow / succeeded`. |
| Provider-side readback of the artifact | **`verified`** | n/a — read-only provider | `untested` | A: the note file on the provider's own store names the run that created it, and that run ID matches the gateway invocation. |
| Denied execution for an ungranted actor | **`verified`** on the agent path | **`verified`** for an unselected tool | `untested` | A: `deny / deny_default`; the write tool was absent from the granted agent's session and the attempted call never reached the provider. **Does not hold on the board Test path — see F2.** |
| Recoverable provider failure and recovery | **`verified`** | `untested` | `untested` | A: provider stopped → `healthStatus error`, `HTTP 502`; restarted → `ok`, no duplicate connection. |
| Catalog refresh after the provider changes | **`verified`, adverse** | `untested` | `untested` | A: three refreshes, each discovering a new tool — and auto-allowing it. **See F1.** |
| Revoke, then reconnect | **`verified`** | `untested` | `untested` | A: grant revoked → `organization_authorization_required`; connection disabled → `deny / deny_disabled_connection`; grant re-added → succeeds on the same connection ID. |
| Redaction of stored credentials | **`verified`** | `untested` | `untested` | A: 0 occurrences of the test credential across 1358 text/JSON columns, the whole instance tree, and five API surfaces — while the provider confirmed the header arrived. |
### Operator verification surfaces
| Capability | Self-hosted, your own instance | Evidence |
| --- | --- | --- |
| `GET /api/companies/:companyId/tools/runtime-health` returns the live `supportMatrix` | **`verified`** for a board actor | Read on both instances; both `supportMatrix` blocks are quoted in the verification report. |
| `GET …/tools/gallery`, `…/tools/connections` | **`verified`** for a board actor | Both returned data to the board actor on the owning instance. |
**Who you are decides whether you can read this.** A run-scoped *agent* JWT gets
`403 {"error":"Board access required"}` on all three routes — executed
19 September 2026 against an instance the agent does not administer. A **board
actor on an instance you own reads them fine.** So: an agent cannot read its own
deployment's support matrix and must ask the operator to paste the
`supportMatrix` block, but an operator is never blocked from it. Do not infer
deployment capability from a product name, a URL someone pasted in a browser, or
the absence of an error.
### Chat and email
Every cell is `untested`: no chat or email provider was connected in the
verification pass. Inbound chat is the one place where a public HTTPS origin is a
hard prerequisite rather than a preference, and it depends on the provider's
delivery mechanism, not on Paperclip's tier.
| Capability | A. Same machine | B. Server/VPS | C. Cloud | Evidence |
| --- | --- | --- | --- | --- |
| Inbound delivery by public HTTPS callback | **`unsupported`** without a public origin | `untested` with a public HTTPS origin; `unsupported` without | `untested` | "For a public callback, distinguish having HTTPS from being reachable by the provider. A private-network HTTPS URL alone is not proof of public reachability." `CHAT-CONNECTOR-UX.md:77-79`. |
| Inbound delivery by outbound socket or polling | `untested` | `untested` | `untested` | Provider-dependent. Establish the mechanism before asserting a prerequisite — `CHAT-CONNECTOR-UX.md:18-22`. |
| Personal identity linking, membership approval | `untested` | `untested` | `untested` | Separate from resource setup; see `references/chat-and-email.md`. |
A self-hoster without a public origin is not out of options: choose a provider
whose inbound path is a socket or polling, or put the instance behind a real
public HTTPS origin. Do not tell them Cloud is the only answer, and do not embed
a server tutorial in the wizard — link maintained setup documentation
(`CHAT-CONNECTOR-UX.md:81-84`).
## Two findings that change what you should tell an operator
These are reproducible product behaviours at commit `9335b7db10`, not advice.
Both are filed for product engineering; until they change, the skill's guidance
has to account for them.
### F1 — a pasted MCP server can widen its own permissions after approval
On every catalog refresh, newly discovered entries are added to the connection's
active managed profile unless `quarantineNewEntries` is set — and for a
**user-pasted URL** it is explicitly set to `false`
(`server/src/services/tool-access.ts:12721`, in the same expression that flags
the connection `unverifiedServer: true`). Curated gallery apps and
Paperclip-managed cloud connectors *do* quarantine. The least-trusted class does
not.
Executed: a connection finished with exactly two tools enabled and access limited
to one agent. The provider then advertised `export_notes`, which returns every
note body in one response. After one refresh, with no operator action:
`policy test ⇒ allow / allow_profile`, `new-tools review queue: pendingCount 0`.
Risk tier does not gate it either — a newly advertised `delete_note`, correctly
classified `destructive`, was also auto-allowed.
**What this means for the skill's advice.** "Start writes at Off and promote
deliberately" is not durable on this build: the server decides. If you paste a
URL you do not control, re-check the effective profile after every refresh, and
treat the connection's tool set as provider-controlled rather than
operator-controlled.
### F2 — the board "Test as agent" call runs with the board's authority
`executeTestCall` builds a session with `actorType: "user"` and the board user's
ID. Agent tool profiles bind to agents, so the board user is not subject to them.
Executed: testing as an **ungranted** agent returned
`HTTP 200 {"decision":"allowed"}` and the read succeeded. Testing `create_note`
as an agent whose policy denied it returned `allowed` — **and the file was
created on the provider.** The policy simulator, asked the same questions with
`actorType: "agent"`, answers correctly.
**What this means for the skill's advice.** A board Test call is not evidence
about an agent, in either direction: it can pass where the agent would be denied,
and it can perform a write the agent's own policy forbids. Use the policy
simulator (`POST /api/companies/{c}/tools/policy/test` with `actorType: "agent"`)
for permission questions, and a real agent run for execution questions.
### F10 — the connect wizard grants the policy but not the install
`POST …/tools/apps/{id}/finish` with `access: {agentIds: [...]}` creates the
tool-profile binding but **no `tool_connection_install` row**. Agent readiness
requires both (`server/src/services/connection-intents.ts:241-248`), so the tool
never enters the agent's session. Both wizard-created connections, on both
instances, showed `installs: []` alongside a correct `allowedToolNames`. The
board's access view showed the tool allowed and the connection green; the agent
got `needs_user_action` and could not run.
**What this means for the skill's advice.** After finishing the wizard, check
`GET /api/tool-connections/{id}/installs` — not just the effective profile.
`PUT …/installs` is the one-call fix. This is exactly the "Connected badge, agent
still cannot do the job" failure the skill names in **Use This When**, reproduced
on the path the skill recommends.
## Deferred
| Item | Reason | Owner |
| --- | --- | --- |
| Cloud parity for every row above | No Cloud instance was reachable, and the self-hosted-first priority defers it | Operator with a Cloud instance, after self-hosted |
| OAuth (DCR / CIMD / own client) on any shape | Needs an authorized provider account | Operator with a provider account |
| `local_stdio` execution | The live `supportMatrix` says shape A supports it, which is not the same as running it | Follow-up under [PAP-18464](/PAP/issues/PAP-18464) |
| Chat and email inbound delivery | Out of scope for the first pass | Follow-up under [PAP-18464](/PAP/issues/PAP-18464) |
| Shape-B write execution and provider-side readback | The shape-B provider is read-only | Operator with a writable third-party provider |
| Link-local denial | Source-cited only; no live call | Low priority — the guard is unconditional |
## Source commits
| Source | Revision | Read |
| --- | --- | --- |
| Paperclip App | `9335b7db10` on `master` | 19 September 2026 |
| `doc/connections/CONNECTOR-PLAYBOOK.md`, `doc/connections/CHAT-CONNECTOR-UX.md` | merged by PR #13675 at `c9e867797939fe069278c4ae660f86f7692ed866` | 19 September 2026 |
Re-read these before relying on a line number. The runbook moved 214 lines in
PR #13675 alone; if a citation disagrees with the commit in front of you, the
commit wins and you record the drift.
+107 -1
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"packageName": "@paperclipai/skills-catalog",
"packageVersion": "0.3.1",
"generatedAt": "2026-08-11T15:05:34.526Z",
"generatedAt": "2026-09-19T16:58:28.123Z",
"skills": [
{
"id": "paperclipai:bundled:docs:doc-maintenance",
@@ -1114,6 +1114,112 @@
"url": "https://github.com/mvanhorn/last30days-skill/tree/v3.3.0/skills/last30days"
}
},
{
"id": "paperclipai:optional:software-development:author-connector-definition",
"key": "paperclipai/optional/software-development/author-connector-definition",
"kind": "optional",
"category": "software-development",
"slug": "author-connector-definition",
"name": "author-connector-definition",
"description": "Author one Paperclip catalog connector definition: probe the provider MCP metadata, fix the five connection axes, add official branding, edit the generator source, regenerate, and get the catalog tests green with evidence. Mechanical authoring only; not research approval, not PR submission.",
"path": "catalog/optional/software-development/author-connector-definition",
"entrypoint": "SKILL.md",
"trustLevel": "markdown_only",
"compatibility": "compatible",
"defaultInstall": false,
"recommendedForRoles": [
"engineer"
],
"requires": [
"git",
"curl",
"node",
"pnpm"
],
"tags": [
"mcp",
"connectors",
"app-definition",
"catalog",
"codegen"
],
"files": [
{
"path": "SKILL.md",
"kind": "skill",
"sizeBytes": 20964,
"sha256": "ecdf045a004da99c6ed9a3384b6000151d6965564d2ed1543c76c94b336811b7"
},
{
"path": "references/catalog-contract.md",
"kind": "reference",
"sizeBytes": 13589,
"sha256": "f8a659b8c5790fd7c7b6d0a7bac1d3e16e175072f18c97ce3e5db1924f9abda3"
},
{
"path": "references/live-acceptance.md",
"kind": "reference",
"sizeBytes": 5670,
"sha256": "24afcc38cfc9e56ca692d6d10e6eeba11d1ca41de939c47cb1caed42afba7928"
},
{
"path": "references/offline-verification.md",
"kind": "reference",
"sizeBytes": 9519,
"sha256": "ca38bcddf376aa1bc2ea5f392032c37cda204af21a412ec4b6e984343087c2e2"
}
],
"contentHash": "sha256:c7a267d390dee25ac65750b46f9aaad69993e08eb42c8133bf52a05701a6953a"
},
{
"id": "paperclipai:optional:software-development:connect-agent-tools",
"key": "paperclipai/optional/software-development/connect-agent-tools",
"kind": "optional",
"category": "software-development",
"slug": "connect-agent-tools",
"name": "connect-agent-tools",
"description": "Get a Paperclip agent working with an outside tool, self-hosted or Cloud: start from the workflow and its proof, establish the deployment and its real reachability, take the smallest supported connection path, and verify a real agent run with provider-side readback. Not for catalog authoring.",
"path": "catalog/optional/software-development/connect-agent-tools",
"entrypoint": "SKILL.md",
"trustLevel": "markdown_only",
"compatibility": "compatible",
"defaultInstall": false,
"recommendedForRoles": [
"engineer",
"operations"
],
"requires": [
"curl"
],
"tags": [
"mcp",
"connectors",
"integrations",
"connections",
"verification"
],
"files": [
{
"path": "SKILL.md",
"kind": "skill",
"sizeBytes": 26730,
"sha256": "7fa19dc00395a5b8214f1a725c259dd1aa97ce580a43d0f8b8a13d2586c83bbb"
},
{
"path": "references/chat-and-email.md",
"kind": "reference",
"sizeBytes": 4945,
"sha256": "c014394e04b46bf34460296936febfe8497926faf9f87ed47c27377acc0f2371"
},
{
"path": "references/deployment-support-matrix.md",
"kind": "reference",
"sizeBytes": 20738,
"sha256": "fa7b868c2c3997e5433ad4845738d7092dc2a7f621e420d7a28dbf1f29e7c2b5"
}
],
"contentHash": "sha256:ab06d5fcaa0ce39865e53d9ee45345cc07d72f4be51ce201d13b195e4f8872ba"
},
{
"id": "paperclipai:optional:software-development:prepare-mcp-integration",
"key": "paperclipai/optional/software-development/prepare-mcp-integration",
@@ -24,6 +24,8 @@ const EXPECTED_OPTIONAL_KEYS = [
"paperclipai/optional/finance/ramp",
"paperclipai/optional/product/design-critique",
"paperclipai/optional/research/last30days",
"paperclipai/optional/software-development/author-connector-definition",
"paperclipai/optional/software-development/connect-agent-tools",
"paperclipai/optional/software-development/prepare-mcp-integration",
];