feat(connections): add verified MCP providers and setup fixes (#15621)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Apps give agents governed access to external tools. > - Several provider MCP servers lacked a supported catalog entry or failed during setup. > - Real browser tests identified specific registration, session, and form defects. > - This pull request adds seven catalog entries and fixes the shared paths used by eleven verified providers. > - Users can connect these providers through the existing Apps flow and control each action. ## Linked Issues or Issue Description **What existing behavior does this improve?** The Apps catalog and remote MCP setup, discovery, and action tester. **Subsystem affected** Shared app definitions, the server tool services, and the Apps UI. **Current behavior** Seven providers lack a catalog entry. Airtable can select an advertised client-metadata flow that fails. Calendly rejects the registration name. Tavily needs an initialized session before a call. Firecrawl exposes invalid defaults for optional nested form objects. HTTPS setup can display a callback that differs from the server callback. **Proposed behavior** Add Calendly, Exa, Firecrawl, GSC Wizard, Parallel Search, Tavily, and Windsor.ai. Preserve Airtable, Linear, Make, and PostHog. Use reviewed provider options through the shared MCP and OAuth paths. Display the callback supplied by the server. Omit untouched optional object inputs. **Reason and benefit** Eleven providers passed bounded reads through the real Paperclip browser action tester. This PR includes that verified set and its required shared fixes. Unfinished providers remain outside this change. AgentMail keeps its existing integration. **Breaking changes** No database migration. Existing OAuth credentials and action policies keep their ownership and access rules. Airtable's reviewed method re-registers a retained client-metadata binding through DCR. Tavily's reviewed method initializes sessions before dispatch. The existing customer, managed, and Vercel OAuth gateway paths now refresh on upstream 401 and return `oauth_refreshed_retry_required` (409), requiring an explicit caller retry. They do not replay the rejected call automatically; the next invocation initializes a fresh credential-scoped session when required. Related catalog work: #15545. This branch preserves current master catalog entries and does not add another Google Workspace integration. Open and closed provider PRs and public MCP issues were searched. No duplicate for this verified set was found. The change extends the shipped Connected Apps roadmap item. ## What Changed - Add seven catalog entries with official provider branding and reviewed permissions. - Update Airtable, Linear, and Make metadata and show Make in Apps. - Add authoritative provider source overrides that use the existing generation and review checks. - Add the reviewed Airtable DCR option and compatible Calendly registration names. - Initialize Tavily sessions before discovery and calls, including retained connections. Keep credential-scoped session caching and single call dispatch. - Use the server's callback URL in the OAuth setup UI. - Omit untouched optional object defaults; keep supplied empty strings, false, zero, and object values intact in the action tester, with strict required-child validation. - Require an explicit retry after OAuth refresh instead of replaying a tools/call with missing session headers. - Record all eleven successful reads, write policies, auth-method limits, and qualification caveats. Omit credentials and private account data. - Find chat connector cards through catalog search in browser tests, so pagination does not hide Slack or other later entries. ## Verification - Real browser qualification: eleven bounded reads passed. Catalog refresh and reload proof are recorded in `doc/connections/verified-mcp-qualification-2026-10-08.md`. - Provider writes and actual agent-adapter sessions were not run. Alternative documented auth methods and expiry refresh remain unqualified. - `pnpm -r typecheck`: passed. - `pnpm build`: passed. - Focused shared/catalog tests: 41 passed. Source override tests: 3 passed. Catalog regeneration fixture suite: 9 passed. Latest form regression suites: 31 passed. Gateway suite: 38 passed. Other focused UI suites passed before these last fixes. - UI token gates and both token sync checks: passed. - The catalog regeneration fixture includes the authoritative provider overrides; its full suite passes. The focused OAuth socket case passed on isolated rerun. - Complete local UI suite: 7,866 passed. CLI suite: 511 passed, 6 skipped. Complete shared suite: 892 passed. - The unchanged AgentMail/ClickUp discovery fallback suite passes all 45 cases. An OpenAI login test passed on isolated rerun. - Local broad database coverage is limited by macOS PostgreSQL shared-memory exhaustion (`shmget: No space left on device`, not disk space). The broad run was interrupted after diagnosis; no host settings or other running services were changed. - CI found that the Slack browser test assumed its catalog card was on the first page. The test now uses catalog search; the Slack case passes locally. Adjacent GitHub and iMessage cases could not start locally because embedded PostgreSQL initialization failed before browser assertions. - Final commit [`febe4520e`](https://github.com/paperclipai/paperclip/commit/febe4520e13d4b3a4121eafc3d22540c2b11379d): all 54 checks passed, with none pending or failed. [CI run](https://github.com/paperclipai/paperclip/actions/runs/37847182065) passed typecheck, build, all general and serialized test suites, all eight browser shards, runner checks, canary dry run, and the aggregate verification gate. - Greptile reviewed that exact final commit at 2026-10-08 21:33 UTC and returned 5/5 with no outstanding findings or unresolved threads. - PR UI preview against the existing local test server: Calendly catalog/setup observed; Firecrawl read passed after expanding More options with nested optional inputs untouched. This retest proves frontend behavior, not the revised OAuth backend against a live provider. <details> <summary>Browser evidence</summary>    </details> ## Risks - Provider registration and consent behavior can change. Airtable's DCR option is explicit and keeps the existing issuer, resource, redirect, and PKCE checks. - Tavily uses initialized sessions without automatic call retries. After a successful OAuth refresh following 401, callers now receive a retry-required result. A later explicit retry can still require reauthorization if the provider rejects the refreshed token. Provider writes are not live-qualified. - Optional object cleanup affects the shared action tester. Regression tests cover absent objects, required children, defaults, and populated values. - GSC Wizard's underlying Google data scopes and Google flow completion were not independently verified. Its account reported paid/trial metadata of unknown origin. No purchase was performed. - No database migration, new AgentMail integration, or change to existing action grants is included. ## Model Used OpenAI GPT-6 through Codex assisted with implementation, research, tool use, and code execution. The root backend model ID and context window are not exposed in this session. The cheaper subagents used OpenAI `gpt-6-luna`. No model context size is inferred. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass — targeted checks and complete UI/CLI/shared suites passed; the broad local database run was blocked by the macOS PostgreSQL startup limitation documented above. The full database/workspace suite passed in CI. - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <[email protected]>
No files matched your search
@@ -317,6 +317,15 @@ only `customer` must not silently fall through to generic DCR. A method
|
||||
containing `dcr` permits the automatic CIMD/DCR tiers. Deployment-preconfigured
|
||||
credentials still take precedence when present.
|
||||
|
||||
A reviewed remote OAuth method may set `oauthClientRegistration: "dcr"` when
|
||||
live provider evidence shows that its advertised CIMD route fails while DCR
|
||||
works. Airtable uses this compatibility option. It requires `dcr` in
|
||||
`ownershipModes`; a deployment-preconfigured client still wins, and a missing
|
||||
registration endpoint fails with `oauth_dcr_not_supported`. Existing CIMD
|
||||
bindings are re-registered through DCR when the method changes. Other methods
|
||||
keep the normal resolution order. This option does not relax issuer, resource,
|
||||
redirect URI, PKCE, or registration-response validation.
|
||||
|
||||
### Credential source is not OAuth client ownership
|
||||
|
||||
`ownershipModes` says who owns the OAuth client registration. It does not say
|
||||
@@ -353,6 +362,16 @@ connection, grant, catalog, profile, policy, and run state; resolves the needed
|
||||
secret version; projects only the reviewed headers/arguments; calls the
|
||||
provider; and writes redacted audit evidence.
|
||||
|
||||
For a reviewed remote MCP method that accepts discovery before initialization
|
||||
but requires an initialized session for calls, set
|
||||
`defaults.mcpSessionRequired: true`. Tavily uses this compatibility preference.
|
||||
Setup persists it for new connections; discovery and execution also resolve the
|
||||
curated method default for retained connections without the config flag. The
|
||||
existing session cache stays scoped to the connection, grant, actor, endpoint,
|
||||
and effective credential headers. Other methods retain their existing discovery fallback
|
||||
and direct-call behavior. Initialize before dispatch; never retry `tools/call`
|
||||
automatically after a provider error, since the first call may have mutated data.
|
||||
|
||||
## Secret Storage And Lifecycle
|
||||
|
||||
Connection credentials use the same secrets architecture as agent, project,
|
||||
@@ -770,7 +789,10 @@ remove it there, and increment its version when changing the default. There is
|
||||
no central instruction-template registry. See
|
||||
[Connection instructions](./CONNECTION-INSTRUCTIONS.md) for the runtime contract.
|
||||
|
||||
1. Add or update the provider in `scripts/ingest-app-definitions.mjs`.
|
||||
1. Add or update the provider in `scripts/ingest-app-definitions.mjs` or its
|
||||
reviewed source inputs at `scripts/app-definition-overrides/<slug>.json`.
|
||||
Overrides use the complete AppDefinition shape and pass through the same
|
||||
branding, permission-review, instruction-template, and validation pipeline.
|
||||
2. Update `packages/shared/src/self-serve-mcp-research.json` when it belongs to
|
||||
that program.
|
||||
3. Add branding provenance and assets first; generation fails closed when
|
||||
@@ -1719,10 +1741,16 @@ supports public clients (`token_endpoint_auth_method: "none"` plus PKCE S256)
|
||||
need **no pre-provisioned OAuth app at all**. At first connect the broker
|
||||
registers a client on the fly and stores it on the connection:
|
||||
|
||||
- Registration request: `client_name` `Paperclip (<instance host>)`,
|
||||
- Registration request: `client_name` `Paperclip <sanitized instance host>`,
|
||||
`redirect_uris` = the instance's own callback, `grant_types`
|
||||
`["authorization_code", "refresh_token"]`, `response_types` `["code"]`,
|
||||
`token_endpoint_auth_method` `"none"`.
|
||||
- Authorization servers may validate `client_name` more narrowly than the
|
||||
human-readable RFC field suggests. Paperclip replaces punctuation in the
|
||||
callback host with hyphens (for example, `localhost:3105` becomes
|
||||
`localhost-3105`), preserving a recognizable instance label while meeting
|
||||
Calendly's documented rule that names contain only alphanumeric characters,
|
||||
hyphens, and spaces ([Calendly MCP DCR requirements](https://developer.calendly.com/docs/mcp/calendly-mcp-server)).
|
||||
- The issued `client_id` is persisted in the connection's OAuth config and any
|
||||
issued `client_secret` becomes a `company_secrets` ref. The registered
|
||||
client is **reused** for every later authorize/refresh on that connection —
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
"https://support.airtable.com/articles/9897799762-using-the-airtable-mcp-server",
|
||||
"https://mcp.airtable.com/.well-known/oauth-protected-resource"
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run"
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "DCR/HTTPS OAuth completed for one selected base only with read/write records, comments and base structure. Bounded base-name search passed in 0.6s. 46 actions (24 Read/22 Write); all 22 Write Off persisted after reload. No record data read or writes.",
|
||||
"evidenceLimit": "An initial consent-flow review issue was disclosed before the completed grant; qualification stayed limited to bounded reads. No provider writes or actual agents ran. Token-refresh and actual runtime behavior remain unqualified."
|
||||
},
|
||||
{
|
||||
"app": "api-key-generic",
|
||||
@@ -39,7 +40,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Actions depend on the operator-supplied API and key; grant read/write on the required resources at the provider. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions.",
|
||||
"keyPermissions": "Actions depend on the operator-supplied API and key; grant read/write on the required resources at the provider. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions.",
|
||||
"evidenceLimit": "Provider documentation was unavailable or not readable during this review; metadata/registered source only. Account-bound proof remains outstanding."
|
||||
},
|
||||
{
|
||||
@@ -140,7 +141,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Create/manage links in authorized groups. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Create/manage links in authorized groups. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "box",
|
||||
@@ -208,7 +209,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Create/stop browser sessions and run browser tasks under the API key. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Create/stop browser sessions and run browser tasks under the API key. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "candid",
|
||||
@@ -282,7 +283,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Cloudflare API actions selected at provider consent or on the API token. Identity scopes alone do not grant account writes. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Cloudflare API actions selected at provider consent or on the API token. Identity scopes alone do not grant account writes. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "cloudinary",
|
||||
@@ -338,7 +339,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Modify documents/tables permitted by the account. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Modify documents/tables permitted by the account. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "cognee",
|
||||
@@ -353,7 +354,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Add/process knowledge and manage authorized datasets. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Add/process knowledge and manage authorized datasets. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "composio",
|
||||
@@ -505,7 +506,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Use an API key for an account with write access to the meetings your agents need to share, rename, move or turn into soundbites. Paperclip cannot increase the key\u2019s permissions."
|
||||
"keyPermissions": "Use an API key for an account with write access to the meetings your agents need to share, rename, move or turn into soundbites. Paperclip cannot increase the key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "gauge",
|
||||
@@ -570,7 +571,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Repository contents, issues, pull requests and other granted toolsets; selected repos and installation/PAT permissions apply. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions.",
|
||||
"keyPermissions": "Repository contents, issues, pull requests and other granted toolsets; selected repos and installation/PAT permissions apply. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions.",
|
||||
"evidenceLimit": "Provider documentation was unavailable or not readable during this review; metadata/registered source only. Account-bound proof remains outstanding."
|
||||
},
|
||||
{
|
||||
@@ -1266,7 +1267,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Launch/manage browsers, profiles, apps and browser automation. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Launch/manage browsers, profiles, apps and browser automation. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "linear",
|
||||
@@ -1282,9 +1283,9 @@
|
||||
"evidence": [
|
||||
"https://mcp.linear.app/.well-known/oauth-protected-resource"
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"evidenceLimit": "Provider documentation was unavailable or not readable during this review; metadata/registered source only. Account-bound proof remains outstanding."
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "User-approved read/write; selected workspace returned one team in 0.3s. IDs omitted. Catalog refresh and reload retained 68 actions; all 30 write actions Off. No provider writes or actual agent runs.",
|
||||
"evidenceLimit": "Browser tester account read only; provider writes, refresh-token expiry, and actual agent-adapter execution remain unqualified. Any agent company installation; UI did not allow narrowing. No actual agent ran."
|
||||
},
|
||||
{
|
||||
"app": "local-falcon",
|
||||
@@ -1318,7 +1319,7 @@
|
||||
"https://www.make.com/.well-known/oauth-authorization-server/mcp"
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"liveProof": "2026-10-08: real Paperclip browser Test dialog environment_get returned authenticated organization/team context. Refresh/reload retained 133 actions; all 59 writes set Off and retained after reload. Company-wide installation in isolated test company, no provider write or actual adapter run. Account values excluded.",
|
||||
"providerDefaultReason": "Make documents selecting scopes and scenarios during its hosted connection flow. Management tools require a paid plan. Do not replace that resource selection with invented MCP client scopes."
|
||||
},
|
||||
{
|
||||
@@ -1355,7 +1356,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Add/update/delete memories under the API key account. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Add/update/delete memories under the API key account. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "miro",
|
||||
@@ -1470,7 +1471,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-10-02",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Project, branch, compute, snapshot, SQL and schema changes within the key\u2019s reach. A project-scoped key limits access to one project with Editor rights; personal and organization keys reach every project they can access. Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Project, branch, compute, snapshot, SQL and schema changes within the key’s reach. A project-scoped key limits access to one project with Editor rights; personal and organization keys reach every project they can access. Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "notion",
|
||||
@@ -1552,7 +1553,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Incident and on-call changes require a full-access API key and role access. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Incident and on-call changes require a full-access API key and role access. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "pagerduty",
|
||||
@@ -1567,7 +1568,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Incident and on-call changes require a full-access API key and role access. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Incident and on-call changes require a full-access API key and role access. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "planetscale",
|
||||
@@ -1619,9 +1620,10 @@
|
||||
"https://posthog.com/docs/model-context-protocol",
|
||||
"https://mcp.posthog.com/.well-known/oauth-protected-resource"
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"providerDefaultReason": "PostHog documents the no-scope MCP setup as read/write, with optional readonly and feature filters. The authorization server advertises account administration unrelated to many tools; retain its MCP consent defaults and the existing explicit filters."
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "Provider read-only preset; active Paperclip project only. Authenticated profile returned in 0.7s; identity values omitted. Catalog refresh and reload retained 380 actions; all 7 write actions Off. No provider writes or actual agent runs.",
|
||||
"providerDefaultReason": "PostHog documents the no-scope MCP setup as read/write, with optional readonly and feature filters. The authorization server advertises account administration unrelated to many tools; retain its MCP consent defaults and the existing explicit filters.",
|
||||
"evidenceLimit": "Browser tester account read only; provider writes, refresh-token expiry, and actual agent-adapter execution remain unqualified. Any agent company installation; UI did not allow narrowing. No actual agent ran."
|
||||
},
|
||||
{
|
||||
"app": "posthog",
|
||||
@@ -1637,7 +1639,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Analytics objects, feature flags, experiments, error triage and other enabled product actions. readonly/feature/tool filters remain enforced. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Analytics objects, feature flags, experiments, error triage and other enabled product actions. readonly/feature/tool filters remain enforced. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "postman",
|
||||
@@ -1705,7 +1707,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Collection/workspace/API actions supported by the selected minimal/code/full endpoint and account role. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Collection/workspace/API actions supported by the selected minimal/code/full endpoint and account role. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "postman",
|
||||
@@ -1722,7 +1724,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Collection/workspace/API actions supported by the selected minimal/code/full endpoint and account role. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Collection/workspace/API actions supported by the selected minimal/code/full endpoint and account role. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "postman",
|
||||
@@ -1739,7 +1741,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Collection/workspace/API actions supported by the selected minimal/code/full endpoint and account role. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Collection/workspace/API actions supported by the selected minimal/code/full endpoint and account role. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "railway",
|
||||
@@ -1793,7 +1795,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Provider-authorized payment/account tools; financial policy gates remain. Live verification outstanding. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions.",
|
||||
"keyPermissions": "Provider-authorized payment/account tools; financial policy gates remain. Live verification outstanding. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions.",
|
||||
"evidenceLimit": "Provider documentation was unavailable or not readable during this review; metadata/registered source only. Account-bound proof remains outstanding."
|
||||
},
|
||||
{
|
||||
@@ -1845,7 +1847,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Edit content permitted by project role; global is MCP authorization, not a bypass of project ACLs. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Edit content permitted by project role; global is MCP authorization, not a bypass of project ACLs. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "sentry",
|
||||
@@ -1992,7 +1994,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Account- and sandbox-specific writes selected during Stripe consent; financial approvals and restricted-key permissions remain. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Account- and sandbox-specific writes selected during Stripe consent; financial approvals and restricted-key permissions remain. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "supabase",
|
||||
@@ -2037,7 +2039,7 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Project/database/environment/storage changes and Edge Function deployment. Project selection and optional read-only configuration remain authoritative. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions."
|
||||
"keyPermissions": "Project/database/environment/storage changes and Edge Function deployment. Project selection and optional read-only configuration remain authoritative. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "superagent",
|
||||
@@ -2297,6 +2299,251 @@
|
||||
],
|
||||
"reviewedAt": "2026-09-30",
|
||||
"liveProof": "not-run"
|
||||
},
|
||||
{
|
||||
"app": "tavily",
|
||||
"method": "mcp-oauth",
|
||||
"auth": "oauth",
|
||||
"policy": "explicit",
|
||||
"requestedScopes": [
|
||||
"openid",
|
||||
"offline_access"
|
||||
],
|
||||
"capability": "read",
|
||||
"supportedActions": "Run public web search and extract content from public pages using the account’s Tavily API key and credit allowance. These are read/search operations; no provider business-record write action is exposed.",
|
||||
"evidence": [
|
||||
"https://docs.tavily.com/documentation/mcp",
|
||||
"https://mcp.tavily.com/.well-known/oauth-protected-resource/mcp",
|
||||
"https://mcp.tavily.com/.well-known/oauth-authorization-server"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "2026-10-08: HTTPS OAuth connected. After the initialized-session fix, query-only public search returned five results in 1.7s. Refresh/reload retained five Read/zero Write actions. No grant expansion or writes.",
|
||||
"evidenceLimit": "Two earlier searches returned HTTP 400; successful call after proactive initialization supports but does not prove cause. Provider writes, refresh-token expiry and actual agent runs unqualified. Any agent company installation in isolated test company; no actual agents ran."
|
||||
},
|
||||
{
|
||||
"app": "calendly",
|
||||
"method": "mcp-oauth",
|
||||
"auth": "oauth",
|
||||
"policy": "explicit",
|
||||
"requestedScopes": [
|
||||
"mcp:scheduling:read",
|
||||
"mcp:scheduling:write"
|
||||
],
|
||||
"capability": "write",
|
||||
"supportedActions": "Read event types and scheduling availability and call supported scheduling write tools under the authorized account; the official MCP documents the same scheduling read/write scope family as the public API.",
|
||||
"evidence": [
|
||||
"https://developer.calendly.com/docs/mcp/calendly-mcp-server",
|
||||
"https://mcp.calendly.com/.well-known/oauth-protected-resource",
|
||||
"https://calendly.com/.well-known/oauth-authorization-server"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "HTTPS DCR/S256 scheduling read/write grant succeeded on retry; provider profile verified the connected account. lookup_current_user passed in 2.5s. 36 actions (25 Read/11 Write); all 11 writes Off persisted after reload.",
|
||||
"evidenceLimit": "Account read only; scheduling/provider writes, expiry refresh, and actual agent runtime unqualified. Any agent installation in isolated test company; no agents ran."
|
||||
},
|
||||
{
|
||||
"app": "exa",
|
||||
"method": "public-search",
|
||||
"auth": "none",
|
||||
"policy": "endpoint",
|
||||
"requestedScopes": [],
|
||||
"capability": "read",
|
||||
"supportedActions": "Public web search and webpage retrieval. Public MCP initialize and tools/list observed only web_search_exa and web_fetch_exa, both marked readOnly/idempotent; account-gated agent/advanced tools are excluded.",
|
||||
"evidence": [
|
||||
"https://exa.ai/docs/get-started/exa-mcp",
|
||||
"https://exa.ai/mcp",
|
||||
"https://mcp.exa.ai/mcp"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "2026-10-08: Paperclip browser Test dialog web_search_exa returned a public repository matching the query in 2.0s; connection persisted after reload and action refresh passed. Public read only; no writes or agent-adapter run."
|
||||
},
|
||||
{
|
||||
"app": "firecrawl",
|
||||
"method": "mcp-oauth",
|
||||
"auth": "oauth",
|
||||
"policy": "explicit",
|
||||
"requestedScopes": [
|
||||
"firecrawl:global",
|
||||
"offline_access"
|
||||
],
|
||||
"capability": "write",
|
||||
"supportedActions": "Full account-authorized Firecrawl MCP surface; web reads plus provider jobs such as crawl/research, which may consume team credits.",
|
||||
"evidence": [
|
||||
"https://docs.firecrawl.dev/mcp-server",
|
||||
"https://www.firecrawl.dev/.well-known/oauth-authorization-server",
|
||||
"https://mcp.firecrawl.dev/.well-known/oauth-protected-resource/v2/mcp-oauth"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "not-run"
|
||||
},
|
||||
{
|
||||
"app": "firecrawl",
|
||||
"method": "mcp-api-key",
|
||||
"auth": "api_key",
|
||||
"policy": "provider-key",
|
||||
"requestedScopes": [],
|
||||
"capability": "write",
|
||||
"supportedActions": "Bearer API key may expose full Firecrawl tool surface, including crawl/research jobs billed to the selected team.",
|
||||
"evidence": [
|
||||
"https://docs.firecrawl.dev/mcp-server"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Create an API key for the intended Firecrawl team with access to the search, scrape, parse, and any job tools you intend to use. Jobs can consume credits; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "firecrawl",
|
||||
"method": "keyless",
|
||||
"auth": "none",
|
||||
"policy": "endpoint",
|
||||
"requestedScopes": [],
|
||||
"capability": "read",
|
||||
"supportedActions": "Keyless Firecrawl exposes firecrawl_search, firecrawl_scrape, and firecrawl_parse; public catalog initialization confirmed those three. These retrieve/search content and process supplied files; keyless usage is limited.",
|
||||
"evidence": [
|
||||
"https://docs.firecrawl.dev/mcp-server",
|
||||
"https://mcp.firecrawl.dev/v2/mcp"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "2026-10-08: Paperclip browser Test dialog firecrawl_scrape returned public page metadata and HTTP 200 (0.6s); connection persisted after reload; action refresh passed. Public read only, no writes or agent-adapter run."
|
||||
},
|
||||
{
|
||||
"app": "parallel-search",
|
||||
"method": "public-search",
|
||||
"auth": "none",
|
||||
"policy": "endpoint",
|
||||
"requestedScopes": [],
|
||||
"capability": "read",
|
||||
"supportedActions": "Public Parallel Search MCP initialize and tools/list observed only web_search and web_fetch; both tool annotations marked read-only/idempotent. Task MCP is a separate endpoint/product.",
|
||||
"evidence": [
|
||||
"https://docs.parallel.ai/integrations/mcp/search-mcp",
|
||||
"https://parallel.ai/articles/add-free-web-search-to-coding-agent",
|
||||
"https://search.parallel.ai/mcp"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "2026-10-08: Paperclip browser Test dialog web_search returned 10 public results in 0.8s; connection persisted after reload and action refresh passed. Public read only; no writes or agent-adapter run."
|
||||
},
|
||||
{
|
||||
"app": "windsor-ai",
|
||||
"method": "mcp-oauth",
|
||||
"auth": "oauth",
|
||||
"policy": "explicit",
|
||||
"requestedScopes": [
|
||||
"create",
|
||||
"delete",
|
||||
"offline_access"
|
||||
],
|
||||
"capability": "write",
|
||||
"supportedActions": "The MCP resource explicitly challenges for create and delete even to initialize/list tools. Read tools query selected connected sources; provider-level delete can expose destructive actions on supported sources. Keep write/destructive actions Off for read qualification. Search Console source authorization is separately read-only.",
|
||||
"evidence": [
|
||||
"https://mcp.windsor.ai/",
|
||||
"https://mcp.windsor.ai/.well-known/oauth-protected-resource",
|
||||
"https://mcp.windsor.ai/.well-known/oauth-authorization-server"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "User-approved create/delete/offline_access MCP scopes; separately approved read-only Search Console source, the selected Search Console property only. 54 field definitions; bounded last_7d request returned 5 daily rows dated 2026-10-01 through 2026-10-05 in 1.5s; metric values omitted. Catalog refresh and reload retained 19 actions; all 5 write actions Off. No provider writes or actual agent runs.",
|
||||
"evidenceLimit": "Browser tester account read only; provider writes, refresh-token expiry, and actual agent-adapter execution remain unqualified. Just agents I pick; no agents selected."
|
||||
},
|
||||
{
|
||||
"app": "windsor-ai",
|
||||
"method": "mcp-api-key",
|
||||
"auth": "api_key",
|
||||
"policy": "provider-key",
|
||||
"requestedScopes": [],
|
||||
"capability": "write",
|
||||
"supportedActions": "Bearer key accesses Windsor MCP; actions depend on source connector/account and may include advertising or commerce changes.",
|
||||
"evidence": [
|
||||
"https://mcp.windsor.ai/"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "Use an account key for the intended Windsor.ai organization and connect only source accounts needed for the task. Review write tools and source-specific effects; Paperclip cannot increase key permissions."
|
||||
},
|
||||
{
|
||||
"app": "gsc-wizard",
|
||||
"method": "mcp-oauth",
|
||||
"auth": "oauth",
|
||||
"policy": "explicit",
|
||||
"requestedScopes": [
|
||||
"mcp:read",
|
||||
"mcp:write"
|
||||
],
|
||||
"capability": "write",
|
||||
"supportedActions": "Read GSC/Bing Webmaster/GA4 and related property data, plus provider-advertised tools for URL submission and other write actions; exact property/action needs catalog review.",
|
||||
"evidence": [
|
||||
"https://www.gscwizard.com/claude-search-console-mcp.html",
|
||||
"https://mcp.gscwizard.com/.well-known/oauth-protected-resource",
|
||||
"https://tool.gscwizard.com/.well-known/oauth-authorization-server"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "HTTPS MCP connection with 112 actions. Account metadata, property list and seven-day performance read for the selected Search Console property passed; all 32 writes Off persisted after reload.",
|
||||
"evidenceLimit": "Google identity screen showed name/profile/email only. Worker resumed to already Connected; Google completion and underlying GSC API scopes not independently verified. Paid/trial metadata provenance unknown; no payment action. No writes or agents ran."
|
||||
},
|
||||
{
|
||||
"app": "gsc-wizard",
|
||||
"method": "mcp-api-key",
|
||||
"auth": "api_key",
|
||||
"policy": "provider-key",
|
||||
"requestedScopes": [],
|
||||
"capability": "write",
|
||||
"supportedActions": "Bearer key may be read-only or read/write depending on key creation; docs cite read analytics, URL inspection, and URL submission tools.",
|
||||
"evidence": [
|
||||
"https://www.gscwizard.com/claude-search-console-mcp.html",
|
||||
"https://mcp.gscwizard.com/"
|
||||
],
|
||||
"reviewedAt": "2026-10-08",
|
||||
"liveProof": "not-run",
|
||||
"keyPermissions": "At key creation, choose read-only or read/write according to the approved task. Read/write keys can reach URL-submission and related provider actions; Paperclip cannot increase an existing key’s permissions."
|
||||
},
|
||||
{
|
||||
"app": "airtable",
|
||||
"method": "mcp-pat",
|
||||
"auth": "api_key",
|
||||
"policy": "provider-key",
|
||||
"requestedScopes": [],
|
||||
"capability": "write",
|
||||
"supportedActions": "Read/update records; creation and editing in allowed bases; token scopes, base resources, roles, and organization allowlisting apply.",
|
||||
"evidence": [
|
||||
"https://support.airtable.com/articles/9897799762-using-the-airtable-mcp-server",
|
||||
"https://support.airtable.com/articles/9934989703-creating-personal-access-tokens"
|
||||
],
|
||||
"keyPermissions": "Choose provider scopes and resource bases for intended actions; token and user/base role permissions both restrict access.",
|
||||
"providerDefaultReason": "The exact required scope set depends on tools and selected base resources; do not infer a broader fixed scope list.",
|
||||
"reviewedAt": "2026-10-08",
|
||||
"evidenceLevel": "provider-documentation-reviewed",
|
||||
"liveProof": "not-run"
|
||||
},
|
||||
{
|
||||
"app": "linear",
|
||||
"method": "mcp-api-key",
|
||||
"auth": "api_key",
|
||||
"policy": "provider-key",
|
||||
"requestedScopes": [],
|
||||
"capability": "write",
|
||||
"supportedActions": "Create/update issues, projects, comments and other authorized workspace objects; key can be restricted to Read, Write, Admin, Create issues, Create comments and teams.",
|
||||
"evidence": [
|
||||
"https://linear.app/docs/mcp",
|
||||
"https://linear.app/docs/api-and-webhooks"
|
||||
],
|
||||
"keyPermissions": "Choose intended permissions and teams when creating the personal API key. Read-only is supported with Read permission only.",
|
||||
"reviewedAt": "2026-10-08",
|
||||
"evidenceLevel": "provider-documentation-reviewed",
|
||||
"liveProof": "not-run"
|
||||
},
|
||||
{
|
||||
"app": "make",
|
||||
"method": "mcp-api-token",
|
||||
"auth": "api_key",
|
||||
"policy": "provider-key",
|
||||
"requestedScopes": [],
|
||||
"capability": "write",
|
||||
"supportedActions": "Run selected scenarios as MCP tools; token scope and scenario access control determine availability.",
|
||||
"evidence": [
|
||||
"https://developers.make.com/mcp-server/connect-using-mcp-token",
|
||||
"https://developers.make.com/mcp-server/connect-using-mcp-token/usage-with-cursor"
|
||||
],
|
||||
"keyPermissions": "Provider-issued Make token. Include documented mcp:use and only additional scopes required by selected scenarios; exact scopes are user selected.",
|
||||
"reviewedAt": "2026-10-08",
|
||||
"evidenceLevel": "provider-documentation-reviewed",
|
||||
"liveProof": "not-run"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"retrievedAt": "2026-10-08",
|
||||
"providers": [
|
||||
{
|
||||
"slug": "tavily",
|
||||
"provider": "Tavily",
|
||||
"file": "ui/public/brands/apps/tavily.png",
|
||||
"sourceUrl": "https://www.tavily.com/apple-icon.png?apple-icon.2rsjznomrsf77.png",
|
||||
"linkedFrom": "https://www.tavily.com/",
|
||||
"retrievedAt": "2026-10-08",
|
||||
"sha256": "7f8dd7b74386f30b061ddfe57e58217d6d32f9318e3f95fdf4130db6a22e60dc",
|
||||
"note": "Official Tavily touch icon; retained without modification."
|
||||
},
|
||||
{
|
||||
"slug": "calendly",
|
||||
"provider": "Calendly",
|
||||
"file": "ui/public/brands/apps/calendly.png",
|
||||
"sourceUrl": "https://calendly.com/media/favicon/apple-touch-icon.png",
|
||||
"linkedFrom": "https://calendly.com/",
|
||||
"retrievedAt": "2026-10-08",
|
||||
"sha256": "96a66579d19466967a6326c6d5bab7195fd8f48dace937e84753674282e75246",
|
||||
"note": "Official Calendly touch icon; retained without modification."
|
||||
},
|
||||
{
|
||||
"slug": "exa",
|
||||
"provider": "Exa",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/exa.svg",
|
||||
"sourceUrl": "https://exa.ai/assets/Exa%20Brand%20Assets.zip",
|
||||
"sourceContext": "Exa Brand Assets/Logo/Icons/Icon Square.svg",
|
||||
"sha256": "1bd417f607768b78e848fd49459a028b1e01e45dd26a455d3292dba4f684a290"
|
||||
},
|
||||
{
|
||||
"slug": "firecrawl",
|
||||
"provider": "Firecrawl",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/firecrawl.svg",
|
||||
"sourceUrl": "https://www.firecrawl.dev/brand/firecrawl-wordmark.svg",
|
||||
"sourceContext": "https://www.firecrawl.dev/press-brand",
|
||||
"sha256": "7edcff63dfe51f3129d039110786da574a8610ee0ed5998666f4663f3d744ae9"
|
||||
},
|
||||
{
|
||||
"slug": "parallel-search",
|
||||
"provider": "Parallel Search",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/parallel.svg",
|
||||
"sourceUrl": "https://assets.parallel.ai/dark-parallel-avatar-270.svg",
|
||||
"sourceContext": "https://search.parallel.ai/mcp (MCP initialize icons)",
|
||||
"sha256": "b02498a0d8a1047d95affc7c09e92d54e560469a45610fa4514f17e460cf695a",
|
||||
"darkAsset": "/brands/apps/parallel-dark.svg",
|
||||
"darkSourceUrl": "https://assets.parallel.ai/white-parallel-avatar-270.svg",
|
||||
"darkSha256": "fc7f2c8b12bad544e8428ebd14d64792a6c03444533255983668af3109e676d1"
|
||||
},
|
||||
{
|
||||
"slug": "windsor-ai",
|
||||
"provider": "Windsor.ai",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/windsor-ai.png",
|
||||
"sourceUrl": "https://windsor.ai/wp-content/uploads/2020/02/favicon-310-300x300.png",
|
||||
"sourceContext": "https://windsor.ai/",
|
||||
"sha256": "0fd54d0fe1df834bbf86e1f738d7bdf1a26a89da9fc4e7d3b7c61e2ab2d81c57"
|
||||
},
|
||||
{
|
||||
"slug": "gsc-wizard",
|
||||
"provider": "GSC Wizard",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/gsc-wizard.png",
|
||||
"sourceUrl": "https://www.gscwizard.com/wizard-logo-icon-256_circle.png",
|
||||
"sourceContext": "https://www.gscwizard.com/",
|
||||
"sha256": "001469a9236ced832f82bcea0b72a9a9a7ff26cab3f58473431b9b2be66c5a19"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
{
|
||||
"as_of": "2026-10-08",
|
||||
"scope": "Eleven providers with successful bounded Paperclip browser action tests; seven new catalog definitions and four existing providers.",
|
||||
"live_read_passes": 11,
|
||||
"provider_write_calls": 0,
|
||||
"actual_agent_runs": 0,
|
||||
"providers": [
|
||||
{
|
||||
"provider": "Airtable",
|
||||
"slug": "airtable",
|
||||
"live_read": "passed-account-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.airtable.com/mcp"
|
||||
},
|
||||
{
|
||||
"key": "mcp-pat",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"server_url": "https://mcp.airtable.com/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"grant": "the selected test base only (one base/one workspace); records including delete, comments, fields/tables read/write.",
|
||||
"tool": "Search for an Airtable base",
|
||||
"result": "Bounded query the selected test base returned that base in 0.6s; ID omitted, no record data read.",
|
||||
"catalog_actions": 46,
|
||||
"read_actions": 24,
|
||||
"write_actions": 22,
|
||||
"write_policy": "All 22 write actions Off; persisted after reload",
|
||||
"human_audience": "Just me",
|
||||
"agent_installation_audience": "Any agent company installation; UI did not allow narrowing.",
|
||||
"actual_adapter_run": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Linear",
|
||||
"slug": "linear",
|
||||
"live_read": "passed-account-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.linear.app/mcp"
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"server_url": "https://mcp.linear.app/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"tool": "List teams",
|
||||
"result": "One workspace team returned in 0.3s; IDs omitted.",
|
||||
"catalog_actions": 68,
|
||||
"read_actions": 38,
|
||||
"write_actions": 30,
|
||||
"grant": "User-approved read/write; selected workspace selected.",
|
||||
"agent_installation_audience": "Any agent company installation; UI did not allow narrowing. No actual agent ran.",
|
||||
"human_audience": "Just me",
|
||||
"write_policy": "All write actions Off; persisted after reload",
|
||||
"reload": "persisted",
|
||||
"actions_refresh": "passed",
|
||||
"actual_adapter_run": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Make",
|
||||
"slug": "make",
|
||||
"live_read": "passed-account-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.make.com"
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-token",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"server_url_template": "https://{zone}/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"actor_label": "Connector Tester",
|
||||
"tool": "environment_get",
|
||||
"result": "Organization/team context returned; account values omitted.",
|
||||
"reload": "persisted",
|
||||
"actions_refresh": "passed",
|
||||
"catalog_actions": 133,
|
||||
"write_actions": 59,
|
||||
"write_policy": "Off; retained after reload",
|
||||
"agent_installation_audience": "Company-wide in isolated test company; UI does not permit narrowing while globally installed.",
|
||||
"actual_adapter_run": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Exa",
|
||||
"slug": "exa",
|
||||
"live_read": "passed-public-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "public-search",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"server_url": "https://mcp.exa.ai/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"actor_label": "Connector Tester",
|
||||
"query": "Official Paperclip public project repository",
|
||||
"expected_url": "https://github.com/paperclipai/paperclip",
|
||||
"reload": "persisted",
|
||||
"actions_refresh": "passed",
|
||||
"latency_seconds": 2.0,
|
||||
"tool": "web_search_exa"
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Firecrawl",
|
||||
"slug": "firecrawl",
|
||||
"live_read": "passed-public-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.firecrawl.dev/v2/mcp-oauth"
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"server_url": "https://mcp.firecrawl.dev/v2/mcp"
|
||||
},
|
||||
{
|
||||
"key": "keyless",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"server_url": "https://mcp.firecrawl.dev/v2/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"actor_label": "Connector Tester",
|
||||
"tool": "firecrawl_scrape",
|
||||
"url": "https://paperclip.ing",
|
||||
"expected": "Paperclip public site markdown; title and sourceURL; HTTP 200",
|
||||
"latency_seconds": 0.6,
|
||||
"reload": "persisted",
|
||||
"actions_refresh": "passed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "PostHog",
|
||||
"slug": "posthog",
|
||||
"live_read": "passed-account-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.posthog.com/mcp"
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"server_url": "https://mcp.posthog.com/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"tool": "Get user (@me)",
|
||||
"result": "Authenticated profile returned in 0.7s; identity values omitted.",
|
||||
"catalog_actions": 380,
|
||||
"read_actions": 373,
|
||||
"write_actions": 7,
|
||||
"grant": "Provider read-only preset; active Paperclip project only.",
|
||||
"agent_installation_audience": "Any agent company installation; UI did not allow narrowing. No actual agent ran.",
|
||||
"human_audience": "Just me",
|
||||
"write_policy": "All write actions Off; persisted after reload",
|
||||
"reload": "persisted",
|
||||
"actions_refresh": "passed",
|
||||
"actual_adapter_run": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Tavily",
|
||||
"slug": "tavily",
|
||||
"live_read": "passed-account-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.tavily.com/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"account_identity": "the verified user verified on provider Settings",
|
||||
"catalog_actions": 5,
|
||||
"read_actions": 5,
|
||||
"write_actions": 0,
|
||||
"actions_refresh": "passed",
|
||||
"reload": "persisted",
|
||||
"actual_adapter_run": false,
|
||||
"read_failures": 2,
|
||||
"http_status": 400,
|
||||
"tool": "Search",
|
||||
"result": "Query-only public web search returned five rows in 1.7s, including official paperclipai/paperclip GitHub result after proactive MCP initialization.",
|
||||
"human_audience": "Just me",
|
||||
"agent_installation_audience": "Any agent company installation; UI did not allow narrowing. No actual agent ran.",
|
||||
"session_initialization": "Curated true default; refreshed actions before one new call.",
|
||||
"diagnostic_limit": "Earlier two calls returned HTTP 400; later pass supports but does not prove the missing-session hypothesis."
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Parallel Search",
|
||||
"slug": "parallel-search",
|
||||
"live_read": "passed-public-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "public-search",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"server_url": "https://search.parallel.ai/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"actor_label": "Connector Tester",
|
||||
"query": "Official Paperclip public project repository",
|
||||
"expected_url": "https://github.com/paperclipai/paperclip",
|
||||
"reload": "persisted",
|
||||
"actions_refresh": "passed",
|
||||
"latency_seconds": 0.8,
|
||||
"tool": "web_search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Calendly",
|
||||
"slug": "calendly",
|
||||
"live_read": "passed-account-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.calendly.com"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"grant": "Same HTTPS Paperclip client, Calendly MCP resource, mcp:scheduling:read and mcp:scheduling:write",
|
||||
"tool": "lookup_current_user",
|
||||
"result": "Succeeded in 2.5s; profile details omitted",
|
||||
"catalog_actions": 36,
|
||||
"read_actions": 25,
|
||||
"write_actions": 11,
|
||||
"write_policy": "All 11 write/destructive actions Off; persisted after reload",
|
||||
"human_audience": "Just me",
|
||||
"agent_installation_audience": "Any agent; narrowing disabled for installation",
|
||||
"authorization_retry": "Initial generic failure; same grant succeeded on retry",
|
||||
"actual_adapter_run": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Windsor.ai",
|
||||
"slug": "windsor-ai",
|
||||
"live_read": "passed-account-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.windsor.ai/"
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"server_url": "https://mcp.windsor.ai/"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"tool": "Get Fields (searchconsole), Get Data",
|
||||
"result": "54 field definitions; bounded last_7d request returned 5 daily rows dated 2026-10-01 through 2026-10-05 in 1.5s; metric values omitted.",
|
||||
"catalog_actions": 19,
|
||||
"read_actions": 14,
|
||||
"write_actions": 5,
|
||||
"grant": "User-approved create/delete/offline_access MCP scopes; separately approved read-only Search Console source, the selected Search Console property only.",
|
||||
"agent_installation_audience": "Just agents I pick; no agents selected.",
|
||||
"human_audience": "Just me",
|
||||
"write_policy": "All write actions Off; persisted after reload",
|
||||
"reload": "persisted",
|
||||
"actions_refresh": "passed",
|
||||
"actual_adapter_run": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "GSC Wizard",
|
||||
"slug": "gsc-wizard",
|
||||
"live_read": "passed-source-tool-test",
|
||||
"live_write": "not-run",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"server_url": "https://mcp.gscwizard.com/mcp"
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"server_url": "https://mcp.gscwizard.com/mcp"
|
||||
}
|
||||
],
|
||||
"proof": {
|
||||
"surface": "Paperclip browser action Test dialog",
|
||||
"mcp_scopes": [
|
||||
"mcp:read",
|
||||
"mcp:write"
|
||||
],
|
||||
"google_identity_scopes_observed": "Name/profile picture/email only",
|
||||
"google_data_scopes": "Not independently verified; MCP scopes are not Google scopes",
|
||||
"tools": [
|
||||
"Get account info",
|
||||
"List Search Console properties",
|
||||
"Get site performance summary"
|
||||
],
|
||||
"result": "Account read succeeded in 0.6s; property list returned only the selected Search Console property, readable/activated/siteOwner; bounded seven-day performance summary succeeded in 1.1s through settled 2026-10-06. Metrics omitted.",
|
||||
"catalog_actions": 112,
|
||||
"write_actions": 32,
|
||||
"write_policy": "All 32 write/destructive actions Off; persisted after reload",
|
||||
"human_audience": "Just me",
|
||||
"agent_installation_audience": "Any agent; narrowing disabled for installation",
|
||||
"provider_access_metadata": "level paid with trial end 7 days after connectedAt; provisioning provenance unknown; no payment action",
|
||||
"actual_adapter_run": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"limits": [
|
||||
"Live proof covers the recorded auth method only. Other documented methods are not independently live-qualified.",
|
||||
"Provider writes, expiry refresh, and real agent-adapter sessions are unqualified.",
|
||||
"GSC Wizard underlying Google data scopes and identity-flow completion provenance were not independently verified; no purchase was performed.",
|
||||
"Tests ran against isolated qualification state; credentials, codes, provider response data, and private account identifiers are omitted."
|
||||
],
|
||||
"repository_checks": "Full repository typecheck and build passed locally. Focused catalog, generator, OAuth/session/callback, and UI checks were run; latest CI results and any local-suite limitations are recorded in https://github.com/paperclipai/paperclip/pull/15621."
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Verified MCP connectors — 2026-10-08
|
||||
|
||||
This change includes eleven providers with successful bounded reads through
|
||||
Paperclip's real browser action tester. Seven are new catalog entries: Calendly,
|
||||
Exa, Firecrawl, GSC Wizard, Parallel Search, Tavily, and Windsor.ai. Airtable,
|
||||
Linear, Make, and PostHog already have catalog entries. AgentMail uses the
|
||||
existing full integration; this change adds no AgentMail method or channel.
|
||||
|
||||
| Provider | Live read | Catalog and reload proof | Write policy during qualification |
|
||||
|---|---|---|---|
|
||||
| Airtable | Bounded base-name search, 0.6s | 46 actions; persisted after reload | All 22 writes Off; grant limited to one test base |
|
||||
| Calendly | Current user, 2.5s | 36 actions; persisted after reload | All 11 writes Off |
|
||||
| Exa | Public search found the official repository | Reload and refresh passed | No write call |
|
||||
| Firecrawl | Public site scrape, HTTP 200, 0.6s | Connected with three actions; reload/refresh passed | No write call |
|
||||
| GSC Wizard | Account, property list, seven-day performance summary, 1.1s | 112 actions; persisted after reload | All 32 writes Off; one selected property read |
|
||||
| Linear | List teams, 0.3s | 68 actions; persisted after reload | All 30 writes Off |
|
||||
| Make | Organization/team metadata | 133 actions; persisted after reload | All 59 writes Off |
|
||||
| Parallel Search | Public search found the official repository | Reload and refresh passed | No write call |
|
||||
| PostHog | Current user, 0.7s | 380 actions; persisted after reload | All seven writes Off; provider read-only preset |
|
||||
| Tavily | Public search returned five results, 1.7s | Five read actions; persisted after reload | No write actions discovered |
|
||||
| Windsor.ai | Source fields and bounded five-day Search Console data, 1.5s | 19 actions; persisted after reload | All five writes Off; Google source read-only |
|
||||
|
||||
The catalog keeps documented read/write capabilities. The test policy above
|
||||
records the qualification settings; it does not change normal catalog defaults
|
||||
or prove provider write behavior. Alternative documented auth methods were not
|
||||
independently live-tested. No provider writes or actual agent sessions ran.
|
||||
Several test-company installations report Any agent because scoped-agent
|
||||
selection is unavailable; no agent runtime was started. Windsor used no agents.
|
||||
|
||||
Airtable requires reviewed DCR registration because its advertised CIMD flow
|
||||
failed live. Calendly requires a compatible client name. Tavily's search passed
|
||||
after proactive MCP initialization; two earlier HTTP 400 failures remain
|
||||
undiagnosed. Calls are never automatically replayed. Firecrawl exposed an
|
||||
optional nested-object form validation issue, which is fixed in the shared form.
|
||||
The setup UI now displays the callback supplied by the server for HTTPS hosts.
|
||||
|
||||
GSC Wizard's resumed tab was already Connected before the worker continued.
|
||||
The Google identity screen showed name/profile/email only; underlying Google
|
||||
Search Console scopes were not independently verified. Provider metadata reports
|
||||
paid/trial access of unknown provisioning provenance; no payment was made.
|
||||
Airtable's worker submitted an expanded grant before revised action-time review.
|
||||
The exact access and error were disclosed, and the user directed continuation
|
||||
with the requested one-base grant. This procedural error is retained in the
|
||||
qualification record; no subsequent grant expansion or provider write occurred.
|
||||
|
||||
[Machine-readable proof](verified-mcp-qualification-2026-10-08.json) and
|
||||
[brand provenance](verified-mcp-brand-provenance-2026-10-08.json) record the scope.
|
||||
Private account identifiers, credentials, OAuth state/codes, and returned metrics
|
||||
are omitted. The original research and unfinished providers are preserved on a
|
||||
separate local checkpoint branch and are outside this PR.
|
||||
|
||||
Repository validation is recorded in the PR description after checks complete.
|
||||
Browser proof establishes these reads, not expiry refresh, provider writes, or
|
||||
execution through an actual agent adapter.
|
||||
|
||||
## PR UI retest
|
||||
|
||||
The PR checkout UI was previewed on a separate loopback port against the existing, authorized local qualification server. This retest covers frontend behavior; it is not a new live proof of the revised server OAuth recovery path. Calendly appeared connected in the catalog and its setup screen displayed before authorization. The Firecrawl scrape tester succeeded after expanding More options, entering only the public `https://paperclip.ing` URL, and leaving nested optional inputs untouched. No new provider authorization or agent heartbeat was started.
|
||||
|
||||
- [Calendly catalog](verified-mcp-qualification-2026-10-08/calendly-catalog.jpg)
|
||||
- [Calendly setup before authorization](verified-mcp-qualification-2026-10-08/calendly-setup.jpg)
|
||||
- [Firecrawl successful read](verified-mcp-qualification-2026-10-08/firecrawl-read.jpg)
|
||||
|
||||
OAuth recovery now refreshes credentials after an upstream 401 and returns `oauth_refreshed_retry_required` without replaying the tool call. The caller must retry explicitly; that invocation creates a session with refreshed credentials when required. The regression test verifies single dispatch on each invocation and fresh session/protocol headers on explicit retry. This behavior also applies to the existing managed and Vercel OAuth gateway paths.
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -72,23 +72,30 @@ import a70 from "./app-definitions/neon.json" with { type: "json" };
|
||||
import a71 from "./app-definitions/superagent.json" with { type: "json" };
|
||||
import a72 from "./app-definitions/telem.json" with { type: "json" };
|
||||
import a73 from "./app-definitions/gauge.json" with { type: "json" };
|
||||
import a74 from "./app-definitions/gmail.json" with { type: "json" };
|
||||
import a75 from "./app-definitions/google-drive.json" with { type: "json" };
|
||||
import a76 from "./app-definitions/google-docs.json" with { type: "json" };
|
||||
import a77 from "./app-definitions/google-sheets.json" with { type: "json" };
|
||||
import a78 from "./app-definitions/google-slides.json" with { type: "json" };
|
||||
import a79 from "./app-definitions/google-calendar.json" with { type: "json" };
|
||||
import a80 from "./app-definitions/google-chat.json" with { type: "json" };
|
||||
import a81 from "./app-definitions/google-people.json" with { type: "json" };
|
||||
import a82 from "./app-definitions/google-workspace-search.json" with { type: "json" };
|
||||
import a83 from "./app-definitions/openai.json" with { type: "json" };
|
||||
import a84 from "./app-definitions/openrouter.json" with { type: "json" };
|
||||
import a85 from "./app-definitions/xai.json" with { type: "json" };
|
||||
import a86 from "./app-definitions/google.json" with { type: "json" };
|
||||
import a87 from "./app-definitions/bedrock.json" with { type: "json" };
|
||||
import a88 from "./app-definitions/responses-api.json" with { type: "json" };
|
||||
import a89 from "./app-definitions/messages-api.json" with { type: "json" };
|
||||
import a90 from "./app-definitions/chat-completions-api.json" with { type: "json" };
|
||||
import a91 from "./app-definitions/local.json" with { type: "json" };
|
||||
import a74 from "./app-definitions/tavily.json" with { type: "json" };
|
||||
import a75 from "./app-definitions/calendly.json" with { type: "json" };
|
||||
import a76 from "./app-definitions/exa.json" with { type: "json" };
|
||||
import a77 from "./app-definitions/firecrawl.json" with { type: "json" };
|
||||
import a78 from "./app-definitions/parallel-search.json" with { type: "json" };
|
||||
import a79 from "./app-definitions/windsor-ai.json" with { type: "json" };
|
||||
import a80 from "./app-definitions/gsc-wizard.json" with { type: "json" };
|
||||
import a81 from "./app-definitions/gmail.json" with { type: "json" };
|
||||
import a82 from "./app-definitions/google-drive.json" with { type: "json" };
|
||||
import a83 from "./app-definitions/google-docs.json" with { type: "json" };
|
||||
import a84 from "./app-definitions/google-sheets.json" with { type: "json" };
|
||||
import a85 from "./app-definitions/google-slides.json" with { type: "json" };
|
||||
import a86 from "./app-definitions/google-calendar.json" with { type: "json" };
|
||||
import a87 from "./app-definitions/google-chat.json" with { type: "json" };
|
||||
import a88 from "./app-definitions/google-people.json" with { type: "json" };
|
||||
import a89 from "./app-definitions/google-workspace-search.json" with { type: "json" };
|
||||
import a90 from "./app-definitions/openai.json" with { type: "json" };
|
||||
import a91 from "./app-definitions/openrouter.json" with { type: "json" };
|
||||
import a92 from "./app-definitions/xai.json" with { type: "json" };
|
||||
import a93 from "./app-definitions/google.json" with { type: "json" };
|
||||
import a94 from "./app-definitions/bedrock.json" with { type: "json" };
|
||||
import a95 from "./app-definitions/responses-api.json" with { type: "json" };
|
||||
import a96 from "./app-definitions/messages-api.json" with { type: "json" };
|
||||
import a97 from "./app-definitions/chat-completions-api.json" with { type: "json" };
|
||||
import a98 from "./app-definitions/local.json" with { type: "json" };
|
||||
import type { AppDefinition } from "./types/app-definition.js";
|
||||
export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80,a81,a82,a83,a84,a85,a86,a87,a88,a89,a90,a91] as AppDefinition[];
|
||||
export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80,a81,a82,a83,a84,a85,a86,a87,a88,a89,a90,a91,a92,a93,a94,a95,a96,a97,a98] as AppDefinition[];
|
||||
@@ -273,7 +273,7 @@ describe("AppDefinition catalog", () => {
|
||||
"google-workspace-search",
|
||||
]),
|
||||
);
|
||||
expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(53);
|
||||
expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(60);
|
||||
expect(BLOCKED_MCP_PROVIDERS.map((entry) => entry.slug)).toEqual([
|
||||
"g2",
|
||||
"vercel",
|
||||
@@ -437,15 +437,15 @@ describe("AppDefinition catalog", () => {
|
||||
expect(channel("slack")?.guidanceMd).toContain("reactions");
|
||||
expect(channel("slack")?.guidanceMd).toContain("direct messages");
|
||||
});
|
||||
it("keeps a complete, unique, dated evidence ledger for all 56 researched MCP providers", () => {
|
||||
it("keeps a complete, unique, dated evidence ledger for all 63 researched MCP providers", () => {
|
||||
// Ledger-wide date reflects the last full re-verification (2026-08-26);
|
||||
// later provider additions carry their own research evidence, but
|
||||
// bumping the shared date would overstate freshness for the other providers.
|
||||
expect(SELF_SERVE_MCP_RESEARCH.verifiedAt).toBe("2026-08-26");
|
||||
expect(SELF_SERVE_MCP_RESEARCH.entries).toHaveLength(56);
|
||||
expect(SELF_SERVE_MCP_RESEARCH.entries).toHaveLength(63);
|
||||
expect(
|
||||
new Set(SELF_SERVE_MCP_RESEARCH.entries.map((entry) => entry.slug)),
|
||||
).toHaveProperty("size", 56);
|
||||
).toHaveProperty("size", 63);
|
||||
for (const entry of SELF_SERVE_MCP_RESEARCH.entries) {
|
||||
expect(new URL(entry.docsUrl).protocol).toBe("https:");
|
||||
expect(new URL(entry.serverUrl).protocol).toBe("https:");
|
||||
@@ -539,6 +539,30 @@ describe("AppDefinition catalog", () => {
|
||||
expect(credentialField).not.toHaveProperty("defaultValue");
|
||||
});
|
||||
|
||||
it("exposes Make with its reviewed browser OAuth and MCP token alternatives", () => {
|
||||
const app = APP_STORE_DEFINITIONS.find((entry) => entry.slug === "make");
|
||||
expect(app).toBeTruthy();
|
||||
expect(APP_STORE_HIDDEN_SLUGS.has("make")).toBe(false);
|
||||
expect(CONNECTABLE_APP_DEFINITIONS.some((entry) => entry.slug === "make")).toBe(true);
|
||||
expect(getAvailableConnectionMethods(app!).map((method) => method.key)).toEqual([
|
||||
"mcp-oauth",
|
||||
"mcp-api-token",
|
||||
]);
|
||||
expect(app!.methods[0]).toMatchObject({
|
||||
transport: "mcp_remote",
|
||||
auth: "oauth",
|
||||
ownershipModes: ["dcr"],
|
||||
defaults: { serverUrl: "https://mcp.make.com" },
|
||||
});
|
||||
expect(app!.methods[1]).toMatchObject({
|
||||
transport: "mcp_remote",
|
||||
auth: "api_key",
|
||||
ownershipModes: ["customer"],
|
||||
defaults: { serverUrlTemplate: "https://{zone}/mcp" },
|
||||
keyPlacement: { location: "header", name: "Authorization", prefix: "Bearer " },
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the reviewed current endpoints and configuration modes", () => {
|
||||
const method = (slug: string, key?: string) =>
|
||||
APP_DEFINITIONS.find((app) => app.slug === slug)?.methods.find(
|
||||
@@ -834,7 +858,6 @@ describe("AppDefinition catalog", () => {
|
||||
"embat",
|
||||
"kernel",
|
||||
"local-falcon",
|
||||
"make",
|
||||
"manufact",
|
||||
"oreilly",
|
||||
"planetscale",
|
||||
@@ -845,7 +868,7 @@ describe("AppDefinition catalog", () => {
|
||||
"ticktick",
|
||||
"xero",
|
||||
]);
|
||||
expect(APP_STORE_DEFINITIONS).toHaveLength(69);
|
||||
expect(APP_STORE_DEFINITIONS).toHaveLength(77);
|
||||
const connectableSlugs = new Set(
|
||||
CONNECTABLE_APP_DEFINITIONS.map((entry) => entry.slug),
|
||||
);
|
||||
@@ -1244,6 +1267,7 @@ describe("AppDefinition catalog", () => {
|
||||
expect(required).toEqual([
|
||||
"clickhouse:mcp-oauth:serviceId",
|
||||
"honcho:mcp-api-key:workspaceId",
|
||||
"make:mcp-api-token:zone",
|
||||
"shopify:storefront-mcp:storeDomain",
|
||||
"shopify:ucp-commerce:storeDomain",
|
||||
"supabase:mcp-api-key:projectRef",
|
||||
@@ -1366,3 +1390,21 @@ it("validates native pool catalog entries without inventing an authentication me
|
||||
expect(appDefinitionSchema.safeParse({ ...entry, methods: APP_STORE_DEFINITIONS[0]!.methods }).success).toBe(false);
|
||||
expect(appDefinitionSchema.safeParse({ ...entry, categories: ["developer"] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("limits explicit DCR registration preference to remote MCP OAuth methods with DCR ownership", () => {
|
||||
const entry = structuredClone(APP_DEFINITIONS.find((app) => app.slug === "airtable")!);
|
||||
entry.methods[0]!.oauthClientRegistration = "dcr";
|
||||
expect(appDefinitionSchema.parse(entry).methods[0]!.oauthClientRegistration).toBe("dcr");
|
||||
|
||||
const customerOwned = structuredClone(entry);
|
||||
customerOwned.methods[0]!.ownershipModes = ["customer"];
|
||||
expect(appDefinitionSchema.safeParse(customerOwned).success).toBe(false);
|
||||
|
||||
const apiKey = structuredClone(entry);
|
||||
apiKey.methods[0]!.auth = "api_key";
|
||||
expect(appDefinitionSchema.safeParse(apiKey).success).toBe(false);
|
||||
|
||||
const nonMcp = structuredClone(entry);
|
||||
nonMcp.methods[0]!.transport = "rest_api";
|
||||
expect(appDefinitionSchema.safeParse(nonMcp).success).toBe(false);
|
||||
});
|
||||
@@ -60,7 +60,6 @@ export const APP_STORE_HIDDEN_SLUGS = new Set([
|
||||
"embat",
|
||||
"kernel",
|
||||
"local-falcon",
|
||||
"make",
|
||||
"manufact",
|
||||
"oreilly",
|
||||
"planetscale",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"oauthClientRegistration": "dcr",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
@@ -45,6 +46,44 @@
|
||||
"warnings": [
|
||||
"An Airtable account; enterprise administrators may need to allowlist the client."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-pat",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a personal access token when browser sign-in is not suitable.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.airtable.com/mcp"
|
||||
},
|
||||
"guidanceMd": "Create an Airtable personal access token with the scopes and base resources required for the actions you intend to allow. MCP calls inherit your Airtable role and base permissions; organization allowlisting may still apply.",
|
||||
"riskTier": "S3",
|
||||
"label": "Use a personal access token",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Airtable personal access token",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "pat...",
|
||||
"secret": true,
|
||||
"helperMd": "Choose provider scopes and resource bases for intended actions; token and user/base role permissions both restrict access."
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"docs": "https://support.airtable.com/articles/9897799762-using-the-airtable-mcp-server",
|
||||
"keys": "https://airtable.com/create/tokens"
|
||||
},
|
||||
"warnings": [
|
||||
"Use a disposable or development base for initial write qualification. Enterprise admins may need to allowlist MCP access."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "calendly",
|
||||
"name": "Calendly",
|
||||
"description": "Review Calendly event types and availability, and use supported scheduling actions.",
|
||||
"categories": [
|
||||
"productivity"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/calendly.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.calendly.com/*"
|
||||
],
|
||||
"docsUrl": "https://developer.calendly.com/docs/mcp/calendly-mcp-server",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with Calendly",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Connect the provider-hosted Calendly MCP server with browser sign-in.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.calendly.com",
|
||||
"scopesHint": [
|
||||
"mcp:scheduling:read",
|
||||
"mcp:scheduling:write"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Check the connected account, event type, timezone, availability, and attendee details before using a scheduling write. Use a test scheduling artifact only when the discovered tool explicitly supports it.",
|
||||
"warnings": [
|
||||
"Calendly MCP requires DCR and OAuth 2.1 authorization-code with PKCE S256. It does not support personal access tokens or manually provisioned static OAuth clients.",
|
||||
"The provider advertises both scheduling read and write scopes for full access; verify the live tool catalog before enabling booking or link creation."
|
||||
],
|
||||
"consoleLinks": {
|
||||
"docs": "https://developer.calendly.com/docs/mcp/calendly-mcp-server"
|
||||
},
|
||||
"riskTier": "S3"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "exa",
|
||||
"name": "Exa",
|
||||
"description": "Search the public web and read source pages with Exa.",
|
||||
"categories": [
|
||||
"data"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/exa.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.exa.ai/*"
|
||||
],
|
||||
"docsUrl": "https://exa.ai/docs/get-started/exa-mcp",
|
||||
"methods": [
|
||||
{
|
||||
"key": "public-search",
|
||||
"label": "Use public Exa search",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use Exa’s public, keyless search MCP.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.exa.ai/mcp"
|
||||
},
|
||||
"guidanceMd": "The public MCP route provides web search and page fetching. Keep citations with results. Account-authenticated Agent and advanced tools are not included in this method.",
|
||||
"riskTier": "S2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "firecrawl",
|
||||
"name": "Firecrawl",
|
||||
"description": "Search, scrape, and parse public web pages with Firecrawl.",
|
||||
"categories": [
|
||||
"data"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/firecrawl.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.firecrawl.dev/*"
|
||||
],
|
||||
"docsUrl": "https://docs.firecrawl.dev/mcp-server",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with Firecrawl",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Use interactive Firecrawl account authorization for the full hosted MCP tool surface.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.firecrawl.dev/v2/mcp-oauth",
|
||||
"scopesHint": [
|
||||
"firecrawl:global",
|
||||
"offline_access"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Sign in and choose the Firecrawl team whose usage should apply. This route exposes the account-authorized tool surface; review paid jobs and action policies before execution.",
|
||||
"riskTier": "S4",
|
||||
"warnings": [
|
||||
"Account authorization exposes Firecrawl’s broader hosted MCP surface, including job-based research tools that may use account credits.",
|
||||
"Crawls and research jobs can use provider credits; review their tool actions and limits before execution."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"label": "Use a Firecrawl API key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a customer-created Firecrawl API key in a Bearer header for unattended or higher-limit access.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.firecrawl.dev/v2/mcp"
|
||||
},
|
||||
"guidanceMd": "Create a Firecrawl API key for the intended team, then connect it with a Bearer header. The authenticated server may expose additional job tools and spend team credits.",
|
||||
"riskTier": "S4",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Use a Firecrawl API key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Enter provider credential",
|
||||
"secret": true,
|
||||
"helperMd": "Create an API key for the intended Firecrawl team with access to the search, scrape, parse, and any job tools you intend to use. Jobs can consume credits; Paperclip cannot increase an existing key’s permissions."
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"keys": "https://docs.firecrawl.dev/mcp-server",
|
||||
"docs": "https://docs.firecrawl.dev/mcp-server"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "keyless",
|
||||
"label": "Try keyless access",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use the rate-limited keyless public web-search route without a Firecrawl credential.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.firecrawl.dev/v2/mcp"
|
||||
},
|
||||
"guidanceMd": "The keyless route currently exposes Search, Scrape, and Parse. Keep requests bounded; these calls can consume the provider’s keyless allowance.",
|
||||
"riskTier": "S2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "gsc-wizard",
|
||||
"name": "GSC Wizard",
|
||||
"description": "Read Google Search Console, Bing Webmaster, and analytics data for connected properties.",
|
||||
"categories": [
|
||||
"analytics"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/gsc-wizard.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.gscwizard.com/*"
|
||||
],
|
||||
"docsUrl": "https://www.gscwizard.com/claude-search-console-mcp.html",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with GSC Wizard",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Use remote OAuth authorization for the GSC Wizard MCP.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.gscwizard.com/mcp",
|
||||
"scopesHint": [
|
||||
"mcp:read",
|
||||
"mcp:write"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Connect the GSC Wizard account and its authorized properties. Check site identity and date bounds before querying; review any URL submission or property-changing action separately.",
|
||||
"riskTier": "S3",
|
||||
"warnings": [
|
||||
"Some tools may submit URLs or otherwise change external indexing state; review action details before use.",
|
||||
"GSC Wizard connects multiple search and analytics providers; confirm the authorized property and action policy for each tool."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"label": "Use a GSC Wizard MCP key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a customer-created GSC Wizard MCP key in a Bearer header.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.gscwizard.com/mcp"
|
||||
},
|
||||
"guidanceMd": "Create a key for the intended account and select the appropriate read or write permission during key creation. Verify each connected property before querying; review URL submission actions separately.",
|
||||
"riskTier": "S3",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Use a GSC Wizard MCP key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Enter provider credential",
|
||||
"secret": true,
|
||||
"helperMd": "At key creation, choose read-only or read/write according to the approved task. Read/write keys can reach URL-submission and related provider actions; Paperclip cannot increase an existing key’s permissions."
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"keys": "https://www.gscwizard.com/claude-search-console-mcp.html",
|
||||
"docs": "https://www.gscwizard.com/claude-search-console-mcp.html"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"write"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Connect Linear for issues and projects. Paperclip registers its own OAuth client with Linear's MCP server, so no developer-console setup is needed.",
|
||||
"guidanceMd": "Connect Linear for issues and projects. Paperclip registers its own OAuth client with Linear MCP; no developer-console setup is needed.",
|
||||
"riskTier": "S2",
|
||||
"requiredResourceFilters": [
|
||||
"workspace",
|
||||
@@ -57,7 +57,50 @@
|
||||
"prefix": "Bearer "
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": "Sign in with Linear",
|
||||
"consoleLinks": {
|
||||
"docs": "https://linear.app/docs/mcp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a Linear API key when browser sign-in is not suitable.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.linear.app/mcp"
|
||||
},
|
||||
"guidanceMd": "Create a Linear API key with the permissions needed for the actions you intend to allow. A key restricted to Read is supported for read-only use; workspace and role access still apply.",
|
||||
"riskTier": "S2",
|
||||
"label": "Use an API key",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Linear API key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"secret": true,
|
||||
"helperMd": "Choose intended permissions and teams when creating the personal API key. Read-only is supported with Read permission only.",
|
||||
"placeholder": "Paste your Linear API key"
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"docs": "https://linear.app/docs/api-and-webhooks"
|
||||
},
|
||||
"warnings": [
|
||||
"Linear’s standard MCP endpoint exposes read and write tools; review the discovered catalog and Paperclip action policies."
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"docsUrl": "https://linear.app/docs/mcp",
|
||||
"redirectConstraints": "https-or-loopback-http"
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
"darkLogoUrl": "/brands/apps/make-dark.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.make.com/*"
|
||||
"https://mcp.make.com/*",
|
||||
"https://*.make.com/mcp"
|
||||
],
|
||||
"docsUrl": "https://developers.make.com/mcp-server",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
@@ -37,6 +38,57 @@
|
||||
"warnings": [
|
||||
"A Make account and access to the scenarios exposed to MCP."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-token",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a Make MCP token when browser sign-in is not suitable.",
|
||||
"defaults": {
|
||||
"serverUrlTemplate": "https://{zone}/mcp"
|
||||
},
|
||||
"guidanceMd": "Create a Make API token with the mcp:use scope and any additional scopes required by the selected scenarios. Enter the zone host from your Make organization. The token is sent as a Bearer header and remains a secret.",
|
||||
"riskTier": "S3",
|
||||
"label": "Use an MCP token",
|
||||
"tenantFields": [
|
||||
{
|
||||
"key": "zone",
|
||||
"label": "Make zone host",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "eu2.make.com",
|
||||
"helperMd": "Enter the zone host for your Make organization exactly as shown in the Make MCP client setup instructions. The server URL is https://<zone>/mcp.",
|
||||
"validation": {
|
||||
"pattern": "^[A-Za-z0-9-]+\\.make\\.com$",
|
||||
"maxLength": 253
|
||||
}
|
||||
}
|
||||
],
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Make MCP token",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Paste your MCP token",
|
||||
"secret": true,
|
||||
"helperMd": "Provider-issued Make token. Include documented mcp:use and only additional scopes required by selected scenarios; exact scopes are user selected."
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"docs": "https://developers.make.com/mcp-server/connect-using-mcp-token"
|
||||
},
|
||||
"warnings": [
|
||||
"A token is a secret; do not place it in the server URL. Scenario availability depends on token scopes and scenario access controls."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "parallel-search",
|
||||
"name": "Parallel Search",
|
||||
"description": "Search the public web and extract source pages with Parallel.",
|
||||
"categories": [
|
||||
"data"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/parallel.svg",
|
||||
"darkLogoUrl": "/brands/apps/parallel-dark.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://search.parallel.ai/*"
|
||||
],
|
||||
"docsUrl": "https://docs.parallel.ai/integrations/mcp/search-mcp",
|
||||
"methods": [
|
||||
{
|
||||
"key": "public-search",
|
||||
"label": "Use public Parallel Search",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use Parallel Search’s public, keyless MCP route.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://search.parallel.ai/mcp"
|
||||
},
|
||||
"guidanceMd": "The public route exposes web_search and web_fetch. Keep source URLs, excerpts, and retrieval context in the result; this is separate from Parallel Task MCP.",
|
||||
"riskTier": "S2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "tavily",
|
||||
"name": "Tavily",
|
||||
"description": "Search the public web and extract page content with Tavily’s hosted MCP server.",
|
||||
"categories": [
|
||||
"analytics"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/tavily.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.tavily.com/*"
|
||||
],
|
||||
"docsUrl": "https://docs.tavily.com/documentation/mcp",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with Tavily",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Connect the provider-hosted Tavily MCP server with browser sign-in.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.tavily.com/mcp",
|
||||
"scopesHint": [
|
||||
"openid",
|
||||
"offline_access"
|
||||
],
|
||||
"mcpSessionRequired": true
|
||||
},
|
||||
"guidanceMd": "Browser sign-in requires a public HTTPS Paperclip callback; local HTTP callbacks are rejected by Tavily. Use Tavily for bounded public research. Set focused queries and relevant domains, cite the source URLs, and distinguish retrieved facts from inference. Research does not authorize contacting a company or submitting forms.",
|
||||
"warnings": [
|
||||
"Tavily OAuth selects the account’s `mcp_auth_default` key when configured; otherwise it may use the personal default or first available key. Review the chosen account and usage allowance.",
|
||||
"Do not use the documented API-key-in-query alternative; OAuth keeps the key out of the server URL."
|
||||
],
|
||||
"consoleLinks": {
|
||||
"docs": "https://docs.tavily.com/documentation/mcp"
|
||||
},
|
||||
"riskTier": "S2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "windsor-ai",
|
||||
"name": "Windsor.ai",
|
||||
"description": "Query connected marketing, analytics, CRM, and warehouse data through Windsor.ai.",
|
||||
"categories": [
|
||||
"analytics"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/windsor-ai.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.windsor.ai/*"
|
||||
],
|
||||
"docsUrl": "https://mcp.windsor.ai/",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with Windsor.ai",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Use browser OAuth for Windsor.ai’s hosted data MCP.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.windsor.ai/",
|
||||
"scopesHint": [
|
||||
"create",
|
||||
"delete",
|
||||
"offline_access"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Browser sign-in requires a public HTTPS Paperclip callback; local HTTP callbacks are rejected by Windsor.ai. Connect Windsor.ai, then verify which advertising, analytics, CRM, or warehouse sources are linked. Keep source account and metric definitions with results. Review any action that changes campaigns, budgets, posts, or listings.",
|
||||
"riskTier": "S4",
|
||||
"warnings": [
|
||||
"Windsor.ai requires the provider-level delete scope to establish the MCP session, including for read-only use. This scope may expose destructive actions; do not approve it unless those provider permissions are acceptable.",
|
||||
"The server includes actions across connected marketing platforms. A source connection does not imply authorization to modify budgets or publish content.",
|
||||
"The server exposes write actions across some connected ad and commerce platforms."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"label": "Use a Windsor.ai API key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a customer-created Windsor.ai API key as a Bearer credential.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.windsor.ai/"
|
||||
},
|
||||
"guidanceMd": "Create a Windsor.ai key for the intended account. Verify connected source accounts and action availability before use; broad tool exposure may include advertising changes.",
|
||||
"riskTier": "S4",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Use a Windsor.ai API key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Enter provider credential",
|
||||
"secret": true,
|
||||
"helperMd": "Use an account key for the intended Windsor.ai organization and connect only source accounts needed for the task. Review write tools and source-specific effects; Paperclip cannot increase key permissions."
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"keys": "https://mcp.windsor.ai/",
|
||||
"docs": "https://mcp.windsor.ai/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -52,6 +52,7 @@ describe("generic connection instructions", () => {
|
||||
"packages/shared/src/self-serve-mcp-research.json",
|
||||
"doc/connections/tool-method-permission-reviews.json",
|
||||
"packages/shared/src/app-definitions",
|
||||
"scripts/app-definition-overrides",
|
||||
]) {
|
||||
mkdirSync(dirname(join(fixture, source)), { recursive: true });
|
||||
cpSync(join(root, source), join(fixture, source), { recursive: true });
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import { APP_DEFINITIONS } from "./app-definitions.generated.js";
|
||||
import { appDefinitionSchema } from "./validators/app-definition.js";
|
||||
|
||||
const appBySlug = (slug: string) => {
|
||||
const app = APP_DEFINITIONS.find((candidate) => candidate.slug === slug);
|
||||
expect(app, "expected app definition for " + slug).toBeDefined();
|
||||
return app!;
|
||||
};
|
||||
|
||||
const methodFor = (slug: string, key: string) => {
|
||||
const method = appBySlug(slug).methods.find((candidate) => candidate.key === key);
|
||||
expect(method, "expected method " + slug + "/" + key).toBeDefined();
|
||||
return method!;
|
||||
};
|
||||
|
||||
const verifiedProviders = [
|
||||
"airtable", "calendly", "exa", "firecrawl", "gsc-wizard", "linear",
|
||||
"make", "parallel-search", "posthog", "tavily", "windsor-ai",
|
||||
];
|
||||
const newProviders = ["calendly", "exa", "firecrawl", "gsc-wizard", "parallel-search", "tavily", "windsor-ai"];
|
||||
|
||||
describe("verified MCP connector definitions", () => {
|
||||
it("keeps the 11 qualified providers on remote MCP methods", () => {
|
||||
expect(new Set(verifiedProviders).size).toBe(11);
|
||||
for (const slug of verifiedProviders) {
|
||||
expect(appBySlug(slug).methods.some((method) => method.transport === "mcp_remote"), slug).toBe(true);
|
||||
expect(appBySlug(slug).availability?.available, slug).not.toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("parses the seven newly added definitions against the shared schema", () => {
|
||||
for (const slug of newProviders) {
|
||||
const definition = appBySlug(slug);
|
||||
expect(appDefinitionSchema.parse(definition)).toEqual(definition);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses reviewed DCR registration for Airtable and retains the manual key method", () => {
|
||||
expect(methodFor("airtable", "mcp-oauth")).toMatchObject({
|
||||
transport: "mcp_remote", auth: "oauth", ownershipModes: ["dcr"], oauthClientRegistration: "dcr",
|
||||
defaults: { serverUrl: "https://mcp.airtable.com/mcp" },
|
||||
});
|
||||
expect(methodFor("airtable", "mcp-pat")).toMatchObject({
|
||||
transport: "mcp_remote", auth: "api_key", keyPlacement: { location: "header", name: "Authorization", prefix: "Bearer " },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps keyless search endpoints credential-free", () => {
|
||||
for (const [slug, key, endpoint] of [
|
||||
["exa", "public-search", "https://mcp.exa.ai/mcp"],
|
||||
["parallel-search", "public-search", "https://search.parallel.ai/mcp"],
|
||||
]) {
|
||||
const method = methodFor(slug!, key!);
|
||||
expect(method).toMatchObject({ transport: "mcp_remote", auth: "none", defaults: { serverUrl: endpoint } });
|
||||
expect(method.credentialFields).toBeUndefined();
|
||||
expect(method.keyPlacement).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("retains Tavily's reviewed MCP session initialization preference", () => {
|
||||
const overridePath = new URL("../../../scripts/app-definition-overrides/tavily.json", import.meta.url);
|
||||
const parsed = appDefinitionSchema.parse(JSON.parse(fs.readFileSync(overridePath, "utf8")));
|
||||
expect(parsed.methods[0]?.defaults?.mcpSessionRequired).toBe(true);
|
||||
const invalidTransport = structuredClone(parsed);
|
||||
invalidTransport.methods[0]!.transport = "rest_api";
|
||||
expect(appDefinitionSchema.safeParse(invalidTransport).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,6 @@
|
||||
{ "slug": "todoist", "name": "Todoist", "wave": 1, "status": "self_serve", "docsUrl": "https://developer.todoist.com/", "serverUrl": "https://ai.todoist.net/mcp", "authMode": "dcr", "prerequisite": "A Todoist account.", "riskTier": "S3" },
|
||||
{ "slug": "webflow", "name": "Webflow", "wave": 1, "status": "self_serve", "docsUrl": "https://developers.webflow.com/mcp/reference/getting-started", "serverUrl": "https://mcp.webflow.com/mcp", "authMode": "dcr", "prerequisite": "A Webflow account; workspace and site roles constrain accessible sites.", "riskTier": "S3" },
|
||||
{ "slug": "wix", "name": "Wix", "wave": 1, "status": "self_serve", "docsUrl": "https://www.wix.com/studio/developers/mcp-server", "serverUrl": "https://mcp.wix.com/mcp", "authMode": "dcr", "prerequisite": "A Wix account with access to the relevant sites.", "riskTier": "S3" },
|
||||
|
||||
{ "slug": "brex", "name": "Brex", "wave": 2, "status": "self_serve", "docsUrl": "https://www.brex.com/support/using-brex-in-ai-apps", "serverUrl": "https://api.brex.com/mcp", "authMode": "dcr", "prerequisite": "Brex early access and an administrator enabling the integration; financial actions require explicit approval.", "riskTier": "S4" },
|
||||
{ "slug": "clickhouse", "name": "ClickHouse", "wave": 2, "status": "self_serve", "docsUrl": "https://clickhouse.com/blog/announcing-managed-clickstack-mcp-server", "serverUrl": "https://mcp.clickhouse.cloud/clickstack", "authMode": "dcr", "prerequisite": "A ClickHouse Cloud ClickStack service and its service ID.", "riskTier": "S4" },
|
||||
{ "slug": "egnyte", "name": "Egnyte", "wave": 2, "status": "self_serve", "docsUrl": "https://developers.egnyte.com/docs/Remote_MCP_Server", "serverUrl": "https://mcp-server.egnyte.com/mcp", "authMode": "dcr", "prerequisite": "An eligible Egnyte plan and administrator approval for external LLM access.", "riskTier": "S3" },
|
||||
@@ -39,7 +38,6 @@
|
||||
{ "slug": "stripe", "name": "Stripe", "wave": 2, "status": "self_serve", "docsUrl": "https://docs.stripe.com/mcp", "serverUrl": "https://mcp.stripe.com", "authMode": "dcr_or_api_key", "prerequisite": "A Stripe account; the server is public preview and payment actions require explicit approval.", "riskTier": "S4" },
|
||||
{ "slug": "supabase", "name": "Supabase", "wave": 2, "status": "self_serve", "docsUrl": "https://supabase.com/docs/guides/ai-tools/mcp", "serverUrl": "https://mcp.supabase.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Supabase account; use a development project and review write actions before connecting production data.", "riskTier": "S4" },
|
||||
{ "slug": "ticket-tailor", "name": "Ticket Tailor", "wave": 2, "status": "self_serve", "docsUrl": "https://developers.tickettailor.com/docs/mcp/authentication/", "serverUrl": "https://mcp.tickettailor.ai/mcp", "authMode": "dcr", "prerequisite": "A Ticket Tailor account; the provider may request an API key during its hosted authorization prompt.", "riskTier": "S3" },
|
||||
|
||||
{ "slug": "asana", "name": "Asana", "wave": 3, "status": "self_serve", "docsUrl": "https://developers.asana.com/docs/integrating-with-asanas-mcp-server", "serverUrl": "https://mcp.asana.com/v2/mcp", "authMode": "dcr", "prerequisite": "An active Asana account. Re-verified 2026-09-28: mcp.asana.com advertises a registration endpoint and issues clients for http://localhost callbacks, so a local Paperclip registers its own OAuth client. Asana refuses non-localhost redirect URIs, so a hosted deployment still registers an app in Asana's developer console.", "riskTier": "S3" },
|
||||
{ "slug": "linear", "name": "Linear", "wave": 3, "status": "self_serve", "docsUrl": "https://linear.app/docs/mcp", "serverUrl": "https://mcp.linear.app/mcp", "authMode": "dcr_cimd", "prerequisite": "An active Linear workspace. Verified 2026-09-28: mcp.linear.app advertises a registration endpoint and client-ID metadata documents, so Paperclip registers its own OAuth client.", "riskTier": "S2" },
|
||||
{ "slug": "box", "name": "Box", "wave": 3, "status": "self_serve", "docsUrl": "https://support.box.com/hc/en-us/articles/43847256139923-Managing-Box-MCP-Servers", "serverUrl": "https://mcp.box.com", "authMode": "customer_oauth", "prerequisite": "A Box administrator creates the OAuth integration and enables AI access.", "riskTier": "S3" },
|
||||
@@ -48,19 +46,24 @@
|
||||
{ "slug": "similarweb", "name": "Similarweb", "wave": 3, "status": "self_serve", "docsUrl": "https://developers.similarweb.com/docs/similarweb-mcp", "serverUrl": "https://mcp.similarweb.com", "authMode": "api_key", "prerequisite": "A Similarweb subscription with API access and an API key.", "riskTier": "S2" },
|
||||
{ "slug": "xero", "name": "Xero", "wave": 3, "status": "self_serve", "docsUrl": "https://developer.xero.com/ai", "serverUrl": "https://mcp.xero.com/mcp", "authMode": "customer_oauth", "prerequisite": "Create a Xero OAuth app and confirm the applicable AI and data-use terms before connecting.", "riskTier": "S4" },
|
||||
{ "slug": "zapier", "name": "Zapier", "wave": 3, "status": "self_serve", "docsUrl": "https://docs.zapier.com/mcp/quickstart", "serverUrl": "https://mcp.zapier.com/", "authMode": "generated_url", "prerequisite": "Create a Zapier MCP server, choose the actions it exposes, and paste its generated connection URL.", "riskTier": "S3" },
|
||||
|
||||
{ "slug": "youcom", "name": "You.com", "wave": 4, "status": "self_serve", "docsUrl": "https://you.com/docs/build-with-agents/mcp-server", "serverUrl": "https://api.you.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A You.com account for browser sign-in, or a You.com API key from you.com/platform for higher rate limits; a keyless free profile is also available.", "riskTier": "S2" },
|
||||
|
||||
{"slug": "fireflies", "name": "Fireflies", "wave": 4, "status": "self_serve", "docsUrl": "https://docs.fireflies.ai/getting-started/mcp-configuration", "serverUrl": "https://api.fireflies.ai/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Fireflies account with access to the meetings you want to use. API keys are available in Settings → Developer Settings.", "riskTier": "S3"},
|
||||
{ "slug": "fireflies", "name": "Fireflies", "wave": 4, "status": "self_serve", "docsUrl": "https://docs.fireflies.ai/getting-started/mcp-configuration", "serverUrl": "https://api.fireflies.ai/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Fireflies account with access to the meetings you want to use. API keys are available in Settings → Developer Settings.", "riskTier": "S3" },
|
||||
{ "slug": "g2", "name": "G2", "wave": "blocked", "status": "blocked", "docsUrl": "https://documentation.g2.com/docs/g2-mcp-server", "serverUrl": "https://mcp.g2.com/mcp", "authMode": "provider_approval", "prerequisite": "G2 must enable cross-application token introspection before an independently registered client can work.", "riskTier": "S3" },
|
||||
{ "slug": "vercel", "name": "Vercel", "wave": "blocked", "status": "blocked", "docsUrl": "https://vercel.com/docs/agent-resources/vercel-mcp", "serverUrl": "https://mcp.vercel.com", "authMode": "provider_approval", "prerequisite": "Vercel currently reviews and approves MCP clients.", "riskTier": "S3" },
|
||||
{ "slug": "zomato", "name": "Zomato", "wave": "blocked", "status": "blocked", "docsUrl": "https://github.com/Zomato/mcp-server-manifest", "serverUrl": "https://mcp-server.zomato.com/mcp", "authMode": "provider_approval", "prerequisite": "Zomato currently limits third-party clients and requires redirect-URI allowlisting.", "riskTier": "S3" },
|
||||
{"slug": "zep", "name": "Zep", "wave": 3, "status": "self_serve", "docsUrl": "https://help.getzep.com/memory-mcp-server", "serverUrl": "https://api.getzep.com/mcp", "authMode": "dcr_cimd", "prerequisite": "A Zep project with Memory MCP enabled, available MCP seats, and Google Workspace or enterprise OIDC configured by its administrator. Project API keys do not authenticate Memory MCP.", "riskTier": "S3"},
|
||||
{"slug": "supermemory", "name": "Supermemory", "wave": 3, "status": "self_serve", "docsUrl": "https://supermemory.ai/docs/supermemory-mcp/mcp", "serverUrl": "https://mcp.supermemory.ai/mcp", "authMode": "dcr", "prerequisite": "Sign in to Supermemory and select the spaces this connection may access. The hosted MCP uses OAuth, not a developer API key.", "riskTier": "S3"},
|
||||
{"slug": "honcho", "name": "Honcho", "wave": 3, "status": "self_serve", "docsUrl": "https://honcho.dev/docs/v3/guides/integrations/mcp", "serverUrl": "https://mcp.honcho.dev", "authMode": "api_key", "prerequisite": "An API key from the Honcho dashboard. Memory is stored in your Honcho account; select workspace and peer identifiers when calling tools.", "riskTier": "S3"},
|
||||
{ "slug": "zep", "name": "Zep", "wave": 3, "status": "self_serve", "docsUrl": "https://help.getzep.com/memory-mcp-server", "serverUrl": "https://api.getzep.com/mcp", "authMode": "dcr_cimd", "prerequisite": "A Zep project with Memory MCP enabled, available MCP seats, and Google Workspace or enterprise OIDC configured by its administrator. Project API keys do not authenticate Memory MCP.", "riskTier": "S3" },
|
||||
{ "slug": "supermemory", "name": "Supermemory", "wave": 3, "status": "self_serve", "docsUrl": "https://supermemory.ai/docs/supermemory-mcp/mcp", "serverUrl": "https://mcp.supermemory.ai/mcp", "authMode": "dcr", "prerequisite": "Sign in to Supermemory and select the spaces this connection may access. The hosted MCP uses OAuth, not a developer API key.", "riskTier": "S3" },
|
||||
{ "slug": "honcho", "name": "Honcho", "wave": 3, "status": "self_serve", "docsUrl": "https://honcho.dev/docs/v3/guides/integrations/mcp", "serverUrl": "https://mcp.honcho.dev", "authMode": "api_key", "prerequisite": "An API key from the Honcho dashboard. Memory is stored in your Honcho account; select workspace and peer identifiers when calling tools.", "riskTier": "S3" },
|
||||
{ "slug": "neon", "name": "Neon", "wave": 4, "status": "self_serve", "docsUrl": "https://neon.com/docs/ai/neon-mcp-server", "serverUrl": "https://mcp.neon.tech/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Neon account. The hosted server grants broad project and database management, so use a development project and review write actions before connecting production data.", "riskTier": "S4" },
|
||||
{ "slug": "superagent", "name": "Superagent", "wave": 4, "status": "self_serve", "docsUrl": "https://www.superagent.sh/docs/mcp", "serverUrl": "https://www.superagent.sh/mcp", "authMode": "api_key", "prerequisite": "Superagent report and triage tools consume organization credits, and deleting a finding is permanent. Set those actions to Ask first before agents run unattended.", "riskTier": "S4" },
|
||||
{ "slug": "telem", "name": "Telem.AI", "wave": 4, "status": "self_serve", "docsUrl": "https://docs.telem.ai", "serverUrl": "https://mcp.telem.ai/mcp", "authMode": "api_key", "prerequisite": "A Telem API key. Create one in the Telem console at app.telem.ai.", "riskTier": "S2" },
|
||||
{ "slug": "gauge", "name": "Gauge", "wave": 4, "status": "self_serve", "docsUrl": "https://docs.withgauge.com/help/mcp", "serverUrl": "https://app.withgauge.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Gauge account. Each connection reaches one Gauge organization: sign-in uses the organization chosen during consent, and an API key uses the organization that created it.", "riskTier": "S3" }
|
||||
{ "slug": "gauge", "name": "Gauge", "wave": 4, "status": "self_serve", "docsUrl": "https://docs.withgauge.com/help/mcp", "serverUrl": "https://app.withgauge.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Gauge account. Each connection reaches one Gauge organization: sign-in uses the organization chosen during consent, and an API key uses the organization that created it.", "riskTier": "S3" },
|
||||
{ "slug": "tavily", "name": "Tavily", "wave": 2, "status": "self_serve", "docsUrl": "https://docs.tavily.com/documentation/mcp", "serverUrl": "https://mcp.tavily.com/mcp", "authMode": "dcr", "prerequisite": "OAuth uses the Tavily API key selected by account settings. Name a key `mcp_auth_default` to select it explicitly; otherwise Tavily uses the personal default key or first available key.", "riskTier": "S2", "verifiedAt": "2026-10-08" },
|
||||
{ "slug": "calendly", "name": "Calendly", "wave": 2, "status": "self_serve", "docsUrl": "https://developer.calendly.com/docs/mcp/calendly-mcp-server", "serverUrl": "https://mcp.calendly.com", "authMode": "dcr", "prerequisite": "Connect with a Calendly account that has the desired scheduling permissions.", "riskTier": "S3", "verifiedAt": "2026-10-08" },
|
||||
{ "slug": "exa", "name": "Exa", "wave": 3, "status": "self_serve", "docsUrl": "https://exa.ai/docs/get-started/exa-mcp", "serverUrl": "https://mcp.exa.ai/mcp", "authMode": "public", "prerequisite": "The public MCP route provides web search and page fetching. Keep citations with results. Account-authenticated Agent and advanced tools are not included in this method.", "riskTier": "S3", "verifiedAt": "2026-10-08" },
|
||||
{ "slug": "firecrawl", "name": "Firecrawl", "wave": 3, "status": "self_serve", "docsUrl": "https://docs.firecrawl.dev/mcp-server", "serverUrl": "https://mcp.firecrawl.dev/v2/mcp-oauth", "authMode": "dcr_or_api_key", "prerequisite": "Sign in and choose the Firecrawl team whose usage should apply. This route exposes the account-authorized tool surface; review paid jobs and action policies before execution.", "riskTier": "S4", "verifiedAt": "2026-10-08" },
|
||||
{ "slug": "parallel-search", "name": "Parallel Search", "wave": 3, "status": "self_serve", "docsUrl": "https://docs.parallel.ai/integrations/mcp/search-mcp", "serverUrl": "https://search.parallel.ai/mcp", "authMode": "public", "prerequisite": "The public route exposes web_search and web_fetch. Keep source URLs, excerpts, and retrieval context in the result; this is separate from Parallel Task MCP.", "riskTier": "S3", "verifiedAt": "2026-10-08" },
|
||||
{ "slug": "windsor-ai", "name": "Windsor.ai", "wave": 3, "status": "self_serve", "docsUrl": "https://mcp.windsor.ai/", "serverUrl": "https://mcp.windsor.ai/", "authMode": "dcr_or_api_key", "prerequisite": "Connect Windsor.ai, then verify which advertising, analytics, CRM, or warehouse sources are linked. Keep source account and metric definitions with results. Review any action that changes campaigns, budgets, posts, or listings.", "riskTier": "S4", "verifiedAt": "2026-10-08" },
|
||||
{ "slug": "gsc-wizard", "name": "GSC Wizard", "wave": 3, "status": "self_serve", "docsUrl": "https://www.gscwizard.com/claude-search-console-mcp.html", "serverUrl": "https://mcp.gscwizard.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "Connect the GSC Wizard account and its authorized properties. Check site identity and date bounds before querying; review any URL submission or property-changing action separately.", "riskTier": "S3", "verifiedAt": "2026-10-08" }
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { ConnectionGrantKind, ToolConnectionOwnership, ToolConnectionPurpos
|
||||
export type AppCategory = "ai"|"analytics"|"commerce"|"communication"|"content"|"data"|"developer"|"productivity"|"other";
|
||||
export type OAuthRedirectConstraints = "https-or-loopback-http";
|
||||
export interface FieldDef { key:string; label:string; type:"text"|"password"|"textarea"|"datetime"|"select"|"checkbox"; required?:boolean; advanced?:boolean; hidden?:boolean; placeholder?:string; helperMd?:string; secret?:boolean; prefix?:string; defaultValue?:string|boolean; validation?:{pattern?:string;maxLength?:number}; options?:Array<{value:string;label:string}>; transport?:{location:"query"|"header";name:string;format?:"string"|"csv"|"boolean";omitFalse?:boolean} }
|
||||
export interface ConnectionMethodDef { ai?: import("../ai-connections.js").AiConnectionMetadata; key:string; label?:string; purpose?:ToolConnectionPurpose; provider?:"slack"|"github"|"discord"|"microsoft-teams"|"telegram"|"agentmail"|"imessage-photon"; transport:ToolConnectionTransport; auth:"oauth"|"api_key"|"none"; oauthStrategy?:"paperclip_cloud_connector"|"paperclip_id_connector"; connectorProfile?:string; oauthClientSecretRequired?:boolean; capabilityProfile?:{key:string;label:string;description?:string}; grantKinds?:ConnectionGrantKind[]; ownershipModes:ToolConnectionOwnership[]; whenToUse:string; defaults?:{serverUrl?:string;serverUrlTemplate?:string;discoveryUrl?:string|null;serviceHost?:string;templateKey?:string;authorizationEndpoint?:string;tokenEndpoint?:string;metadataUrl?:string;scopesHint?:string[];oauthAuthorizationParams?:{access_type?:"offline";prompt?:"consent"};toolArgumentDefaults?:Record<string,unknown>}; tenantFields?:FieldDef[]; extensionFields?:FieldDef[]; configRequirements?:{atLeastOneOf?:string[]}; credentialFields?:FieldDef[]; keyPlacement?:{location:"header"|"query"|"body_json"|"env";name:string;prefix?:string|null}; credentialSources?:{vercelConnect?:{services:string[];principalModes:VercelConnectPrincipalMode[];scopes:string[];header:{name:string;prefix?:string|null}}}; guidanceMd:string; consoleLinks?:{register?:string;keys?:string;settings?:string;docs?:string}; warnings?:string[]; variants?:Array<{key:string;label:string;whenToUse:string;tenantFields?:FieldDef[]}>; riskTier:"S1"|"S2"|"S3"|"S4"; requiredResourceFilters?:string[] }
|
||||
export interface ConnectionMethodDef { ai?: import("../ai-connections.js").AiConnectionMetadata; key:string; label?:string; purpose?:ToolConnectionPurpose; provider?:"slack"|"github"|"discord"|"microsoft-teams"|"telegram"|"agentmail"|"imessage-photon"; transport:ToolConnectionTransport; auth:"oauth"|"api_key"|"none"; oauthStrategy?:"paperclip_cloud_connector"|"paperclip_id_connector"; connectorProfile?:string; oauthClientSecretRequired?:boolean; oauthClientRegistration?:"dcr"; capabilityProfile?:{key:string;label:string;description?:string}; grantKinds?:ConnectionGrantKind[]; ownershipModes:ToolConnectionOwnership[]; whenToUse:string; defaults?:{serverUrl?:string;serverUrlTemplate?:string;discoveryUrl?:string|null;serviceHost?:string;templateKey?:string;authorizationEndpoint?:string;tokenEndpoint?:string;metadataUrl?:string;scopesHint?:string[];oauthAuthorizationParams?:{access_type?:"offline";prompt?:"consent"};toolArgumentDefaults?:Record<string,unknown>;mcpSessionRequired?:boolean}; tenantFields?:FieldDef[]; extensionFields?:FieldDef[]; configRequirements?:{atLeastOneOf?:string[]}; credentialFields?:FieldDef[]; keyPlacement?:{location:"header"|"query"|"body_json"|"env";name:string;prefix?:string|null}; credentialSources?:{vercelConnect?:{services:string[];principalModes:VercelConnectPrincipalMode[];scopes:string[];header:{name:string;prefix?:string|null}}}; guidanceMd:string; consoleLinks?:{register?:string;keys?:string;settings?:string;docs?:string}; warnings?:string[]; variants?:Array<{key:string;label:string;whenToUse:string;tenantFields?:FieldDef[]}>; riskTier:"S1"|"S2"|"S3"|"S4"; requiredResourceFilters?:string[] }
|
||||
export interface AppDefinition { aiConnectionRouter?: { pluginKey: string }; agentInstructions?: import("../connection-instructions.js").ConnectionInstructionTemplate; schemaVersion:1; slug:string; name:string; description:string; categories:AppCategory[]; tags?:string[]; featured?:boolean; branding:{logoUrl:string;darkLogoUrl?:string;backgroundColor?:string;accentColor?:string}; urlPatterns:string[]; docsUrl?:string; setupPrerequisite?:{title:string;description:string;steps?:string[];actionLabel:string;actionUrl:string}; redirectConstraints?:OAuthRedirectConstraints; methods:ConnectionMethodDef[]; suggestable?:boolean; availability?:{available:boolean;reason?:string;robotEmail?:string}; ownershipAvailability?:Partial<Record<ToolConnectionOwnership,boolean>> }
|
||||
|
||||
export type SelfServeMcpAuthMode =
|
||||
@@ -12,6 +12,7 @@ export type SelfServeMcpAuthMode =
|
||||
| "customer_oauth"
|
||||
| "api_key"
|
||||
| "generated_url"
|
||||
| "public"
|
||||
| "provider_approval";
|
||||
|
||||
export interface SelfServeMcpResearchEntry {
|
||||
@@ -24,6 +25,7 @@ export interface SelfServeMcpResearchEntry {
|
||||
authMode: SelfServeMcpAuthMode;
|
||||
prerequisite: string;
|
||||
riskTier: "S1" | "S2" | "S3" | "S4";
|
||||
verifiedAt?: string;
|
||||
}
|
||||
|
||||
export interface SelfServeMcpResearchManifest {
|
||||
|
||||
@@ -7,7 +7,7 @@ const appBrandAssetUrlSchema=z.string().refine((value)=>{
|
||||
try{return new URL(value).protocol==="https:";}catch{return false;}
|
||||
},{message:"Brand assets must be HTTPS URLs or local /brands/apps SVG/PNG paths"});
|
||||
const field=z.object({key:z.string().min(1),label:z.string().min(1),type:z.enum(["text","password","textarea","datetime","select","checkbox"]),required:z.boolean().optional(),advanced:z.boolean().optional(),hidden:z.boolean().optional(),placeholder:z.string().optional(),helperMd:z.string().optional(),secret:z.boolean().optional(),prefix:z.string().optional(),defaultValue:z.union([z.string(),z.boolean()]).optional(),validation:z.object({pattern:z.string().optional(),maxLength:z.number().int().positive().optional()}).optional(),options:z.array(z.object({value:z.string(),label:z.string()})).optional(),transport:z.object({location:z.enum(["query","header"]),name:z.string().min(1),format:z.enum(["string","csv","boolean"]).optional(),omitFalse:z.boolean().optional()}).optional()}).superRefine((v,c)=>{if(v.required&&v.type!=="checkbox"&&!v.placeholder)c.addIssue({code:"custom",message:"Required fields need placeholders",path:["placeholder"]});if(v.type==="select"&&(!v.options||v.options.length===0))c.addIssue({code:"custom",message:"Select fields need options",path:["options"]});if(v.hidden&&v.defaultValue===undefined)c.addIssue({code:"custom",message:"Hidden fields need defaults",path:["defaultValue"]})});
|
||||
export const connectionMethodDefSchema=z.object({key:z.string().min(1),label:z.string().min(1).optional(),purpose:z.union([toolConnectionPurposeSchema,z.literal("ai")]).optional(),provider:z.enum(["slack","github","discord","microsoft-teams","telegram","agentmail","imessage-photon"]).optional(),transport:z.union([toolConnectionTransportSchema,z.literal("runtime_auth")]),ai:aiConnectionMetadataSchema.optional(),auth:z.enum(["oauth","api_key","none"]),oauthStrategy:z.enum(["paperclip_cloud_connector","paperclip_id_connector"]).optional(),connectorProfile:z.string().regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/).optional(),oauthClientSecretRequired:z.boolean().optional(),capabilityProfile:z.object({key:z.string().min(1),label:z.string().min(1),description:z.string().min(1).optional()}).optional(),grantKinds:z.array(connectionGrantKindSchema).min(1).optional(),ownershipModes:z.array(toolConnectionOwnershipSchema).min(1),whenToUse:z.string().min(1),defaults:z.object({serverUrl:z.string().url().optional(),serverUrlTemplate:z.string().regex(/^https:\/\//).optional(),discoveryUrl:z.string().url().nullable().optional(),serviceHost:z.string().optional(),templateKey:z.string().optional(),authorizationEndpoint:z.string().url().optional(),tokenEndpoint:z.string().url().optional(),metadataUrl:z.string().url().optional(),scopesHint:z.array(z.string()).optional(),oauthAuthorizationParams:z.object({access_type:z.literal("offline").optional(),prompt:z.literal("consent").optional()}).optional(),toolArgumentDefaults:z.record(z.string(),z.unknown()).optional()}).optional(),tenantFields:z.array(field).optional(),extensionFields:z.array(field).optional(),configRequirements:z.object({atLeastOneOf:z.array(z.string().min(1)).min(1).optional()}).optional(),credentialFields:z.array(field).optional(),keyPlacement:z.object({location:z.enum(["header","query","body_json","env"]),name:z.string().min(1),prefix:z.string().nullable().optional()}).optional(),credentialSources:z.object({vercelConnect:z.object({services:z.array(z.string().min(1)).min(1),principalModes:z.array(z.enum(["app","user"])).min(1),scopes:z.array(z.string().min(1)).min(1),header:z.object({name:z.string().min(1),prefix:z.string().nullable().optional()})}).optional()}).optional(),guidanceMd:z.string().min(1),consoleLinks:z.object({register:z.string().url().optional(),keys:z.string().url().optional(),settings:z.string().url().optional(),docs:z.string().url().optional()}).optional(),warnings:z.array(z.string()).optional(),variants:z.array(z.object({key:z.string(),label:z.string(),whenToUse:z.string(),tenantFields:z.array(field).optional()})).optional(),riskTier:z.enum(["S1","S2","S3","S4"]),requiredResourceFilters:z.array(z.string()).optional()}).superRefine((v,c)=>{if((v.transport==="runtime_auth")!==Boolean(v.ai))c.addIssue({code:"custom",message:"Runtime authentication requires AI metadata and AI metadata requires runtime_auth",path:["ai"]});if(v.ai&&!AI_CONNECTION_CAPABILITIES[v.ai.provider].methods[v.ai.method])c.addIssue({code:"custom",message:"Unsupported AI sign-in method",path:["ai","method"]});if(v.ai&&v.auth!==(v.ai.method==="subscription"?"oauth":"api_key"))c.addIssue({code:"custom",message:"AI sign-in method must match authentication",path:["auth"]});const purpose=v.purpose??"tool";if((purpose==="ai")!==(v.transport==="runtime_auth"))c.addIssue({code:"custom",message:"AI methods require runtime_auth and runtime_auth requires AI purpose",path:["purpose"]});if(v.transport==="chat_sdk"&&purpose!=="channel")c.addIssue({code:"custom",message:"Chat SDK methods must be channel connections",path:["purpose"]});if(purpose==="channel"&&v.transport!=="chat_sdk"&&!(v.provider==="agentmail"&&v.transport==="rest_api"))c.addIssue({code:"custom",message:"Channel connections must use the Chat SDK transport",path:["transport"]});if(purpose==="channel"&&!v.provider)c.addIssue({code:"custom",message:"Channel connections require a chat provider",path:["provider"]});if(v.auth==="api_key"&&!v.keyPlacement&&purpose!=="channel")c.addIssue({code:"custom",message:"API-key tool methods require keyPlacement",path:["keyPlacement"]});if(v.oauthStrategy&&v.auth!=="oauth")c.addIssue({code:"custom",message:"OAuth strategies require OAuth auth",path:["oauthStrategy"]});if(v.oauthStrategy&&!v.connectorProfile)c.addIssue({code:"custom",message:"Paperclip Cloud connector methods require connectorProfile",path:["connectorProfile"]});if(v.connectorProfile&&!v.oauthStrategy)c.addIssue({code:"custom",message:"connectorProfile requires a Paperclip Cloud OAuth strategy",path:["connectorProfile"]});if(v.credentialSources?.vercelConnect&&(v.transport!=="mcp_remote"||v.auth==="none"))c.addIssue({code:"custom",message:"Vercel Connect requires an authenticated remote MCP method",path:["credentialSources","vercelConnect"]});const keys=new Set([...(v.tenantFields??[]),...(v.extensionFields??[])].map((entry)=>entry.key));for(const key of v.configRequirements?.atLeastOneOf??[])if(!keys.has(key))c.addIssue({code:"custom",mLine truncated
|
||||
export const connectionMethodDefSchema=z.object({key:z.string().min(1),label:z.string().min(1).optional(),purpose:z.union([toolConnectionPurposeSchema,z.literal("ai")]).optional(),provider:z.enum(["slack","github","discord","microsoft-teams","telegram","agentmail","imessage-photon"]).optional(),transport:z.union([toolConnectionTransportSchema,z.literal("runtime_auth")]),ai:aiConnectionMetadataSchema.optional(),auth:z.enum(["oauth","api_key","none"]),oauthStrategy:z.enum(["paperclip_cloud_connector","paperclip_id_connector"]).optional(),connectorProfile:z.string().regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/).optional(),oauthClientSecretRequired:z.boolean().optional(),oauthClientRegistration:z.literal("dcr").optional(),capabilityProfile:z.object({key:z.string().min(1),label:z.string().min(1),description:z.string().min(1).optional()}).optional(),grantKinds:z.array(connectionGrantKindSchema).min(1).optional(),ownershipModes:z.array(toolConnectionOwnershipSchema).min(1),whenToUse:z.string().min(1),defaults:z.object({serverUrl:z.string().url().optional(),serverUrlTemplate:z.string().regex(/^https:\/\//).optional(),discoveryUrl:z.string().url().nullable().optional(),serviceHost:z.string().optional(),templateKey:z.string().optional(),authorizationEndpoint:z.string().url().optional(),tokenEndpoint:z.string().url().optional(),metadataUrl:z.string().url().optional(),scopesHint:z.array(z.string()).optional(),oauthAuthorizationParams:z.object({access_type:z.literal("offline").optional(),prompt:z.literal("consent").optional()}).optional(),toolArgumentDefaults:z.record(z.string(),z.unknown()).optional(),mcpSessionRequired:z.boolean().optional()}).optional(),tenantFields:z.array(field).optional(),extensionFields:z.array(field).optional(),configRequirements:z.object({atLeastOneOf:z.array(z.string().min(1)).min(1).optional()}).optional(),credentialFields:z.array(field).optional(),keyPlacement:z.object({location:z.enum(["header","query","body_json","env"]),name:z.string().min(1),prefix:z.string().nullable().optional()}).optional(),credentialSources:z.object({vercelConnect:z.object({services:z.array(z.string().min(1)).min(1),principalModes:z.array(z.enum(["app","user"])).min(1),scopes:z.array(z.string().min(1)).min(1),header:z.object({name:z.string().min(1),prefix:z.string().nullable().optional()})}).optional()}).optional(),guidanceMd:z.string().min(1),consoleLinks:z.object({register:z.string().url().optional(),keys:z.string().url().optional(),settings:z.string().url().optional(),docs:z.string().url().optional()}).optional(),warnings:z.array(z.string()).optional(),variants:z.array(z.object({key:z.string(),label:z.string(),whenToUse:z.string(),tenantFields:z.array(field).optional()})).optional(),riskTier:z.enum(["S1","S2","S3","S4"]),requiredResourceFilters:z.array(z.string()).optional()}).superRefine((v,c)=>{if((v.transport==="runtime_auth")!==Boolean(v.ai))c.addIssue({code:"custom",message:"Runtime authentication requires AI metadata and AI metadata requires runtime_auth",path:["ai"]});if(v.ai&&!AI_CONNECTION_CAPABILITIES[v.ai.provider].methods[v.ai.method])c.addIssue({code:"custom",message:"Unsupported AI sign-in method",path:["ai","method"]});if(v.ai&&v.auth!==(v.ai.method==="subscription"?"oauth":"api_key"))c.addIssue({code:"custom",message:"AI sign-in method must match authentication",path:["auth"]});if(v.oauthClientRegistration&&(v.auth!=="oauth"||v.transport!=="mcp_remote"||!v.ownershipModes.includes("dcr")))c.addIssue({code:"custom",message:"DCR client registration requires a remote MCP OAuth method with DCR ownership",path:["oauthClientRegistration"]});if(v.defaults?.mcpSessionRequired&&v.transport!=="mcp_remote")c.addIssue({code:"custom",message:"MCP session initialization preferences require remote MCP transport",path:["defaults","mcpSessionRequired"]});const purpose=v.purpose??"tool";if((purpose==="ai")!==(v.transport==="runtime_auth"))c.addIssue({code:"custom",message:"AI methods require runtime_auth and runtime_auth requires AI purpose",path:["purpose"]});if(v.transport==="chat_sdk"&&purpose!=="channel")c.addIssue({code:"custom",message:"Chat SDK methods must be channel connections",path:["purpose"]});if(purpose==="channel"&&v.transport!=="chat_sdk"&&!(v.provider==="agentmail"&&v.transport==="rest_api"))c.addIssue({code:"custom",message:"Channel connections must use the Chat SDK transport",path:["transport"]});if(purpose==="channel"&&!v.provider)c.addIssue({code:"custom",message:"Channel connections require a chat provider",path:["provider"]});if(v.auth==="api_key"&&!v.keyPlacement&&purpose!=="channel")c.addIssue({code:"custom",message:"API-key tool methods require keyPlacement",path:["keyPlacement"]});if(v.oauthStrategy&&v.auth!=="oauth")c.addIssue({code:"custom",message:"OAuth strategies require OAuth auth",path:["oauthStrategy"]});if(v.oauthStrategy&&!v.connectorProfile)c.addIssue({code:"custom",message:"Paperclip Cloud connector methods require connectorProfile",path:["connectorProfile"]});if(v.connectorProfile&&!Line truncated
|
||||
export const appDefinitionSchema=z.object({agentInstructions:connectionInstructionTemplateSchema.optional(),aiConnectionRouter:z.object({pluginKey:z.string().min(1)}).optional(),schemaVersion:z.literal(1),slug:z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),name:z.string().min(1),description:z.string().min(1),categories:z.array(z.enum(["ai","analytics","commerce","communication","content","data","developer","productivity","other"])).min(1),tags:z.array(z.string().min(1)).optional(),featured:z.boolean().optional(),branding:z.object({logoUrl:appBrandAssetUrlSchema,darkLogoUrl:appBrandAssetUrlSchema.optional(),backgroundColor:z.string().optional(),accentColor:z.string().optional()}),urlPatterns:z.array(z.string()),docsUrl:z.string().url().optional(),setupPrerequisite:z.object({title:z.string().min(1),description:z.string().min(1),steps:z.array(z.string().min(1)).min(1).optional(),actionLabel:z.string().min(1),actionUrl:z.string().url()} ).optional(),redirectConstraints:z.enum(["https-or-loopback-http"]).optional(),methods:z.array(connectionMethodDefSchema),suggestable:z.boolean().optional(),availability:z.object({available:z.boolean(),reason:z.string().optional(),robotEmail:z.string().optional()}).optional(),ownershipAvailability:z.object({platform_shared:z.boolean().optional(),platform_provisioned:z.boolean().optional(),customer:z.boolean().optional(),dcr:z.boolean().optional()}).optional()}).superRefine((value, ctx) => {
|
||||
if (value.aiConnectionRouter) {
|
||||
if (value.methods.length !== 0) ctx.addIssue({ code: "custom", message: "Pooled connectors use existing connections, not authentication methods", path: ["methods"] });
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
const makeFixture = () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-app-overrides-"));
|
||||
for (const relative of [
|
||||
"scripts/ingest-app-definitions.mjs",
|
||||
"scripts/app-definition-overrides",
|
||||
"packages/shared/src/app-definitions",
|
||||
"packages/shared/src/self-serve-mcp-research.json",
|
||||
"doc/connections/tool-method-permission-reviews.json",
|
||||
"ui/public/brands/apps/manifest.json",
|
||||
]) {
|
||||
const source = path.join(repoRoot, relative);
|
||||
const target = path.join(root, relative);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.cpSync(source, target, { recursive: true });
|
||||
}
|
||||
return root;
|
||||
};
|
||||
|
||||
const runIngestion = (root) =>
|
||||
spawnSync(process.execPath, ["scripts/ingest-app-definitions.mjs", "--definitions-only"], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
test("provider overrides keep manifest branding and pass normal review validation", (t) => {
|
||||
const root = makeFixture();
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
const definitionPath = path.join(root, "packages/shared/src/app-definitions/github.json");
|
||||
const definition = JSON.parse(fs.readFileSync(definitionPath, "utf8"));
|
||||
definition.name = "GitHub override fixture";
|
||||
definition.branding = { logoUrl: "https://untrusted.invalid/logo.svg" };
|
||||
fs.writeFileSync(
|
||||
path.join(root, "scripts/app-definition-overrides/github.json"),
|
||||
JSON.stringify(definition),
|
||||
);
|
||||
|
||||
const result = runIngestion(root);
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const emitted = JSON.parse(fs.readFileSync(definitionPath, "utf8"));
|
||||
assert.equal(emitted.name, "GitHub override fixture");
|
||||
assert.deepEqual(emitted.branding, {
|
||||
logoUrl: "/brands/apps/github.svg",
|
||||
darkLogoUrl: "/brands/apps/github-dark.svg",
|
||||
});
|
||||
});
|
||||
|
||||
test("provider overrides cannot add an unreviewed tool method", (t) => {
|
||||
const root = makeFixture();
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
const definitionPath = path.join(root, "packages/shared/src/app-definitions/github.json");
|
||||
const definition = JSON.parse(fs.readFileSync(definitionPath, "utf8"));
|
||||
definition.methods.push({
|
||||
key: "mcp-unreviewed-fixture",
|
||||
transport: "mcp_remote",
|
||||
auth: "api_key",
|
||||
ownershipModes: ["customer"],
|
||||
defaults: { serverUrl: "https://mcp.github.com" },
|
||||
credentialFields: [
|
||||
{ key: "authorization", label: "Token", type: "password", required: true, placeholder: "token", secret: true },
|
||||
],
|
||||
keyPlacement: { location: "header", name: "Authorization", prefix: "Bearer " },
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(root, "scripts/app-definition-overrides/github.json"),
|
||||
JSON.stringify(definition),
|
||||
);
|
||||
|
||||
const result = runIngestion(root);
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /github\/mcp-unreviewed-fixture: permission review required/);
|
||||
});
|
||||
|
||||
test("Tavily curated MCP methods opt into initialized sessions", (t) => {
|
||||
const root = makeFixture();
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
const overridePath = path.join(root, "scripts/app-definition-overrides/tavily.json");
|
||||
const override = JSON.parse(fs.readFileSync(overridePath, "utf8"));
|
||||
assert.equal(override.methods[0].defaults.mcpSessionRequired, true);
|
||||
|
||||
const result = runIngestion(root);
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const emittedPath = path.join(root, "packages/shared/src/app-definitions/tavily.json");
|
||||
const emitted = JSON.parse(fs.readFileSync(emittedPath, "utf8"));
|
||||
assert.equal(emitted.methods[0].defaults.mcpSessionRequired, true);
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "airtable",
|
||||
"name": "Airtable",
|
||||
"description": "Connect Airtable's provider-hosted MCP server.",
|
||||
"categories": [
|
||||
"data"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/airtable.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.airtable.com/*"
|
||||
],
|
||||
"docsUrl": "https://support.airtable.com/articles/9897799762-using-the-airtable-mcp-server",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"oauthClientRegistration": "dcr",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Use browser sign-in for the provider-hosted MCP server.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.airtable.com/mcp",
|
||||
"scopesHint": [
|
||||
"data.records:read",
|
||||
"data.records:write",
|
||||
"schema.bases:read",
|
||||
"schema.bases:write",
|
||||
"data.recordComments:read",
|
||||
"data.recordComments:write",
|
||||
"workspacesAndBases:read"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Connect Airtable in the browser. An Airtable account; enterprise administrators may need to allowlist the client.",
|
||||
"riskTier": "S3",
|
||||
"label": "Sign in with Airtable",
|
||||
"consoleLinks": {
|
||||
"docs": "https://support.airtable.com/articles/9897799762-using-the-airtable-mcp-server"
|
||||
},
|
||||
"warnings": [
|
||||
"An Airtable account; enterprise administrators may need to allowlist the client."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-pat",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a personal access token when browser sign-in is not suitable.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.airtable.com/mcp"
|
||||
},
|
||||
"guidanceMd": "Create an Airtable personal access token with the scopes and base resources required for the actions you intend to allow. MCP calls inherit your Airtable role and base permissions; organization allowlisting may still apply.",
|
||||
"riskTier": "S3",
|
||||
"label": "Use a personal access token",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Airtable personal access token",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "pat...",
|
||||
"secret": true,
|
||||
"helperMd": "Choose the scopes and base resources required for your supported read and write actions. Airtable permissions and selected base resources still apply; Paperclip cannot increase an issued token’s access."
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"docs": "https://support.airtable.com/articles/9897799762-using-the-airtable-mcp-server",
|
||||
"keys": "https://airtable.com/create/tokens"
|
||||
},
|
||||
"warnings": [
|
||||
"Use a disposable or development base for initial write qualification. Enterprise admins may need to allowlist MCP access."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "calendly",
|
||||
"name": "Calendly",
|
||||
"description": "Review Calendly event types and availability, and use supported scheduling actions.",
|
||||
"categories": [
|
||||
"productivity"
|
||||
],
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/calendly.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.calendly.com/*"
|
||||
],
|
||||
"docsUrl": "https://developer.calendly.com/docs/mcp/calendly-mcp-server",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with Calendly",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Connect the provider-hosted Calendly MCP server with browser sign-in.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.calendly.com",
|
||||
"scopesHint": [
|
||||
"mcp:scheduling:read",
|
||||
"mcp:scheduling:write"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Check the connected account, event type, timezone, availability, and attendee details before using a scheduling write. Use a test scheduling artifact only when the discovered tool explicitly supports it.",
|
||||
"warnings": [
|
||||
"Calendly MCP requires DCR and OAuth 2.1 authorization-code with PKCE S256. It does not support personal access tokens or manually provisioned static OAuth clients.",
|
||||
"The provider advertises both scheduling read and write scopes for full access; verify the live tool catalog before enabling booking or link creation."
|
||||
],
|
||||
"consoleLinks": {
|
||||
"docs": "https://developer.calendly.com/docs/mcp/calendly-mcp-server"
|
||||
},
|
||||
"riskTier": "S3"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "exa",
|
||||
"name": "Exa",
|
||||
"description": "Search the public web and read source pages with Exa.",
|
||||
"categories": [
|
||||
"data"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/exa.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.exa.ai/*"
|
||||
],
|
||||
"docsUrl": "https://exa.ai/docs/get-started/exa-mcp",
|
||||
"methods": [
|
||||
{
|
||||
"key": "public-search",
|
||||
"label": "Use public Exa search",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use Exa’s public, keyless search MCP.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.exa.ai/mcp"
|
||||
},
|
||||
"guidanceMd": "The public MCP route provides web search and page fetching. Keep citations with results. Account-authenticated Agent and advanced tools are not included in this method.",
|
||||
"riskTier": "S2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "firecrawl",
|
||||
"name": "Firecrawl",
|
||||
"description": "Search, scrape, and parse public web pages with Firecrawl.",
|
||||
"categories": [
|
||||
"data"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/firecrawl.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.firecrawl.dev/*"
|
||||
],
|
||||
"docsUrl": "https://docs.firecrawl.dev/mcp-server",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with Firecrawl",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Use interactive Firecrawl account authorization for the full hosted MCP tool surface.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.firecrawl.dev/v2/mcp-oauth",
|
||||
"scopesHint": [
|
||||
"firecrawl:global",
|
||||
"offline_access"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Sign in and choose the Firecrawl team whose usage should apply. This route exposes the account-authorized tool surface; review paid jobs and action policies before execution.",
|
||||
"riskTier": "S4",
|
||||
"warnings": [
|
||||
"Account authorization exposes Firecrawl’s broader hosted MCP surface, including job-based research tools that may use account credits.",
|
||||
"Crawls and research jobs can use provider credits; review their tool actions and limits before execution."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"label": "Use a Firecrawl API key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a customer-created Firecrawl API key in a Bearer header for unattended or higher-limit access.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.firecrawl.dev/v2/mcp"
|
||||
},
|
||||
"guidanceMd": "Create a Firecrawl API key for the intended team, then connect it with a Bearer header. The authenticated server may expose additional job tools and spend team credits.",
|
||||
"riskTier": "S4",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Use a Firecrawl API key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Enter provider credential",
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"keys": "https://docs.firecrawl.dev/mcp-server",
|
||||
"docs": "https://docs.firecrawl.dev/mcp-server"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "keyless",
|
||||
"label": "Try keyless access",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use the rate-limited keyless public web-search route without a Firecrawl credential.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.firecrawl.dev/v2/mcp"
|
||||
},
|
||||
"guidanceMd": "The keyless route currently exposes Search, Scrape, and Parse. Keep requests bounded; these calls can consume the provider’s keyless allowance.",
|
||||
"riskTier": "S2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "gsc-wizard",
|
||||
"name": "GSC Wizard",
|
||||
"description": "Read Google Search Console, Bing Webmaster, and analytics data for connected properties.",
|
||||
"categories": [
|
||||
"analytics"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/gsc-wizard.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.gscwizard.com/*"
|
||||
],
|
||||
"docsUrl": "https://www.gscwizard.com/claude-search-console-mcp.html",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with GSC Wizard",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Use remote OAuth authorization for the GSC Wizard MCP.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.gscwizard.com/mcp",
|
||||
"scopesHint": [
|
||||
"mcp:read",
|
||||
"mcp:write"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Connect the GSC Wizard account and its authorized properties. Check site identity and date bounds before querying; review any URL submission or property-changing action separately.",
|
||||
"riskTier": "S3",
|
||||
"warnings": [
|
||||
"Some tools may submit URLs or otherwise change external indexing state; review action details before use.",
|
||||
"GSC Wizard connects multiple search and analytics providers; confirm the authorized property and action policy for each tool."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"label": "Use a GSC Wizard MCP key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a customer-created GSC Wizard MCP key in a Bearer header.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.gscwizard.com/mcp"
|
||||
},
|
||||
"guidanceMd": "Create a key for the intended account and select the appropriate read or write permission during key creation. Verify each connected property before querying; review URL submission actions separately.",
|
||||
"riskTier": "S3",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Use a GSC Wizard MCP key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Enter provider credential",
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"keys": "https://www.gscwizard.com/claude-search-console-mcp.html",
|
||||
"docs": "https://www.gscwizard.com/claude-search-console-mcp.html"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "linear",
|
||||
"name": "Linear",
|
||||
"description": "Create, update, and read Linear issues.",
|
||||
"categories": [
|
||||
"productivity"
|
||||
],
|
||||
"featured": true,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/linear.svg",
|
||||
"darkLogoUrl": "/brands/apps/linear-dark.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.linear.app/*"
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr",
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use the provider-hosted connection for the quickest setup.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.linear.app/mcp",
|
||||
"authorizationEndpoint": "https://linear.app/oauth/authorize",
|
||||
"tokenEndpoint": "https://api.linear.app/oauth/token",
|
||||
"scopesHint": [
|
||||
"read",
|
||||
"write"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Connect Linear for issues and projects. Paperclip registers its own OAuth client with Linear MCP; no developer-console setup is needed.",
|
||||
"riskTier": "S2",
|
||||
"requiredResourceFilters": [
|
||||
"workspace",
|
||||
"team",
|
||||
"project"
|
||||
],
|
||||
"credentialSources": {
|
||||
"vercelConnect": {
|
||||
"services": [
|
||||
"linear"
|
||||
],
|
||||
"principalModes": [
|
||||
"user"
|
||||
],
|
||||
"scopes": [
|
||||
"read",
|
||||
"write"
|
||||
],
|
||||
"header": {
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": "Sign in with Linear",
|
||||
"consoleLinks": {
|
||||
"docs": "https://linear.app/docs/mcp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a Linear API key when browser sign-in is not suitable.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.linear.app/mcp"
|
||||
},
|
||||
"guidanceMd": "Create a Linear API key with the permissions needed for the actions you intend to allow. A key restricted to Read is supported for read-only use; workspace and role access still apply.",
|
||||
"riskTier": "S2",
|
||||
"label": "Use an API key",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Linear API key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"secret": true,
|
||||
"helperMd": "Grant Read and Write only when those actions are intended. A Read-only key is supported; the key cannot exceed the owning user\u2019s workspace access.",
|
||||
"placeholder": "Paste your Linear API key"
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"docs": "https://linear.app/docs/api-and-webhooks"
|
||||
},
|
||||
"warnings": [
|
||||
"Linear\u2019s standard MCP endpoint exposes read and write tools; review the discovered catalog and Paperclip action policies."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "make",
|
||||
"name": "Make",
|
||||
"description": "Connect Make's provider-hosted MCP server.",
|
||||
"categories": [
|
||||
"productivity"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/make.svg",
|
||||
"darkLogoUrl": "/brands/apps/make-dark.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.make.com/*",
|
||||
"https://*.make.com/mcp"
|
||||
],
|
||||
"docsUrl": "https://developers.make.com/mcp-server",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Use browser sign-in for the provider-hosted MCP server.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.make.com"
|
||||
},
|
||||
"guidanceMd": "Connect Make in the browser. A Make account and access to the scenarios exposed to MCP.",
|
||||
"riskTier": "S3",
|
||||
"label": "Sign in with Make",
|
||||
"consoleLinks": {
|
||||
"docs": "https://developers.make.com/mcp-server"
|
||||
},
|
||||
"warnings": [
|
||||
"A Make account and access to the scenarios exposed to MCP."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-token",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a Make MCP token when browser sign-in is not suitable.",
|
||||
"defaults": {
|
||||
"serverUrlTemplate": "https://{zone}/mcp"
|
||||
},
|
||||
"guidanceMd": "Create a Make API token with the mcp:use scope and any additional scopes required by the selected scenarios. Enter the zone host from your Make organization. The token is sent as a Bearer header and remains a secret.",
|
||||
"riskTier": "S3",
|
||||
"label": "Use an MCP token",
|
||||
"tenantFields": [
|
||||
{
|
||||
"key": "zone",
|
||||
"label": "Make zone host",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "eu2.make.com",
|
||||
"helperMd": "Enter the zone host for your Make organization exactly as shown in the Make MCP client setup instructions. The server URL is https://<zone>/mcp.",
|
||||
"validation": {
|
||||
"pattern": "^[A-Za-z0-9-]+\\.make\\.com$",
|
||||
"maxLength": 253
|
||||
}
|
||||
}
|
||||
],
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Make MCP token",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Paste your MCP token",
|
||||
"secret": true,
|
||||
"helperMd": "Create a Make API token that includes the provider-documented mcp:use scope and only other scopes needed by the scenario tools you intend to use."
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"docs": "https://developers.make.com/mcp-server/connect-using-mcp-token"
|
||||
},
|
||||
"warnings": [
|
||||
"A token is a secret; do not place it in the server URL. Scenario availability depends on token scopes and scenario access controls."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "parallel-search",
|
||||
"name": "Parallel Search",
|
||||
"description": "Search the public web and extract source pages with Parallel.",
|
||||
"categories": [
|
||||
"data"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/parallel.svg",
|
||||
"darkLogoUrl": "/brands/apps/parallel-dark.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://search.parallel.ai/*"
|
||||
],
|
||||
"docsUrl": "https://docs.parallel.ai/integrations/mcp/search-mcp",
|
||||
"methods": [
|
||||
{
|
||||
"key": "public-search",
|
||||
"label": "Use public Parallel Search",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use Parallel Search’s public, keyless MCP route.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://search.parallel.ai/mcp"
|
||||
},
|
||||
"guidanceMd": "The public route exposes web_search and web_fetch. Keep source URLs, excerpts, and retrieval context in the result; this is separate from Parallel Task MCP.",
|
||||
"riskTier": "S2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "tavily",
|
||||
"name": "Tavily",
|
||||
"description": "Search the public web and extract page content with Tavily\u2019s hosted MCP server.",
|
||||
"categories": [
|
||||
"analytics"
|
||||
],
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/tavily.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.tavily.com/*"
|
||||
],
|
||||
"docsUrl": "https://docs.tavily.com/documentation/mcp",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with Tavily",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Connect the provider-hosted Tavily MCP server with browser sign-in.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.tavily.com/mcp",
|
||||
"scopesHint": [
|
||||
"openid",
|
||||
"offline_access"
|
||||
],
|
||||
"mcpSessionRequired": true
|
||||
},
|
||||
"guidanceMd": "Browser sign-in requires a public HTTPS Paperclip callback; local HTTP callbacks are rejected by Tavily. Use Tavily for bounded public research. Set focused queries and relevant domains, cite the source URLs, and distinguish retrieved facts from inference. Research does not authorize contacting a company or submitting forms.",
|
||||
"warnings": [
|
||||
"Tavily OAuth selects the account\u2019s `mcp_auth_default` key when configured; otherwise it may use the personal default or first available key. Review the chosen account and usage allowance.",
|
||||
"Do not use the documented API-key-in-query alternative; OAuth keeps the key out of the server URL."
|
||||
],
|
||||
"consoleLinks": {
|
||||
"docs": "https://docs.tavily.com/documentation/mcp"
|
||||
},
|
||||
"riskTier": "S2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "windsor-ai",
|
||||
"name": "Windsor.ai",
|
||||
"description": "Query connected marketing, analytics, CRM, and warehouse data through Windsor.ai.",
|
||||
"categories": [
|
||||
"analytics"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/windsor-ai.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://mcp.windsor.ai/*"
|
||||
],
|
||||
"docsUrl": "https://mcp.windsor.ai/",
|
||||
"redirectConstraints": "https-or-loopback-http",
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp-oauth",
|
||||
"label": "Sign in with Windsor.ai",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "oauth",
|
||||
"ownershipModes": [
|
||||
"dcr"
|
||||
],
|
||||
"whenToUse": "Use browser OAuth for Windsor.ai\u2019s hosted data MCP.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.windsor.ai/",
|
||||
"scopesHint": [
|
||||
"create",
|
||||
"delete",
|
||||
"offline_access"
|
||||
]
|
||||
},
|
||||
"guidanceMd": "Browser sign-in requires a public HTTPS Paperclip callback; local HTTP callbacks are rejected by Windsor.ai. Connect Windsor.ai, then verify which advertising, analytics, CRM, or warehouse sources are linked. Keep source account and metric definitions with results. Review any action that changes campaigns, budgets, posts, or listings.",
|
||||
"riskTier": "S4",
|
||||
"warnings": [
|
||||
"Windsor.ai requires the provider-level delete scope to establish the MCP session, including for read-only use. This scope may expose destructive actions; do not approve it unless those provider permissions are acceptable.",
|
||||
"The server includes actions across connected marketing platforms. A source connection does not imply authorization to modify budgets or publish content.",
|
||||
"The server exposes write actions across some connected ad and commerce platforms."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp-api-key",
|
||||
"label": "Use a Windsor.ai API key",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use a customer-created Windsor.ai API key as a Bearer credential.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.windsor.ai/"
|
||||
},
|
||||
"guidanceMd": "Create a Windsor.ai key for the intended account. Verify connected source accounts and action availability before use; broad tool exposure may include advertising changes.",
|
||||
"riskTier": "S4",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "authorization",
|
||||
"label": "Use a Windsor.ai API key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Enter provider credential",
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"keyPlacement": {
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"prefix": "Bearer "
|
||||
},
|
||||
"consoleLinks": {
|
||||
"keys": "https://mcp.windsor.ai/",
|
||||
"docs": "https://mcp.windsor.ai/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -185,7 +185,7 @@ const apps = [
|
||||
key: "email-agent", label: "Email with an agent", purpose: "channel", provider: "agentmail", transport: "rest_api", auth: "api_key", ownershipModes: ["customer"],
|
||||
whenToUse: "Assign an inbox to an agent and manage email conversations in tasks.", credentialFields: [{ key: "apiKey", label: "AgentMail API key", type: "password", placeholder: "am_…", required: true, secret: true }],
|
||||
guidanceMd: "Connect an AgentMail API key, then create or select an inbox for your agent. WebSocket receiving works without a public URL.",
|
||||
consoleLinks: { keys: "https://console.agentmail.to", docs: "https://docs.agentmail.to/inboxes" }, riskTier: "S3", requiredResourceFilters: ["inbox"]
|
||||
consoleLinks: { keys: "https://console.agentmail.to/dashboard/api-keys", docs: "https://docs.agentmail.to/inboxes" }, riskTier: "S3", requiredResourceFilters: ["inbox"]
|
||||
}],
|
||||
[
|
||||
"zapier",
|
||||
@@ -1758,7 +1758,10 @@ for (const entry of researchManifest.entries) {
|
||||
}
|
||||
let methods = specialMethodsFor(entry);
|
||||
if (!methods) {
|
||||
if (entry.authMode === "customer_oauth")
|
||||
if (entry.authMode === "public")
|
||||
methods = [method("public-mcp", "mcp_remote", "none", { serverUrl: entry.serverUrl }, entry.riskTier,
|
||||
"Connect the provider's public MCP server without an account credential.", { label: "Connect public tools" })];
|
||||
else if (entry.authMode === "customer_oauth")
|
||||
methods = [customerOAuthMethodFor(entry)];
|
||||
else if (entry.authMode === "api_key") methods = [apiKeyMethodFor(entry)];
|
||||
else {
|
||||
@@ -1949,6 +1952,23 @@ for (const { slug, name, provider, subscription, envKey, url, description } of a
|
||||
// AI account flow; saved REST connections remain removable through Connections.
|
||||
app.methods = [...methods, ...app.methods.filter(method => method.transport !== "rest_api")];
|
||||
}
|
||||
// Reviewed provider-specific source data stays separate from generated output.
|
||||
// Apply it after legacy/research inputs, then run the same branding, permission,
|
||||
// instruction-template and validation pipeline as every other definition.
|
||||
const overridesDirectory = path.join(root, "scripts/app-definition-overrides");
|
||||
if (fs.existsSync(overridesDirectory)) {
|
||||
for (const fileName of fs.readdirSync(overridesDirectory).filter(name => name.endsWith(".json")).sort()) {
|
||||
const override = JSON.parse(fs.readFileSync(path.join(overridesDirectory, fileName), "utf8"));
|
||||
if (!override.slug || fileName !== `${override.slug}.json` || !Array.isArray(override.methods)) {
|
||||
throw new Error(`${fileName}: invalid provider definition source`);
|
||||
}
|
||||
override.branding = brandingFor(override.slug);
|
||||
const existingIndex = apps.findIndex(app => app.slug === override.slug);
|
||||
if (existingIndex === -1) apps.push(override);
|
||||
else apps[existingIndex] = { ...apps[existingIndex], ...override };
|
||||
}
|
||||
}
|
||||
|
||||
// Every tool method has a checked-in permission review. Discovery metadata is
|
||||
// evidence for reviewers, never a runtime instruction to request more scopes.
|
||||
const permissionReviews = JSON.parse(fs.readFileSync(
|
||||
|
||||
@@ -50,6 +50,49 @@ describe("remote connector lifecycle", () => {
|
||||
});
|
||||
return { service, requests, add: () => { added = true; }, remove: () => { removed = true; }, restore: () => { removed = false; } };
|
||||
}
|
||||
it("advertises the initiating OAuth callback and preserves explicitly configured origins", async () => {
|
||||
const org = await company();
|
||||
const app = express();
|
||||
app.use((req, _res, next) => { req.actor = { type: "board", userId: actor.actorId, source: "local_implicit", isInstanceAdmin: true }; next(); });
|
||||
app.use("/api", toolAccessRoutes(db, { paperclipCloudConnector: null }));
|
||||
|
||||
const publicBaseUrlEnv = process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL;
|
||||
const otherPublicUrlNames = ["BETTER_AUTH_URL", "BETTER_AUTH_BASE_URL", "PAPERCLIP_PUBLIC_URL", "PAPERCLIP_MANAGED_RUNTIME_PUBLIC_URL"] as const;
|
||||
const otherPublicUrlEnv = Object.fromEntries(otherPublicUrlNames.map((name) => [name, process.env[name]]));
|
||||
try {
|
||||
for (const name of otherPublicUrlNames) process.env[name] = "";
|
||||
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL = "https://paperclip.example.test";
|
||||
const configured = await request(app).get(`/api/companies/${org.id}/tools/gallery`);
|
||||
expect(configured.status).toBe(200);
|
||||
expect(configured.body.oauthCallbackUrl).toBe("https://paperclip.example.test/api/tools/oauth/callback");
|
||||
|
||||
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL = "http://127.0.0.1:3105";
|
||||
const configuredLoopback = await request(app).get(`/api/companies/${org.id}/tools/gallery`);
|
||||
expect(configuredLoopback.status).toBe(200);
|
||||
expect(configuredLoopback.body.oauthCallbackUrl).toBe("http://127.0.0.1:3105/api/tools/oauth/callback");
|
||||
|
||||
delete process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL;
|
||||
const inferredLoopback = await request(app)
|
||||
.get(`/api/companies/${org.id}/tools/gallery`)
|
||||
.set("host", "127.0.0.1:3105");
|
||||
expect(inferredLoopback.status).toBe(200);
|
||||
expect(inferredLoopback.body.oauthCallbackUrl).toBe("http://localhost:3105/api/tools/oauth/callback");
|
||||
|
||||
const unsupported = await request(app)
|
||||
.get(`/api/companies/${org.id}/tools/gallery`)
|
||||
.set("host", "paperclip.internal.test");
|
||||
expect(unsupported.status).toBe(200);
|
||||
expect(unsupported.body).not.toHaveProperty("oauthCallbackUrl");
|
||||
} finally {
|
||||
if (publicBaseUrlEnv === undefined) delete process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL;
|
||||
else process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL = publicBaseUrlEnv;
|
||||
for (const name of otherPublicUrlNames) {
|
||||
const value = otherPublicUrlEnv[name];
|
||||
if (value === undefined) delete process.env[name];
|
||||
else process.env[name] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
it.each([undefined, false])("allows all aggregator setup with stored setting %s", async (legacyValue) => {
|
||||
await db.delete(instanceSettings);
|
||||
if (legacyValue !== undefined) await db.insert(instanceSettings).values({ experimental: { enableMcpAggregators: legacyValue } });
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
} from "@paperclipai/db";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import {
|
||||
APP_DEFINITIONS,
|
||||
APP_STORE_HIDDEN_SLUGS,
|
||||
GITHUB_CONNECTOR_PROFILES,
|
||||
GOOGLE_WORKSPACE_CONNECTOR_PROFILE_IDS,
|
||||
@@ -112,6 +113,17 @@ function createTestToolAccessService(
|
||||
});
|
||||
}
|
||||
|
||||
function optIntoAirtableDcrRegistration() {
|
||||
const method = APP_DEFINITIONS.find((app) => app.slug === "airtable")!.methods
|
||||
.find((entry) => entry.key === "mcp-oauth")!;
|
||||
const original = method.oauthClientRegistration;
|
||||
method.oauthClientRegistration = "dcr";
|
||||
return () => {
|
||||
if (original === undefined) delete method.oauthClientRegistration;
|
||||
else method.oauthClientRegistration = original;
|
||||
};
|
||||
}
|
||||
|
||||
function fakeGoogleWorkspaceConnector(
|
||||
companyId: string,
|
||||
userId: string,
|
||||
@@ -5214,7 +5226,18 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
"telem",
|
||||
]),
|
||||
);
|
||||
expect(res.body.apps).toHaveLength(69);
|
||||
expect(res.body.apps.length).toBeGreaterThanOrEqual(69);
|
||||
expect(res.body.apps.map((app: { slug: string }) => app.slug)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"calendly",
|
||||
"exa",
|
||||
"firecrawl",
|
||||
"gsc-wizard",
|
||||
"parallel-search",
|
||||
"tavily",
|
||||
"windsor-ai",
|
||||
]),
|
||||
);
|
||||
for (const slug of ["openrouter", "bedrock", "responses-api", "messages-api", "chat-completions-api", "local"]) {
|
||||
expect(res.body.apps.find((app: { slug: string }) => app.slug === slug).tags).toContain("model-provider");
|
||||
}
|
||||
@@ -11073,6 +11096,204 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses Airtable DCR when both DCR and CIMD are advertised", async () => {
|
||||
vi.stubEnv("PAPERCLIP_PUBLIC_URL", "https://paperclip.example");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_AIRTABLE_CLIENT_ID", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_AIRTABLE_CLIENT_SECRET", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_CLIENT_ID", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_CLIENT_SECRET", "");
|
||||
const restoreMethod = optIntoAirtableDcrRegistration();
|
||||
try {
|
||||
const company = await createCompany(db);
|
||||
let metadataLookups = 0;
|
||||
let registrations = 0;
|
||||
const service = createTestToolAccessService(db, {
|
||||
oauthClientMetadataLookup: async () => {
|
||||
metadataLookups++;
|
||||
return [{ address: "93.184.216.34", family: 4 }];
|
||||
},
|
||||
});
|
||||
const connected = await service.connectGalleryApp(company.id, {
|
||||
galleryKey: "airtable", connectionMethodKey: "mcp-oauth", name: "Airtable DCR",
|
||||
});
|
||||
const redirectUri = "https://paperclip.example/api/tools/oauth/callback";
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => {
|
||||
const href = String(url);
|
||||
if (href === "https://mcp.airtable.com/.well-known/oauth-protected-resource/mcp") {
|
||||
return mcpHttpResponse({
|
||||
resource: "https://mcp.airtable.com",
|
||||
authorization_servers: ["https://airtable.com/oauth2/v1"],
|
||||
scopes_supported: ["data.records:read", "data.records:write"],
|
||||
});
|
||||
}
|
||||
if (href === "https://airtable.com/.well-known/oauth-authorization-server/oauth2/v1") {
|
||||
return mcpHttpResponse({
|
||||
issuer: "https://airtable.com/oauth2/v1",
|
||||
authorization_endpoint: "https://airtable.com/oauth2/v1/authorize",
|
||||
token_endpoint: "https://airtable.com/oauth2/v1/token",
|
||||
registration_endpoint: "https://airtable.com/oauth2/v1/register",
|
||||
client_id_metadata_document_supported: true,
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
token_endpoint_auth_methods_supported: ["none"],
|
||||
});
|
||||
}
|
||||
if (href === "https://airtable.com/oauth2/v1/register") {
|
||||
registrations++;
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({
|
||||
redirect_uris: [redirectUri],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
token_endpoint_auth_method: "none",
|
||||
application_type: "web",
|
||||
});
|
||||
return mcpHttpResponse({
|
||||
client_id: "airtable-dcr-client",
|
||||
redirect_uris: [redirectUri],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
token_endpoint_auth_method: "none",
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch ${href}`);
|
||||
});
|
||||
|
||||
const start = await service.startOAuth(company.id, connected.connectionId, {
|
||||
redirectUri, actor: { actorType: "user", actorId: "board" },
|
||||
});
|
||||
|
||||
expect(start.registrationSource).toBe("dcr");
|
||||
expect(new URL(start.authorizationUrl).searchParams.get("client_id")).toBe("airtable-dcr-client");
|
||||
expect(registrations).toBe(1);
|
||||
expect(metadataLookups).toBe(0);
|
||||
} finally {
|
||||
restoreMethod();
|
||||
}
|
||||
});
|
||||
|
||||
it("rebinds an Airtable CIMD client through DCR when the curated method opts in", async () => {
|
||||
vi.stubEnv("PAPERCLIP_PUBLIC_URL", "https://paperclip.example");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_AIRTABLE_CLIENT_ID", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_AIRTABLE_CLIENT_SECRET", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_CLIENT_ID", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_CLIENT_SECRET", "");
|
||||
const company = await createCompany(db);
|
||||
const service = createTestToolAccessService(db, {
|
||||
oauthClientMetadataLookup: async () => [{ address: "93.184.216.34", family: 4 }],
|
||||
});
|
||||
const connected = await service.connectGalleryApp(company.id, {
|
||||
galleryKey: "airtable", connectionMethodKey: "mcp-oauth", name: "Airtable migration",
|
||||
});
|
||||
const redirectUri = "https://paperclip.example/api/tools/oauth/callback";
|
||||
let registrations = 0;
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => {
|
||||
const href = String(url);
|
||||
if (href === "https://mcp.airtable.com/.well-known/oauth-protected-resource/mcp") {
|
||||
return mcpHttpResponse({
|
||||
resource: "https://mcp.airtable.com",
|
||||
authorization_servers: ["https://airtable.com/oauth2/v1"],
|
||||
});
|
||||
}
|
||||
if (href === "https://airtable.com/.well-known/oauth-authorization-server/oauth2/v1") {
|
||||
return mcpHttpResponse({
|
||||
issuer: "https://airtable.com/oauth2/v1",
|
||||
authorization_endpoint: "https://airtable.com/oauth2/v1/authorize",
|
||||
token_endpoint: "https://airtable.com/oauth2/v1/token",
|
||||
registration_endpoint: "https://airtable.com/oauth2/v1/register",
|
||||
client_id_metadata_document_supported: true,
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
token_endpoint_auth_methods_supported: ["none"],
|
||||
});
|
||||
}
|
||||
if (href === "https://airtable.com/oauth2/v1/register") {
|
||||
registrations++;
|
||||
return mcpHttpResponse({
|
||||
client_id: "airtable-rebound-dcr-client",
|
||||
redirect_uris: [redirectUri],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
token_endpoint_auth_method: "none",
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch ${href}`);
|
||||
});
|
||||
|
||||
const airtableMethod = APP_DEFINITIONS.find((app) => app.slug === "airtable")!.methods
|
||||
.find((entry) => entry.key === "mcp-oauth")!;
|
||||
const originalRegistration = airtableMethod.oauthClientRegistration;
|
||||
try {
|
||||
// Generated Airtable metadata now opts into DCR. Remove that curated
|
||||
// flag here to model an existing client binding created before the opt-in.
|
||||
delete airtableMethod.oauthClientRegistration;
|
||||
const cimdStart = await service.startOAuth(company.id, connected.connectionId, {
|
||||
redirectUri, actor: { actorType: "user", actorId: "board" },
|
||||
});
|
||||
expect(cimdStart.registrationSource).toBe("cimd");
|
||||
|
||||
airtableMethod.oauthClientRegistration = "dcr";
|
||||
const dcrStart = await service.startOAuth(company.id, connected.connectionId, {
|
||||
redirectUri, actor: { actorType: "user", actorId: "board" },
|
||||
});
|
||||
expect(dcrStart.registrationSource).toBe("dcr");
|
||||
expect(new URL(dcrStart.authorizationUrl).searchParams.get("client_id")).toBe("airtable-rebound-dcr-client");
|
||||
expect(registrations).toBe(1);
|
||||
} finally {
|
||||
if (originalRegistration === undefined) delete airtableMethod.oauthClientRegistration;
|
||||
else airtableMethod.oauthClientRegistration = originalRegistration;
|
||||
}
|
||||
});
|
||||
|
||||
it("does not fall back to Airtable CIMD when DCR is required but unavailable", async () => {
|
||||
vi.stubEnv("PAPERCLIP_PUBLIC_URL", "https://paperclip.example");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_AIRTABLE_CLIENT_ID", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_AIRTABLE_CLIENT_SECRET", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_CLIENT_ID", "");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_CLIENT_SECRET", "");
|
||||
const restoreMethod = optIntoAirtableDcrRegistration();
|
||||
try {
|
||||
const company = await createCompany(db);
|
||||
let metadataLookups = 0;
|
||||
const service = createTestToolAccessService(db, {
|
||||
oauthClientMetadataLookup: async () => {
|
||||
metadataLookups++;
|
||||
return [{ address: "93.184.216.34", family: 4 }];
|
||||
},
|
||||
});
|
||||
const connected = await service.connectGalleryApp(company.id, {
|
||||
galleryKey: "airtable", connectionMethodKey: "mcp-oauth", name: "Airtable DCR unavailable",
|
||||
});
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => {
|
||||
const href = String(url);
|
||||
if (href === "https://mcp.airtable.com/.well-known/oauth-protected-resource/mcp") {
|
||||
return mcpHttpResponse({
|
||||
resource: "https://mcp.airtable.com",
|
||||
authorization_servers: ["https://airtable.com/oauth2/v1"],
|
||||
});
|
||||
}
|
||||
if (href === "https://airtable.com/.well-known/oauth-authorization-server/oauth2/v1") {
|
||||
return mcpHttpResponse({
|
||||
issuer: "https://airtable.com/oauth2/v1",
|
||||
authorization_endpoint: "https://airtable.com/oauth2/v1/authorize",
|
||||
token_endpoint: "https://airtable.com/oauth2/v1/token",
|
||||
client_id_metadata_document_supported: true,
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
token_endpoint_auth_methods_supported: ["none"],
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch ${href}`);
|
||||
});
|
||||
|
||||
await expect(service.startOAuth(company.id, connected.connectionId, {
|
||||
redirectUri: "https://paperclip.example/api/tools/oauth/callback",
|
||||
actor: { actorType: "user", actorId: "board" },
|
||||
})).rejects.toMatchObject({ details: { code: "oauth_dcr_not_supported" } });
|
||||
expect(metadataLookups).toBe(0);
|
||||
const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, connected.connectionId));
|
||||
expect((connection.config.oauth as Record<string, unknown> | undefined)?.clientId).toBeUndefined();
|
||||
} finally {
|
||||
restoreMethod();
|
||||
}
|
||||
});
|
||||
|
||||
it("registers Linear against its MCP authorization server instead of the pinned console endpoints", async () => {
|
||||
vi.stubEnv("PAPERCLIP_PUBLIC_URL", "https://paperclip.example");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_LINEAR_CLIENT_ID", "");
|
||||
@@ -11121,7 +11342,7 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
|
||||
const connectRes = await request(app)
|
||||
.post(`/api/companies/${company.id}/tools/apps/connect`)
|
||||
.send({ galleryKey: "linear", name: "Linear", grantKind: "user" })
|
||||
.send({ galleryKey: "linear", connectionMethodKey: "mcp-oauth", name: "Linear", grantKind: "user" })
|
||||
.expect(201);
|
||||
|
||||
const startUrl = new URL(connectRes.body.auth.startUrl);
|
||||
@@ -11131,6 +11352,52 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
expect(fetched.some((href) => href.startsWith("https://linear.app/"))).toBe(false);
|
||||
});
|
||||
|
||||
it("registers Calendly with a provider-compatible DCR client name", async () => {
|
||||
const company = await createCompany(db);
|
||||
const service = createTestToolAccessService(db);
|
||||
const connected = await service.connectGalleryApp(company.id, {
|
||||
galleryKey: "calendly", connectionMethodKey: "mcp-oauth", name: "Calendly",
|
||||
});
|
||||
const redirectUri = "http://localhost:3105/api/tools/oauth/callback";
|
||||
let registrationBody: Record<string, unknown> | null = null;
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => {
|
||||
const href = String(url);
|
||||
if (href.includes("oauth-protected-resource")) return mcpHttpResponse({
|
||||
resource: "https://mcp.calendly.com/", authorization_servers: ["https://calendly.com/"],
|
||||
scopes_supported: ["mcp:scheduling:read", "mcp:scheduling:write"],
|
||||
});
|
||||
if (href.includes("oauth-authorization-server")) return mcpHttpResponse({
|
||||
issuer: "https://calendly.com/", authorization_endpoint: "https://calendly.com/oauth/authorize",
|
||||
token_endpoint: "https://calendly.com/oauth/token", registration_endpoint: "https://calendly.com/oauth/register",
|
||||
code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"],
|
||||
});
|
||||
if (href === "https://calendly.com/oauth/register") {
|
||||
registrationBody = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
||||
return mcpHttpResponse({
|
||||
client_id: "calendly-public-client", redirect_uris: [redirectUri],
|
||||
grant_types: ["authorization_code", "refresh_token"], response_types: ["code"],
|
||||
token_endpoint_auth_method: "none",
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch ${href}`);
|
||||
});
|
||||
|
||||
const result = await service.startOAuth(company.id, connected.connectionId, {
|
||||
redirectUri, actor: { actorType: "user", actorId: "board" },
|
||||
});
|
||||
|
||||
expect(registrationBody).toMatchObject({
|
||||
client_name: "Paperclip localhost-3105",
|
||||
redirect_uris: [redirectUri],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
token_endpoint_auth_method: "none",
|
||||
application_type: "web",
|
||||
});
|
||||
expect(registrationBody?.client_name).toMatch(/^[A-Za-z0-9 -]+$/);
|
||||
expect(new URL(result.authorizationUrl).searchParams.get("client_id")).toBe("calendly-public-client");
|
||||
});
|
||||
|
||||
it("returns a pre-scoped personal Notion callback directly to Permissions", async () => {
|
||||
vi.stubEnv("PAPERCLIP_PUBLIC_URL", "https://paperclip.example");
|
||||
vi.stubEnv("PAPERCLIP_TOOL_OAUTH_NOTION_CLIENT_ID", "");
|
||||
@@ -12038,7 +12305,7 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
).toBe("notion-dcr-client");
|
||||
expect(registrationBodies).toEqual([
|
||||
{
|
||||
client_name: "Paperclip (paperclip-dev.tail29c1aa.ts.net)",
|
||||
client_name: "Paperclip paperclip-dev-tail29c1aa-ts-net",
|
||||
redirect_uris: [redirectUri],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
|
||||
@@ -1275,11 +1275,19 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("initializes a fresh Streamable HTTP session before a stateful tools/call", async () => {
|
||||
it.each([
|
||||
["persisted session preference", true],
|
||||
["retained connection curated default", false],
|
||||
] as const)("initializes Tavily's session before its first tools/call from the %s", async (_caseName, persistPreference) => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
const { connection } = await createRemoteMcpToolFixture(db, company.id);
|
||||
await db.update(toolConnections).set({
|
||||
config: { url: "https://example.invalid/mcp", mcpSessionRequired: true },
|
||||
config: {
|
||||
url: "https://mcp.tavily.com/mcp",
|
||||
sourceTemplateKey: "tavily",
|
||||
connectionMethodKey: "mcp-oauth",
|
||||
...(persistPreference ? { mcpSessionRequired: true } : {}),
|
||||
},
|
||||
}).where(eq(toolConnections.id, connection.id));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
@@ -1341,6 +1349,36 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps direct tools/call for generic connections without the curated session preference", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
const { connection } = await createRemoteMcpToolFixture(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow read tools",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
const requests: string[] = [];
|
||||
const gateway = createTestToolGatewayService(db, {
|
||||
remoteHttpRequest: async (_url, init) => {
|
||||
const payload = JSON.parse(String(init.body)) as { method?: string; id?: string };
|
||||
requests.push(payload.method ?? "");
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id: payload.id,
|
||||
result: { content: [{ type: "text", text: "generic ok" }] },
|
||||
});
|
||||
},
|
||||
});
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: tool!.name, parameters: {} }))
|
||||
.resolves.toMatchObject({ status: "completed" });
|
||||
expect(requests).toEqual(["tools/call"]);
|
||||
});
|
||||
|
||||
it("hides cached Chat unread filters and blocks agent and board requests before provider dispatch", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
const { connection, catalogEntry } = await createRemoteMcpToolFixture(db, company.id);
|
||||
@@ -1449,7 +1487,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("injects Vercel tokens at dispatch and refreshes exactly once after an upstream 401", async () => {
|
||||
it("refreshes Vercel tokens after an upstream 401 without replaying the tool call", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
const { connection } = await createRemoteMcpToolFixture(db, company.id);
|
||||
await db.update(toolConnections).set({
|
||||
@@ -1511,17 +1549,16 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
const result = await gateway.executeTool({
|
||||
await expect(gateway.executeTool({
|
||||
sessionToken: session.token,
|
||||
tool: tool!.name,
|
||||
parameters: {},
|
||||
})).rejects.toMatchObject({
|
||||
status: 409,
|
||||
reasonCode: "oauth_refreshed_retry_required",
|
||||
});
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(authorizationHeaders).toEqual([
|
||||
"Bearer stale-provider-bearer",
|
||||
"Bearer fresh-provider-bearer",
|
||||
]);
|
||||
expect(authorizationHeaders).toEqual(["Bearer stale-provider-bearer"]);
|
||||
expect(getToken).toHaveBeenCalledTimes(2);
|
||||
expect(getToken.mock.calls[1]?.[1]).toEqual({ forceRefresh: true });
|
||||
expect(evict).toHaveBeenCalledTimes(1);
|
||||
@@ -1639,7 +1676,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
expect(refreshed).toEqual(upstreamFailure ? [grants[1]!.id] : [grants[1]!.id, grants[0]!.id]);
|
||||
});
|
||||
|
||||
it("refreshes a customer OAuth grant once and retries after an upstream 401", async () => {
|
||||
it("refreshes customer OAuth after a session 401 without replaying the call, then uses a fresh session on explicit retry", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
const { connection } = await createRemoteMcpToolFixture(db, company.id);
|
||||
const accessSecret = await secretService(db).create(company.id, {
|
||||
@@ -1661,6 +1698,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
credentialRefs: [{ name: "oauth.access_token", placement: "header", key: "Authorization", prefix: "Bearer ", secretId: accessSecret.id, versionSelector: "latest" }],
|
||||
config: {
|
||||
url: "https://example.invalid/mcp",
|
||||
mcpSessionRequired: true,
|
||||
oauth: {
|
||||
provider: "fixture",
|
||||
tokenUrl: "https://example.invalid/oauth/token",
|
||||
@@ -1701,13 +1739,37 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
return db.select().from(connectionGrants).where(eq(connectionGrants.id, input.grantId))
|
||||
.then((rows) => rows[0]!);
|
||||
});
|
||||
const authorizationHeaders: string[] = [];
|
||||
const requests: Array<{ method: string; authorization: string | null; sessionId: string | null; protocolVersion: string | null }> = [];
|
||||
let callDispatches = 0;
|
||||
const gateway = createTestToolGatewayService(db, {
|
||||
oauthGrantRefresher,
|
||||
remoteHttpRequest: async (_url, init) => {
|
||||
authorizationHeaders.push(new Headers(init.headers).get("authorization") ?? "");
|
||||
if (authorizationHeaders.length === 1) return new Response(null, { status: 401 });
|
||||
const requestBody = JSON.parse(String(init.body)) as { id: string };
|
||||
const requestBody = JSON.parse(String(init.body)) as { method: string; id?: string };
|
||||
const requestHeaders = new Headers(init.headers);
|
||||
requests.push({
|
||||
method: requestBody.method,
|
||||
authorization: requestHeaders.get("authorization"),
|
||||
sessionId: requestHeaders.get("mcp-session-id"),
|
||||
protocolVersion: requestHeaders.get("mcp-protocol-version"),
|
||||
});
|
||||
if (requestBody.method === "initialize") {
|
||||
const fresh = requestHeaders.get("authorization") === "Bearer fresh-customer-token";
|
||||
return new Response(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: requestBody.id,
|
||||
result: {
|
||||
protocolVersion: "2025-06-18",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "oauth-session", version: "1" },
|
||||
},
|
||||
}), { status: 200, headers: {
|
||||
"content-type": "application/json",
|
||||
"mcp-session-id": fresh ? "fresh-session" : "stale-session",
|
||||
} });
|
||||
}
|
||||
if (requestBody.method === "notifications/initialized") return new Response(null, { status: 202 });
|
||||
callDispatches += 1;
|
||||
if (callDispatches === 1) return new Response(null, { status: 401 });
|
||||
return new Response(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: requestBody.id,
|
||||
@@ -1719,22 +1781,37 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
const result = await gateway.executeTool({
|
||||
const execute = () => gateway.executeTool({
|
||||
sessionToken: session.token,
|
||||
tool: tool!.name,
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(authorizationHeaders).toEqual([
|
||||
"Bearer stale-customer-token",
|
||||
"Bearer fresh-customer-token",
|
||||
await expect(execute()).rejects.toMatchObject({
|
||||
status: 409,
|
||||
reasonCode: "oauth_refreshed_retry_required",
|
||||
});
|
||||
expect(callDispatches).toBe(1);
|
||||
expect(requests.map(({ method }) => method)).toEqual([
|
||||
"initialize", "notifications/initialized", "tools/call",
|
||||
]);
|
||||
expect(oauthGrantRefresher).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(oauthGrantRefresher).mock.calls[1]?.[0]).toMatchObject({ forceRefresh: true });
|
||||
expect(requests.every((request) => request.authorization === "Bearer stale-customer-token")).toBe(true);
|
||||
expect(requests[2]).toMatchObject({ sessionId: "stale-session", protocolVersion: "2025-06-18" });
|
||||
|
||||
const result = await execute();
|
||||
expect(result.status).toBe("completed");
|
||||
expect(callDispatches).toBe(2);
|
||||
expect(requests.map(({ method }) => method)).toEqual([
|
||||
"initialize", "notifications/initialized", "tools/call",
|
||||
"initialize", "notifications/initialized", "tools/call",
|
||||
]);
|
||||
expect(requests.slice(3).every((request) => request.authorization === "Bearer fresh-customer-token")).toBe(true);
|
||||
expect(requests[5]).toMatchObject({ sessionId: "fresh-session", protocolVersion: "2025-06-18" });
|
||||
expect(oauthGrantRefresher).toHaveBeenCalledTimes(3);
|
||||
expect(vi.mocked(oauthGrantRefresher).mock.calls.some(([input]) => input.forceRefresh === true)).toBe(true);
|
||||
});
|
||||
|
||||
it("marks a managed OAuth grant reconnect-required after one rejected refresh retry", async () => {
|
||||
it("refreshes a managed OAuth grant after 401 without replaying the call", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
const { connection } = await createRemoteMcpToolFixture(db, company.id);
|
||||
const accessSecret = await secretService(db).create(company.id, {
|
||||
@@ -1811,16 +1888,13 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
sessionToken: session.token,
|
||||
tool: tool!.name,
|
||||
parameters: {},
|
||||
})).rejects.toMatchObject({ reasonCode: "mcp_remote_status" });
|
||||
})).rejects.toMatchObject({ reasonCode: "oauth_refreshed_retry_required" });
|
||||
|
||||
expect(authorizationHeaders).toEqual([
|
||||
"Bearer stale-managed-token",
|
||||
"Bearer fresh-managed-token",
|
||||
]);
|
||||
expect(authorizationHeaders).toEqual(["Bearer stale-managed-token"]);
|
||||
expect(oauthGrantRefresher).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(oauthGrantRefresher).mock.calls[1]?.[0]).toMatchObject({ forceRefresh: true });
|
||||
const [storedGrant] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, grant.id));
|
||||
expect(storedGrant?.status).toBe("needs_reauthorization");
|
||||
expect(storedGrant?.status).toBe("active");
|
||||
});
|
||||
|
||||
it("fails clearly when remote MCP elicitation has no issue interaction path", async () => {
|
||||
|
||||
@@ -327,7 +327,7 @@ export function toolAccessRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
function requestLoopbackBaseUrl(req: Request) {
|
||||
function requestLoopbackBaseUrl(req: Request, forInitiation = false) {
|
||||
const host = req.get("host")?.trim();
|
||||
if (!host) return null;
|
||||
try {
|
||||
@@ -346,8 +346,7 @@ export function toolAccessRoutes(
|
||||
// interoperable spelling and retain the browser's exact origin as OAuth
|
||||
// state for popup postMessage below.
|
||||
if (
|
||||
req.method !== "GET"
|
||||
&& req.method !== "HEAD"
|
||||
(forInitiation || (req.method !== "GET" && req.method !== "HEAD"))
|
||||
&& (options.deploymentMode ?? "local_trusted") === "local_trusted"
|
||||
&& parsed.protocol === "http:"
|
||||
&& parsed.hostname !== "localhost"
|
||||
@@ -414,11 +413,11 @@ export function toolAccessRoutes(
|
||||
return null;
|
||||
}
|
||||
|
||||
function oauthRedirectUri(req: Request) {
|
||||
function oauthRedirectUri(req: Request, forInitiation = false) {
|
||||
const baseUrl = configuredPublicBaseUrl()
|
||||
?? trustedBrowserBaseUrl(req)
|
||||
?? enrolledConnectorBaseUrl(req)
|
||||
?? requestLoopbackBaseUrl(req);
|
||||
?? requestLoopbackBaseUrl(req, forInitiation);
|
||||
if (!baseUrl) {
|
||||
throw unprocessable(
|
||||
"This Paperclip needs a browser-reachable HTTPS address (or loopback HTTP) before browser sign-in can start.",
|
||||
@@ -821,7 +820,22 @@ function connectorEnrollmentPrincipal(req: Request): string {
|
||||
: [];
|
||||
const vercelConnect = vercelConnectIntegrationStatus();
|
||||
const { enableMemoryConnectors } = await instanceSettingsService(db).getExperimental();
|
||||
let oauthCallbackUrl: string | undefined;
|
||||
try {
|
||||
// Setup must display the callback used by its initiating POST. Provider
|
||||
// callbacks themselves retain their exact request-derived redirect URI.
|
||||
oauthCallbackUrl = oauthRedirectUri(req, true);
|
||||
} catch (error) {
|
||||
const details = error instanceof HttpError
|
||||
&& error.details
|
||||
&& typeof error.details === "object"
|
||||
&& !Array.isArray(error.details)
|
||||
? error.details as Record<string, unknown>
|
||||
: null;
|
||||
if (details?.code !== "oauth_redirect_origin_unsupported") throw error;
|
||||
}
|
||||
res.json({
|
||||
...(oauthCallbackUrl ? { oauthCallbackUrl } : {}),
|
||||
capabilities: await describeConnectionCreateCapabilities(req, companyId),
|
||||
credentialSources: {
|
||||
vercelConnect: {
|
||||
|
||||
@@ -7149,7 +7149,20 @@ export function toolAccessService(
|
||||
return sendRemote({ method: "POST", headers: mcpHttpRequestHeaders(requestHeaders),
|
||||
body: JSON.stringify({ jsonrpc: "2.0", id: listRequestId, method: "tools/list", params: cursor ? { cursor } : {} }) });
|
||||
};
|
||||
let usedInitializedSession = connection.config.mcpSessionRequired === true;
|
||||
const connectionConfig = asRecord(connection.config);
|
||||
const sourceTemplateKey = typeof connectionConfig?.sourceTemplateKey === "string"
|
||||
? connectionConfig.sourceTemplateKey
|
||||
: null;
|
||||
const connectionMethodKey = typeof connectionConfig?.connectionMethodKey === "string"
|
||||
? connectionConfig.connectionMethodKey
|
||||
: null;
|
||||
const curatedMethodRequiresMcpSession = Boolean(
|
||||
sourceTemplateKey && connectionMethodKey &&
|
||||
getConnectableAppDefinition(sourceTemplateKey)?.methods.some((method) =>
|
||||
method.key === connectionMethodKey && method.defaults?.mcpSessionRequired === true
|
||||
),
|
||||
);
|
||||
let usedInitializedSession = connectionConfig?.mcpSessionRequired === true || curatedMethodRequiresMcpSession;
|
||||
let response: Response;
|
||||
if (usedInitializedSession) {
|
||||
try {
|
||||
@@ -7194,7 +7207,7 @@ export function toolAccessService(
|
||||
}
|
||||
if (
|
||||
usedInitializedSession &&
|
||||
connection.config.mcpSessionRequired !== true
|
||||
connectionConfig?.mcpSessionRequired !== true
|
||||
) {
|
||||
const nextConfig = { ...connection.config, mcpSessionRequired: true };
|
||||
await db
|
||||
@@ -9954,8 +9967,15 @@ export function toolAccessService(
|
||||
);
|
||||
|
||||
const host = new URL(input.redirectUri).host;
|
||||
const clientNameHost = host
|
||||
.replace(/[^A-Za-z0-9-]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
const requestedMetadata = {
|
||||
client_name: `Paperclip (${host})`,
|
||||
// Some DCR servers restrict client_name to alphanumeric characters,
|
||||
// hyphens and spaces. Keep the deployment host recognizable without
|
||||
// sending punctuation such as dots, colons or parentheses.
|
||||
client_name: `Paperclip ${clientNameHost}`,
|
||||
redirect_uris: [input.redirectUri],
|
||||
grant_types: [
|
||||
"authorization_code",
|
||||
@@ -10427,7 +10447,14 @@ export function toolAccessService(
|
||||
source: "preconfigured" as const,
|
||||
};
|
||||
}
|
||||
const metadataDocumentUrl = input.endpoints
|
||||
const method = input.galleryEntry
|
||||
? connectionMethodForConnection(input.galleryEntry, input.connection)
|
||||
: null;
|
||||
const dcrRequired = method?.oauthClientRegistration === "dcr";
|
||||
// A reviewed provider compatibility choice may require DCR even when the
|
||||
// server also advertises CIMD. Do not resolve/adopt a metadata URL for that
|
||||
// method; the default for every other method remains CIMD-first.
|
||||
const metadataDocumentUrl = !dcrRequired && input.endpoints
|
||||
.clientIdMetadataDocumentSupported
|
||||
? await resolveOAuthClientIdMetadataDocumentUrl(
|
||||
input.redirectUri,
|
||||
@@ -10486,6 +10513,12 @@ export function toolAccessService(
|
||||
},
|
||||
);
|
||||
}
|
||||
if (dcrRequired && !input.endpoints.registrationUrl) {
|
||||
throw unprocessable(
|
||||
"This provider requires dynamic client registration, but its authorization server did not advertise a registration endpoint.",
|
||||
{ code: "oauth_dcr_not_supported" },
|
||||
);
|
||||
}
|
||||
|
||||
const key = `${input.connection.id}:${input.redirectUri}`;
|
||||
return singleFlight(oauthRegistrationFlights, key, async () => {
|
||||
@@ -10527,8 +10560,9 @@ export function toolAccessService(
|
||||
source: storedOAuthClientRegistrationSource(bound),
|
||||
};
|
||||
}
|
||||
// 3. Client ID Metadata Documents: no registration call at all, so prefer
|
||||
// them over DCR when the authorization server advertises support.
|
||||
// 3. By default, Client ID Metadata Documents need no registration call,
|
||||
// so prefer them over DCR when the authorization server advertises
|
||||
// support. Curated DCR opt-ins leave metadataDocumentUrl null above.
|
||||
if (metadataDocumentUrl) {
|
||||
const adopted = await adoptClientIdMetadataDocument({
|
||||
connection: latest,
|
||||
@@ -12771,6 +12805,9 @@ export function toolAccessService(
|
||||
sourceTemplateKey: galleryEntry.slug,
|
||||
connectionMethodKey: method?.key,
|
||||
methodConfig: normalizedMethodConfig?.values ?? {},
|
||||
...(method?.defaults?.mcpSessionRequired === true
|
||||
? { mcpSessionRequired: true }
|
||||
: {}),
|
||||
// Grant-backed setup keeps the full discovered catalog selectable;
|
||||
// the wizard projects the app's action defaults into policies at
|
||||
// finish time instead of using catalog quarantine as access state.
|
||||
|
||||
@@ -91,6 +91,7 @@ import type {
|
||||
UpdateToolMcpGateway,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
getConnectableAppDefinition,
|
||||
isGitHubConnectorProfileId,
|
||||
isGoogleWorkspaceConnectorProfileId,
|
||||
type GitHubConnectorProfileId,
|
||||
@@ -470,6 +471,21 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function connectionRequiresMcpSession(connection: { config: unknown }): boolean {
|
||||
const config = asRecord(connection.config);
|
||||
if (config?.mcpSessionRequired === true) return true;
|
||||
const sourceTemplateKey = typeof config?.sourceTemplateKey === "string"
|
||||
? config.sourceTemplateKey
|
||||
: null;
|
||||
const connectionMethodKey = typeof config?.connectionMethodKey === "string"
|
||||
? config.connectionMethodKey
|
||||
: null;
|
||||
if (!sourceTemplateKey || !connectionMethodKey) return false;
|
||||
return getConnectableAppDefinition(sourceTemplateKey)?.methods.some((method) =>
|
||||
method.key === connectionMethodKey && method.defaults?.mcpSessionRequired === true
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
async function mapWithConcurrency<T, R>(
|
||||
items: readonly T[],
|
||||
concurrency: number,
|
||||
@@ -5972,6 +5988,27 @@ export function createToolGatewayService(
|
||||
// letting the tighter default cut a legitimately slow tool short.
|
||||
responseTimeoutMs: ms,
|
||||
});
|
||||
const requireExplicitRetryAfterOAuthRefresh = async (refreshedResponse: Response): Promise<never> => {
|
||||
// The provider rejected this dispatch, but automatic replay is unsafe:
|
||||
// a server can apply a request before returning 401. A caller retry
|
||||
// starts a fresh credential-scoped MCP session and dispatches once.
|
||||
if (mcpSession) forgetMcpHttpSession(mcpSession);
|
||||
await refreshedResponse.body?.cancel().catch(() => undefined);
|
||||
execution.response = {
|
||||
httpStatus: refreshedResponse.status,
|
||||
contentType: refreshedResponse.headers.get("content-type"),
|
||||
bodySizeBytes: 0,
|
||||
upstreamRequestId:
|
||||
refreshedResponse.headers.get("x-request-id") ??
|
||||
refreshedResponse.headers.get("traceparent"),
|
||||
};
|
||||
throw new ToolGatewayHttpError(
|
||||
409,
|
||||
"The provider rejected the saved OAuth token, which has now been refreshed. Retry the action explicitly; the original call was not replayed.",
|
||||
"oauth_refreshed_retry_required",
|
||||
{ connectionId: connection.id, catalogEntryId: entry.id, execution },
|
||||
);
|
||||
};
|
||||
if (isRailwayEndpoint(connection.config.url) && normalizeRailwayToolName(entry.toolName).startsWith(RAILWAY_TOOL_PREFIX)) {
|
||||
if (!isRailwayConnection(connection) || connection.config.railwayApiStatus !== "available") {
|
||||
throw new ToolGatewayHttpError(422, "Railway API access is not verified. Refresh actions or reconnect this Railway connection.", "railway_api_not_verified");
|
||||
@@ -6002,7 +6039,7 @@ export function createToolGatewayService(
|
||||
};
|
||||
}
|
||||
let requestHeaders = headers;
|
||||
if (connection.config.mcpSessionRequired === true) {
|
||||
if (connectionRequiresMcpSession(connection)) {
|
||||
const scope = `${connection.id}:grant:${grant.id}:actor:${session.agentId}:${endpoint}`;
|
||||
requestHeaders = await getMcpHttpSession({
|
||||
scope,
|
||||
@@ -6066,24 +6103,7 @@ export function createToolGatewayService(
|
||||
});
|
||||
headers = builtHeaders.headers;
|
||||
headerSummary = builtHeaders.summary;
|
||||
response = await dispatchRemote(endpoint, {
|
||||
...requestInit,
|
||||
headers: mcpHttpRequestHeaders(headers),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
await db
|
||||
.update(connectionGrants)
|
||||
.set({
|
||||
status: "needs_reauthorization",
|
||||
updatedAt: new Date(options.now?.() ?? Date.now()),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(connectionGrants.id, grant.id),
|
||||
eq(connectionGrants.companyId, connection.companyId),
|
||||
),
|
||||
);
|
||||
}
|
||||
await requireExplicitRetryAfterOAuthRefresh(response);
|
||||
}
|
||||
if (
|
||||
response.status === 401 &&
|
||||
@@ -6106,10 +6126,7 @@ export function createToolGatewayService(
|
||||
});
|
||||
headers = builtHeaders.headers;
|
||||
headerSummary = builtHeaders.summary;
|
||||
response = await dispatchRemote(endpoint, {
|
||||
...requestInit,
|
||||
headers: mcpHttpRequestHeaders(headers),
|
||||
});
|
||||
await requireExplicitRetryAfterOAuthRefresh(response);
|
||||
}
|
||||
if (
|
||||
response.status === 401 &&
|
||||
@@ -6146,10 +6163,7 @@ export function createToolGatewayService(
|
||||
});
|
||||
headers = builtHeaders.headers;
|
||||
headerSummary = builtHeaders.summary;
|
||||
response = await dispatchRemote(endpoint, {
|
||||
...requestInit,
|
||||
headers: mcpHttpRequestHeaders(headers),
|
||||
});
|
||||
await requireExplicitRetryAfterOAuthRefresh(response);
|
||||
}
|
||||
const sessionExpired = response.status === 404 && new Headers(requestHeaders).has("mcp-session-id");
|
||||
if (sessionExpired) {
|
||||
|
||||
@@ -20,6 +20,15 @@ import {
|
||||
GITHUB_PRIVATE_KEY_PASTE_FIXTURE,
|
||||
} from "./chat-adapters-ui.shared";
|
||||
|
||||
async function findConnectorBySearch(page: Page, query: string, slug: string) {
|
||||
const search = page.getByRole("searchbox", { name: "Search connectors" });
|
||||
await expect(search).toBeVisible();
|
||||
await search.fill(query);
|
||||
const connector = page.locator(`[role="listitem"][data-app-slug="${slug}"]`);
|
||||
await expect(connector).toBeVisible({ timeout: 30_000 });
|
||||
return connector;
|
||||
}
|
||||
|
||||
async function exerciseGitHubReviewSetup(page: Page, mock: ChatMock, seed: Seed, provider: ProviderCase) {
|
||||
await expect(page.getByRole("heading", { name: "Choose agent", exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Choose an agent", exact: true }).click();
|
||||
@@ -289,10 +298,7 @@ test.describe.serial("native chat adapter UI", () => {
|
||||
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible(
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
const connector = page.locator(
|
||||
'[role="listitem"][data-app-slug="github"]',
|
||||
);
|
||||
await expect(connector).toBeVisible();
|
||||
const connector = await findConnectorBySearch(page, "GitHub", "github");
|
||||
// Without the cloud connector GitHub's default method is a token, so the
|
||||
// card's verb is "Add key" rather than "Connect".
|
||||
await connector.getByRole("button", { name: "Add key GitHub" }).click();
|
||||
@@ -367,17 +373,15 @@ test.describe.serial("native chat adapter UI", () => {
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Connectors" }),
|
||||
).toBeVisible({ timeout: 30_000 });
|
||||
const connector = page.locator(
|
||||
`[role="listitem"][data-app-slug="${provider.slug}"]`,
|
||||
);
|
||||
await expect(connector).toBeVisible({ timeout: 30_000 });
|
||||
let connector = await findConnectorBySearch(page, provider.name, provider.slug);
|
||||
if (provider.provider === "github") {
|
||||
const tools = page.locator('[role="listitem"][data-app-slug="github"]');
|
||||
const tools = await findConnectorBySearch(page, "GitHub", "github");
|
||||
await tools.getByRole("button", { name: "Add key GitHub", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Change", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Connect GitHub as" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Choose how to connect" })).toHaveCount(0);
|
||||
await page.goto(`/${seed.prefix}/apps`);
|
||||
connector = await findConnectorBySearch(page, provider.name, provider.slug);
|
||||
}
|
||||
await connector
|
||||
.getByRole("button", { name: `Connect ${provider.provider === "github" ? "GitHub Code Review Bot" : provider.name}` })
|
||||
@@ -980,10 +984,7 @@ test.describe("iMessage Photon setup and management", () => {
|
||||
theme,
|
||||
);
|
||||
await page.goto(`/${seed.prefix}/apps`);
|
||||
const card = page.locator(
|
||||
'[role="listitem"][data-app-slug="imessage-photon"]',
|
||||
);
|
||||
await expect(card).toBeVisible();
|
||||
const card = await findConnectorBySearch(page, "iMessage Photon", "imessage-photon");
|
||||
await card
|
||||
.getByRole("button", { name: "Connect iMessage Photon" })
|
||||
.click();
|
||||
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1024" height="1024" fill="#0143D9"/>
|
||||
<path d="M730 289.911C730 290.14 729.921 290.362 729.777 290.54L550.318 512L729.777 733.46C729.921 733.638 730 733.86 730 734.089V774.628C730 775.18 729.552 775.628 729 775.628H295C294.448 775.628 294 775.18 294 774.628V249.372C294 248.82 294.448 248.372 295 248.372H729C729.552 248.372 730 248.82 730 249.372V289.911ZM382.086 719.771H646.647L514.365 556.401L382.086 719.771ZM349.897 538.829V670.72L456.688 538.829H349.897ZM349.897 482.972H454.91L349.897 353.279V482.972ZM514.365 467.598L646.647 304.229H382.086L514.365 467.598Z" fill="black"/>
|
||||
<path d="M730 289.911C730 290.14 729.921 290.362 729.777 290.54L550.318 512L729.777 733.46C729.921 733.638 730 733.86 730 734.089V774.628C730 775.18 729.552 775.628 729 775.628H295C294.448 775.628 294 775.18 294 774.628V249.372C294 248.82 294.448 248.372 295 248.372H729C729.552 248.372 730 248.82 730 249.372V289.911ZM382.086 719.771H646.647L514.365 556.401L382.086 719.771ZM349.897 538.829V670.72L456.688 538.829H349.897ZM349.897 482.972H454.91L349.897 353.279V482.972ZM514.365 467.598L646.647 304.229H382.086L514.365 467.598Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg width="172" height="40" viewBox="0 0 172 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.3606 12.8281C21.8137 13.2873 20.6476 14.3261 19.7936 15.4544C19.6102 15.6966 19.228 15.5146 19.3008 15.2178C20.936 8.49401 18.7759 2.90556 12.0422 0.154735C11.7006 0.0147436 11.345 0.321324 11.4346 0.679702C14.4977 12.9779 1.61412 11.9406 3.24224 25.8823C3.27024 26.1217 3.00145 26.2855 2.80546 26.1455C2.19509 25.7073 1.51332 24.7932 1.04575 24.1506C0.908555 23.9616 0.611769 24.0148 0.548773 24.2402C0.176391 25.5869 0 26.8553 0 28.1152C0 33.0149 2.51847 37.328 6.33048 39.8283C6.54887 39.9711 6.82886 39.7667 6.75466 39.5161C6.55867 38.8581 6.44808 38.1638 6.43968 37.4456C6.43968 37.0046 6.46768 36.5539 6.53627 36.1339C6.69587 35.0784 7.06265 34.0732 7.67862 33.1577C9.79111 29.9869 14.0259 26.9239 13.3497 22.7647C13.3063 22.5015 13.6171 22.328 13.8131 22.5085C16.7964 25.2342 17.3871 28.9005 16.8972 32.1889C16.8552 32.4745 17.2135 32.6271 17.3941 32.4031C17.8505 31.832 18.4077 31.3308 19.0138 30.9542C19.165 30.8604 19.3666 30.9318 19.424 31.0998C19.7614 32.0811 20.2626 33.0023 20.7358 33.9234C21.3013 35.0308 21.6023 36.2949 21.5547 37.6332C21.5309 38.2842 21.4231 38.9141 21.2425 39.5133C21.1655 39.7667 21.4427 39.9781 21.6653 39.8325C25.4801 37.3322 28 33.0191 28 28.1166C28 26.4129 27.7018 24.7428 27.1376 23.1777C25.9547 19.8949 22.9533 17.4297 23.712 13.1515C23.7484 12.9471 23.5594 12.7693 23.3606 12.8281Z" fill="#FA5D19" style="fill:#FA5D19;fill:color(display-p3 0.9816 0.3634 0.0984);fill-opacity:1;"/>
|
||||
<path d="M41 34.0521V10.9618H55.7586V14.3264H44.7969V21.0226H53.8436V24.2882H44.7969V34.0521H41Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
<path d="M59.9569 14.7882C58.7352 14.7882 57.7777 13.8976 57.7777 12.6441C57.7777 11.3906 58.7352 10.5 59.9569 10.5C61.1785 10.5 62.136 11.3906 62.136 12.6441C62.136 13.8976 61.1785 14.7882 59.9569 14.7882ZM58.1409 34.0521V17.1632H61.7068V34.0521H58.1409Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
<path d="M73.5885 17.1632H74.3809V20.4948H72.796C69.6264 20.4948 68.6029 22.9687 68.6029 25.5747V34.0521H65.0371V17.1632H68.2067L68.6029 19.7031C69.4613 18.2847 70.815 17.1632 73.5885 17.1632Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
<path d="M83.632 34.25C78.3163 34.25 74.9816 30.8194 74.9816 25.6406C74.9816 20.4288 78.3163 16.9653 83.3019 16.9653C88.1884 16.9653 91.457 20.066 91.5561 25.0139C91.5561 25.4427 91.5231 25.9045 91.457 26.3663H78.7125V26.5972C78.8116 29.467 80.6275 31.3472 83.4339 31.3472C85.613 31.3472 87.1979 30.2587 87.6931 28.3785H91.2589C90.6646 31.7101 87.8252 34.25 83.632 34.25ZM78.8446 23.7604H87.8582C87.561 21.2535 85.8112 19.8351 83.3349 19.8351C81.0567 19.8351 79.1087 21.3524 78.8446 23.7604Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
<path d="M102.033 34.25C96.9151 34.25 93.6465 30.9184 93.6465 25.6406C93.6465 20.4288 97.0142 16.9653 102.132 16.9653C106.49 16.9653 109.197 19.3733 109.891 23.1997H106.16C105.698 21.2205 104.278 20 102.066 20C99.1933 20 97.3113 22.309 97.3113 25.6406C97.3113 28.9392 99.1933 31.2153 102.066 31.2153C104.245 31.2153 105.698 29.9618 106.127 28.0156H109.891C109.23 31.842 106.358 34.25 102.033 34.25Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
<path d="M121.006 17.1632H121.799V20.4948H120.214C117.044 20.4948 116.021 22.9687 116.021 25.5747V34.0521H112.455V17.1632H115.625L116.021 19.7031C116.879 18.2847 118.233 17.1632 121.006 17.1632Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
<path d="M130.614 16.9653C135.104 16.9653 137.679 19.1094 137.679 23.1007V34.0521H134.576L134.279 31.6441C133.123 33.1615 131.505 34.25 128.831 34.25C125.133 34.25 122.657 32.4358 122.657 29.3021C122.657 25.8385 125.166 23.8924 129.92 23.8924H134.147V22.8698C134.147 20.9896 132.793 19.8351 130.449 19.8351C128.336 19.8351 126.916 20.8247 126.652 22.309H123.152C123.515 19.0104 126.355 16.9653 130.614 16.9653ZM129.425 31.4792C132.397 31.4792 134.114 29.7309 134.147 27.125V26.5312H129.722C127.51 26.5312 126.289 27.3559 126.289 29.0712C126.289 30.4896 127.477 31.4792 129.425 31.4792Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
<path d="M144.653 34.0521L139.139 17.1632H142.903L146.766 30.0937L150.629 17.1632H153.897L157.595 30.0937L161.59 17.1632H165.222L159.609 34.0521H155.779L152.214 22.5729L148.516 34.0521H144.653Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
<path d="M166.934 34.0521V10.9618H170.5V34.0521H166.934Z" fill="#262626" style="fill:#262626;fill:color(display-p3 0.1500 0.1500 0.1500);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -266,7 +266,7 @@
|
||||
{
|
||||
"slug": "make",
|
||||
"provider": "Make",
|
||||
"catalogVisible": false,
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/make.svg",
|
||||
"darkAsset": "/brands/apps/make-dark.svg"
|
||||
},
|
||||
@@ -597,6 +597,49 @@
|
||||
"provider": "Responses API",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/api-key-generic.svg"
|
||||
},
|
||||
{
|
||||
"slug": "calendly",
|
||||
"provider": "Calendly",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/calendly.png"
|
||||
},
|
||||
{
|
||||
"slug": "exa",
|
||||
"provider": "Exa",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/exa.svg"
|
||||
},
|
||||
{
|
||||
"slug": "firecrawl",
|
||||
"provider": "Firecrawl",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/firecrawl.svg"
|
||||
},
|
||||
{
|
||||
"slug": "gsc-wizard",
|
||||
"provider": "GSC Wizard",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/gsc-wizard.png"
|
||||
},
|
||||
{
|
||||
"slug": "parallel-search",
|
||||
"provider": "Parallel Search",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/parallel.svg",
|
||||
"darkAsset": "/brands/apps/parallel-dark.svg"
|
||||
},
|
||||
{
|
||||
"slug": "tavily",
|
||||
"provider": "Tavily",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/tavily.png"
|
||||
},
|
||||
{
|
||||
"slug": "windsor-ai",
|
||||
"provider": "Windsor.ai",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/windsor-ai.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg width="270" height="270" viewBox="0 0 270 270" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="270" height="270" rx="44.6943" fill="#FCFBFA"/>
|
||||
<path d="M214.682 117.391H170.297C170.415 118.089 170.512 118.798 170.609 119.507H100.002C99.8726 120.442 99.7432 121.387 99.6353 122.322H54.98C54.7642 123.719 54.5701 125.126 54.4191 126.555H99.2255C99.15 127.49 99.096 128.436 99.0421 129.37H171.569C171.602 130.069 171.634 130.778 171.656 131.487H216.408V129.231C216.117 125.191 215.545 121.237 214.682 117.391Z" fill="#1D1C1A"/>
|
||||
<path d="M171.569 140.642H99.0421C99.0852 141.587 99.15 142.522 99.2255 143.457H54.4191C54.5701 144.875 54.7534 146.283 54.98 147.69H99.6353C99.7432 148.636 99.8619 149.57 100.002 150.505H170.609C170.512 151.214 170.404 151.913 170.297 152.622H214.682C215.545 148.775 216.117 144.821 216.408 140.782V138.525H171.656C171.634 139.235 171.602 139.933 171.569 140.642Z" fill="#1D1C1A"/>
|
||||
<path d="M168.377 161.785H102.213C102.451 162.73 102.71 163.665 102.968 164.6H59.5858C60.1467 166.028 60.7507 167.436 61.3871 168.833H104.252C104.565 169.778 104.878 170.713 105.212 171.648H165.357C165.098 172.357 164.839 173.055 164.569 173.764H206.711C209.16 169.305 211.188 164.589 212.762 159.668H168.873C168.711 160.377 168.538 161.075 168.366 161.785H168.377Z" fill="#1D1C1A"/>
|
||||
<path d="M160.6 182.929H110.001C110.465 183.875 110.95 184.82 111.446 185.744H71.9253C73.101 187.195 74.3199 188.602 75.5819 189.977H113.798C114.359 190.923 114.93 191.868 115.513 192.792H155.099C154.656 193.501 154.203 194.21 153.74 194.909H190.057C194.684 190.708 198.816 185.97 202.375 180.812H161.614C161.279 181.522 160.956 182.231 160.61 182.929H160.6Z" fill="#1D1C1A"/>
|
||||
<path d="M146.966 204.063H123.635C124.422 205.019 125.221 205.954 126.04 206.878H97.8125C108.966 212.658 121.629 215.967 135.058 216C135.144 216 135.219 216 135.295 216C135.37 216 135.457 216 135.532 216C152.424 215.946 168.107 210.767 181.083 201.936H148.648C148.088 202.645 147.527 203.354 146.955 204.052L146.966 204.063Z" fill="#1D1C1A"/>
|
||||
<path d="M123.635 65.9475H146.966C147.538 66.6458 148.099 67.3549 148.659 68.0641H181.094C168.107 59.2324 152.435 54.0537 135.543 54C135.457 54 135.381 54 135.306 54C135.23 54 135.144 54 135.069 54C121.629 54.043 108.977 57.3414 97.8235 63.1218H126.051C125.232 64.0458 124.433 64.9912 123.646 65.9367L123.635 65.9475Z" fill="#1D1C1A"/>
|
||||
<path d="M110.001 87.093H160.6C160.945 87.7913 161.279 88.5005 161.603 89.2096H202.365C198.805 84.0524 194.674 79.3142 190.047 75.1133H153.729C154.193 75.8116 154.646 76.5208 155.088 77.2299H115.502C114.92 78.1646 114.348 79.0994 113.787 80.0448H75.5712C74.2985 81.4093 73.0796 82.8168 71.9147 84.278H111.436C110.94 85.2128 110.465 86.1475 109.99 87.093H110.001Z" fill="#1D1C1A"/>
|
||||
<path d="M102.224 108.226H168.387C168.56 108.924 168.733 109.633 168.894 110.342H212.784C211.209 105.422 209.181 100.705 206.733 96.2461H164.591C164.86 96.9445 165.119 97.6536 165.378 98.3627H105.233C104.899 99.2974 104.575 100.232 104.273 101.178H61.4086C60.7722 102.564 60.1682 103.971 59.6073 105.411H102.99C102.731 106.346 102.472 107.28 102.235 108.226H102.224Z" fill="#1D1C1A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg width="270" height="270" viewBox="0 0 270 270" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="270" height="270" rx="44.6943" fill="#1D1C1A"/>
|
||||
<path d="M214.682 117.391H170.297C170.415 118.089 170.512 118.798 170.609 119.507H100.002C99.8728 120.442 99.7433 121.387 99.6355 122.322H54.9801C54.7643 123.719 54.5702 125.126 54.4192 126.555H99.2256C99.1501 127.49 99.0961 128.436 99.0422 129.37H171.569C171.602 130.069 171.634 130.778 171.656 131.487H216.408V129.231C216.117 125.191 215.545 121.237 214.682 117.391Z" fill="#FCFBFA"/>
|
||||
<path d="M171.569 140.642H99.0422C99.0854 141.587 99.1501 142.522 99.2256 143.457H54.4192C54.5702 144.875 54.7536 146.283 54.9801 147.69H99.6355C99.7433 148.636 99.862 149.57 100.002 150.505H170.609C170.512 151.214 170.405 151.913 170.297 152.622H214.682C215.545 148.775 216.117 144.821 216.408 140.782V138.525H171.656C171.634 139.235 171.602 139.933 171.569 140.642Z" fill="#FCFBFA"/>
|
||||
<path d="M168.377 161.785H102.214C102.451 162.73 102.71 163.665 102.969 164.6H59.586C60.1469 166.028 60.7509 167.436 61.3873 168.833H104.252C104.565 169.778 104.878 170.713 105.212 171.648H165.357C165.098 172.357 164.839 173.055 164.569 173.764H206.711C209.16 169.305 211.188 164.589 212.763 159.668H168.873C168.711 160.377 168.539 161.075 168.366 161.785H168.377Z" fill="#FCFBFA"/>
|
||||
<path d="M160.6 182.929H110.001C110.465 183.875 110.95 184.82 111.446 185.744H71.9254C73.1011 187.195 74.3199 188.602 75.5819 189.977H113.798C114.359 190.923 114.93 191.868 115.513 192.792H155.099C154.656 193.501 154.203 194.21 153.74 194.909H190.057C194.685 190.708 198.816 185.97 202.375 180.812H161.614C161.279 181.522 160.956 182.231 160.611 182.929H160.6Z" fill="#FCFBFA"/>
|
||||
<path d="M146.966 204.063H123.635C124.422 205.019 125.221 205.954 126.04 206.878H97.8126C108.966 212.658 121.629 215.967 135.058 216C135.144 216 135.22 216 135.295 216C135.371 216 135.457 216 135.532 216C152.424 215.946 168.107 210.767 181.083 201.936H148.649C148.088 202.645 147.527 203.354 146.955 204.052L146.966 204.063Z" fill="#FCFBFA"/>
|
||||
<path d="M123.635 65.9475H146.966C147.538 66.6458 148.099 67.3549 148.66 68.0641H181.094C168.107 59.2324 152.435 54.0537 135.543 54C135.457 54 135.382 54 135.306 54C135.231 54 135.144 54 135.069 54C121.629 54.043 108.977 57.3414 97.8236 63.1218H126.051C125.232 64.0458 124.433 64.9912 123.646 65.9367L123.635 65.9475Z" fill="#FCFBFA"/>
|
||||
<path d="M110.001 87.093H160.6C160.945 87.7913 161.28 88.5005 161.603 89.2096H202.365C198.805 84.0524 194.674 79.3142 190.047 75.1133H153.729C154.193 75.8116 154.646 76.5208 155.088 77.2299H115.502C114.92 78.1646 114.348 79.0994 113.787 80.0448H75.5714C74.2986 81.4093 73.0797 82.8168 71.9148 84.278H111.436C110.94 85.2128 110.465 86.1475 109.991 87.093H110.001Z" fill="#FCFBFA"/>
|
||||
<path d="M102.224 108.226H168.388C168.56 108.924 168.733 109.633 168.895 110.342H212.784C211.209 105.422 209.181 100.705 206.733 96.2461H164.591C164.86 96.9445 165.119 97.6536 165.378 98.3627H105.234C104.899 99.2974 104.576 100.232 104.274 101.178H61.4088C60.7724 102.564 60.1683 103.971 59.6075 105.411H102.99C102.731 106.346 102.472 107.28 102.235 108.226H102.224Z" fill="#FCFBFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -87,6 +87,8 @@ export type ToolPoliciesResponse = { policies: ToolPolicy[] };
|
||||
export type ToolProfilesResponse = { profiles: ToolProfileWithDetails[] };
|
||||
export type ToolGalleryResponse = {
|
||||
apps: AppDefinition[];
|
||||
/** Server-authoritative OAuth callback URI, omitted when this deployment has no supported browser origin. */
|
||||
oauthCallbackUrl?: string;
|
||||
capabilities: ToolConnectionCreateCapabilities;
|
||||
credentialSources: {
|
||||
vercelConnect: {
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { JsonSchemaForm, getDefaultValues } from "./JsonSchemaForm";
|
||||
import {
|
||||
JsonSchemaForm,
|
||||
type JsonSchemaNode,
|
||||
getDefaultValues,
|
||||
validateJsonSchemaForm,
|
||||
} from "./JsonSchemaForm";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
@@ -605,3 +610,95 @@ describe("JsonSchemaForm enum rendering", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("JsonSchemaForm optional nested objects", () => {
|
||||
const schema: JsonSchemaNode = {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string" },
|
||||
profile: {
|
||||
type: "object",
|
||||
required: ["Name"],
|
||||
properties: {
|
||||
Name: { type: "string" },
|
||||
locale: { type: "string" },
|
||||
},
|
||||
},
|
||||
queryOptions: {
|
||||
type: "object",
|
||||
required: ["Prompt"],
|
||||
properties: {
|
||||
Prompt: { type: "string" },
|
||||
mode: { type: "string" },
|
||||
},
|
||||
},
|
||||
screenshotOptions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
viewport: {
|
||||
type: "object",
|
||||
required: ["Width", "Height"],
|
||||
properties: {
|
||||
Width: { type: "integer" },
|
||||
Height: { type: "integer" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("omits untouched optional object trees and accepts their defaults", () => {
|
||||
const defaults = getDefaultValues(schema);
|
||||
|
||||
expect(defaults).toEqual({});
|
||||
expect(validateJsonSchemaForm(schema, defaults)).toEqual({});
|
||||
expect(
|
||||
validateJsonSchemaForm(schema, {
|
||||
profile: {},
|
||||
queryOptions: {},
|
||||
screenshotOptions: { viewport: {} },
|
||||
}),
|
||||
).toEqual({
|
||||
"/profile/Name": "This field is required",
|
||||
"/queryOptions/Prompt": "This field is required",
|
||||
"/screenshotOptions/viewport/Width": "This field is required",
|
||||
"/screenshotOptions/viewport/Height": "This field is required",
|
||||
});
|
||||
|
||||
const nestedSchema: JsonSchemaNode = {
|
||||
type: "object",
|
||||
required: ["config"],
|
||||
properties: {
|
||||
config: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
queryOptions: {
|
||||
type: "object",
|
||||
required: ["Prompt"],
|
||||
properties: { Prompt: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const nestedDefaults = getDefaultValues(nestedSchema);
|
||||
expect(nestedDefaults).toEqual({ config: { enabled: false } });
|
||||
expect(validateJsonSchemaForm(nestedSchema, nestedDefaults)).toEqual({});
|
||||
});
|
||||
|
||||
it("validates required descendants once an optional object is populated", () => {
|
||||
expect(
|
||||
validateJsonSchemaForm(schema, {
|
||||
profile: { locale: "en" },
|
||||
queryOptions: { mode: "search" },
|
||||
screenshotOptions: { viewport: { Width: 1280 } },
|
||||
}),
|
||||
).toEqual({
|
||||
"/profile/Name": "This field is required",
|
||||
"/queryOptions/Prompt": "This field is required",
|
||||
"/screenshotOptions/viewport/Height": "This field is required",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -164,7 +164,19 @@ export function getDefaultForSchema(schema: JsonSchemaNode): unknown {
|
||||
case "object": {
|
||||
if (!schema.properties) return {};
|
||||
const obj: Record<string, unknown> = {};
|
||||
const requiredProperties = new Set(schema.required ?? []);
|
||||
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
||||
// An optional object with no explicit default must stay absent until
|
||||
// the user supplies a value. Materializing `{}` here makes recursive
|
||||
// validation require all of its children even when the object was
|
||||
// never touched.
|
||||
if (
|
||||
resolveType(propSchema) === "object" &&
|
||||
!requiredProperties.has(key) &&
|
||||
propSchema.default === undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const def = getDefaultForSchema(propSchema);
|
||||
if (def !== undefined) obj[key] = def;
|
||||
}
|
||||
@@ -286,7 +298,12 @@ export function validateJsonSchemaForm(
|
||||
}
|
||||
|
||||
// Recurse into objects
|
||||
if (type === "object" && propSchema.properties && typeof value === "object" && value !== null) {
|
||||
if (
|
||||
type === "object" &&
|
||||
propSchema.properties &&
|
||||
typeof value === "object" &&
|
||||
value !== null
|
||||
) {
|
||||
Object.assign(
|
||||
errors,
|
||||
validateJsonSchemaForm(propSchema, value as Record<string, unknown>, fieldPath),
|
||||
@@ -328,8 +345,16 @@ export function validateJsonSchemaForm(
|
||||
export function getDefaultValues(schema: JsonSchemaNode): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
const properties = schema.properties ?? {};
|
||||
const requiredProperties = new Set(schema.required ?? []);
|
||||
|
||||
for (const [key, propSchema] of Object.entries(properties)) {
|
||||
if (
|
||||
resolveType(propSchema) === "object" &&
|
||||
!requiredProperties.has(key) &&
|
||||
propSchema.default === undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const def = getDefaultForSchema(propSchema);
|
||||
if (def !== undefined) {
|
||||
result[key] = def;
|
||||
|
||||
@@ -984,6 +984,10 @@ function StandardConnectionSetupFlow({
|
||||
} : data, [connectionIntentId, aiConnection?.provider, aiConnection?.method, aiConnection?.mode]),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
const callbackUrlForSetup = oauthCallbackUrlForBrowser(
|
||||
window.location.origin,
|
||||
galleryQuery.data?.oauthCallbackUrl,
|
||||
);
|
||||
// Use the same visible catalog for cards and every branded URL shortcut.
|
||||
// Generic custom URLs remain usable without selecting a hidden provider.
|
||||
const visibleGalleryApps = useMemo(
|
||||
@@ -2025,7 +2029,7 @@ function StandardConnectionSetupFlow({
|
||||
<OAuthClientFields
|
||||
entry={automaticOAuthEntry}
|
||||
method={automaticCustomerClientMethod}
|
||||
callbackUrl={oauthCallbackUrlForBrowser()}
|
||||
callbackUrl={callbackUrlForSetup}
|
||||
clientId={curatedOAuthClientId}
|
||||
onClientIdChange={setCuratedOAuthClientId}
|
||||
clientSecret={curatedOAuthClientSecret}
|
||||
@@ -2426,6 +2430,7 @@ function StandardConnectionSetupFlow({
|
||||
oauthClientId={curatedOAuthClientId}
|
||||
onOAuthClientIdChange={setCuratedOAuthClientId}
|
||||
oauthClientSecret={curatedOAuthClientSecret}
|
||||
oauthCallbackUrl={callbackUrlForSetup}
|
||||
canReuseOAuthClientSecret={Boolean(
|
||||
identityConnection?.config?.oauth
|
||||
&& (identityConnection.config.oauth as Record<string, unknown>).clientId === curatedOAuthClientId.trim()
|
||||
@@ -3397,6 +3402,7 @@ function KeyStep({
|
||||
oauthClientId,
|
||||
onOAuthClientIdChange,
|
||||
oauthClientSecret,
|
||||
oauthCallbackUrl: serverCallbackUrl,
|
||||
canReuseOAuthClientSecret,
|
||||
onOAuthClientSecretChange,
|
||||
credentialSource,
|
||||
@@ -3423,6 +3429,7 @@ function KeyStep({
|
||||
oauthClientId: string;
|
||||
onOAuthClientIdChange: (next: string) => void;
|
||||
oauthClientSecret: string;
|
||||
oauthCallbackUrl: string;
|
||||
canReuseOAuthClientSecret: boolean;
|
||||
onOAuthClientSecretChange: (next: string) => void;
|
||||
credentialSource: ToolConnectionCredentialSource;
|
||||
@@ -3520,7 +3527,7 @@ function KeyStep({
|
||||
);
|
||||
const vercelConnectorFilled = !usingVercel || vercelConnector.trim().length > 0;
|
||||
const oauthCallbackUrl = method?.auth === "oauth" && acceptsCustomerOAuthClient
|
||||
? oauthCallbackUrlForBrowser()
|
||||
? serverCallbackUrl
|
||||
: null;
|
||||
const allConfigFields = [...(method?.tenantFields ?? []), ...(method?.extensionFields ?? [])];
|
||||
const configFields = allConfigFields.filter((field) => !field.hidden);
|
||||
|
||||
@@ -1209,7 +1209,10 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
|
||||
});
|
||||
|
||||
it("collects customer-owned OAuth client details for a curated manual OAuth app", async () => {
|
||||
listGalleryMock.mockResolvedValue({ apps: [BOX] });
|
||||
listGalleryMock.mockResolvedValue({
|
||||
apps: [BOX],
|
||||
oauthCallbackUrl: "https://paperclip.example.test/api/tools/oauth/callback",
|
||||
});
|
||||
mockParams.appKey = "box";
|
||||
await render();
|
||||
await passAccessStep();
|
||||
@@ -1222,7 +1225,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
|
||||
);
|
||||
expect(container.textContent).toContain("Paperclip callback URL");
|
||||
expect(container.textContent).toContain(
|
||||
"http://localhost:3000/api/tools/oauth/callback",
|
||||
"https://paperclip.example.test/api/tools/oauth/callback",
|
||||
);
|
||||
expect(buttonByText("Continue to sign in")?.disabled).toBe(true);
|
||||
|
||||
|
||||
@@ -157,6 +157,163 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("Permissions action Test dialog", () => {
|
||||
it("forwards an explicitly cleared optional text field", async () => {
|
||||
const textEntry = {
|
||||
...entry,
|
||||
inputSchema: { type: "object", properties: { update_text: { type: "string" } } },
|
||||
} as ToolCatalogEntry;
|
||||
runTestCallMock.mockResolvedValue({ decision: "allowed", invocationId: "empty-text" });
|
||||
|
||||
await renderDialog(textEntry);
|
||||
const moreOptions = [...document.body.querySelectorAll("button")].find((button) => button.textContent?.trim() === "More options");
|
||||
expect(moreOptions).toBeTruthy();
|
||||
await act(() => moreOptions!.click());
|
||||
const input = document.body.querySelector<HTMLInputElement>('input[aria-label="Update Text"]');
|
||||
expect(input).toBeTruthy();
|
||||
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
||||
await act(() => {
|
||||
setValue!.call(input, "draft");
|
||||
input!.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
await act(() => {
|
||||
setValue!.call(input, "");
|
||||
input!.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
const runButton = [...document.body.querySelectorAll("button")].find((button) => button.textContent?.trim() === "Run");
|
||||
await act(() => runButton!.click());
|
||||
await flushReact();
|
||||
|
||||
expect(runTestCallMock).toHaveBeenCalledWith("conn-1", {
|
||||
agentId: "agent-ceo",
|
||||
toolName: "list_projects",
|
||||
parameters: { update_text: "" },
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards false, zero, and an explicit object default without normalization", async () => {
|
||||
const defaultsEntry = {
|
||||
...entry,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean", default: false },
|
||||
count: { type: "integer", default: 0 },
|
||||
options: {
|
||||
type: "object",
|
||||
default: { enabled: false, count: 0 },
|
||||
required: ["enabled", "count"],
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
count: { type: "integer" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ToolCatalogEntry;
|
||||
runTestCallMock.mockResolvedValue({ decision: "allowed", invocationId: "defaults" });
|
||||
|
||||
await renderDialog(defaultsEntry);
|
||||
const runButton = [...document.body.querySelectorAll("button")].find((button) => button.textContent?.trim() === "Run");
|
||||
await act(() => runButton!.click());
|
||||
await flushReact();
|
||||
|
||||
expect(runTestCallMock).toHaveBeenCalledWith("conn-1", {
|
||||
agentId: "agent-ceo",
|
||||
toolName: "list_projects",
|
||||
parameters: { enabled: false, count: 0, options: { enabled: false, count: 0 } },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects an explicitly supplied empty optional object with required children", async () => {
|
||||
const objectEntry = {
|
||||
...entry,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
options: {
|
||||
type: "object",
|
||||
default: {},
|
||||
required: ["name"],
|
||||
properties: { name: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ToolCatalogEntry;
|
||||
|
||||
await renderDialog(objectEntry);
|
||||
const runButton = [...document.body.querySelectorAll("button")].find((button) => button.textContent?.trim() === "Run");
|
||||
await act(() => runButton!.click());
|
||||
await flushReact();
|
||||
|
||||
expect(document.body.textContent).toContain("This field is required");
|
||||
expect(runTestCallMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs the actual ActionTester with Firecrawl's minimal URL input after opening More options", async () => {
|
||||
const firecrawlEntry = {
|
||||
...entry,
|
||||
id: "firecrawl-scrape",
|
||||
toolName: "firecrawl_scrape",
|
||||
title: "Firecrawl scrape",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string" },
|
||||
profile: { type: "object", required: ["name"], properties: { name: { type: "string" } } },
|
||||
queryOptions: {
|
||||
type: "object",
|
||||
required: ["prompt"],
|
||||
properties: {
|
||||
mode: { type: "string", enum: ["directQuote", "freeform"], default: "freeform" },
|
||||
prompt: { type: "string" },
|
||||
},
|
||||
},
|
||||
screenshotOptions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
viewport: {
|
||||
type: "object",
|
||||
required: ["width", "height"],
|
||||
properties: { width: { type: "number" }, height: { type: "number" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ToolCatalogEntry;
|
||||
getTestAgentAccessMock.mockResolvedValue({
|
||||
access: {
|
||||
connectionId: "conn-1", toolCount: 1, allowedCount: 1, askFirstCount: 0, offCount: 0,
|
||||
lastChangedAt: null, lastChangedByAgentId: null, lastChangedByName: null,
|
||||
tools: [{ toolName: "firecrawl_scrape", gatewayToolName: "firecrawl__firecrawl_scrape", displayName: "Firecrawl scrape", risk: "read", decision: "allowed", reasonCode: null, matchedPolicyIds: [] }],
|
||||
},
|
||||
});
|
||||
runTestCallMock.mockResolvedValue({ decision: "allowed", invocationId: "firecrawl-minimal" });
|
||||
|
||||
await renderDialog(firecrawlEntry);
|
||||
const moreOptions = [...document.body.querySelectorAll("button")].find((button) => button.textContent?.trim() === "More options");
|
||||
expect(moreOptions).toBeTruthy();
|
||||
await act(() => moreOptions!.click());
|
||||
const urlInput = document.body.querySelector<HTMLInputElement>('input[aria-label="Url"]');
|
||||
expect(urlInput, document.body.textContent).toBeTruthy();
|
||||
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
||||
await act(() => {
|
||||
setValue!.call(urlInput, "https://paperclip.ing");
|
||||
urlInput!.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
const runButton = [...document.body.querySelectorAll("button")].find((button) => button.textContent?.trim() === "Run");
|
||||
expect(runButton).toBeTruthy();
|
||||
await act(() => runButton!.click());
|
||||
await flushReact();
|
||||
|
||||
expect(document.body.textContent).not.toContain("This field is required");
|
||||
expect(runTestCallMock).toHaveBeenCalledWith("conn-1", {
|
||||
agentId: "agent-ceo",
|
||||
toolName: "firecrawl_scrape",
|
||||
parameters: { url: "https://paperclip.ing" },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders structured MCP rows and keeps the full response behind the raw disclosure", async () => {
|
||||
runTestCallMock.mockResolvedValue({
|
||||
decision: "allowed", invocationId: "structured",
|
||||
|
||||
@@ -454,11 +454,11 @@ function ActionTester({
|
||||
}, [running]);
|
||||
|
||||
const run = useMutation({
|
||||
mutationFn: async () => {
|
||||
mutationFn: async (parameters: Record<string, unknown>) => {
|
||||
const result = await toolsApi.runTestCall(connectionId, {
|
||||
agentId: agent.id,
|
||||
toolName: entry.toolName,
|
||||
parameters: values,
|
||||
parameters,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
@@ -496,7 +496,7 @@ function ActionTester({
|
||||
setElapsedMs(0);
|
||||
setOutcome(null);
|
||||
setRunning(true);
|
||||
run.mutate();
|
||||
run.mutate(values);
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
|
||||
@@ -38,6 +38,13 @@ describe("endpointHost", () => {
|
||||
});
|
||||
|
||||
describe("oauthCallbackUrlForBrowser", () => {
|
||||
it("prefers the callback URL advertised by the server", () => {
|
||||
expect(oauthCallbackUrlForBrowser(
|
||||
"http://localhost:3000",
|
||||
"https://paperclip.example.test/api/tools/oauth/callback",
|
||||
)).toBe("https://paperclip.example.test/api/tools/oauth/callback");
|
||||
});
|
||||
|
||||
it("uses localhost for local HTTP callbacks to match the API authorization request", () => {
|
||||
expect(oauthCallbackUrlForBrowser("http://127.0.0.1:3200")).toBe(
|
||||
"http://localhost:3200/api/tools/oauth/callback",
|
||||
|
||||
@@ -42,7 +42,11 @@ export function endpointHost(url: string): string | null {
|
||||
* they point at the same machine. The API therefore uses `localhost` for local
|
||||
* HTTP OAuth, so the setup form must advertise that same canonical spelling.
|
||||
*/
|
||||
export function oauthCallbackUrlForBrowser(origin: string = window.location.origin): string {
|
||||
export function oauthCallbackUrlForBrowser(
|
||||
origin: string = window.location.origin,
|
||||
serverCallbackUrl?: string,
|
||||
): string {
|
||||
if (serverCallbackUrl) return serverCallbackUrl;
|
||||
const callbackUrl = new URL("/api/tools/oauth/callback", origin);
|
||||
const hostname = callbackUrl.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
||||
if (
|
||||
|
||||