From fcb2e99e8f8e8194396acb45b7fb771a881af168 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:08:34 -0500 Subject: [PATCH] feat(apps): expand the self-serve connection catalog (#12344) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - A useful app store needs accurate and selectable provider definitions. > - Local brand assets now cover the expanded provider set. > - Provider methods differ in transport, authentication, ownership, and required scope. > - This pull request expands the catalog and encodes those provider contracts. > - The benefit is a larger self-serve store with explicit setup choices. ## Linked Issues or Issue Description Refs #11965 This is stack 6 of 11. It depends on stack 5 and replaces another reviewable part of #11965. ## What Changed - Add and update provider definitions for the self-serve catalog. - Add Google Workspace connection methods and capability profiles. - Add catalog generation, ingestion, URL matching, and contract tests. - Update legacy key tests to use a provider that still uses header credentials. ## Verification - `pnpm --filter @paperclipai/server typecheck` - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/tool-access-service.test.ts` - Result: 164 tests passed. - `pnpm build` ## Risks - An incorrect provider definition can offer the wrong setup method. - Contract tests verify transport, authentication, and provider URL behavior. - The change does not add a database migration. > I checked `ROADMAP.md`. This stack continues the existing app connection work from #11965 and does not duplicate another planned item. ## Model Used OpenAI Codex, GPT-5. The runtime model ID and context window were not exposed. The model used reasoning, tool use, and code execution. ## 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 `Refs #` or (b) described the issue in this pull request - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [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 --- .../shared/src/app-definitions-url.test.ts | 16 +- .../shared/src/app-definitions.generated.ts | 55 ++- .../src/app-definitions.ingestion-report.json | 365 +++++++++++------- packages/shared/src/app-definitions.test.ts | 190 ++++++++- packages/shared/src/app-definitions.ts | 165 +++++++- .../shared/src/app-definitions/airtable.json | 41 ++ .../shared/src/app-definitions/anthropic.json | 3 +- .../src/app-definitions/api-key-generic.json | 2 +- .../shared/src/app-definitions/asana.json | 42 ++ .../shared/src/app-definitions/beehiiv.json | 41 ++ .../shared/src/app-definitions/bitly.json | 78 ++++ packages/shared/src/app-definitions/box.json | 42 ++ packages/shared/src/app-definitions/brex.json | 43 +++ .../shared/src/app-definitions/candid.json | 41 ++ .../src/app-definitions/clickhouse.json | 58 +++ .../src/app-definitions/cloudflare.json | 78 ++++ .../src/app-definitions/cloudinary.json | 42 ++ packages/shared/src/app-definitions/coda.json | 80 ++++ .../shared/src/app-definitions/composio.json | 15 +- .../shared/src/app-definitions/context7.json | 3 +- .../shared/src/app-definitions/egnyte.json | 41 ++ .../shared/src/app-definitions/embat.json | 41 ++ .../shared/src/app-definitions/github.json | 3 +- .../shared/src/app-definitions/gmail.json | 133 ++++++- .../src/app-definitions/google-calendar.json | 184 +++++++++ .../src/app-definitions/google-chat.json | 193 +++++++++ .../src/app-definitions/google-docs.json | 183 +++++++++ .../src/app-definitions/google-drive.json | 179 +++++++++ .../src/app-definitions/google-people.json | 108 ++++++ .../src/app-definitions/google-sheets.json | 176 ++++++++- .../src/app-definitions/google-slides.json | 183 +++++++++ .../google-workspace-search.json | 111 ++++++ .../src/app-definitions/hugging-face.json | 44 +++ packages/shared/src/app-definitions/jira.json | 66 ++++ .../shared/src/app-definitions/kernel.json | 78 ++++ .../shared/src/app-definitions/linear.json | 25 +- .../src/app-definitions/local-falcon.json | 41 ++ packages/shared/src/app-definitions/make.json | 42 ++ .../shared/src/app-definitions/manufact.json | 41 ++ packages/shared/src/app-definitions/mem0.json | 56 +++ packages/shared/src/app-definitions/miro.json | 42 ++ .../shared/src/app-definitions/mixpanel.json | 42 ++ .../shared/src/app-definitions/netlify.json | 41 ++ .../shared/src/app-definitions/notion.json | 25 +- .../src/app-definitions/oauth-generic.json | 2 +- .../shared/src/app-definitions/oreilly.json | 78 ++++ .../shared/src/app-definitions/pagerduty.json | 93 +++++ .../src/app-definitions/planetscale.json | 91 +++++ .../shared/src/app-definitions/posthog.json | 77 +++- .../shared/src/app-definitions/postman.json | 194 ++++++++++ .../shared/src/app-definitions/razorpay.json | 81 ++++ .../shared/src/app-definitions/resend.json | 42 ++ .../shared/src/app-definitions/sanity.json | 79 ++++ .../shared/src/app-definitions/sentry.json | 7 +- .../shared/src/app-definitions/shopify.json | 57 +++ .../src/app-definitions/similarweb.json | 57 +++ .../shared/src/app-definitions/slack.json | 5 +- .../shared/src/app-definitions/stripe.json | 80 ++++ .../shared/src/app-definitions/supabase.json | 164 ++++++++ .../src/app-definitions/ticket-tailor.json | 42 ++ .../shared/src/app-definitions/ticktick.json | 41 ++ .../shared/src/app-definitions/todoist.json | 41 ++ .../shared/src/app-definitions/vercel.json | 9 +- .../shared/src/app-definitions/webflow.json | 41 ++ packages/shared/src/app-definitions/wix.json | 41 ++ packages/shared/src/app-definitions/xero.json | 53 +++ .../shared/src/app-definitions/zapier.json | 33 +- .../shared/src/google-workspace-connectors.ts | 41 ++ packages/shared/src/index.ts | 1 + .../shared/src/self-serve-mcp-research.json | 55 +++ .../shared/src/self-serve-mcp-research.ts | 10 + packages/shared/src/types/app-definition.ts | 35 +- packages/shared/src/types/tool-access.ts | 1 + .../shared/src/validators/app-definition.ts | 10 +- scripts/ingest-app-definitions.mjs | 150 ++++++- .../src/__tests__/tool-access-service.test.ts | 87 +++-- server/src/services/tool-access.ts | 8 +- ui/src/components/OnboardingWizard.test.tsx | 23 +- ui/src/components/OnboardingWizard.tsx | 6 +- ui/src/pages/apps/AppsConnect.test.tsx | 91 +++-- ui/src/pages/apps/AppsConnect.tsx | 8 +- 81 files changed, 5057 insertions(+), 355 deletions(-) create mode 100644 packages/shared/src/app-definitions/airtable.json create mode 100644 packages/shared/src/app-definitions/asana.json create mode 100644 packages/shared/src/app-definitions/beehiiv.json create mode 100644 packages/shared/src/app-definitions/bitly.json create mode 100644 packages/shared/src/app-definitions/box.json create mode 100644 packages/shared/src/app-definitions/brex.json create mode 100644 packages/shared/src/app-definitions/candid.json create mode 100644 packages/shared/src/app-definitions/clickhouse.json create mode 100644 packages/shared/src/app-definitions/cloudflare.json create mode 100644 packages/shared/src/app-definitions/cloudinary.json create mode 100644 packages/shared/src/app-definitions/coda.json create mode 100644 packages/shared/src/app-definitions/egnyte.json create mode 100644 packages/shared/src/app-definitions/embat.json create mode 100644 packages/shared/src/app-definitions/google-calendar.json create mode 100644 packages/shared/src/app-definitions/google-chat.json create mode 100644 packages/shared/src/app-definitions/google-docs.json create mode 100644 packages/shared/src/app-definitions/google-drive.json create mode 100644 packages/shared/src/app-definitions/google-people.json create mode 100644 packages/shared/src/app-definitions/google-slides.json create mode 100644 packages/shared/src/app-definitions/google-workspace-search.json create mode 100644 packages/shared/src/app-definitions/hugging-face.json create mode 100644 packages/shared/src/app-definitions/jira.json create mode 100644 packages/shared/src/app-definitions/kernel.json create mode 100644 packages/shared/src/app-definitions/local-falcon.json create mode 100644 packages/shared/src/app-definitions/make.json create mode 100644 packages/shared/src/app-definitions/manufact.json create mode 100644 packages/shared/src/app-definitions/mem0.json create mode 100644 packages/shared/src/app-definitions/miro.json create mode 100644 packages/shared/src/app-definitions/mixpanel.json create mode 100644 packages/shared/src/app-definitions/netlify.json create mode 100644 packages/shared/src/app-definitions/oreilly.json create mode 100644 packages/shared/src/app-definitions/pagerduty.json create mode 100644 packages/shared/src/app-definitions/planetscale.json create mode 100644 packages/shared/src/app-definitions/postman.json create mode 100644 packages/shared/src/app-definitions/razorpay.json create mode 100644 packages/shared/src/app-definitions/resend.json create mode 100644 packages/shared/src/app-definitions/sanity.json create mode 100644 packages/shared/src/app-definitions/shopify.json create mode 100644 packages/shared/src/app-definitions/similarweb.json create mode 100644 packages/shared/src/app-definitions/stripe.json create mode 100644 packages/shared/src/app-definitions/supabase.json create mode 100644 packages/shared/src/app-definitions/ticket-tailor.json create mode 100644 packages/shared/src/app-definitions/ticktick.json create mode 100644 packages/shared/src/app-definitions/todoist.json create mode 100644 packages/shared/src/app-definitions/webflow.json create mode 100644 packages/shared/src/app-definitions/wix.json create mode 100644 packages/shared/src/app-definitions/xero.json create mode 100644 packages/shared/src/google-workspace-connectors.ts create mode 100644 packages/shared/src/self-serve-mcp-research.json create mode 100644 packages/shared/src/self-serve-mcp-research.ts diff --git a/packages/shared/src/app-definitions-url.test.ts b/packages/shared/src/app-definitions-url.test.ts index 13fe37be4a..5503545e72 100644 --- a/packages/shared/src/app-definitions-url.test.ts +++ b/packages/shared/src/app-definitions-url.test.ts @@ -18,9 +18,19 @@ describe("tool app gallery URL matching", () => { expect(getAppDefinitionForUrl("https://docs.googleapis.com/drive/v3/files")).toBeNull(); }); - it("does not list Google Drive until its OAuth client flow is supported", () => { - expect(CONNECTABLE_APP_DEFINITIONS.map((app) => app.slug)).not.toContain("google-drive"); - expect(getAppDefinitionForUrl("https://mcp.google.com/drive")).toBeNull(); + it("lists each reviewed Google Workspace MCP endpoint independently", () => { + expect(CONNECTABLE_APP_DEFINITIONS.map((app) => app.slug)).toEqual(expect.arrayContaining([ + "gmail", + "google-drive", + "google-docs", + "google-sheets", + "google-slides", + "google-calendar", + "google-chat", + "google-people", + "google-workspace-search", + ])); + expect(getAppDefinitionForUrl("https://drivemcp.googleapis.com/mcp/v1")?.slug).toBe("google-drive"); }); it("lists Composio as a connectable API-key app", () => { diff --git a/packages/shared/src/app-definitions.generated.ts b/packages/shared/src/app-definitions.generated.ts index 617d99f25a..bc8d6190e1 100644 --- a/packages/shared/src/app-definitions.generated.ts +++ b/packages/shared/src/app-definitions.generated.ts @@ -4,14 +4,61 @@ import a2 from "./app-definitions/slack.json" with { type: "json" }; import a3 from "./app-definitions/notion.json" with { type: "json" }; import a4 from "./app-definitions/posthog.json" with { type: "json" }; import a5 from "./app-definitions/linear.json" with { type: "json" }; -import a6 from "./app-definitions/google-sheets.json" with { type: "json" }; -import a7 from "./app-definitions/context7.json" with { type: "json" }; +import a6 from "./app-definitions/context7.json" with { type: "json" }; +import a7 from "./app-definitions/shopify.json" with { type: "json" }; import a8 from "./app-definitions/composio.json" with { type: "json" }; import a9 from "./app-definitions/oauth-generic.json" with { type: "json" }; import a10 from "./app-definitions/api-key-generic.json" with { type: "json" }; import a11 from "./app-definitions/sentry.json" with { type: "json" }; import a12 from "./app-definitions/vercel.json" with { type: "json" }; import a13 from "./app-definitions/anthropic.json" with { type: "json" }; -import a14 from "./app-definitions/gmail.json" with { type: "json" }; +import a14 from "./app-definitions/jira.json" with { type: "json" }; +import a15 from "./app-definitions/airtable.json" with { type: "json" }; +import a16 from "./app-definitions/beehiiv.json" with { type: "json" }; +import a17 from "./app-definitions/bitly.json" with { type: "json" }; +import a18 from "./app-definitions/candid.json" with { type: "json" }; +import a19 from "./app-definitions/cloudflare.json" with { type: "json" }; +import a20 from "./app-definitions/cloudinary.json" with { type: "json" }; +import a21 from "./app-definitions/coda.json" with { type: "json" }; +import a22 from "./app-definitions/hugging-face.json" with { type: "json" }; +import a23 from "./app-definitions/kernel.json" with { type: "json" }; +import a24 from "./app-definitions/local-falcon.json" with { type: "json" }; +import a25 from "./app-definitions/make.json" with { type: "json" }; +import a26 from "./app-definitions/manufact.json" with { type: "json" }; +import a27 from "./app-definitions/miro.json" with { type: "json" }; +import a28 from "./app-definitions/netlify.json" with { type: "json" }; +import a29 from "./app-definitions/oreilly.json" with { type: "json" }; +import a30 from "./app-definitions/planetscale.json" with { type: "json" }; +import a31 from "./app-definitions/resend.json" with { type: "json" }; +import a32 from "./app-definitions/ticktick.json" with { type: "json" }; +import a33 from "./app-definitions/todoist.json" with { type: "json" }; +import a34 from "./app-definitions/webflow.json" with { type: "json" }; +import a35 from "./app-definitions/wix.json" with { type: "json" }; +import a36 from "./app-definitions/brex.json" with { type: "json" }; +import a37 from "./app-definitions/clickhouse.json" with { type: "json" }; +import a38 from "./app-definitions/egnyte.json" with { type: "json" }; +import a39 from "./app-definitions/embat.json" with { type: "json" }; +import a40 from "./app-definitions/mixpanel.json" with { type: "json" }; +import a41 from "./app-definitions/postman.json" with { type: "json" }; +import a42 from "./app-definitions/razorpay.json" with { type: "json" }; +import a43 from "./app-definitions/sanity.json" with { type: "json" }; +import a44 from "./app-definitions/stripe.json" with { type: "json" }; +import a45 from "./app-definitions/supabase.json" with { type: "json" }; +import a46 from "./app-definitions/ticket-tailor.json" with { type: "json" }; +import a47 from "./app-definitions/asana.json" with { type: "json" }; +import a48 from "./app-definitions/box.json" with { type: "json" }; +import a49 from "./app-definitions/mem0.json" with { type: "json" }; +import a50 from "./app-definitions/pagerduty.json" with { type: "json" }; +import a51 from "./app-definitions/similarweb.json" with { type: "json" }; +import a52 from "./app-definitions/xero.json" with { type: "json" }; +import a53 from "./app-definitions/gmail.json" with { type: "json" }; +import a54 from "./app-definitions/google-drive.json" with { type: "json" }; +import a55 from "./app-definitions/google-docs.json" with { type: "json" }; +import a56 from "./app-definitions/google-sheets.json" with { type: "json" }; +import a57 from "./app-definitions/google-slides.json" with { type: "json" }; +import a58 from "./app-definitions/google-calendar.json" with { type: "json" }; +import a59 from "./app-definitions/google-chat.json" with { type: "json" }; +import a60 from "./app-definitions/google-people.json" with { type: "json" }; +import a61 from "./app-definitions/google-workspace-search.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] 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] as AppDefinition[]; diff --git a/packages/shared/src/app-definitions.ingestion-report.json b/packages/shared/src/app-definitions.ingestion-report.json index 4796938b1a..a7b60c7c16 100644 --- a/packages/shared/src/app-definitions.ingestion-report.json +++ b/packages/shared/src/app-definitions.ingestion-report.json @@ -10,9 +10,8 @@ "label": "initial (managed setup, Advanced expanded)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -23,6 +22,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -42,6 +42,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -57,6 +58,7 @@ "transport": "rest_api", "auth": "api_key", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 2 } @@ -72,9 +74,9 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [ - "platform_shared", "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 5 } @@ -92,6 +94,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 1, "linkCount": 0 }, @@ -102,6 +105,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 3 } @@ -117,6 +121,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 3, "linkCount": 0 }, @@ -124,9 +129,8 @@ "label": "Step 3 Configure — path: MCP — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 7, "linkCount": 2 }, @@ -137,6 +141,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 9, "linkCount": 4 }, @@ -145,6 +150,7 @@ "transport": "rest_api", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 5 }, @@ -155,6 +161,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 10, "linkCount": 3 } @@ -172,9 +179,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -185,6 +191,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -202,9 +209,9 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [ - "platform_shared", "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 5 } @@ -219,9 +226,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -232,6 +238,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -249,6 +256,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 0, "linkCount": 0 }, @@ -257,6 +265,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 }, @@ -267,6 +276,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -283,9 +293,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -296,6 +305,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -313,6 +323,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -330,6 +341,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -347,6 +359,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -361,9 +374,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -374,6 +386,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -391,6 +404,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 0, "linkCount": 0 }, @@ -399,6 +413,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 }, @@ -409,6 +424,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -425,9 +441,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -438,6 +453,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -454,9 +470,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -467,6 +482,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -483,9 +499,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -496,6 +511,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -515,6 +531,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -532,6 +549,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -547,6 +565,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -562,6 +581,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 0, "linkCount": 0 }, @@ -570,6 +590,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 }, @@ -580,6 +601,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -599,6 +621,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -616,6 +639,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -631,6 +655,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 5 } @@ -648,6 +673,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -663,6 +689,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -677,9 +704,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -690,6 +716,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -709,6 +736,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -723,9 +751,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -736,6 +763,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -753,6 +781,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 2 } @@ -770,6 +799,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -785,9 +815,9 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [ - "platform_shared", "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 5 } @@ -802,9 +832,8 @@ "label": "Step 2 Configure — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 3, "linkCount": 2 }, @@ -815,6 +844,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 10, "linkCount": 2 } @@ -834,6 +864,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -851,6 +882,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -866,6 +898,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -881,6 +914,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -895,9 +929,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -908,6 +941,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -925,6 +959,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 0 } @@ -940,6 +975,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 3, "linkCount": 0 }, @@ -947,9 +983,8 @@ "label": "Step 3 Configure — path: MCP — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -960,6 +995,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 }, @@ -968,6 +1004,7 @@ "transport": "rest_api", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 6 } @@ -986,6 +1023,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 3, "linkCount": 0 }, @@ -993,9 +1031,8 @@ "label": "Step 3 Configure — path: MCP — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -1006,6 +1043,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 }, @@ -1016,6 +1054,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 9, "linkCount": 4 } @@ -1033,9 +1072,8 @@ "label": "Step 2 Configure — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 2, "linkCount": 2 }, @@ -1046,6 +1084,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 2 } @@ -1063,6 +1102,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 5 } @@ -1077,9 +1117,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 5, "linkCount": 2 }, @@ -1090,6 +1129,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 2 } @@ -1109,6 +1149,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 2 } @@ -1123,9 +1164,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 5, "linkCount": 2 }, @@ -1136,6 +1176,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 2 } @@ -1152,9 +1193,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -1165,6 +1205,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -1181,9 +1222,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -1194,6 +1234,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -1210,9 +1251,8 @@ "label": "initial (managed setup)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 5, "linkCount": 2 }, @@ -1223,6 +1263,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 2 } @@ -1239,9 +1280,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 5, "linkCount": 0 }, @@ -1252,6 +1292,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 0 } @@ -1269,6 +1310,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 0, "linkCount": 0 }, @@ -1277,6 +1319,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 }, @@ -1287,6 +1330,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1306,6 +1350,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1320,9 +1365,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -1333,6 +1377,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -1352,6 +1397,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1367,6 +1413,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 3, "linkCount": 0 }, @@ -1374,9 +1421,8 @@ "label": "Step 3 Configure — path: MCP — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -1387,6 +1433,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 }, @@ -1395,6 +1442,7 @@ "transport": "rest_api", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 5 }, @@ -1405,6 +1453,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 10, "linkCount": 4 } @@ -1423,9 +1472,9 @@ "transport": "rest_api", "auth": "oauth", "ownershipModes": [ - "platform_shared", "dcr" ], + "externalCredentialCustody": "vercel_connect", "fieldCount": 1, "linkCount": 2 }, @@ -1436,6 +1485,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 10, "linkCount": 2 } @@ -1451,6 +1501,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 3, "linkCount": 0 } @@ -1470,6 +1521,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1487,6 +1539,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1501,9 +1554,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -1514,6 +1566,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -1531,9 +1584,9 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [ - "platform_shared", "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -1551,6 +1604,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1568,6 +1622,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1582,9 +1637,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 5, "linkCount": 0 }, @@ -1595,6 +1649,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 0 } @@ -1611,9 +1666,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -1624,6 +1678,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -1640,9 +1695,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -1653,6 +1707,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -1672,6 +1727,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1686,9 +1742,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -1699,6 +1754,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -1718,6 +1774,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1735,6 +1792,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1750,6 +1808,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 3, "linkCount": 0 }, @@ -1757,9 +1816,8 @@ "label": "Step 3 Configure — path: MCP — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 7, "linkCount": 2 }, @@ -1770,6 +1828,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 9, "linkCount": 4 }, @@ -1778,6 +1837,7 @@ "transport": "rest_api", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 2 }, @@ -1788,6 +1848,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 4 }, @@ -1798,6 +1859,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 10, "linkCount": 4 } @@ -1815,9 +1877,8 @@ "label": "initial — Step 2 Configure (managed package install view)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 0, "linkCount": 2 }, @@ -1825,9 +1886,8 @@ "label": "Step 2 Configure — managed view after clicking \"change\" (domain editor)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 1, "linkCount": 2 }, @@ -1838,6 +1898,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 1, "linkCount": 0 } @@ -1857,6 +1918,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 3, "linkCount": 0 }, @@ -1864,9 +1926,8 @@ "label": "Step 3 Configure — path: MCP — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 2 }, @@ -1877,6 +1938,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 }, @@ -1885,6 +1947,7 @@ "transport": "rest_api", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 5 }, @@ -1895,6 +1958,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 10, "linkCount": 4 } @@ -1915,6 +1979,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1932,6 +1997,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -1946,9 +2012,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -1959,6 +2024,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -1976,6 +2042,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 1, "linkCount": 0 } @@ -1990,9 +2057,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -2003,6 +2069,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2019,9 +2086,8 @@ "label": "Step 2 Configure — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 2, "linkCount": 2 }, @@ -2032,6 +2098,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 9, "linkCount": 2 } @@ -2049,6 +2116,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 0, "linkCount": 0 }, @@ -2057,6 +2125,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 4, "linkCount": 2 }, @@ -2065,6 +2134,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 1, "linkCount": 2 } @@ -2083,6 +2153,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 5 } @@ -2097,9 +2168,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -2110,6 +2180,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2126,9 +2197,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -2139,6 +2209,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2158,6 +2229,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -2172,9 +2244,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -2185,6 +2256,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2201,9 +2273,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 5, "linkCount": 0 }, @@ -2214,6 +2285,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 0 } @@ -2230,9 +2302,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -2243,6 +2314,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2260,6 +2332,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 0, "linkCount": 0 }, @@ -2268,6 +2341,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 }, @@ -2278,6 +2352,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 4 } @@ -2295,6 +2370,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 2 }, @@ -2305,6 +2381,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 2 } @@ -2322,6 +2399,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 3, "linkCount": 0 }, @@ -2329,9 +2407,8 @@ "label": "Step 3 Configure — path: MCP — tab: Managed (default)", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 7, "linkCount": 2 }, @@ -2342,6 +2419,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 9, "linkCount": 4 }, @@ -2350,6 +2428,7 @@ "transport": "rest_api", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 7, "linkCount": 5 }, @@ -2360,6 +2439,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 10, "linkCount": 3 } @@ -2377,9 +2457,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -2390,6 +2469,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2407,6 +2487,7 @@ "transport": "mcp_remote", "auth": null, "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 1, "linkCount": 0 } @@ -2424,9 +2505,9 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [ - "platform_shared", "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2441,9 +2522,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -2454,6 +2534,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2471,6 +2552,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 5 } @@ -2485,9 +2567,8 @@ "label": "initial (managed setup, step 2 \"Configure\")", "transport": "mcp_remote", "auth": null, - "ownershipModes": [ - "platform_shared" - ], + "ownershipModes": [], + "externalCredentialCustody": "vercel_connect", "fieldCount": 6, "linkCount": 0 }, @@ -2498,6 +2579,7 @@ "ownershipModes": [ "customer" ], + "externalCredentialCustody": null, "fieldCount": 8, "linkCount": 0 } @@ -2515,6 +2597,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [], + "externalCredentialCustody": null, "fieldCount": 6, "linkCount": 5 } diff --git a/packages/shared/src/app-definitions.test.ts b/packages/shared/src/app-definitions.test.ts index 1bbec347ec..d16f335550 100644 --- a/packages/shared/src/app-definitions.test.ts +++ b/packages/shared/src/app-definitions.test.ts @@ -1,18 +1,200 @@ import { describe,expect,it } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { APP_DEFINITIONS } from "./app-definitions.generated.js"; -import { recommendedDefaultsForApp } from "./app-definitions.js"; +import { APP_STORE_DEFINITIONS, APP_STORE_HIDDEN_SLUGS, CONNECTABLE_APP_DEFINITIONS, appSupportsCatalogSetup, getAvailableConnectionMethod, getRecommendedConnectionMethod, recommendedDefaultsForApp, resolveConnectionMethodServerUrl } from "./app-definitions.js"; +import { BLOCKED_MCP_PROVIDERS, SELF_SERVE_MCP_CANDIDATES, SELF_SERVE_MCP_RESEARCH } from "./self-serve-mcp-research.js"; import { appDefinitionsSchema } from "./validators/app-definition.js"; describe("AppDefinition catalog",()=>{ it("validates all Wave 1 definitions",()=>expect(()=>appDefinitionsSchema.parse(APP_DEFINITIONS)).not.toThrow()); - it("contains the reviewed providers",()=>expect(APP_DEFINITIONS.map((app)=>app.slug)).toEqual(["zapier","github","slack","notion","posthog","linear","google-sheets","context7","composio","oauth-generic","api-key-generic","sentry","vercel","anthropic","gmail"])); + it("contains every established provider plus the reviewed self-serve catalog",()=>{ + expect(APP_DEFINITIONS.map((app)=>app.slug)).toEqual(expect.arrayContaining(["zapier","github","slack","notion","posthog","linear","google-sheets","context7","composio","oauth-generic","api-key-generic","sentry","vercel","anthropic","gmail","google-drive","google-docs","google-slides","google-calendar","google-chat","google-people","google-workspace-search"])); + expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(43); + expect(BLOCKED_MCP_PROVIDERS.map((entry)=>entry.slug)).toEqual(["g2","vercel","zomato"]); + const definitionSlugs=new Set(APP_DEFINITIONS.map((app)=>app.slug)); + const connectableSlugs=new Set(CONNECTABLE_APP_DEFINITIONS.map((app)=>app.slug)); + expect(SELF_SERVE_MCP_CANDIDATES.filter((entry)=>!definitionSlugs.has(entry.slug))).toEqual([]); + expect(SELF_SERVE_MCP_CANDIDATES.filter((entry)=>!connectableSlugs.has(entry.slug))).toEqual([]); + for(const entry of BLOCKED_MCP_PROVIDERS)expect(connectableSlugs.has(entry.slug)).toBe(false); + }); + it("keeps a complete, unique, dated evidence ledger for all 46 researched MCP providers",()=>{ + expect(SELF_SERVE_MCP_RESEARCH.verifiedAt).toBe("2026-08-26"); + expect(SELF_SERVE_MCP_RESEARCH.entries).toHaveLength(46); + expect(new Set(SELF_SERVE_MCP_RESEARCH.entries.map((entry)=>entry.slug))).toHaveProperty("size",46); + 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:"); + expect(entry.authMode).toBeTruthy(); + expect(entry.prerequisite.length).toBeGreaterThan(10); + expect(["S1","S2","S3","S4"]).toContain(entry.riskTier); + } + }); + it("uses the reviewed current endpoints and configuration modes",()=>{ + const method=(slug:string,key?:string)=>APP_DEFINITIONS.find((app)=>app.slug===slug)?.methods.find((candidate)=>!key||candidate.key===key); + expect(method("jira")?.defaults?.serverUrl).toBe("https://mcp.atlassian.com/v1/mcp/authv2"); + expect(method("jira")?.defaults?.scopesHint).toEqual([ + "read:me", + "read:account", + "offline_access", + "email", + "read:jira-work", + "write:jira-work", + "search:confluence", + "read:confluence-user", + "read:page:confluence", + "write:page:confluence", + "read:comment:confluence", + "write:comment:confluence", + "read:space:confluence", + "read:hierarchical-content:confluence", + "write:component:compass", + "read:component:compass", + "read:scorecard:compass", + "write:scorecard:compass", + "read:event:compass", + "read:metric:compass", + "read:all:twg", + "write:all:twg", + ]); + expect(method("cloudinary")?.defaults?.serverUrl).toBe("https://asset-management.mcp.cloudinary.com/mcp"); + expect(method("kernel")?.defaults?.serverUrl).toBe("https://mcp.onkernel.com/mcp"); + expect(method("resend")?.defaults?.serverUrl).toBe("https://mcp.resend.com/mcp"); + expect(method("clickhouse")?.defaults?.serverUrl).toBe("https://mcp.clickhouse.cloud/clickstack"); + expect(method("clickhouse")?.tenantFields?.[0]?.transport).toEqual({location:"header",name:"x-service-id"}); + expect(method("mem0")).toMatchObject({auth:"api_key",keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "}}); + expect(method("mem0")?.defaults?.serverUrl).toBe("https://mcp.mem0.ai/mcp/"); + expect(method("xero")?.defaults?.scopesHint).toEqual([ + "openid","profile","email","offline_access","accounting.settings","accounting.invoices.read","accounting.reports.aged.read","accounting.reports.balancesheet.read","accounting.reports.profitandloss.read", + ]); + expect(APP_DEFINITIONS.find((app)=>app.slug==="pagerduty")?.methods.map((candidate)=>({key:candidate.key,serverUrl:candidate.defaults?.serverUrl}))).toEqual([ + {key:"mcp-api-key-us",serverUrl:"https://mcp.pagerduty.com/mcp"}, + {key:"mcp-api-key-eu",serverUrl:"https://mcp.eu.pagerduty.com/mcp"}, + ]); + expect(method("context7")).toMatchObject({auth:"none",defaults:{serverUrl:"https://mcp.context7.com/mcp"}}); + expect(APP_DEFINITIONS.find((app)=>app.slug==="planetscale")?.methods.map((candidate)=>candidate.key)).toEqual(["mcp-oauth","mcp-insights-only"]); + expect(APP_DEFINITIONS.find((app)=>app.slug==="postman")?.methods.map((candidate)=>candidate.key)).toEqual([ + "mcp-oauth-minimal","mcp-oauth-code","mcp-oauth-full","mcp-eu-key-minimal","mcp-eu-key-code","mcp-eu-key-full", + ]); + expect(method("supabase")?.tenantFields?.find((field)=>field.key==="readOnly")?.defaultValue).toBe(false); + expect(method("asana")?.ownershipModes).toEqual(["customer"]); + expect(method("zapier")).toMatchObject({key:"generated-url",auth:"none",defaults:{}}); + expect(method("zapier")?.credentialFields).toBeUndefined(); + }); it("uses discovery-first Notion MCP OAuth metadata",()=>{ const notion=APP_DEFINITIONS.find((app)=>app.slug==="notion"); expect(notion?.redirectConstraints).toBe("https-or-loopback-http"); expect(notion?.methods[0]?.defaults).toEqual({serverUrl:"https://mcp.notion.com/mcp"}); }); it("preserves required Linear OAuth scopes",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="linear")?.methods[0]?.defaults?.scopesHint).toEqual(["read","write"])); + it("requests only Hugging Face's MCP read scope",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="hugging-face")?.methods[0]?.defaults?.scopesHint).toEqual(["read-mcp"])); it("defaults S2-S4 write and destructive actions to ask-first",()=>{for(const app of APP_DEFINITIONS)for(const method of app.methods)expect(recommendedDefaultsForApp(app,method.key)).toEqual({access:"all_agents",askFirstRiskLevels:method.riskTier==="S1"?[]:["write","destructive"]})}); - it("keeps Gmail personal-only and bound to the Paperclip ID broker",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="gmail")?.methods[0]).toMatchObject({oauthStrategy:"paperclip_id_connector",grantKinds:["user"],defaults:{serverUrl:"https://gmailmcp.googleapis.com/mcp/v1",scopesHint:["https://www.googleapis.com/auth/gmail.readonly","https://www.googleapis.com/auth/gmail.compose"]}})); - it("offers PostHog OAuth and API-key methods with broad defaults and advanced narrowing",()=>{const posthog=APP_DEFINITIONS.find((app)=>app.slug==="posthog");expect(posthog?.methods.map((method)=>method.key)).toEqual(["mcp-oauth","mcp-api-key"]);for(const method of posthog?.methods??[]){expect(method.riskTier).toBe("S3");expect(method.tenantFields?.find((field)=>field.key==="readOnly")?.defaultValue).toBe(false);expect(method.tenantFields?.find((field)=>field.key==="projectId")?.transport).toEqual({location:"header",name:"x-posthog-project-id"});expect(method.tenantFields?.filter((field)=>field.advanced).map((field)=>field.key)).toEqual(["features","tools","mode"]);expect(method.configRequirements).toBeUndefined();expect(method.requiredResourceFilters).toEqual(["project"])}}); + it("defaults explicit read/write capability groups to their write-capable method",()=>{ + const drive=APP_DEFINITIONS.find((app)=>app.slug==="google-drive")!; + const gmail=APP_DEFINITIONS.find((app)=>app.slug==="gmail")!; + expect(getAvailableConnectionMethod(drive)?.key).toBe("customer-write-oauth"); + expect(getAvailableConnectionMethod(gmail)?.key).toBe("customer-draft-oauth"); + expect(getRecommendedConnectionMethod(drive.methods.filter((candidate)=>candidate.ownershipModes.includes("customer")))?.key).toBe("customer-write-oauth"); + }); + it("explains Google Workspace Developer Preview enrollment before connection",()=>{ + const googleWorkspaceMcpSlugs=["gmail","google-drive","google-docs","google-slides","google-calendar","google-chat","google-people","google-workspace-search"]; + for(const slug of googleWorkspaceMcpSlugs){ + const prerequisite=APP_DEFINITIONS.find((app)=>app.slug===slug)?.setupPrerequisite; + expect(prerequisite?.actionUrl,slug).toBe("https://developers.google.com/workspace/preview"); + expect(prerequisite?.description,slug).toContain("does not enable unrelated Paperclip customers"); + expect(prerequisite?.steps?.join(" "),slug).toContain("final project-registration email"); + } + }); + it("withholds unverified and reserved providers from the app store without deleting their definitions",()=>{ + expect([...APP_STORE_HIDDEN_SLUGS].sort()).toEqual([ + "beehiiv","bitly","brex","candid","coda","composio","context7","egnyte","embat","github","kernel","local-falcon","make","manufact","oreilly","planetscale","razorpay","sanity","similarweb","slack","ticket-tailor","ticktick","xero", + ]); + expect(APP_STORE_DEFINITIONS).toHaveLength(35); + const connectableSlugs=new Set(CONNECTABLE_APP_DEFINITIONS.map((entry)=>entry.slug)); + const storeSlugs=new Set(APP_STORE_DEFINITIONS.map((entry)=>entry.slug)); + for(const slug of APP_STORE_HIDDEN_SLUGS){ + expect(connectableSlugs.has(slug),slug).toBe(true); + expect(storeSlugs.has(slug),slug).toBe(false); + } + }); + it("ships complete local branding provenance for all 35 store-visible providers",()=>{ + const uiPublic=path.resolve(path.dirname(fileURLToPath(import.meta.url)),"../../../ui/public"); + const manifest=JSON.parse(fs.readFileSync(path.join(uiPublic,"brands/apps/manifest.json"),"utf8")) as {providers:Array<{slug:string;catalogVisible:boolean;localAsset:string;darkAsset?:string;officialSourceUrl:string;upstreamAssetUrl:string;assetType:"svg"|"png";darkVariantRequired:boolean}>}; + const visible=manifest.providers.filter((entry)=>entry.catalogVisible); + expect(visible).toHaveLength(35); + expect(new Set(visible.map((entry)=>entry.slug))).toHaveProperty("size",35); + expect(new Set(visible.map((entry)=>entry.localAsset))).toHaveProperty("size",35); + expect(new Set(APP_STORE_DEFINITIONS.map((entry)=>entry.slug))).toEqual(new Set(visible.map((entry)=>entry.slug))); + for(const app of APP_STORE_DEFINITIONS){ + const provenance=visible.find((entry)=>entry.slug===app.slug)!; + expect(provenance).toBeTruthy(); + expect(provenance.localAsset).toBe(app.branding.logoUrl); + expect(provenance.darkAsset).toBe(app.branding.darkLogoUrl); + expect(provenance.darkVariantRequired).toBe(Boolean(provenance.darkAsset)); + expect(new URL(provenance.officialSourceUrl).protocol).toBe("https:"); + expect(new URL(provenance.upstreamAssetUrl).protocol).toBe("https:"); + expect(provenance.localAsset).toMatch(/^\/brands\/apps\/.+\.(svg|png)$/); + expect(provenance.localAsset).not.toContain("google.com/s2/favicons"); + const asset=fs.readFileSync(path.join(uiPublic,provenance.localAsset)); + if(provenance.assetType==="png"){ + expect(asset.subarray(0,8).toString("hex")).toBe("89504e470d0a1a0a"); + expect(asset.readUInt32BE(16)).toBeGreaterThanOrEqual(128); + expect(asset.readUInt32BE(20)).toBeGreaterThanOrEqual(128); + }else{ + const svg=asset.toString("utf8"); + expect(svg).toMatch(/^{ + const definitions=new Map(CONNECTABLE_APP_DEFINITIONS.map((entry)=>[entry.slug,entry])); + for(const candidate of SELF_SERVE_MCP_CANDIDATES)expect(appSupportsCatalogSetup(definitions.get(candidate.slug))).toBe(true); + for(const blocked of BLOCKED_MCP_PROVIDERS)expect(definitions.has(blocked.slug)).toBe(false); + }); + it("ships each Google Workspace surface as an independent personal OAuth app",()=>{ + const expected={ + gmail:"https://gmailmcp.googleapis.com/mcp/v1", + "google-drive":"https://drivemcp.googleapis.com/mcp/v1", + "google-docs":"https://docsmcp.googleapis.com/mcp/v1", + "google-sheets":"https://sheetsmcp.googleapis.com/mcp/v1", + "google-slides":"https://slidesmcp.googleapis.com/mcp/v1", + "google-calendar":"https://calendarmcp.googleapis.com/mcp/v1", + "google-chat":"https://chatmcp.googleapis.com/mcp/v1", + "google-people":"https://people.googleapis.com/mcp/v1", + "google-workspace-search":"https://workspacemcp.googleapis.com/mcp/v1", + } as const; + for(const [slug,serverUrl] of Object.entries(expected)){ + const app=APP_DEFINITIONS.find((candidate)=>candidate.slug===slug); + expect(app,slug).toBeTruthy(); + expect(app?.methods.some((method)=>method.oauthStrategy==="paperclip_id_connector")).toBe(true); + for(const method of app?.methods.filter((candidate)=>candidate.auth==="oauth")??[]){ + expect(method.grantKinds,`${slug}:${method.key}`).toEqual(["user"]); + expect(method.defaults?.serverUrl,`${slug}:${method.key}`).toBe(serverUrl); + expect(method.capabilityProfile,`${slug}:${method.key}`).toBeTruthy(); + } + } + }); + it("does not advertise a managed Gmail read-only method before profile-scoped connector support",()=>{ + const gmail=APP_DEFINITIONS.find((app)=>app.slug==="gmail"); + const managedMethods=gmail?.methods.filter((method)=>method.oauthStrategy==="paperclip_id_connector")??[]; + expect(managedMethods.map((method)=>method.key)).toEqual(["paperclip-draft"]); + expect(managedMethods[0]?.defaults?.scopesHint).toEqual([ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/gmail.compose", + ]); + }); + it("configures Shopify's official tenant-scoped Storefront MCP without OAuth",()=>{const method=APP_DEFINITIONS.find((app)=>app.slug==="shopify")?.methods[0];expect(method).toMatchObject({key:"storefront-mcp",auth:"none",defaults:{serverUrlTemplate:"https://{storeDomain}/api/mcp"},tenantFields:[expect.objectContaining({key:"storeDomain",required:true})]});expect(resolveConnectionMethodServerUrl(method!,{storeDomain:"paperclip-demo.myshopify.com"})).toBe("https://paperclip-demo.myshopify.com/api/mcp");expect(resolveConnectionMethodServerUrl(method!,{})).toBeNull()}); + it("offers PostHog OAuth and API-key methods with zero-config defaults and advanced narrowing",()=>{const posthog=APP_DEFINITIONS.find((app)=>app.slug==="posthog");expect(posthog?.methods.map((method)=>method.key)).toEqual(["mcp-oauth","mcp-api-key"]);for(const method of posthog?.methods??[]){const projectField=method.tenantFields?.find((field)=>field.key==="projectId");expect(method.riskTier).toBe("S3");expect(method.tenantFields?.find((field)=>field.key==="readOnly")).toMatchObject({defaultValue:false,advanced:true});expect(projectField).toMatchObject({advanced:true,transport:{location:"header",name:"x-posthog-project-id"}});expect(projectField?.required).not.toBe(true);expect(method.tenantFields?.filter((field)=>field.advanced).map((field)=>field.key)).toEqual(["projectId","readOnly","features","tools"]);expect(method.tenantFields?.find((field)=>field.key==="mode")).toMatchObject({hidden:true,defaultValue:"tools",transport:{location:"query",name:"mode"}});expect(method.configRequirements).toBeUndefined();expect(method.requiredResourceFilters).toBeUndefined();expect(method.guidanceMd).toContain("optional advanced controls")}}); + it("requires only reviewed provider or safety-boundary configuration on the default path",()=>{const required=APP_DEFINITIONS.flatMap((app)=>app.methods.flatMap((method)=>[...(method.tenantFields??[]),...(method.extensionFields??[])].filter((field)=>field.required&&field.advanced!==true&&!field.hidden).map((field)=>`${app.slug}:${method.key}:${field.key}`))).sort();expect(required).toEqual(["clickhouse:mcp-oauth:serviceId","shopify:storefront-mcp:storeDomain","supabase:mcp-api-key:projectRef","supabase:mcp-oauth:projectRef"])}); + it("limits Vercel Connect setup to the reviewed pilot methods",()=>{ + const reviewed=APP_DEFINITIONS.flatMap((app)=>app.methods.flatMap((method)=>method.credentialSources?.vercelConnect?[{slug:app.slug,key:method.key,review:method.credentialSources.vercelConnect}]:[])); + expect(reviewed.map(({slug,key})=>`${slug}:${key}`).sort()).toEqual(["linear:mcp-oauth","notion:mcp-oauth","posthog:mcp-api-key","posthog:mcp-oauth"]); + expect(reviewed.find(({slug})=>slug==="linear")?.review).toMatchObject({services:["linear"],principalModes:["user"],scopes:["read","write"],header:{name:"Authorization",prefix:"Bearer "}}); + expect(reviewed.find(({slug,key})=>slug==="posthog"&&key==="mcp-oauth")?.review.services).toEqual(["posthog","mcp.posthog.com/mcp"]); + expect(reviewed.find(({slug,key})=>slug==="posthog"&&key==="mcp-api-key")?.review.principalModes).toEqual(["app"]); + expect(APP_DEFINITIONS.find((app)=>app.slug==="vercel")?.availability?.available).toBe(false); + }); it("enforces method and field invariants",()=>{for(const app of APP_DEFINITIONS)for(const method of app.methods){if(method.auth==="api_key")expect(method.keyPlacement).toBeTruthy();if(method.auth==="oauth")expect(method.ownershipModes.length).toBeGreaterThan(0);for(const field of method.credentialFields??[])if(field.required&&field.type!=="checkbox")expect(field.placeholder).toBeTruthy()}}); }); diff --git a/packages/shared/src/app-definitions.ts b/packages/shared/src/app-definitions.ts index bb57e13f8f..9fdb48c85a 100644 --- a/packages/shared/src/app-definitions.ts +++ b/packages/shared/src/app-definitions.ts @@ -1,8 +1,10 @@ import { APP_DEFINITIONS } from "./app-definitions.generated.js"; +import { SELF_SERVE_MCP_CANDIDATES } from "./self-serve-mcp-research.js"; import type { AppDefinition, ConnectionMethodDef, FieldDef } from "./types/app-definition.js"; import type { ToolConnectionOwnership } from "./types/tool-access.js"; -const CONNECTABLE_APP_SLUGS = new Set([ +export const CONNECTABLE_APP_SLUGS = new Set([ + ...SELF_SERVE_MCP_CANDIDATES.map((entry) => entry.slug), "zapier", "github", "slack", @@ -11,14 +13,58 @@ const CONNECTABLE_APP_SLUGS = new Set([ "linear", "google-sheets", "context7", + "shopify", "composio", "gmail", + "google-drive", + "google-docs", + "google-slides", + "google-calendar", + "google-chat", + "google-people", + "google-workspace-search", ]); export const CONNECTABLE_APP_DEFINITIONS = APP_DEFINITIONS.filter((app) => CONNECTABLE_APP_SLUGS.has(app.slug) ); +/** + * Definitions retained for existing connections and later verification, but + * intentionally withheld from the customer-facing store. Keeping visibility + * separate from recognition avoids breaking saved connections when a provider + * is pulled from Browse or reserved for a future first-party experience. + */ +export const APP_STORE_HIDDEN_SLUGS = new Set([ + "beehiiv", + "bitly", + "brex", + "candid", + "coda", + "composio", + "context7", + "egnyte", + "embat", + "github", + "kernel", + "local-falcon", + "make", + "manufact", + "oreilly", + "planetscale", + "razorpay", + "sanity", + "similarweb", + "slack", + "ticket-tailor", + "ticktick", + "xero", +]); + +export const APP_STORE_DEFINITIONS = CONNECTABLE_APP_DEFINITIONS.filter((app) => + !APP_STORE_HIDDEN_SLUGS.has(app.slug) +); + export const DEFAULT_OWNERSHIP_AVAILABILITY: Record = { platform_shared: false, platform_provisioned: false, @@ -30,6 +76,14 @@ export function getConnectableAppDefinition(slug: string): AppDefinition | null return CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === slug) ?? null; } +export function getAppStoreDefinition(slug: string): AppDefinition | null { + return APP_STORE_DEFINITIONS.find((app) => app.slug === slug) ?? null; +} + +export function isAppStoreVisibleSlug(slug: string | null | undefined): boolean { + return Boolean(slug && !APP_STORE_HIDDEN_SLUGS.has(slug) && CONNECTABLE_APP_SLUGS.has(slug)); +} + function wildcardPatternToRegExp(pattern: string): RegExp { const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"); return new RegExp(`^${escaped}$`, "i"); @@ -57,22 +111,125 @@ export function getAvailableConnectionMethods(app: AppDefinition): ConnectionMet ); } +/** + * Pick the method that gives a new connection the app's useful write surface. + * + * Google Workspace publishes separate read and write capability groups. The + * read method is intentionally listed first for documentation, but treating + * array order as a product default silently created read-only connections and + * left every write action Off. Capability metadata is the durable signal; apps + * without an explicit write/draft capability retain their declared order. + */ +export function getRecommendedConnectionMethod( + methods: readonly ConnectionMethodDef[], +): ConnectionMethodDef | null { + return methods.find((method) => { + const capabilityKey = method.capabilityProfile?.key; + return capabilityKey === "write" || capabilityKey === "draft"; + }) ?? methods[0] ?? null; +} + export function getAvailableConnectionMethod( app: AppDefinition, methodKey?: string | null, ): ConnectionMethodDef | null { const methods = getAvailableConnectionMethods(app); - return methodKey ? methods.find((method) => method.key === methodKey) ?? null : methods[0] ?? null; + return methodKey + ? methods.find((method) => method.key === methodKey) ?? null + : getRecommendedConnectionMethod(methods); +} + +export function connectionMethodSupportsAutomaticOAuth(method: ConnectionMethodDef | null | undefined): boolean { + return method?.auth === "oauth" && ( + method.oauthStrategy === "paperclip_id_connector" + || method.ownershipModes.includes("dcr") + ); +} + +export function connectionMethodAcceptsCustomerOAuthClient(method: ConnectionMethodDef | null | undefined): boolean { + return method?.auth === "oauth" + && !method.oauthStrategy + && method.ownershipModes.includes("customer"); +} + +export function connectionMethodSupportsCatalogSetup(method: ConnectionMethodDef | null | undefined): boolean { + if (!method) return false; + if (method.auth === "none" || method.auth === "api_key") return true; + return connectionMethodSupportsAutomaticOAuth(method) + || connectionMethodAcceptsCustomerOAuthClient(method); +} + +export function connectionMethodRequiresConfiguration(method: ConnectionMethodDef | null | undefined): boolean { + if (!method) return false; + const visibleTenantFields = method.tenantFields?.filter((field) => !field.hidden) ?? []; + const visibleExtensionFields = method.extensionFields?.filter((field) => !field.hidden) ?? []; + return Boolean( + method.credentialFields?.length + || visibleTenantFields.length + || visibleExtensionFields.length + || method.configRequirements?.atLeastOneOf?.length + // "Use your own OAuth app" is an advanced alternative when DCR/CIMD is + // available, not a required setup field. Only customer-client-only methods + // must stop on the configuration screen. + || ( + connectionMethodAcceptsCustomerOAuthClient(method) + && !connectionMethodSupportsAutomaticOAuth(method) + ), + ); +} + +export function appSupportsCatalogSetup(app: AppDefinition | null | undefined): boolean { + return Boolean(app && getAvailableConnectionMethods(app).some(connectionMethodSupportsCatalogSetup)); +} + +export function isConnectableAppSlug(slug: string | null | undefined): boolean { + return Boolean(slug && CONNECTABLE_APP_SLUGS.has(slug)); +} + +export function appSupportsAutomaticOAuth(app: AppDefinition | null | undefined): boolean { + return Boolean(app && getAvailableConnectionMethods(app).some(connectionMethodSupportsAutomaticOAuth)); +} + +export function appAcceptsCustomerOAuthClient(app: AppDefinition | null | undefined): boolean { + return Boolean(app && getAvailableConnectionMethods(app).some(connectionMethodAcceptsCustomerOAuthClient)); } export function credentialConfigPath(field: FieldDef): string { return `credentials.${field.key}`; } +export function resolveConnectionMethodServerUrl( + method: ConnectionMethodDef, + configValues: Record, +): string | null { + const template = method.defaults?.serverUrlTemplate; + if (!template) return method.defaults?.serverUrl ?? null; + + let missingValue = false; + const resolved = template.replace(/\{([a-zA-Z0-9_-]+)\}/g, (_placeholder, key: string) => { + const value = configValues[key]; + if (value === undefined || String(value).trim().length === 0) { + missingValue = true; + return ""; + } + return encodeURIComponent(String(value).trim()); + }); + if (missingValue) return null; + + try { + return new URL(resolved).toString(); + } catch { + return null; + } +} + export function recommendedDefaultsForApp(app: AppDefinition, methodKey?: string | null): Record { - const method = getAvailableConnectionMethod(app, methodKey); + const normalizedMethodKey = app.slug === "gmail" && methodKey === "paperclip-id-oauth" ? "paperclip-draft" : methodKey; + const method = normalizedMethodKey + ? app.methods.find((candidate) => candidate.key === normalizedMethodKey) ?? null + : getAvailableConnectionMethod(app, null); return { access: "all_agents", - askFirstRiskLevels: method?.riskTier === "S1" ? [] : ["write", "destructive"], + askFirstRiskLevels: method && method.riskTier !== "S1" ? ["write", "destructive"] : [], }; } diff --git a/packages/shared/src/app-definitions/airtable.json b/packages/shared/src/app-definitions/airtable.json new file mode 100644 index 0000000000..e07676ca40 --- /dev/null +++ b/packages/shared/src/app-definitions/airtable.json @@ -0,0 +1,41 @@ +{ + "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", + "ownershipModes": [ + "dcr" + ], + "whenToUse": "Use browser sign-in for the provider-hosted MCP server.", + "defaults": { + "serverUrl": "https://mcp.airtable.com/mcp" + }, + "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." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/anthropic.json b/packages/shared/src/app-definitions/anthropic.json index 4fbf1dbe5d..a52b73a4fe 100644 --- a/packages/shared/src/app-definitions/anthropic.json +++ b/packages/shared/src/app-definitions/anthropic.json @@ -8,7 +8,8 @@ ], "featured": false, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=anthropic.com&sz=128" + "logoUrl": "/brands/apps/anthropic.svg", + "darkLogoUrl": "/brands/apps/anthropic-dark.svg" }, "urlPatterns": [ "https://api.anthropic.com/*" diff --git a/packages/shared/src/app-definitions/api-key-generic.json b/packages/shared/src/app-definitions/api-key-generic.json index 6ef035757d..7ba7863f73 100644 --- a/packages/shared/src/app-definitions/api-key-generic.json +++ b/packages/shared/src/app-definitions/api-key-generic.json @@ -8,7 +8,7 @@ ], "featured": false, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=openapis.org&sz=128" + "logoUrl": "/brands/apps/api-key-generic.svg" }, "urlPatterns": [], "methods": [ diff --git a/packages/shared/src/app-definitions/asana.json b/packages/shared/src/app-definitions/asana.json new file mode 100644 index 0000000000..d144ead8be --- /dev/null +++ b/packages/shared/src/app-definitions/asana.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "slug": "asana", + "name": "Asana", + "description": "Connect Asana's provider-hosted MCP server.", + "categories": [ + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/asana.svg" + }, + "urlPatterns": [ + "https://mcp.asana.com/*" + ], + "docsUrl": "https://developers.asana.com/docs/integrating-with-asanas-mcp-server", + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "mcp-own-oauth", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Register an OAuth app with Asana, then enter its client ID and secret.", + "defaults": { + "serverUrl": "https://mcp.asana.com/v2/mcp" + }, + "guidanceMd": "Connect Asana in the browser. Create an Asana MCP OAuth app and register Paperclip's callback URI; DCR is not supported.", + "riskTier": "S3", + "label": "Use your own OAuth app", + "consoleLinks": { + "register": "https://developers.asana.com/docs/integrating-with-asanas-mcp-server", + "docs": "https://developers.asana.com/docs/integrating-with-asanas-mcp-server" + }, + "warnings": [ + "Create an Asana MCP OAuth app and register Paperclip's callback URI; DCR is not supported." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/beehiiv.json b/packages/shared/src/app-definitions/beehiiv.json new file mode 100644 index 0000000000..fe1ec45a38 --- /dev/null +++ b/packages/shared/src/app-definitions/beehiiv.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "beehiiv", + "name": "beehiiv", + "description": "Connect beehiiv's provider-hosted MCP server.", + "categories": [ + "content" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/beehiiv.png" + }, + "urlPatterns": [ + "https://mcp.beehiiv.com/*" + ], + "docsUrl": "https://www.beehiiv.com/features/mcp/getting-started", + "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.beehiiv.com/mcp" + }, + "guidanceMd": "Connect beehiiv in the browser. A beehiiv account; the subscription plan controls available write capabilities.", + "riskTier": "S3", + "label": "Sign in with beehiiv", + "consoleLinks": { + "docs": "https://www.beehiiv.com/features/mcp/getting-started" + }, + "warnings": [ + "A beehiiv account; the subscription plan controls available write capabilities." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/bitly.json b/packages/shared/src/app-definitions/bitly.json new file mode 100644 index 0000000000..4bd0bee7db --- /dev/null +++ b/packages/shared/src/app-definitions/bitly.json @@ -0,0 +1,78 @@ +{ + "schemaVersion": 1, + "slug": "bitly", + "name": "Bitly", + "description": "Connect Bitly's provider-hosted MCP server.", + "categories": [ + "analytics" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/bitly.svg" + }, + "urlPatterns": [ + "https://api-ssl.bitly.com/*" + ], + "docsUrl": "https://dev.bitly.com/bitly-mcp/overview/quickstart/", + "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://api-ssl.bitly.com/v4/mcp" + }, + "guidanceMd": "Connect Bitly in the browser. A Bitly account with either browser authorization or an API token.", + "riskTier": "S2", + "label": "Sign in with Bitly", + "consoleLinks": { + "docs": "https://dev.bitly.com/bitly-mcp/overview/quickstart/" + }, + "warnings": [ + "A Bitly account with either browser authorization or an API token." + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://api-ssl.bitly.com/v4/mcp" + }, + "guidanceMd": "Use a customer-created Bitly key. A Bitly account with either browser authorization or an API token.", + "riskTier": "S2", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Bitly API key", + "type": "password", + "required": true, + "placeholder": "Paste your Bitly API token", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://dev.bitly.com/bitly-mcp/overview/quickstart/", + "docs": "https://dev.bitly.com/bitly-mcp/overview/quickstart/" + }, + "warnings": [ + "A Bitly account with either browser authorization or an API token." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/box.json b/packages/shared/src/app-definitions/box.json new file mode 100644 index 0000000000..f8c45a35b1 --- /dev/null +++ b/packages/shared/src/app-definitions/box.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "slug": "box", + "name": "Box", + "description": "Connect Box's provider-hosted MCP server.", + "categories": [ + "content" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/box.svg" + }, + "urlPatterns": [ + "https://mcp.box.com/*" + ], + "docsUrl": "https://support.box.com/hc/en-us/articles/43847256139923-Managing-Box-MCP-Servers", + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "mcp-own-oauth", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Register an OAuth app with Box, then enter its client ID and secret.", + "defaults": { + "serverUrl": "https://mcp.box.com" + }, + "guidanceMd": "Connect Box in the browser. A Box administrator creates the OAuth integration and enables AI access.", + "riskTier": "S3", + "label": "Use your own OAuth app", + "consoleLinks": { + "register": "https://support.box.com/hc/en-us/articles/43847256139923-Managing-Box-MCP-Servers", + "docs": "https://support.box.com/hc/en-us/articles/43847256139923-Managing-Box-MCP-Servers" + }, + "warnings": [ + "A Box administrator creates the OAuth integration and enables AI access." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/brex.json b/packages/shared/src/app-definitions/brex.json new file mode 100644 index 0000000000..f02b41a13d --- /dev/null +++ b/packages/shared/src/app-definitions/brex.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": 1, + "slug": "brex", + "name": "Brex", + "description": "Connect Brex's provider-hosted MCP server.", + "categories": [ + "commerce" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/brex.svg", + "darkLogoUrl": "/brands/apps/brex-dark.svg" + }, + "urlPatterns": [ + "https://api.brex.com/*" + ], + "docsUrl": "https://www.brex.com/support/using-brex-in-ai-apps", + "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://api.brex.com/mcp" + }, + "guidanceMd": "Connect Brex in the browser. Brex early access and an administrator enabling the integration; financial actions require explicit approval.", + "riskTier": "S4", + "label": "Sign in with Brex", + "consoleLinks": { + "docs": "https://www.brex.com/support/using-brex-in-ai-apps" + }, + "warnings": [ + "Brex early access and an administrator enabling the integration; financial actions require explicit approval.", + "Financial or destructive actions must be explicitly approved before execution." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/candid.json b/packages/shared/src/app-definitions/candid.json new file mode 100644 index 0000000000..27d296682f --- /dev/null +++ b/packages/shared/src/app-definitions/candid.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "candid", + "name": "Candid", + "description": "Connect Candid's provider-hosted MCP server.", + "categories": [ + "data" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/candid.png" + }, + "urlPatterns": [ + "https://mcp.candid.org/*" + ], + "docsUrl": "https://learning.candid.org/getting-started-with-the-candid-mcp-connector/375441", + "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.candid.org/mcp" + }, + "guidanceMd": "Connect Candid in the browser. A Candid account with access to the MCP connector.", + "riskTier": "S2", + "label": "Sign in with Candid", + "consoleLinks": { + "docs": "https://learning.candid.org/getting-started-with-the-candid-mcp-connector/375441" + }, + "warnings": [ + "A Candid account with access to the MCP connector." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/clickhouse.json b/packages/shared/src/app-definitions/clickhouse.json new file mode 100644 index 0000000000..e0579241a1 --- /dev/null +++ b/packages/shared/src/app-definitions/clickhouse.json @@ -0,0 +1,58 @@ +{ + "schemaVersion": 1, + "slug": "clickhouse", + "name": "ClickHouse", + "description": "Connect ClickHouse's provider-hosted MCP server.", + "categories": [ + "data" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/clickhouse.svg" + }, + "urlPatterns": [ + "https://mcp.clickhouse.cloud/*" + ], + "docsUrl": "https://clickhouse.com/blog/announcing-managed-clickstack-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.clickhouse.cloud/clickstack" + }, + "guidanceMd": "Connect ClickHouse in the browser. A ClickHouse Cloud ClickStack service and its service ID.", + "riskTier": "S4", + "label": "Sign in with ClickHouse", + "consoleLinks": { + "docs": "https://clickhouse.com/blog/announcing-managed-clickstack-mcp-server" + }, + "warnings": [ + "A ClickHouse Cloud ClickStack service and its service ID." + ], + "tenantFields": [ + { + "key": "serviceId", + "label": "ClickHouse Cloud service ID", + "type": "text", + "required": true, + "placeholder": "11e1031f-9a13-4cac-9bc7-d4ec9286ec17", + "helperMd": "Copy the service ID from ClickStack → Team Settings → API & Agents.", + "transport": { + "location": "header", + "name": "x-service-id" + } + } + ], + "requiredResourceFilters": [ + "service" + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/cloudflare.json b/packages/shared/src/app-definitions/cloudflare.json new file mode 100644 index 0000000000..b90ec69dcc --- /dev/null +++ b/packages/shared/src/app-definitions/cloudflare.json @@ -0,0 +1,78 @@ +{ + "schemaVersion": 1, + "slug": "cloudflare", + "name": "Cloudflare", + "description": "Connect Cloudflare's provider-hosted MCP server.", + "categories": [ + "developer" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/cloudflare.svg" + }, + "urlPatterns": [ + "https://mcp.cloudflare.com/*" + ], + "docsUrl": "https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/", + "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.cloudflare.com/mcp" + }, + "guidanceMd": "Connect Cloudflare in the browser. A Cloudflare account with access to the resources being connected.", + "riskTier": "S3", + "label": "Sign in with Cloudflare", + "consoleLinks": { + "docs": "https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/" + }, + "warnings": [ + "A Cloudflare account with access to the resources being connected." + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.cloudflare.com/mcp" + }, + "guidanceMd": "Use a customer-created Cloudflare key. A Cloudflare account with access to the resources being connected.", + "riskTier": "S3", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Cloudflare API key", + "type": "password", + "required": true, + "placeholder": "Paste your Cloudflare API token", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/", + "docs": "https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/" + }, + "warnings": [ + "A Cloudflare account with access to the resources being connected." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/cloudinary.json b/packages/shared/src/app-definitions/cloudinary.json new file mode 100644 index 0000000000..027a8294c3 --- /dev/null +++ b/packages/shared/src/app-definitions/cloudinary.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "slug": "cloudinary", + "name": "Cloudinary", + "description": "Connect Cloudinary's provider-hosted MCP server.", + "categories": [ + "content" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/cloudinary.svg", + "darkLogoUrl": "/brands/apps/cloudinary-dark.svg" + }, + "urlPatterns": [ + "https://asset-management.mcp.cloudinary.com/*" + ], + "docsUrl": "https://cloudinary.com/documentation/cloudinary_llm_mcp", + "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://asset-management.mcp.cloudinary.com/mcp" + }, + "guidanceMd": "Connect Cloudinary in the browser. A Cloudinary account; authorization is limited by the signed-in user's roles.", + "riskTier": "S3", + "label": "Sign in with Cloudinary", + "consoleLinks": { + "docs": "https://cloudinary.com/documentation/cloudinary_llm_mcp" + }, + "warnings": [ + "A Cloudinary account; authorization is limited by the signed-in user's roles." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/coda.json b/packages/shared/src/app-definitions/coda.json new file mode 100644 index 0000000000..515aa17401 --- /dev/null +++ b/packages/shared/src/app-definitions/coda.json @@ -0,0 +1,80 @@ +{ + "schemaVersion": 1, + "slug": "coda", + "name": "Coda", + "description": "Connect Coda's provider-hosted MCP server.", + "categories": [ + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/coda.svg" + }, + "urlPatterns": [ + "https://coda.io/*" + ], + "docsUrl": "https://help.coda.io/hc/en-us/articles/44722769665549-Security-recommendations-for-the-Coda-MCP", + "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://coda.io/apis/mcp" + }, + "guidanceMd": "Connect Coda in the browser. A Coda account; the hosted MCP service is currently beta.", + "riskTier": "S3", + "label": "Sign in with Coda", + "consoleLinks": { + "docs": "https://help.coda.io/hc/en-us/articles/44722769665549-Security-recommendations-for-the-Coda-MCP" + }, + "warnings": [ + "A Coda account; the hosted MCP service is currently beta.", + "This provider's hosted MCP server is currently beta or preview." + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://coda.io/apis/mcp" + }, + "guidanceMd": "Use a customer-created Coda key. A Coda account; the hosted MCP service is currently beta.", + "riskTier": "S3", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Coda API key", + "type": "password", + "required": true, + "placeholder": "Paste your Coda API token", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://help.coda.io/hc/en-us/articles/44722769665549-Security-recommendations-for-the-Coda-MCP", + "docs": "https://help.coda.io/hc/en-us/articles/44722769665549-Security-recommendations-for-the-Coda-MCP" + }, + "warnings": [ + "A Coda account; the hosted MCP service is currently beta.", + "This provider's hosted MCP server is currently beta or preview." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/composio.json b/packages/shared/src/app-definitions/composio.json index 470b0651b5..29f3efe967 100644 --- a/packages/shared/src/app-definitions/composio.json +++ b/packages/shared/src/app-definitions/composio.json @@ -3,18 +3,25 @@ "slug": "composio", "name": "Composio", "description": "Connect Composio so Paperclip can discover and manage the toolkits in your project.", - "categories": ["productivity"], + "categories": [ + "productivity" + ], "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=composio.dev&sz=128" + "logoUrl": "/brands/apps/composio.svg", + "darkLogoUrl": "/brands/apps/composio-dark.svg" }, - "urlPatterns": ["https://backend.composio.dev/*"], + "urlPatterns": [ + "https://backend.composio.dev/*" + ], "methods": [ { "key": "api-key", "transport": "rest_api", "auth": "api_key", - "ownershipModes": ["customer"], + "ownershipModes": [ + "customer" + ], "whenToUse": "Use a project API key from the Composio project that owns the toolkits and connected accounts.", "defaults": { "serviceHost": "backend.composio.dev" diff --git a/packages/shared/src/app-definitions/context7.json b/packages/shared/src/app-definitions/context7.json index 46a3fcfac0..04a0bedb59 100644 --- a/packages/shared/src/app-definitions/context7.json +++ b/packages/shared/src/app-definitions/context7.json @@ -8,7 +8,8 @@ ], "featured": false, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=context7.com&sz=128" + "logoUrl": "/brands/apps/context7.svg", + "darkLogoUrl": "/brands/apps/context7-dark.svg" }, "urlPatterns": [ "https://mcp.context7.com/*" diff --git a/packages/shared/src/app-definitions/egnyte.json b/packages/shared/src/app-definitions/egnyte.json new file mode 100644 index 0000000000..72a50f3313 --- /dev/null +++ b/packages/shared/src/app-definitions/egnyte.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "egnyte", + "name": "Egnyte", + "description": "Connect Egnyte's provider-hosted MCP server.", + "categories": [ + "content" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/egnyte.svg" + }, + "urlPatterns": [ + "https://mcp-server.egnyte.com/*" + ], + "docsUrl": "https://developers.egnyte.com/docs/Remote_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-server.egnyte.com/mcp" + }, + "guidanceMd": "Connect Egnyte in the browser. An eligible Egnyte plan and administrator approval for external LLM access.", + "riskTier": "S3", + "label": "Sign in with Egnyte", + "consoleLinks": { + "docs": "https://developers.egnyte.com/docs/Remote_MCP_Server" + }, + "warnings": [ + "An eligible Egnyte plan and administrator approval for external LLM access." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/embat.json b/packages/shared/src/app-definitions/embat.json new file mode 100644 index 0000000000..ed9df4d1e6 --- /dev/null +++ b/packages/shared/src/app-definitions/embat.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "embat", + "name": "Embat", + "description": "Connect Embat's provider-hosted MCP server.", + "categories": [ + "commerce" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/embat.svg" + }, + "urlPatterns": [ + "https://tellme.embat.io/*" + ], + "docsUrl": "https://tellme.embat.io/.well-known/oauth-protected-resource/mcp", + "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://tellme.embat.io/mcp" + }, + "guidanceMd": "Connect Embat in the browser. An Embat account; pilot the connection because provider setup documentation is sparse.", + "riskTier": "S4", + "label": "Sign in with Embat", + "consoleLinks": { + "docs": "https://tellme.embat.io/.well-known/oauth-protected-resource/mcp" + }, + "warnings": [ + "An Embat account; pilot the connection because provider setup documentation is sparse." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/github.json b/packages/shared/src/app-definitions/github.json index 9194f39720..3863206738 100644 --- a/packages/shared/src/app-definitions/github.json +++ b/packages/shared/src/app-definitions/github.json @@ -8,7 +8,8 @@ ], "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=github.com&sz=128" + "logoUrl": "/brands/apps/github.svg", + "darkLogoUrl": "/brands/apps/github-dark.svg" }, "urlPatterns": [ "https://api.githubcopilot.com/mcp/*" diff --git a/packages/shared/src/app-definitions/gmail.json b/packages/shared/src/app-definitions/gmail.json index 21952a44ab..3ef758bee1 100644 --- a/packages/shared/src/app-definitions/gmail.json +++ b/packages/shared/src/app-definitions/gmail.json @@ -3,24 +3,90 @@ "slug": "gmail", "name": "Gmail", "description": "Search and read Gmail messages and create drafts without enabling mail sending.", - "categories": ["communication", "productivity"], + "categories": [ + "communication", + "productivity" + ], "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=gmail.com&sz=128" + "logoUrl": "/brands/apps/gmail.svg" + }, + "urlPatterns": [ + "https://gmailmcp.googleapis.com/*" + ], + "docsUrl": "https://developers.google.com/workspace/gmail/api/reference/mcp", + "setupPrerequisite": { + "title": "Google Developer Preview access required", + "description": "Google must register both the Workspace email used to authorize Paperclip and the Google Cloud project that owns the OAuth client. Registration is limited to those emails and projects; it does not enable unrelated Paperclip customers.", + "steps": [ + "Apply with the Workspace email that will sign in and the Google Cloud project that owns the OAuth client.", + "Wait for the Google Group notification and then Google's final project-registration email, usually within a couple of days.", + "Add every additional tester email or Cloud project through Google's member request forms before connecting." + ], + "actionLabel": "Apply or verify Developer Preview enrollment", + "actionUrl": "https://developers.google.com/workspace/preview" }, - "urlPatterns": ["https://gmailmcp.googleapis.com/*"], - "docsUrl": "https://developers.google.com/workspace/guides/configure-mcp-servers", "redirectConstraints": "https-or-loopback-http", "methods": [ { - "key": "paperclip-id-oauth", - "label": "Connect Gmail", + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Search and read messages, threads, drafts, and labels." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned Google OAuth client for read-only Gmail access.", + "defaults": { + "serverUrl": "https://gmailmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/gmail.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Gmail and Gmail MCP APIs in your Google Cloud project, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "paperclip-draft", + "label": "Connect with Paperclip", "transport": "mcp_remote", "auth": "oauth", "oauthStrategy": "paperclip_id_connector", - "grantKinds": ["user"], - "ownershipModes": ["customer"], - "whenToUse": "Use Paperclip ID for a personal Gmail connection with centrally registered Google OAuth.", + "connectorProfile": "gmail.draft", + "capabilityProfile": { + "key": "draft", + "label": "Read & create drafts", + "description": "Read Gmail and create drafts for review in Gmail." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed Google OAuth to read Gmail and create drafts.", "defaults": { "serverUrl": "https://gmailmcp.googleapis.com/mcp/v1", "scopesHint": [ @@ -28,11 +94,54 @@ "https://www.googleapis.com/auth/gmail.compose" ] }, - "guidanceMd": "Connect your Gmail identity. Paperclip can search and read mail and create drafts. Sending mail is not enabled.", + "guidanceMd": "Connect Gmail to search and read mail and create drafts. Sending mail is permanently disabled.", "warnings": [ - "This connection is personal. Agents need an explicit install, profile, and delegation before they can use it." + "Draft creation requires approval. Sending mail is not enabled.", + "Google Workspace MCP servers are in Developer Preview." ], - "riskTier": "S3" + "riskTier": "S4" + }, + { + "key": "customer-draft-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "draft", + "label": "Read & create drafts", + "description": "Read Gmail and create drafts for review in Gmail." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned Google OAuth client to read Gmail and create drafts.", + "defaults": { + "serverUrl": "https://gmailmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/gmail.compose" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Gmail and Gmail MCP APIs in your Google Cloud project, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Draft creation requires approval. Sending mail is not enabled.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" } ] } diff --git a/packages/shared/src/app-definitions/google-calendar.json b/packages/shared/src/app-definitions/google-calendar.json new file mode 100644 index 0000000000..9c7ae1f70f --- /dev/null +++ b/packages/shared/src/app-definitions/google-calendar.json @@ -0,0 +1,184 @@ +{ + "schemaVersion": 1, + "slug": "google-calendar", + "name": "Google Calendar", + "description": "Read calendars and manage Google Calendar events.", + "categories": [ + "productivity" + ], + "featured": true, + "branding": { + "logoUrl": "/brands/apps/google-calendar.svg" + }, + "urlPatterns": [ + "https://calendarmcp.googleapis.com/*", + "https://calendar.google.com/*" + ], + "docsUrl": "https://developers.google.com/workspace/calendar/api/reference/mcp", + "setupPrerequisite": { + "title": "Google Developer Preview access required", + "description": "Google must register both the Workspace email used to authorize Paperclip and the Google Cloud project that owns the OAuth client. Registration is limited to those emails and projects; it does not enable unrelated Paperclip customers.", + "steps": [ + "Apply with the Workspace email that will sign in and the Google Cloud project that owns the OAuth client.", + "Wait for the Google Group notification and then Google's final project-registration email, usually within a couple of days.", + "Add every additional tester email or Cloud project through Google's member request forms before connecting." + ], + "actionLabel": "Apply or verify Developer Preview enrollment", + "actionUrl": "https://developers.google.com/workspace/preview" + }, + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "paperclip-read", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "calendar.read", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Read calendars, events, and availability." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for read-only Calendar access.", + "defaults": { + "serverUrl": "https://calendarmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/calendar.calendarlist.readonly", + "https://www.googleapis.com/auth/calendar.events.freebusy", + "https://www.googleapis.com/auth/calendar.events.readonly" + ] + }, + "guidanceMd": "Connect Google Calendar to read schedules and availability.", + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Read calendars, events, and availability." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client for read-only Calendar access.", + "defaults": { + "serverUrl": "https://calendarmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/calendar.calendarlist.readonly", + "https://www.googleapis.com/auth/calendar.events.freebusy", + "https://www.googleapis.com/auth/calendar.events.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Calendar and Calendar MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "paperclip-write", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "calendar.write", + "capabilityProfile": { + "key": "write", + "label": "Read & manage", + "description": "Create, update, respond to, and delete events." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth to read and manage Calendar events.", + "defaults": { + "serverUrl": "https://calendarmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/calendar.calendarlist.readonly", + "https://www.googleapis.com/auth/calendar.events.freebusy", + "https://www.googleapis.com/auth/calendar.events" + ] + }, + "guidanceMd": "Connect Google Calendar to read and manage events.", + "warnings": [ + "All event mutations require approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + }, + { + "key": "customer-write-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "write", + "label": "Read & manage", + "description": "Create, update, respond to, and delete events." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client to read and manage Calendar events.", + "defaults": { + "serverUrl": "https://calendarmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/calendar.calendarlist.readonly", + "https://www.googleapis.com/auth/calendar.events.freebusy", + "https://www.googleapis.com/auth/calendar.events" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Calendar and Calendar MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "All event mutations require approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + } + ] +} diff --git a/packages/shared/src/app-definitions/google-chat.json b/packages/shared/src/app-definitions/google-chat.json new file mode 100644 index 0000000000..89f97f8a4d --- /dev/null +++ b/packages/shared/src/app-definitions/google-chat.json @@ -0,0 +1,193 @@ +{ + "schemaVersion": 1, + "slug": "google-chat", + "name": "Google Chat", + "description": "Search and read Google Chat conversations and send messages.", + "categories": [ + "communication", + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/google-chat.svg" + }, + "urlPatterns": [ + "https://chatmcp.googleapis.com/*", + "https://chat.google.com/*" + ], + "docsUrl": "https://developers.google.com/workspace/chat/api/reference/mcp", + "setupPrerequisite": { + "title": "Google Developer Preview access required", + "description": "Google must register both the Workspace email used to authorize Paperclip and the Google Cloud project that owns the OAuth client. Registration is limited to those emails and projects; it does not enable unrelated Paperclip customers.", + "steps": [ + "Apply with the Workspace email that will sign in and the Google Cloud project that owns the OAuth client.", + "Wait for the Google Group notification and then Google's final project-registration email, usually within a couple of days.", + "Add every additional tester email or Cloud project through Google's member request forms before connecting." + ], + "actionLabel": "Apply or verify Developer Preview enrollment", + "actionUrl": "https://developers.google.com/workspace/preview" + }, + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "paperclip-read", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "chat.read", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Search conversations and read messages." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for read-only Chat access.", + "defaults": { + "serverUrl": "https://chatmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/chat.spaces.readonly", + "https://www.googleapis.com/auth/chat.memberships.readonly", + "https://www.googleapis.com/auth/chat.messages.readonly", + "https://www.googleapis.com/auth/chat.users.readstate.readonly" + ] + }, + "guidanceMd": "Connect Google Chat to search conversations and read messages.", + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Search conversations and read messages." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client for read-only Chat access.", + "defaults": { + "serverUrl": "https://chatmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/chat.spaces.readonly", + "https://www.googleapis.com/auth/chat.memberships.readonly", + "https://www.googleapis.com/auth/chat.messages.readonly", + "https://www.googleapis.com/auth/chat.users.readstate.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Chat and Chat MCP APIs, configure a Chat app, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "A Google Chat app must be configured in the Cloud project.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "paperclip-write", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "chat.write", + "capabilityProfile": { + "key": "write", + "label": "Read & send", + "description": "Read Chat and send messages." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth to read Chat and send messages.", + "defaults": { + "serverUrl": "https://chatmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/chat.spaces.readonly", + "https://www.googleapis.com/auth/chat.memberships.readonly", + "https://www.googleapis.com/auth/chat.messages.readonly", + "https://www.googleapis.com/auth/chat.users.readstate.readonly", + "https://www.googleapis.com/auth/chat.messages.create" + ] + }, + "guidanceMd": "Connect Google Chat to read conversations and send approved messages.", + "warnings": [ + "Sending messages requires approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + }, + { + "key": "customer-write-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "write", + "label": "Read & send", + "description": "Read Chat and send messages." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client to read Chat and send messages.", + "defaults": { + "serverUrl": "https://chatmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/chat.spaces.readonly", + "https://www.googleapis.com/auth/chat.memberships.readonly", + "https://www.googleapis.com/auth/chat.messages.readonly", + "https://www.googleapis.com/auth/chat.users.readstate.readonly", + "https://www.googleapis.com/auth/chat.messages.create" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Chat and Chat MCP APIs, configure a Chat app, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Sending messages requires approval.", + "A Google Chat app must be configured in the Cloud project.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + } + ] +} diff --git a/packages/shared/src/app-definitions/google-docs.json b/packages/shared/src/app-definitions/google-docs.json new file mode 100644 index 0000000000..c3d3caa39e --- /dev/null +++ b/packages/shared/src/app-definitions/google-docs.json @@ -0,0 +1,183 @@ +{ + "schemaVersion": 1, + "slug": "google-docs", + "name": "Google Docs", + "description": "Read and update Google Docs documents.", + "categories": [ + "content", + "productivity" + ], + "featured": true, + "branding": { + "logoUrl": "/brands/apps/google-docs.svg" + }, + "urlPatterns": [ + "https://docsmcp.googleapis.com/*", + "https://docs.google.com/document/*" + ], + "docsUrl": "https://developers.google.com/workspace/docs/api/reference/mcp", + "setupPrerequisite": { + "title": "Google Developer Preview access required", + "description": "Google must register both the Workspace email used to authorize Paperclip and the Google Cloud project that owns the OAuth client. Registration is limited to those emails and projects; it does not enable unrelated Paperclip customers.", + "steps": [ + "Apply with the Workspace email that will sign in and the Google Cloud project that owns the OAuth client.", + "Wait for the Google Group notification and then Google's final project-registration email, usually within a couple of days.", + "Add every additional tester email or Cloud project through Google's member request forms before connecting." + ], + "actionLabel": "Apply or verify Developer Preview enrollment", + "actionUrl": "https://developers.google.com/workspace/preview" + }, + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "paperclip-read", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "docs.read", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Read document text and structure." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for read-only Docs access.", + "defaults": { + "serverUrl": "https://docsmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/documents.readonly" + ] + }, + "guidanceMd": "Connect Google Docs to read documents.", + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Read document text and structure." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client for read-only Docs access.", + "defaults": { + "serverUrl": "https://docsmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/documents.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Drive, Docs, and Docs MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "paperclip-write", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "docs.write", + "capabilityProfile": { + "key": "write", + "label": "Read & edit", + "description": "Read and update documents." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth to read and update Docs.", + "defaults": { + "serverUrl": "https://docsmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/drive.file", + "https://www.googleapis.com/auth/documents" + ] + }, + "guidanceMd": "Connect Google Docs to read and update documents.", + "warnings": [ + "Document updates require approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + }, + { + "key": "customer-write-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "write", + "label": "Read & edit", + "description": "Read and update documents." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client to read and update Docs.", + "defaults": { + "serverUrl": "https://docsmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/drive.file", + "https://www.googleapis.com/auth/documents" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Drive, Docs, and Docs MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Document updates require approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + } + ] +} diff --git a/packages/shared/src/app-definitions/google-drive.json b/packages/shared/src/app-definitions/google-drive.json new file mode 100644 index 0000000000..e16ec02f51 --- /dev/null +++ b/packages/shared/src/app-definitions/google-drive.json @@ -0,0 +1,179 @@ +{ + "schemaVersion": 1, + "slug": "google-drive", + "name": "Google Drive", + "description": "Search, read, create, and copy files in Google Drive.", + "categories": [ + "content", + "productivity" + ], + "featured": true, + "branding": { + "logoUrl": "/brands/apps/google-drive.svg" + }, + "urlPatterns": [ + "https://drivemcp.googleapis.com/*", + "https://drive.google.com/*" + ], + "docsUrl": "https://developers.google.com/workspace/drive/api/reference/mcp", + "setupPrerequisite": { + "title": "Google Developer Preview access required", + "description": "Google must register both the Workspace email used to authorize Paperclip and the Google Cloud project that owns the OAuth client. Registration is limited to those emails and projects; it does not enable unrelated Paperclip customers.", + "steps": [ + "Apply with the Workspace email that will sign in and the Google Cloud project that owns the OAuth client.", + "Wait for the Google Group notification and then Google's final project-registration email, usually within a couple of days.", + "Add every additional tester email or Cloud project through Google's member request forms before connecting." + ], + "actionLabel": "Apply or verify Developer Preview enrollment", + "actionUrl": "https://developers.google.com/workspace/preview" + }, + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "paperclip-read", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "drive.read", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Search and read files and metadata." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for read-only Drive access.", + "defaults": { + "serverUrl": "https://drivemcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly" + ] + }, + "guidanceMd": "Connect Google Drive to search and read files. Google's Developer Preview Program must register the signed-in Workspace account and Google Cloud project before tools can run.", + "warnings": [ + "Before connecting, enroll the signed-in Workspace account and Google Cloud project in Google's Developer Preview Program and wait for the registration confirmation." + ], + "riskTier": "S3" + }, + { + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Search and read files and metadata." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client for read-only Drive access.", + "defaults": { + "serverUrl": "https://drivemcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enroll the Workspace account and Google Cloud project in Google's Developer Preview Program, enable the Drive and Drive MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Before connecting, enroll the signed-in Workspace account and Google Cloud project in Google's Developer Preview Program and wait for the registration confirmation." + ], + "riskTier": "S3" + }, + { + "key": "paperclip-write", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "drive.write", + "capabilityProfile": { + "key": "write", + "label": "Read & create", + "description": "Read files and create or copy files." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for Drive read and create access.", + "defaults": { + "serverUrl": "https://drivemcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/drive.file" + ] + }, + "guidanceMd": "Connect Google Drive to read files and create or copy app-accessible files. Google's Developer Preview Program must register the signed-in Workspace account and Google Cloud project before tools can run.", + "warnings": [ + "File creation and copying require approval.", + "Before connecting, enroll the signed-in Workspace account and Google Cloud project in Google's Developer Preview Program and wait for the registration confirmation." + ], + "riskTier": "S4" + }, + { + "key": "customer-write-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "write", + "label": "Read & create", + "description": "Read files and create or copy files." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client for Drive read and create access.", + "defaults": { + "serverUrl": "https://drivemcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/drive.file" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enroll the Workspace account and Google Cloud project in Google's Developer Preview Program, enable the Drive and Drive MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "File creation and copying require approval.", + "Before connecting, enroll the signed-in Workspace account and Google Cloud project in Google's Developer Preview Program and wait for the registration confirmation." + ], + "riskTier": "S4" + } + ] +} diff --git a/packages/shared/src/app-definitions/google-people.json b/packages/shared/src/app-definitions/google-people.json new file mode 100644 index 0000000000..4e3ba3de05 --- /dev/null +++ b/packages/shared/src/app-definitions/google-people.json @@ -0,0 +1,108 @@ +{ + "schemaVersion": 1, + "slug": "google-people", + "name": "Google People", + "description": "Search contacts and directory profiles with the Google People API.", + "categories": [ + "communication", + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/google-people.svg" + }, + "urlPatterns": [ + "https://people.googleapis.com/*" + ], + "docsUrl": "https://developers.google.com/people/api/mcp", + "setupPrerequisite": { + "title": "Google Developer Preview access required", + "description": "Google must register both the Workspace email used to authorize Paperclip and the Google Cloud project that owns the OAuth client. Registration is limited to those emails and projects; it does not enable unrelated Paperclip customers.", + "steps": [ + "Apply with the Workspace email that will sign in and the Google Cloud project that owns the OAuth client.", + "Wait for the Google Group notification and then Google's final project-registration email, usually within a couple of days.", + "Add every additional tester email or Cloud project through Google's member request forms before connecting." + ], + "actionLabel": "Apply or verify Developer Preview enrollment", + "actionUrl": "https://developers.google.com/workspace/preview" + }, + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "paperclip-read", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "people.read", + "capabilityProfile": { + "key": "read", + "label": "Read contacts", + "description": "Search contacts, directory people, and your profile." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for Google People access.", + "defaults": { + "serverUrl": "https://people.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/directory.readonly", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/contacts.readonly" + ] + }, + "guidanceMd": "Connect Google People to search contacts and directory profiles.", + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Read contacts", + "description": "Search contacts, directory people, and your profile." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client for Google People access.", + "defaults": { + "serverUrl": "https://people.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/directory.readonly", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/contacts.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the People and People MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Directory search availability depends on your Workspace account.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + } + ] +} diff --git a/packages/shared/src/app-definitions/google-sheets.json b/packages/shared/src/app-definitions/google-sheets.json index c46562e634..b67e83929d 100644 --- a/packages/shared/src/app-definitions/google-sheets.json +++ b/packages/shared/src/app-definitions/google-sheets.json @@ -2,27 +2,187 @@ "schemaVersion": 1, "slug": "google-sheets", "name": "Google Sheets", - "description": "Read and update selected spreadsheets.", + "description": "Read and update Google Sheets spreadsheets.", "categories": [ - "data" + "data", + "productivity" ], - "featured": false, + "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=sheets.google.com&sz=128" + "logoUrl": "/brands/apps/google-sheets.svg" }, "urlPatterns": [ + "https://sheetsmcp.googleapis.com/*", "https://docs.google.com/spreadsheets/*", "https://sheets.google.com/*" ], + "docsUrl": "https://developers.google.com/workspace/sheets/api/reference/mcp", + "redirectConstraints": "https-or-loopback-http", "methods": [ { - "key": "local", - "transport": "local_stdio", - "auth": "none", + "key": "paperclip-read", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "sheets.read", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Read spreadsheet values and structure." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for read-only Sheets access.", + "defaults": { + "serverUrl": "https://sheetsmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/spreadsheets.readonly" + ] + }, + "guidanceMd": "Connect Google Sheets to read spreadsheets.", + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Read spreadsheet values and structure." + }, + "grantKinds": [ + "user" + ], "ownershipModes": [ "customer" ], - "whenToUse": "Use credentials from your provider account.", + "whenToUse": "Use a customer-owned OAuth client for read-only Sheets access.", + "defaults": { + "serverUrl": "https://sheetsmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/spreadsheets.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Drive, Sheets, and Sheets MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "paperclip-write", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "sheets.write", + "capabilityProfile": { + "key": "write", + "label": "Read & edit", + "description": "Read and update spreadsheet values, formulas, and dimensions." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth to read and update Sheets.", + "defaults": { + "serverUrl": "https://sheetsmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/drive.file", + "https://www.googleapis.com/auth/spreadsheets" + ] + }, + "guidanceMd": "Connect Google Sheets to read and update spreadsheets.", + "warnings": [ + "Spreadsheet updates require approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + }, + { + "key": "customer-write-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "write", + "label": "Read & edit", + "description": "Read and update spreadsheet values, formulas, and dimensions." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client to read and update Sheets.", + "defaults": { + "serverUrl": "https://sheetsmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/drive.file", + "https://www.googleapis.com/auth/spreadsheets" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Drive, Sheets, and Sheets MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Spreadsheet updates require approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + }, + { + "key": "local", + "label": "Use the Paperclip robot account", + "transport": "local_stdio", + "auth": "none", + "capabilityProfile": { + "key": "robot", + "label": "Share selected sheets", + "description": "Share only named spreadsheets with the Paperclip robot account." + }, + "ownershipModes": [ + "customer" + ], + "whenToUse": "Share selected spreadsheets with the Paperclip robot account instead of connecting a Google identity.", "defaults": { "templateKey": "paperclip.google-sheets" }, diff --git a/packages/shared/src/app-definitions/google-slides.json b/packages/shared/src/app-definitions/google-slides.json new file mode 100644 index 0000000000..32323f6214 --- /dev/null +++ b/packages/shared/src/app-definitions/google-slides.json @@ -0,0 +1,183 @@ +{ + "schemaVersion": 1, + "slug": "google-slides", + "name": "Google Slides", + "description": "Read and update Google Slides presentations.", + "categories": [ + "content", + "productivity" + ], + "featured": true, + "branding": { + "logoUrl": "/brands/apps/google-slides.svg" + }, + "urlPatterns": [ + "https://slidesmcp.googleapis.com/*", + "https://docs.google.com/presentation/*" + ], + "docsUrl": "https://developers.google.com/workspace/slides/api/reference/mcp", + "setupPrerequisite": { + "title": "Google Developer Preview access required", + "description": "Google must register both the Workspace email used to authorize Paperclip and the Google Cloud project that owns the OAuth client. Registration is limited to those emails and projects; it does not enable unrelated Paperclip customers.", + "steps": [ + "Apply with the Workspace email that will sign in and the Google Cloud project that owns the OAuth client.", + "Wait for the Google Group notification and then Google's final project-registration email, usually within a couple of days.", + "Add every additional tester email or Cloud project through Google's member request forms before connecting." + ], + "actionLabel": "Apply or verify Developer Preview enrollment", + "actionUrl": "https://developers.google.com/workspace/preview" + }, + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "paperclip-read", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "slides.read", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Read presentation slides and content." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for read-only Slides access.", + "defaults": { + "serverUrl": "https://slidesmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/presentations.readonly" + ] + }, + "guidanceMd": "Connect Google Slides to read presentations.", + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Read only", + "description": "Read presentation slides and content." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client for read-only Slides access.", + "defaults": { + "serverUrl": "https://slidesmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/presentations.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Drive, Slides, and Slides MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "paperclip-write", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "slides.write", + "capabilityProfile": { + "key": "write", + "label": "Read & edit", + "description": "Read and update presentations." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth to read and update Slides.", + "defaults": { + "serverUrl": "https://slidesmcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/drive.file", + "https://www.googleapis.com/auth/presentations" + ] + }, + "guidanceMd": "Connect Google Slides to read and update presentations.", + "warnings": [ + "Presentation updates require approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + }, + { + "key": "customer-write-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "write", + "label": "Read & edit", + "description": "Read and update presentations." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client to read and update Slides.", + "defaults": { + "serverUrl": "https://slidesmcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/drive.file", + "https://www.googleapis.com/auth/presentations" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Drive, Slides, and Slides MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/configure-mcp-servers" + }, + "warnings": [ + "Presentation updates require approval.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S4" + } + ] +} diff --git a/packages/shared/src/app-definitions/google-workspace-search.json b/packages/shared/src/app-definitions/google-workspace-search.json new file mode 100644 index 0000000000..5f31acc0eb --- /dev/null +++ b/packages/shared/src/app-definitions/google-workspace-search.json @@ -0,0 +1,111 @@ +{ + "schemaVersion": 1, + "slug": "google-workspace-search", + "name": "Google Workspace Search", + "description": "Search Gmail, Drive, Calendar, and Chat through one read-only Google Workspace search tool.", + "categories": [ + "data", + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/google-workspace-search.svg" + }, + "urlPatterns": [ + "https://workspacemcp.googleapis.com/*" + ], + "docsUrl": "https://developers.google.com/workspace/guides/universal-search-mcp", + "setupPrerequisite": { + "title": "Google Developer Preview access required", + "description": "Google must register both the Workspace email used to authorize Paperclip and the Google Cloud project that owns the OAuth client. Registration is limited to those emails and projects; it does not enable unrelated Paperclip customers.", + "steps": [ + "Apply with the Workspace email that will sign in and the Google Cloud project that owns the OAuth client.", + "Wait for the Google Group notification and then Google's final project-registration email, usually within a couple of days.", + "Add every additional tester email or Cloud project through Google's member request forms before connecting." + ], + "actionLabel": "Apply or verify Developer Preview enrollment", + "actionUrl": "https://developers.google.com/workspace/preview" + }, + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "paperclip-read", + "label": "Connect with Paperclip", + "transport": "mcp_remote", + "auth": "oauth", + "oauthStrategy": "paperclip_id_connector", + "connectorProfile": "workspace-search.read", + "capabilityProfile": { + "key": "read", + "label": "Search Workspace", + "description": "Search Gmail, Drive, Calendar, and Chat without write access." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "platform_shared" + ], + "whenToUse": "Use Paperclip-managed OAuth for cross-product Workspace search.", + "defaults": { + "serverUrl": "https://workspacemcp.googleapis.com/mcp/v1", + "scopesHint": [ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/calendar.readonly", + "https://www.googleapis.com/auth/chat.messages.readonly" + ] + }, + "guidanceMd": "Connect Google Workspace Search for one read-only search tool spanning Gmail, Drive, Calendar, and Chat.", + "warnings": [ + "This requests read access to all four supported search corpora.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + }, + { + "key": "customer-read-oauth", + "label": "Use your own Google OAuth app", + "transport": "mcp_remote", + "auth": "oauth", + "capabilityProfile": { + "key": "read", + "label": "Search Workspace", + "description": "Search Gmail, Drive, Calendar, and Chat without write access." + }, + "grantKinds": [ + "user" + ], + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a customer-owned OAuth client for cross-product Workspace search.", + "defaults": { + "serverUrl": "https://workspacemcp.googleapis.com/mcp/v1", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "metadataUrl": "https://accounts.google.com/.well-known/openid-configuration", + "scopesHint": [ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/calendar.readonly", + "https://www.googleapis.com/auth/chat.messages.readonly" + ], + "oauthAuthorizationParams": { + "access_type": "offline", + "prompt": "consent" + } + }, + "guidanceMd": "Enable the Gmail, Drive, Calendar, Chat, and Workspace MCP APIs, then register Paperclip's callback URI.", + "consoleLinks": { + "register": "https://console.cloud.google.com/auth/clients", + "docs": "https://developers.google.com/workspace/guides/universal-search-mcp" + }, + "warnings": [ + "This requests read access to all four supported search corpora.", + "Google Workspace MCP servers are in Developer Preview." + ], + "riskTier": "S3" + } + ] +} diff --git a/packages/shared/src/app-definitions/hugging-face.json b/packages/shared/src/app-definitions/hugging-face.json new file mode 100644 index 0000000000..a4da34753d --- /dev/null +++ b/packages/shared/src/app-definitions/hugging-face.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "slug": "hugging-face", + "name": "Hugging Face", + "description": "Connect Hugging Face's provider-hosted MCP server.", + "categories": [ + "ai" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/hugging-face.svg" + }, + "urlPatterns": [ + "https://huggingface.co/*" + ], + "docsUrl": "https://huggingface.co/docs/hub/agents-mcp", + "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://huggingface.co/mcp?login&gradio=none", + "scopesHint": [ + "read-mcp" + ] + }, + "guidanceMd": "Connect Hugging Face in the browser. A Hugging Face account.", + "riskTier": "S2", + "label": "Sign in with Hugging Face", + "consoleLinks": { + "docs": "https://huggingface.co/docs/hub/agents-mcp" + }, + "warnings": [ + "A Hugging Face account." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/jira.json b/packages/shared/src/app-definitions/jira.json new file mode 100644 index 0000000000..6906449649 --- /dev/null +++ b/packages/shared/src/app-definitions/jira.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": 1, + "slug": "jira", + "name": "Jira", + "description": "Connect Jira's provider-hosted MCP server.", + "categories": [ + "productivity" + ], + "featured": true, + "branding": { + "logoUrl": "/brands/apps/jira.svg", + "darkLogoUrl": "/brands/apps/jira-dark.svg" + }, + "urlPatterns": [ + "https://mcp.atlassian.com/*" + ], + "docsUrl": "https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-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.atlassian.com/v1/mcp/authv2", + "scopesHint": [ + "read:me", + "read:account", + "offline_access", + "email", + "read:jira-work", + "write:jira-work", + "search:confluence", + "read:confluence-user", + "read:page:confluence", + "write:page:confluence", + "read:comment:confluence", + "write:comment:confluence", + "read:space:confluence", + "read:hierarchical-content:confluence", + "write:component:compass", + "read:component:compass", + "read:scorecard:compass", + "write:scorecard:compass", + "read:event:compass", + "read:metric:compass", + "read:all:twg", + "write:all:twg" + ] + }, + "guidanceMd": "Connect Jira in the browser. An active Jira or Confluence site; tenant policy may require an administrator to approve the client.", + "riskTier": "S3", + "label": "Sign in with Jira", + "consoleLinks": { + "docs": "https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-mcp-server/" + }, + "warnings": [ + "An active Jira or Confluence site; tenant policy may require an administrator to approve the client." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/kernel.json b/packages/shared/src/app-definitions/kernel.json new file mode 100644 index 0000000000..4625e859ef --- /dev/null +++ b/packages/shared/src/app-definitions/kernel.json @@ -0,0 +1,78 @@ +{ + "schemaVersion": 1, + "slug": "kernel", + "name": "Kernel", + "description": "Connect Kernel's provider-hosted MCP server.", + "categories": [ + "developer" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/kernel.svg" + }, + "urlPatterns": [ + "https://mcp.onkernel.com/*" + ], + "docsUrl": "https://www.kernel.sh/docs/reference/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.onkernel.com/mcp" + }, + "guidanceMd": "Connect Kernel in the browser. A Kernel account with either browser authorization or an API key.", + "riskTier": "S3", + "label": "Sign in with Kernel", + "consoleLinks": { + "docs": "https://www.kernel.sh/docs/reference/mcp-server/" + }, + "warnings": [ + "A Kernel account with either browser authorization or an API key." + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.onkernel.com/mcp" + }, + "guidanceMd": "Use a customer-created Kernel key. A Kernel account with either browser authorization or an API key.", + "riskTier": "S3", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Kernel API key", + "type": "password", + "required": true, + "placeholder": "Paste your Kernel API key", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "X-API-Key", + "prefix": null + }, + "consoleLinks": { + "keys": "https://www.kernel.sh/docs/reference/mcp-server/", + "docs": "https://www.kernel.sh/docs/reference/mcp-server/" + }, + "warnings": [ + "A Kernel account with either browser authorization or an API key." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/linear.json b/packages/shared/src/app-definitions/linear.json index 2ee840e37f..6e1311f0fd 100644 --- a/packages/shared/src/app-definitions/linear.json +++ b/packages/shared/src/app-definitions/linear.json @@ -8,7 +8,7 @@ ], "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=linear.app&sz=128" + "logoUrl": "/brands/apps/linear.svg" }, "urlPatterns": [ "https://mcp.linear.app/*" @@ -19,8 +19,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [ - "customer", - "dcr" + "customer" ], "whenToUse": "Use the provider-hosted connection for the quickest setup.", "defaults": { @@ -38,7 +37,25 @@ "workspace", "team", "project" - ] + ], + "credentialSources": { + "vercelConnect": { + "services": [ + "linear" + ], + "principalModes": [ + "user" + ], + "scopes": [ + "read", + "write" + ], + "header": { + "name": "Authorization", + "prefix": "Bearer " + } + } + } } ] } diff --git a/packages/shared/src/app-definitions/local-falcon.json b/packages/shared/src/app-definitions/local-falcon.json new file mode 100644 index 0000000000..775df913bb --- /dev/null +++ b/packages/shared/src/app-definitions/local-falcon.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "local-falcon", + "name": "Local Falcon", + "description": "Connect Local Falcon's provider-hosted MCP server.", + "categories": [ + "analytics" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/local-falcon.png" + }, + "urlPatterns": [ + "https://mcp.localfalcon.com/*" + ], + "docsUrl": "https://docs.localfalcon.com/", + "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.localfalcon.com" + }, + "guidanceMd": "Connect Local Falcon in the browser. A Local Falcon account with MCP access.", + "riskTier": "S2", + "label": "Sign in with Local Falcon", + "consoleLinks": { + "docs": "https://docs.localfalcon.com/" + }, + "warnings": [ + "A Local Falcon account with MCP access." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/make.json b/packages/shared/src/app-definitions/make.json new file mode 100644 index 0000000000..8b5584c1db --- /dev/null +++ b/packages/shared/src/app-definitions/make.json @@ -0,0 +1,42 @@ +{ + "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/*" + ], + "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." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/manufact.json b/packages/shared/src/app-definitions/manufact.json new file mode 100644 index 0000000000..c396e6992d --- /dev/null +++ b/packages/shared/src/app-definitions/manufact.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "manufact", + "name": "Manufact", + "description": "Connect Manufact's provider-hosted MCP server.", + "categories": [ + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/manufact.svg" + }, + "urlPatterns": [ + "https://mcp.manufact.com/*" + ], + "docsUrl": "https://docs.manufact.com/mcp", + "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.manufact.com/mcp" + }, + "guidanceMd": "Connect Manufact in the browser. A Manufact account with MCP access.", + "riskTier": "S3", + "label": "Sign in with Manufact", + "consoleLinks": { + "docs": "https://docs.manufact.com/mcp" + }, + "warnings": [ + "A Manufact account with MCP access." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/mem0.json b/packages/shared/src/app-definitions/mem0.json new file mode 100644 index 0000000000..27e0558f82 --- /dev/null +++ b/packages/shared/src/app-definitions/mem0.json @@ -0,0 +1,56 @@ +{ + "schemaVersion": 1, + "slug": "mem0", + "name": "Mem0", + "description": "Connect Mem0's provider-hosted MCP server.", + "categories": [ + "ai" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/mem0.svg" + }, + "urlPatterns": [ + "https://mcp.mem0.ai/*" + ], + "docsUrl": "https://docs.mem0.ai/platform/mem0-mcp", + "methods": [ + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.mem0.ai/mcp/" + }, + "guidanceMd": "Use a customer-created Mem0 key. A Mem0 API key; the live server currently requires the slash-normalized endpoint.", + "riskTier": "S3", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Mem0 API key", + "type": "password", + "required": true, + "placeholder": "m0sk_...", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://docs.mem0.ai/platform/mem0-mcp", + "docs": "https://docs.mem0.ai/platform/mem0-mcp" + }, + "warnings": [ + "A Mem0 API key; the live server currently requires the slash-normalized endpoint." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/miro.json b/packages/shared/src/app-definitions/miro.json new file mode 100644 index 0000000000..69616784f8 --- /dev/null +++ b/packages/shared/src/app-definitions/miro.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "slug": "miro", + "name": "Miro", + "description": "Connect Miro's provider-hosted MCP server.", + "categories": [ + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/miro.svg", + "darkLogoUrl": "/brands/apps/miro-dark.svg" + }, + "urlPatterns": [ + "https://mcp.miro.com/*" + ], + "docsUrl": "https://help.miro.com/hc/en-us/articles/31625301583890-How-to-enable-Miro-s-MCP-Server-user-guide", + "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.miro.com/" + }, + "guidanceMd": "Connect Miro in the browser. A Miro account; enterprise administrators may restrict third-party MCP clients.", + "riskTier": "S3", + "label": "Sign in with Miro", + "consoleLinks": { + "docs": "https://help.miro.com/hc/en-us/articles/31625301583890-How-to-enable-Miro-s-MCP-Server-user-guide" + }, + "warnings": [ + "A Miro account; enterprise administrators may restrict third-party MCP clients." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/mixpanel.json b/packages/shared/src/app-definitions/mixpanel.json new file mode 100644 index 0000000000..2c30d8a0f9 --- /dev/null +++ b/packages/shared/src/app-definitions/mixpanel.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "slug": "mixpanel", + "name": "Mixpanel", + "description": "Connect Mixpanel's provider-hosted MCP server.", + "categories": [ + "analytics" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/mixpanel.svg" + }, + "urlPatterns": [ + "https://mcp.mixpanel.com/*" + ], + "docsUrl": "https://mixpanel.com/blog/mixpanel-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.mixpanel.com/mcp" + }, + "guidanceMd": "Connect Mixpanel in the browser. A Mixpanel account; the hosted MCP server is currently beta.", + "riskTier": "S3", + "label": "Sign in with Mixpanel", + "consoleLinks": { + "docs": "https://mixpanel.com/blog/mixpanel-mcp-server/" + }, + "warnings": [ + "A Mixpanel account; the hosted MCP server is currently beta.", + "This provider's hosted MCP server is currently beta or preview." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/netlify.json b/packages/shared/src/app-definitions/netlify.json new file mode 100644 index 0000000000..8bb2e0bdff --- /dev/null +++ b/packages/shared/src/app-definitions/netlify.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "netlify", + "name": "Netlify", + "description": "Connect Netlify's provider-hosted MCP server.", + "categories": [ + "developer" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/netlify.svg" + }, + "urlPatterns": [ + "https://netlify-mcp.netlify.app/*" + ], + "docsUrl": "https://docs.netlify.com/build/build-with-ai/agent-setup-guides/agent-setup-overview/", + "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://netlify-mcp.netlify.app/mcp" + }, + "guidanceMd": "Connect Netlify in the browser. A Netlify account with access to the relevant team and sites.", + "riskTier": "S3", + "label": "Sign in with Netlify", + "consoleLinks": { + "docs": "https://docs.netlify.com/build/build-with-ai/agent-setup-guides/agent-setup-overview/" + }, + "warnings": [ + "A Netlify account with access to the relevant team and sites." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/notion.json b/packages/shared/src/app-definitions/notion.json index fe5ab22b2f..eef7be0b75 100644 --- a/packages/shared/src/app-definitions/notion.json +++ b/packages/shared/src/app-definitions/notion.json @@ -8,7 +8,8 @@ ], "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=notion.so&sz=128" + "logoUrl": "/brands/apps/notion.svg", + "darkLogoUrl": "/brands/apps/notion-dark.svg" }, "urlPatterns": [ "https://mcp.notion.com/*" @@ -32,8 +33,26 @@ "workspace", "page", "database" - ] + ], + "credentialSources": { + "vercelConnect": { + "services": [ + "notion" + ], + "principalModes": [ + "user" + ], + "scopes": [ + "*" + ], + "header": { + "name": "Authorization", + "prefix": "Bearer " + } + } + } } ], - "redirectConstraints": "https-or-loopback-http" + "redirectConstraints": "https-or-loopback-http", + "docsUrl": "https://developers.notion.com/guides/mcp/build-mcp-client" } diff --git a/packages/shared/src/app-definitions/oauth-generic.json b/packages/shared/src/app-definitions/oauth-generic.json index b344ba13a4..b9a7ece3c2 100644 --- a/packages/shared/src/app-definitions/oauth-generic.json +++ b/packages/shared/src/app-definitions/oauth-generic.json @@ -8,7 +8,7 @@ ], "featured": false, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=oauth.net&sz=128" + "logoUrl": "/brands/apps/oauth-generic.svg" }, "urlPatterns": [], "methods": [ diff --git a/packages/shared/src/app-definitions/oreilly.json b/packages/shared/src/app-definitions/oreilly.json new file mode 100644 index 0000000000..7719caceda --- /dev/null +++ b/packages/shared/src/app-definitions/oreilly.json @@ -0,0 +1,78 @@ +{ + "schemaVersion": 1, + "slug": "oreilly", + "name": "O'Reilly", + "description": "Connect O'Reilly's provider-hosted MCP server.", + "categories": [ + "content" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/oreilly.svg" + }, + "urlPatterns": [ + "https://api.oreilly.com/*" + ], + "docsUrl": "https://learning.oreilly.com/apidocs/mcp/content/", + "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://api.oreilly.com/api/content-discovery/v1/mcp/" + }, + "guidanceMd": "Connect O'Reilly in the browser. An O'Reilly Learning subscription with MCP or API access.", + "riskTier": "S2", + "label": "Sign in with O'Reilly", + "consoleLinks": { + "docs": "https://learning.oreilly.com/apidocs/mcp/content/" + }, + "warnings": [ + "An O'Reilly Learning subscription with MCP or API access." + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://api.oreilly.com/api/content-discovery/v1/mcp/" + }, + "guidanceMd": "Use a customer-created O'Reilly key. An O'Reilly Learning subscription with MCP or API access.", + "riskTier": "S2", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "O'Reilly API key", + "type": "password", + "required": true, + "placeholder": "Paste your O'Reilly API token", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://learning.oreilly.com/apidocs/mcp/content/", + "docs": "https://learning.oreilly.com/apidocs/mcp/content/" + }, + "warnings": [ + "An O'Reilly Learning subscription with MCP or API access." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/pagerduty.json b/packages/shared/src/app-definitions/pagerduty.json new file mode 100644 index 0000000000..c699b7eb57 --- /dev/null +++ b/packages/shared/src/app-definitions/pagerduty.json @@ -0,0 +1,93 @@ +{ + "schemaVersion": 1, + "slug": "pagerduty", + "name": "PagerDuty", + "description": "Connect PagerDuty's provider-hosted MCP server.", + "categories": [ + "developer" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/pagerduty.svg" + }, + "urlPatterns": [ + "https://mcp.pagerduty.com/*" + ], + "docsUrl": "https://support.pagerduty.com/main/docs/pagerduty-mcp-server", + "methods": [ + { + "key": "mcp-api-key-us", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.pagerduty.com/mcp" + }, + "guidanceMd": "Use a customer-created PagerDuty key. A PagerDuty API token; choose the regional endpoint that hosts the account.", + "riskTier": "S4", + "label": "US service region", + "credentialFields": [ + { + "key": "authorization", + "label": "PagerDuty API key", + "type": "password", + "required": true, + "placeholder": "Paste your PagerDuty user API token", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Token token=" + }, + "consoleLinks": { + "keys": "https://support.pagerduty.com/main/docs/pagerduty-mcp-server", + "docs": "https://support.pagerduty.com/main/docs/pagerduty-mcp-server" + }, + "warnings": [ + "A PagerDuty API token; choose the regional endpoint that hosts the account." + ] + }, + { + "key": "mcp-api-key-eu", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.eu.pagerduty.com/mcp" + }, + "guidanceMd": "Use a customer-created PagerDuty key. A PagerDuty API token; choose the regional endpoint that hosts the account.", + "riskTier": "S4", + "label": "EU service region", + "credentialFields": [ + { + "key": "authorization", + "label": "PagerDuty API key", + "type": "password", + "required": true, + "placeholder": "Paste your PagerDuty user API token", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Token token=" + }, + "consoleLinks": { + "keys": "https://support.pagerduty.com/main/docs/pagerduty-mcp-server", + "docs": "https://support.pagerduty.com/main/docs/pagerduty-mcp-server" + }, + "warnings": [ + "A PagerDuty API token; choose the regional endpoint that hosts the account." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/planetscale.json b/packages/shared/src/app-definitions/planetscale.json new file mode 100644 index 0000000000..da45725635 --- /dev/null +++ b/packages/shared/src/app-definitions/planetscale.json @@ -0,0 +1,91 @@ +{ + "schemaVersion": 1, + "slug": "planetscale", + "name": "PlanetScale", + "description": "Connect PlanetScale's provider-hosted MCP server.", + "categories": [ + "data" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/planetscale.svg", + "darkLogoUrl": "/brands/apps/planetscale-dark.svg" + }, + "urlPatterns": [ + "https://mcp.pscale.dev/*" + ], + "docsUrl": "https://planetscale.com/docs/connect/mcp", + "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.pscale.dev/mcp/planetscale" + }, + "guidanceMd": "Connect PlanetScale in the browser. A PlanetScale account; database and branch access are chosen during authorization.", + "riskTier": "S4", + "label": "Database access", + "consoleLinks": { + "docs": "https://planetscale.com/docs/connect/mcp" + }, + "warnings": [ + "A PlanetScale account; database and branch access are chosen during authorization." + ], + "tenantFields": [ + { + "key": "project", + "label": "Project or database", + "type": "text", + "advanced": true, + "placeholder": "Optional project or database name", + "helperMd": "Records the intended database boundary; final access is selected during PlanetScale authorization." + }, + { + "key": "branch", + "label": "Branch", + "type": "text", + "advanced": true, + "placeholder": "Optional branch name", + "helperMd": "Records the intended branch boundary; final access is selected during PlanetScale authorization." + } + ], + "requiredResourceFilters": [ + "organization", + "database", + "branch" + ] + }, + { + "key": "mcp-insights-only", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "dcr" + ], + "whenToUse": "Use query insights and schema recommendations without query execution tools.", + "defaults": { + "serverUrl": "https://mcp.pscale.dev/mcp/planetscale-insights-only" + }, + "guidanceMd": "Connect PlanetScale in the browser. A PlanetScale account; database and branch access are chosen during authorization.", + "riskTier": "S4", + "label": "Insights only", + "consoleLinks": { + "docs": "https://planetscale.com/docs/connect/mcp" + }, + "warnings": [ + "A PlanetScale account; database and branch access are chosen during authorization." + ], + "requiredResourceFilters": [ + "organization", + "database", + "branch" + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/posthog.json b/packages/shared/src/app-definitions/posthog.json index 033af16cd5..c74b5d2e40 100644 --- a/packages/shared/src/app-definitions/posthog.json +++ b/packages/shared/src/app-definitions/posthog.json @@ -2,13 +2,14 @@ "schemaVersion": 1, "slug": "posthog", "name": "PostHog", - "description": "Analyze product usage, errors, feature flags, and experiments in a pinned PostHog project.", + "description": "Analyze product usage, errors, feature flags, and experiments with PostHog's hosted MCP server.", "categories": [ "analytics" ], "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=posthog.com&sz=128" + "logoUrl": "/brands/apps/posthog.svg", + "darkLogoUrl": "/brands/apps/posthog-dark.svg" }, "urlPatterns": [ "https://mcp.posthog.com/*" @@ -26,16 +27,16 @@ "defaults": { "serverUrl": "https://mcp.posthog.com/mcp" }, - "guidanceMd": "Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.", + "guidanceMd": "Connect with PostHog's recommended defaults. Project pinning, read-only access, and catalog filters are optional advanced controls.", "riskTier": "S3", "tenantFields": [ { "key": "projectId", - "label": "Project ID", + "label": "Pin to project ID", "type": "text", - "required": true, - "placeholder": "12345", - "helperMd": "Find the numeric project ID in PostHog project settings.", + "advanced": true, + "placeholder": "Optional numeric project ID", + "helperMd": "Optional. Pin this connection to one project and remove PostHog's project-switching tool.", "validation": { "pattern": "^[0-9]+$", "maxLength": 32 @@ -49,6 +50,7 @@ "key": "readOnly", "label": "Read-only mode", "type": "checkbox", + "advanced": true, "defaultValue": false, "helperMd": "Turn on to hide tools that can change PostHog data.", "transport": { @@ -94,7 +96,7 @@ "key": "mode", "label": "Tool response mode", "type": "select", - "advanced": true, + "hidden": true, "required": true, "placeholder": "Individual tools", "defaultValue": "tools", @@ -111,12 +113,27 @@ } } ], - "requiredResourceFilters": [ - "project" - ], "label": "Sign in with PostHog", "consoleLinks": { "docs": "https://posthog.com/docs/model-context-protocol" + }, + "credentialSources": { + "vercelConnect": { + "services": [ + "posthog", + "mcp.posthog.com/mcp" + ], + "principalModes": [ + "user" + ], + "scopes": [ + "*" + ], + "header": { + "name": "Authorization", + "prefix": "Bearer " + } + } } }, { @@ -130,16 +147,16 @@ "defaults": { "serverUrl": "https://mcp.posthog.com/mcp" }, - "guidanceMd": "Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.", + "guidanceMd": "Connect with PostHog's recommended defaults. Project pinning, read-only access, and catalog filters are optional advanced controls.", "riskTier": "S3", "tenantFields": [ { "key": "projectId", - "label": "Project ID", + "label": "Pin to project ID", "type": "text", - "required": true, - "placeholder": "12345", - "helperMd": "Find the numeric project ID in PostHog project settings.", + "advanced": true, + "placeholder": "Optional numeric project ID", + "helperMd": "Optional. Pin this connection to one project and remove PostHog's project-switching tool.", "validation": { "pattern": "^[0-9]+$", "maxLength": 32 @@ -153,6 +170,7 @@ "key": "readOnly", "label": "Read-only mode", "type": "checkbox", + "advanced": true, "defaultValue": false, "helperMd": "Turn on to hide tools that can change PostHog data.", "transport": { @@ -198,7 +216,7 @@ "key": "mode", "label": "Tool response mode", "type": "select", - "advanced": true, + "hidden": true, "required": true, "placeholder": "Individual tools", "defaultValue": "tools", @@ -215,9 +233,6 @@ } } ], - "requiredResourceFilters": [ - "project" - ], "label": "Use a personal API key", "credentialFields": [ { @@ -237,7 +252,27 @@ "consoleLinks": { "keys": "https://posthog.com/docs/model-context-protocol/faq", "docs": "https://posthog.com/docs/model-context-protocol/faq" + }, + "credentialSources": { + "vercelConnect": { + "services": [ + "posthog", + "mcp.posthog.com/mcp" + ], + "principalModes": [ + "app" + ], + "scopes": [ + "*" + ], + "header": { + "name": "Authorization", + "prefix": "Bearer " + } + } } } - ] + ], + "docsUrl": "https://posthog.com/docs/model-context-protocol", + "redirectConstraints": "https-or-loopback-http" } diff --git a/packages/shared/src/app-definitions/postman.json b/packages/shared/src/app-definitions/postman.json new file mode 100644 index 0000000000..07e3937646 --- /dev/null +++ b/packages/shared/src/app-definitions/postman.json @@ -0,0 +1,194 @@ +{ + "schemaVersion": 1, + "slug": "postman", + "name": "Postman", + "description": "Connect Postman's provider-hosted MCP server.", + "categories": [ + "developer" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/postman.svg" + }, + "urlPatterns": [ + "https://mcp.postman.com/*" + ], + "docsUrl": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server", + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "mcp-oauth-minimal", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "dcr" + ], + "whenToUse": "Use browser sign-in for the provider-hosted MCP server.", + "defaults": { + "serverUrl": "https://mcp.postman.com/minimal" + }, + "guidanceMd": "Connect Postman in the browser. A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints.", + "riskTier": "S3", + "label": "US · Minimal", + "consoleLinks": { + "docs": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server" + }, + "warnings": [ + "A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints." + ] + }, + { + "key": "mcp-oauth-code", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "dcr" + ], + "whenToUse": "Use browser sign-in for the provider-hosted MCP server.", + "defaults": { + "serverUrl": "https://mcp.postman.com/code" + }, + "guidanceMd": "Connect Postman in the browser. A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints.", + "riskTier": "S3", + "label": "US · Code", + "consoleLinks": { + "docs": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server" + }, + "warnings": [ + "A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints." + ] + }, + { + "key": "mcp-oauth-full", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "dcr" + ], + "whenToUse": "Use browser sign-in for the provider-hosted MCP server.", + "defaults": { + "serverUrl": "https://mcp.postman.com/mcp" + }, + "guidanceMd": "Connect Postman in the browser. A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints.", + "riskTier": "S3", + "label": "US · Full", + "consoleLinks": { + "docs": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server" + }, + "warnings": [ + "A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints." + ] + }, + { + "key": "mcp-eu-key-minimal", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.eu.postman.com/minimal" + }, + "guidanceMd": "Use a customer-created Postman key. A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints.", + "riskTier": "S3", + "label": "EU · Minimal", + "credentialFields": [ + { + "key": "authorization", + "label": "Postman API key", + "type": "password", + "required": true, + "placeholder": "PMAK-...", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "X-API-Key", + "prefix": null + }, + "consoleLinks": { + "keys": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server", + "docs": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server" + }, + "warnings": [ + "A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints." + ] + }, + { + "key": "mcp-eu-key-code", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.eu.postman.com/code" + }, + "guidanceMd": "Use a customer-created Postman key. A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints.", + "riskTier": "S3", + "label": "EU · Code", + "credentialFields": [ + { + "key": "authorization", + "label": "Postman API key", + "type": "password", + "required": true, + "placeholder": "PMAK-...", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "X-API-Key", + "prefix": null + }, + "consoleLinks": { + "keys": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server", + "docs": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server" + }, + "warnings": [ + "A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints." + ] + }, + { + "key": "mcp-eu-key-full", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.eu.postman.com/mcp" + }, + "guidanceMd": "Use a customer-created Postman key. A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints.", + "riskTier": "S3", + "label": "EU · Full", + "credentialFields": [ + { + "key": "authorization", + "label": "Postman API key", + "type": "password", + "required": true, + "placeholder": "PMAK-...", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "X-API-Key", + "prefix": null + }, + "consoleLinks": { + "keys": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server", + "docs": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server" + }, + "warnings": [ + "A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/razorpay.json b/packages/shared/src/app-definitions/razorpay.json new file mode 100644 index 0000000000..c84f1e15bc --- /dev/null +++ b/packages/shared/src/app-definitions/razorpay.json @@ -0,0 +1,81 @@ +{ + "schemaVersion": 1, + "slug": "razorpay", + "name": "Razorpay", + "description": "Connect Razorpay's provider-hosted MCP server.", + "categories": [ + "commerce" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/razorpay.svg", + "darkLogoUrl": "/brands/apps/razorpay-dark.svg" + }, + "urlPatterns": [ + "https://mcp.razorpay.com/*" + ], + "docsUrl": "https://razorpay.com/docs/mcp-server/oauth/", + "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.razorpay.com/mcp" + }, + "guidanceMd": "Connect Razorpay in the browser. A Razorpay account; financial or destructive actions always require explicit approval.", + "riskTier": "S4", + "label": "Sign in with Razorpay", + "consoleLinks": { + "docs": "https://razorpay.com/docs/mcp-server/oauth/" + }, + "warnings": [ + "A Razorpay account; financial or destructive actions always require explicit approval.", + "Financial or destructive actions must be explicitly approved before execution." + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.razorpay.com/mcp" + }, + "guidanceMd": "Use a customer-created Razorpay key. A Razorpay account; financial or destructive actions always require explicit approval.", + "riskTier": "S4", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Razorpay API key", + "type": "password", + "required": true, + "placeholder": "Paste the base64-encoded key ID and secret", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Basic " + }, + "consoleLinks": { + "keys": "https://razorpay.com/docs/mcp-server/oauth/", + "docs": "https://razorpay.com/docs/mcp-server/oauth/" + }, + "warnings": [ + "A Razorpay account; financial or destructive actions always require explicit approval.", + "Financial or destructive actions must be explicitly approved before execution." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/resend.json b/packages/shared/src/app-definitions/resend.json new file mode 100644 index 0000000000..f9953b279a --- /dev/null +++ b/packages/shared/src/app-definitions/resend.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "slug": "resend", + "name": "Resend", + "description": "Connect Resend's provider-hosted MCP server.", + "categories": [ + "communication" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/resend.svg", + "darkLogoUrl": "/brands/apps/resend-dark.svg" + }, + "urlPatterns": [ + "https://mcp.resend.com/*" + ], + "docsUrl": "https://resend.com/changelog/remote-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.resend.com/mcp" + }, + "guidanceMd": "Connect Resend in the browser. A Resend account with access to the relevant domains.", + "riskTier": "S3", + "label": "Sign in with Resend", + "consoleLinks": { + "docs": "https://resend.com/changelog/remote-mcp-server" + }, + "warnings": [ + "A Resend account with access to the relevant domains." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/sanity.json b/packages/shared/src/app-definitions/sanity.json new file mode 100644 index 0000000000..bc4079ed1c --- /dev/null +++ b/packages/shared/src/app-definitions/sanity.json @@ -0,0 +1,79 @@ +{ + "schemaVersion": 1, + "slug": "sanity", + "name": "Sanity", + "description": "Connect Sanity's provider-hosted MCP server.", + "categories": [ + "content" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/sanity.svg", + "darkLogoUrl": "/brands/apps/sanity-dark.svg" + }, + "urlPatterns": [ + "https://mcp.sanity.io/*" + ], + "docsUrl": "https://www.sanity.io/docs/ai/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.sanity.io" + }, + "guidanceMd": "Connect Sanity in the browser. A Sanity account with access to the relevant projects and datasets.", + "riskTier": "S3", + "label": "Sign in with Sanity", + "consoleLinks": { + "docs": "https://www.sanity.io/docs/ai/mcp-server" + }, + "warnings": [ + "A Sanity account with access to the relevant projects and datasets." + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.sanity.io" + }, + "guidanceMd": "Use a customer-created Sanity key. A Sanity account with access to the relevant projects and datasets.", + "riskTier": "S3", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Sanity API key", + "type": "password", + "required": true, + "placeholder": "sk...", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://www.sanity.io/docs/ai/mcp-server", + "docs": "https://www.sanity.io/docs/ai/mcp-server" + }, + "warnings": [ + "A Sanity account with access to the relevant projects and datasets." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/sentry.json b/packages/shared/src/app-definitions/sentry.json index 3b69843387..98fc155680 100644 --- a/packages/shared/src/app-definitions/sentry.json +++ b/packages/shared/src/app-definitions/sentry.json @@ -8,7 +8,8 @@ ], "featured": false, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=sentry.io&sz=128" + "logoUrl": "/brands/apps/sentry.svg", + "darkLogoUrl": "/brands/apps/sentry-dark.svg" }, "urlPatterns": [ "https://mcp.sentry.dev/*" @@ -35,5 +36,7 @@ "environment" ] } - ] + ], + "docsUrl": "https://mcp.sentry.dev/.well-known/oauth-authorization-server", + "redirectConstraints": "https-or-loopback-http" } diff --git a/packages/shared/src/app-definitions/shopify.json b/packages/shared/src/app-definitions/shopify.json new file mode 100644 index 0000000000..46b80a9d13 --- /dev/null +++ b/packages/shared/src/app-definitions/shopify.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 1, + "slug": "shopify", + "name": "Shopify", + "description": "Search a store's products and policies, and manage shopping carts.", + "categories": [ + "commerce" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/shopify.svg" + }, + "urlPatterns": [ + "https://*.myshopify.com/api/mcp" + ], + "methods": [ + { + "key": "storefront-mcp", + "transport": "mcp_remote", + "auth": "none", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a store's public myshopify.com domain. No Shopify app or OAuth registration is required.", + "defaults": { + "serverUrlTemplate": "https://{storeDomain}/api/mcp" + }, + "guidanceMd": "Connect Shopify's official Storefront MCP server for shopper-facing catalog, policy, and cart tools.", + "riskTier": "S3", + "label": "Connect a Shopify storefront", + "tenantFields": [ + { + "key": "storeDomain", + "label": "Store domain", + "type": "text", + "required": true, + "placeholder": "your-store.myshopify.com", + "helperMd": "Enter the full myshopify.com domain without https://.", + "validation": { + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*\\.myshopify\\.com$", + "maxLength": 255 + } + } + ], + "consoleLinks": { + "docs": "https://shopify.dev/docs/apps/build/storefront-mcp/servers/storefront" + }, + "warnings": [ + "This is Shopify's Storefront MCP, not Admin API access. It does not manage merchant products, orders, or customers.", + "The storefront must be publicly reachable. Password-protected or restricted trial stores can return HTTP 401." + ], + "requiredResourceFilters": [ + "store" + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/similarweb.json b/packages/shared/src/app-definitions/similarweb.json new file mode 100644 index 0000000000..fc31306050 --- /dev/null +++ b/packages/shared/src/app-definitions/similarweb.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 1, + "slug": "similarweb", + "name": "Similarweb", + "description": "Connect Similarweb's provider-hosted MCP server.", + "categories": [ + "analytics" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/similarweb.svg", + "darkLogoUrl": "/brands/apps/similarweb-dark.svg" + }, + "urlPatterns": [ + "https://mcp.similarweb.com/*" + ], + "docsUrl": "https://developers.similarweb.com/docs/similarweb-mcp", + "methods": [ + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.similarweb.com" + }, + "guidanceMd": "Use a customer-created Similarweb key. A Similarweb subscription with API access and an API key.", + "riskTier": "S2", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Similarweb API key", + "type": "password", + "required": true, + "placeholder": "Paste your Similarweb API key", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "api-key", + "prefix": null + }, + "consoleLinks": { + "keys": "https://developers.similarweb.com/docs/similarweb-mcp", + "docs": "https://developers.similarweb.com/docs/similarweb-mcp" + }, + "warnings": [ + "A Similarweb subscription with API access and an API key." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/slack.json b/packages/shared/src/app-definitions/slack.json index 463994178a..3a714679db 100644 --- a/packages/shared/src/app-definitions/slack.json +++ b/packages/shared/src/app-definitions/slack.json @@ -8,7 +8,7 @@ ], "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=slack.com&sz=128" + "logoUrl": "/brands/apps/slack.png" }, "urlPatterns": [ "https://mcp.slack.com/*" @@ -19,8 +19,7 @@ "transport": "mcp_remote", "auth": "oauth", "ownershipModes": [ - "customer", - "dcr" + "customer" ], "whenToUse": "Use the provider-hosted connection for the quickest setup.", "defaults": { diff --git a/packages/shared/src/app-definitions/stripe.json b/packages/shared/src/app-definitions/stripe.json new file mode 100644 index 0000000000..ad2124f590 --- /dev/null +++ b/packages/shared/src/app-definitions/stripe.json @@ -0,0 +1,80 @@ +{ + "schemaVersion": 1, + "slug": "stripe", + "name": "Stripe", + "description": "Connect Stripe's provider-hosted MCP server.", + "categories": [ + "commerce" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/stripe.svg" + }, + "urlPatterns": [ + "https://mcp.stripe.com/*" + ], + "docsUrl": "https://docs.stripe.com/mcp", + "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.stripe.com" + }, + "guidanceMd": "Connect Stripe in the browser. A Stripe account; the server is public preview and payment actions require explicit approval.", + "riskTier": "S4", + "label": "Sign in with Stripe", + "consoleLinks": { + "docs": "https://docs.stripe.com/mcp" + }, + "warnings": [ + "A Stripe account; the server is public preview and payment actions require explicit approval.", + "Financial or destructive actions must be explicitly approved before execution." + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.stripe.com" + }, + "guidanceMd": "Use a customer-created Stripe key. A Stripe account; the server is public preview and payment actions require explicit approval.", + "riskTier": "S4", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Stripe API key", + "type": "password", + "required": true, + "placeholder": "sk_...", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://docs.stripe.com/mcp", + "docs": "https://docs.stripe.com/mcp" + }, + "warnings": [ + "A Stripe account; the server is public preview and payment actions require explicit approval.", + "Financial or destructive actions must be explicitly approved before execution." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/supabase.json b/packages/shared/src/app-definitions/supabase.json new file mode 100644 index 0000000000..afdf03cda0 --- /dev/null +++ b/packages/shared/src/app-definitions/supabase.json @@ -0,0 +1,164 @@ +{ + "schemaVersion": 1, + "slug": "supabase", + "name": "Supabase", + "description": "Connect Supabase's provider-hosted MCP server.", + "categories": [ + "data" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/supabase.svg" + }, + "urlPatterns": [ + "https://mcp.supabase.com/*" + ], + "docsUrl": "https://supabase.com/docs/guides/ai-tools/mcp", + "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.supabase.com/mcp" + }, + "guidanceMd": "Connect Supabase in the browser and scope the connection to one development project. Write tools start enabled and remain governed by Paperclip's action policies.", + "riskTier": "S4", + "label": "Sign in with Supabase", + "consoleLinks": { + "docs": "https://supabase.com/docs/guides/ai-tools/mcp" + }, + "warnings": [ + "A Supabase account; use a development project and review write actions before connecting production data.", + "Do not connect production data unless you have reviewed Supabase's MCP security guidance." + ], + "tenantFields": [ + { + "key": "projectRef", + "label": "Project reference", + "type": "text", + "required": true, + "placeholder": "abcdefghijklmnopqrst", + "helperMd": "Scope the connection to one development project.", + "transport": { + "location": "query", + "name": "project_ref" + } + }, + { + "key": "readOnly", + "label": "Read-only mode", + "type": "checkbox", + "defaultValue": false, + "helperMd": "Enable this to prevent the connection from changing the database.", + "transport": { + "location": "query", + "name": "read_only", + "format": "boolean" + } + }, + { + "key": "features", + "label": "Feature groups", + "type": "textarea", + "advanced": true, + "placeholder": "database,docs", + "helperMd": "Optional comma-separated feature groups.", + "transport": { + "location": "query", + "name": "features", + "format": "csv" + } + } + ], + "requiredResourceFilters": [ + "project" + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.supabase.com/mcp" + }, + "guidanceMd": "Use a customer-created Supabase key scoped to one development project. Write tools start enabled and remain governed by Paperclip's action policies.", + "riskTier": "S4", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Supabase API key", + "type": "password", + "required": true, + "placeholder": "sbp_...", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://supabase.com/docs/guides/ai-tools/mcp", + "docs": "https://supabase.com/docs/guides/ai-tools/mcp" + }, + "warnings": [ + "A Supabase account; use a development project and review write actions before connecting production data.", + "Do not connect production data unless you have reviewed Supabase's MCP security guidance." + ], + "tenantFields": [ + { + "key": "projectRef", + "label": "Project reference", + "type": "text", + "required": true, + "placeholder": "abcdefghijklmnopqrst", + "helperMd": "Scope the connection to one development project.", + "transport": { + "location": "query", + "name": "project_ref" + } + }, + { + "key": "readOnly", + "label": "Read-only mode", + "type": "checkbox", + "defaultValue": false, + "helperMd": "Enable this to prevent the connection from changing the database.", + "transport": { + "location": "query", + "name": "read_only", + "format": "boolean" + } + }, + { + "key": "features", + "label": "Feature groups", + "type": "textarea", + "advanced": true, + "placeholder": "database,docs", + "helperMd": "Optional comma-separated feature groups.", + "transport": { + "location": "query", + "name": "features", + "format": "csv" + } + } + ], + "requiredResourceFilters": [ + "project" + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/ticket-tailor.json b/packages/shared/src/app-definitions/ticket-tailor.json new file mode 100644 index 0000000000..6fd269f205 --- /dev/null +++ b/packages/shared/src/app-definitions/ticket-tailor.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "slug": "ticket-tailor", + "name": "Ticket Tailor", + "description": "Connect Ticket Tailor's provider-hosted MCP server.", + "categories": [ + "commerce" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/ticket-tailor.svg", + "darkLogoUrl": "/brands/apps/ticket-tailor-dark.svg" + }, + "urlPatterns": [ + "https://mcp.tickettailor.ai/*" + ], + "docsUrl": "https://developers.tickettailor.com/docs/mcp/authentication/", + "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.tickettailor.ai/mcp" + }, + "guidanceMd": "Connect Ticket Tailor in the browser. A Ticket Tailor account; the provider may request an API key during its hosted authorization prompt.", + "riskTier": "S3", + "label": "Sign in with Ticket Tailor", + "consoleLinks": { + "docs": "https://developers.tickettailor.com/docs/mcp/authentication/" + }, + "warnings": [ + "A Ticket Tailor account; the provider may request an API key during its hosted authorization prompt." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/ticktick.json b/packages/shared/src/app-definitions/ticktick.json new file mode 100644 index 0000000000..ef676cb980 --- /dev/null +++ b/packages/shared/src/app-definitions/ticktick.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "ticktick", + "name": "TickTick", + "description": "Connect TickTick's provider-hosted MCP server.", + "categories": [ + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/ticktick.svg" + }, + "urlPatterns": [ + "https://mcp.ticktick.com/*" + ], + "docsUrl": "https://help.ticktick.com/articles/7438129581631995904", + "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.ticktick.com" + }, + "guidanceMd": "Connect TickTick in the browser. A TickTick account with MCP access.", + "riskTier": "S3", + "label": "Sign in with TickTick", + "consoleLinks": { + "docs": "https://help.ticktick.com/articles/7438129581631995904" + }, + "warnings": [ + "A TickTick account with MCP access." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/todoist.json b/packages/shared/src/app-definitions/todoist.json new file mode 100644 index 0000000000..79d21387d9 --- /dev/null +++ b/packages/shared/src/app-definitions/todoist.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "todoist", + "name": "Todoist", + "description": "Connect Todoist's provider-hosted MCP server.", + "categories": [ + "productivity" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/todoist.svg" + }, + "urlPatterns": [ + "https://ai.todoist.net/*" + ], + "docsUrl": "https://developer.todoist.com/", + "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://ai.todoist.net/mcp" + }, + "guidanceMd": "Connect Todoist in the browser. A Todoist account.", + "riskTier": "S3", + "label": "Sign in with Todoist", + "consoleLinks": { + "docs": "https://developer.todoist.com/" + }, + "warnings": [ + "A Todoist account." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/vercel.json b/packages/shared/src/app-definitions/vercel.json index fc720e71f2..57baef5f21 100644 --- a/packages/shared/src/app-definitions/vercel.json +++ b/packages/shared/src/app-definitions/vercel.json @@ -8,7 +8,8 @@ ], "featured": false, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=vercel.com&sz=128" + "logoUrl": "/brands/apps/vercel.svg", + "darkLogoUrl": "/brands/apps/vercel-dark.svg" }, "urlPatterns": [ "https://mcp.vercel.com/*" @@ -34,5 +35,9 @@ "environment" ] } - ] + ], + "availability": { + "available": false, + "reason": "Vercel currently reviews and approves MCP clients." + } } diff --git a/packages/shared/src/app-definitions/webflow.json b/packages/shared/src/app-definitions/webflow.json new file mode 100644 index 0000000000..f77c24e479 --- /dev/null +++ b/packages/shared/src/app-definitions/webflow.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "webflow", + "name": "Webflow", + "description": "Connect Webflow's provider-hosted MCP server.", + "categories": [ + "content" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/webflow.svg" + }, + "urlPatterns": [ + "https://mcp.webflow.com/*" + ], + "docsUrl": "https://developers.webflow.com/mcp/reference/getting-started", + "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.webflow.com/mcp" + }, + "guidanceMd": "Connect Webflow in the browser. A Webflow account; workspace and site roles constrain accessible sites.", + "riskTier": "S3", + "label": "Sign in with Webflow", + "consoleLinks": { + "docs": "https://developers.webflow.com/mcp/reference/getting-started" + }, + "warnings": [ + "A Webflow account; workspace and site roles constrain accessible sites." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/wix.json b/packages/shared/src/app-definitions/wix.json new file mode 100644 index 0000000000..bee9a71cf1 --- /dev/null +++ b/packages/shared/src/app-definitions/wix.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "wix", + "name": "Wix", + "description": "Connect Wix's provider-hosted MCP server.", + "categories": [ + "content" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/wix.svg" + }, + "urlPatterns": [ + "https://mcp.wix.com/*" + ], + "docsUrl": "https://www.wix.com/studio/developers/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.wix.com/mcp" + }, + "guidanceMd": "Connect Wix in the browser. A Wix account with access to the relevant sites.", + "riskTier": "S3", + "label": "Sign in with Wix", + "consoleLinks": { + "docs": "https://www.wix.com/studio/developers/mcp-server" + }, + "warnings": [ + "A Wix account with access to the relevant sites." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/xero.json b/packages/shared/src/app-definitions/xero.json new file mode 100644 index 0000000000..3a9de0e9aa --- /dev/null +++ b/packages/shared/src/app-definitions/xero.json @@ -0,0 +1,53 @@ +{ + "schemaVersion": 1, + "slug": "xero", + "name": "Xero", + "description": "Connect Xero's provider-hosted MCP server.", + "categories": [ + "commerce" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/xero.svg" + }, + "urlPatterns": [ + "https://mcp.xero.com/*" + ], + "docsUrl": "https://developer.xero.com/ai", + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "mcp-own-oauth", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Register an OAuth app with Xero, then enter its client ID and secret.", + "defaults": { + "serverUrl": "https://mcp.xero.com/mcp", + "scopesHint": [ + "openid", + "profile", + "email", + "offline_access", + "accounting.settings", + "accounting.invoices.read", + "accounting.reports.aged.read", + "accounting.reports.balancesheet.read", + "accounting.reports.profitandloss.read" + ] + }, + "guidanceMd": "Connect Xero in the browser. Create a Xero OAuth app and confirm the applicable AI and data-use terms before connecting.", + "riskTier": "S4", + "label": "Use your own OAuth app", + "consoleLinks": { + "register": "https://developer.xero.com/ai", + "docs": "https://developer.xero.com/ai" + }, + "warnings": [ + "Create a Xero OAuth app and confirm the applicable AI and data-use terms before connecting." + ] + } + ] +} diff --git a/packages/shared/src/app-definitions/zapier.json b/packages/shared/src/app-definitions/zapier.json index 016771b654..3e18a1fdac 100644 --- a/packages/shared/src/app-definitions/zapier.json +++ b/packages/shared/src/app-definitions/zapier.json @@ -8,40 +8,25 @@ ], "featured": true, "branding": { - "logoUrl": "https://www.google.com/s2/favicons?domain=zapier.com&sz=128" + "logoUrl": "/brands/apps/zapier.svg" }, "urlPatterns": [ "https://mcp.zapier.com/*" ], "methods": [ { - "key": "mcp-key", + "key": "generated-url", "transport": "mcp_remote", - "auth": "api_key", + "auth": "none", "ownershipModes": [ "customer" ], - "whenToUse": "Use the provider-hosted connection for the quickest setup.", - "defaults": { - "serverUrl": "https://mcp.zapier.com/api/mcp" - }, - "guidanceMd": "Create a Zapier MCP connection, then paste its token here.", + "whenToUse": "Use the complete provider-generated MCP URL from Zapier.", + "defaults": {}, + "guidanceMd": "Create a Zapier MCP server, then paste the complete generated connection URL. The token remains embedded in that URL.", "riskTier": "S3", - "credentialFields": [ - { - "key": "authorization", - "label": "Zapier MCP token", - "type": "password", - "required": true, - "placeholder": "Paste your Zapier token", - "secret": true - } - ], - "keyPlacement": { - "location": "header", - "name": "Authorization", - "prefix": "Bearer " - } + "label": "Paste generated MCP URL" } - ] + ], + "docsUrl": "https://docs.zapier.com/mcp/quickstart" } diff --git a/packages/shared/src/google-workspace-connectors.ts b/packages/shared/src/google-workspace-connectors.ts new file mode 100644 index 0000000000..ed4645eb42 --- /dev/null +++ b/packages/shared/src/google-workspace-connectors.ts @@ -0,0 +1,41 @@ +export const GOOGLE_WORKSPACE_CONNECTOR_PROFILE_IDS = [ + "gmail.read", "gmail.draft", "drive.read", "drive.write", "docs.read", "docs.write", + "sheets.read", "sheets.write", "slides.read", "slides.write", "calendar.read", + "calendar.write", "chat.read", "chat.write", "people.read", "workspace-search.read", +] as const; + +export type GoogleWorkspaceConnectorProfileId = (typeof GOOGLE_WORKSPACE_CONNECTOR_PROFILE_IDS)[number]; + +const auth = (scope: string) => `https://www.googleapis.com/auth/${scope}`; + +export const GOOGLE_WORKSPACE_CONNECTOR_PROFILES: Readonly> = { + "gmail.read": def("gmail", "https://gmailmcp.googleapis.com/mcp/v1", [auth("gmail.readonly")]), + "gmail.draft": def("gmail", "https://gmailmcp.googleapis.com/mcp/v1", [auth("gmail.readonly"), auth("gmail.compose")], ["create_draft"]), + "drive.read": def("google-drive", "https://drivemcp.googleapis.com/mcp/v1", [auth("drive.readonly")]), + "drive.write": def("google-drive", "https://drivemcp.googleapis.com/mcp/v1", [auth("drive.readonly"), auth("drive.file")], ["copy_file", "create_file"]), + "docs.read": def("google-docs", "https://docsmcp.googleapis.com/mcp/v1", [auth("drive.readonly"), auth("documents.readonly")]), + "docs.write": def("google-docs", "https://docsmcp.googleapis.com/mcp/v1", [auth("drive.readonly"), auth("drive.file"), auth("documents")], ["update_doc"]), + "sheets.read": def("google-sheets", "https://sheetsmcp.googleapis.com/mcp/v1", [auth("drive.readonly"), auth("spreadsheets.readonly")]), + "sheets.write": def("google-sheets", "https://sheetsmcp.googleapis.com/mcp/v1", [auth("drive.readonly"), auth("drive.file"), auth("spreadsheets")], ["update_spreadsheet", "update_values", "update_formulas", "insert_dimension"]), + "slides.read": def("google-slides", "https://slidesmcp.googleapis.com/mcp/v1", [auth("drive.readonly"), auth("presentations.readonly")]), + "slides.write": def("google-slides", "https://slidesmcp.googleapis.com/mcp/v1", [auth("drive.readonly"), auth("drive.file"), auth("presentations")], ["update_presentation"]), + "calendar.read": def("google-calendar", "https://calendarmcp.googleapis.com/mcp/v1", [auth("calendar.calendarlist.readonly"), auth("calendar.events.freebusy"), auth("calendar.events.readonly")]), + "calendar.write": def("google-calendar", "https://calendarmcp.googleapis.com/mcp/v1", [auth("calendar.calendarlist.readonly"), auth("calendar.events.freebusy"), auth("calendar.events")], ["create_event", "update_event", "delete_event", "respond_to_event"]), + "chat.read": def("google-chat", "https://chatmcp.googleapis.com/mcp/v1", [auth("chat.spaces.readonly"), auth("chat.memberships.readonly"), auth("chat.messages.readonly"), auth("chat.users.readstate.readonly")]), + "chat.write": def("google-chat", "https://chatmcp.googleapis.com/mcp/v1", [auth("chat.spaces.readonly"), auth("chat.memberships.readonly"), auth("chat.messages.readonly"), auth("chat.users.readstate.readonly"), auth("chat.messages.create")], ["send_message"]), + "people.read": def("google-people", "https://people.googleapis.com/mcp/v1", [auth("directory.readonly"), auth("userinfo.profile"), auth("contacts.readonly")]), + "workspace-search.read": def("google-workspace-search", "https://workspacemcp.googleapis.com/mcp/v1", [auth("gmail.readonly"), auth("drive.readonly"), auth("calendar.readonly"), auth("chat.messages.readonly")]), +}; + +export function isGoogleWorkspaceConnectorProfileId(value: string): value is GoogleWorkspaceConnectorProfileId { + return Object.prototype.hasOwnProperty.call(GOOGLE_WORKSPACE_CONNECTOR_PROFILES, value); +} + +function def(appSlug: string, serverUrl: string, scopes: readonly string[], writeTools: readonly string[] = []) { + return { appSlug, serverUrl, scopes, writeTools }; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 29f316dff2..2b47673218 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -269,6 +269,7 @@ export { getAvailableConnectionMethods, getConnectableAppDefinition, recommendedDefaultsForApp, + resolveConnectionMethodServerUrl, } from "./app-definitions.js"; export { APP_DEFINITIONS } from "./app-definitions.generated.js"; export * from "./validators/status-card.js"; diff --git a/packages/shared/src/self-serve-mcp-research.json b/packages/shared/src/self-serve-mcp-research.json new file mode 100644 index 0000000000..32374b802c --- /dev/null +++ b/packages/shared/src/self-serve-mcp-research.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": 1, + "verifiedAt": "2026-08-26", + "entries": [ + { "slug": "jira", "name": "Jira", "wave": 1, "status": "self_serve", "docsUrl": "https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-mcp-server/", "serverUrl": "https://mcp.atlassian.com/v1/mcp/authv2", "authMode": "dcr_cimd", "prerequisite": "An active Jira or Confluence site; tenant policy may require an administrator to approve the client.", "riskTier": "S3" }, + { "slug": "airtable", "name": "Airtable", "wave": 1, "status": "self_serve", "docsUrl": "https://support.airtable.com/articles/9897799762-using-the-airtable-mcp-server", "serverUrl": "https://mcp.airtable.com/mcp", "authMode": "dcr", "prerequisite": "An Airtable account; enterprise administrators may need to allowlist the client.", "riskTier": "S3" }, + { "slug": "beehiiv", "name": "beehiiv", "wave": 1, "status": "self_serve", "docsUrl": "https://www.beehiiv.com/features/mcp/getting-started", "serverUrl": "https://mcp.beehiiv.com/mcp", "authMode": "dcr", "prerequisite": "A beehiiv account; the subscription plan controls available write capabilities.", "riskTier": "S3" }, + { "slug": "bitly", "name": "Bitly", "wave": 1, "status": "self_serve", "docsUrl": "https://dev.bitly.com/bitly-mcp/overview/quickstart/", "serverUrl": "https://api-ssl.bitly.com/v4/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Bitly account with either browser authorization or an API token.", "riskTier": "S2" }, + { "slug": "candid", "name": "Candid", "wave": 1, "status": "self_serve", "docsUrl": "https://learning.candid.org/getting-started-with-the-candid-mcp-connector/375441", "serverUrl": "https://mcp.candid.org/mcp", "authMode": "dcr", "prerequisite": "A Candid account with access to the MCP connector.", "riskTier": "S2" }, + { "slug": "cloudflare", "name": "Cloudflare", "wave": 1, "status": "self_serve", "docsUrl": "https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/", "serverUrl": "https://mcp.cloudflare.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Cloudflare account with access to the resources being connected.", "riskTier": "S3" }, + { "slug": "cloudinary", "name": "Cloudinary", "wave": 1, "status": "self_serve", "docsUrl": "https://cloudinary.com/documentation/cloudinary_llm_mcp", "serverUrl": "https://asset-management.mcp.cloudinary.com/mcp", "authMode": "dcr", "prerequisite": "A Cloudinary account; authorization is limited by the signed-in user's roles.", "riskTier": "S3" }, + { "slug": "coda", "name": "Coda", "wave": 1, "status": "self_serve", "docsUrl": "https://help.coda.io/hc/en-us/articles/44722769665549-Security-recommendations-for-the-Coda-MCP", "serverUrl": "https://coda.io/apis/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Coda account; the hosted MCP service is currently beta.", "riskTier": "S3" }, + { "slug": "hugging-face", "name": "Hugging Face", "wave": 1, "status": "self_serve", "docsUrl": "https://huggingface.co/docs/hub/agents-mcp", "serverUrl": "https://huggingface.co/mcp?login&gradio=none", "authMode": "dcr_cimd", "prerequisite": "A Hugging Face account.", "riskTier": "S2" }, + { "slug": "kernel", "name": "Kernel", "wave": 1, "status": "self_serve", "docsUrl": "https://www.kernel.sh/docs/reference/mcp-server/", "serverUrl": "https://mcp.onkernel.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Kernel account with either browser authorization or an API key.", "riskTier": "S3" }, + { "slug": "local-falcon", "name": "Local Falcon", "wave": 1, "status": "self_serve", "docsUrl": "https://docs.localfalcon.com/", "serverUrl": "https://mcp.localfalcon.com", "authMode": "dcr", "prerequisite": "A Local Falcon account with MCP access.", "riskTier": "S2" }, + { "slug": "make", "name": "Make", "wave": 1, "status": "self_serve", "docsUrl": "https://developers.make.com/mcp-server", "serverUrl": "https://mcp.make.com", "authMode": "dcr", "prerequisite": "A Make account and access to the scenarios exposed to MCP.", "riskTier": "S3" }, + { "slug": "manufact", "name": "Manufact", "wave": 1, "status": "self_serve", "docsUrl": "https://docs.manufact.com/mcp", "serverUrl": "https://mcp.manufact.com/mcp", "authMode": "dcr", "prerequisite": "A Manufact account with MCP access.", "riskTier": "S3" }, + { "slug": "miro", "name": "Miro", "wave": 1, "status": "self_serve", "docsUrl": "https://help.miro.com/hc/en-us/articles/31625301583890-How-to-enable-Miro-s-MCP-Server-user-guide", "serverUrl": "https://mcp.miro.com/", "authMode": "dcr", "prerequisite": "A Miro account; enterprise administrators may restrict third-party MCP clients.", "riskTier": "S3" }, + { "slug": "netlify", "name": "Netlify", "wave": 1, "status": "self_serve", "docsUrl": "https://docs.netlify.com/build/build-with-ai/agent-setup-guides/agent-setup-overview/", "serverUrl": "https://netlify-mcp.netlify.app/mcp", "authMode": "dcr", "prerequisite": "A Netlify account with access to the relevant team and sites.", "riskTier": "S3" }, + { "slug": "notion", "name": "Notion", "wave": 1, "status": "self_serve", "docsUrl": "https://developers.notion.com/guides/mcp/build-mcp-client", "serverUrl": "https://mcp.notion.com/mcp", "authMode": "dcr", "prerequisite": "A Notion account and access to the pages or databases being shared.", "riskTier": "S3" }, + { "slug": "oreilly", "name": "O'Reilly", "wave": 1, "status": "self_serve", "docsUrl": "https://learning.oreilly.com/apidocs/mcp/content/", "serverUrl": "https://api.oreilly.com/api/content-discovery/v1/mcp/", "authMode": "dcr_or_api_key", "prerequisite": "An O'Reilly Learning subscription with MCP or API access.", "riskTier": "S2" }, + { "slug": "planetscale", "name": "PlanetScale", "wave": 1, "status": "self_serve", "docsUrl": "https://planetscale.com/docs/connect/mcp", "serverUrl": "https://mcp.pscale.dev/mcp/planetscale", "authMode": "dcr", "prerequisite": "A PlanetScale account; database and branch access are chosen during authorization.", "riskTier": "S4" }, + { "slug": "posthog", "name": "PostHog", "wave": 1, "status": "self_serve", "docsUrl": "https://posthog.com/docs/model-context-protocol", "serverUrl": "https://mcp.posthog.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A PostHog account; personal API keys and optional organization or project pinning are supported as alternatives and advanced controls.", "riskTier": "S3" }, + { "slug": "resend", "name": "Resend", "wave": 1, "status": "self_serve", "docsUrl": "https://resend.com/changelog/remote-mcp-server", "serverUrl": "https://mcp.resend.com/mcp", "authMode": "dcr", "prerequisite": "A Resend account with access to the relevant domains.", "riskTier": "S3" }, + { "slug": "sentry", "name": "Sentry", "wave": 1, "status": "self_serve", "docsUrl": "https://mcp.sentry.dev/.well-known/oauth-authorization-server", "serverUrl": "https://mcp.sentry.dev/mcp", "authMode": "dcr_cimd", "prerequisite": "A Sentry account with access to the relevant organizations and projects.", "riskTier": "S3" }, + { "slug": "ticktick", "name": "TickTick", "wave": 1, "status": "self_serve", "docsUrl": "https://help.ticktick.com/articles/7438129581631995904", "serverUrl": "https://mcp.ticktick.com", "authMode": "dcr", "prerequisite": "A TickTick account with MCP access.", "riskTier": "S3" }, + { "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" }, + { "slug": "embat", "name": "Embat", "wave": 2, "status": "self_serve", "docsUrl": "https://tellme.embat.io/.well-known/oauth-protected-resource/mcp", "serverUrl": "https://tellme.embat.io/mcp", "authMode": "dcr_cimd", "prerequisite": "An Embat account; pilot the connection because provider setup documentation is sparse.", "riskTier": "S4" }, + { "slug": "mixpanel", "name": "Mixpanel", "wave": 2, "status": "self_serve", "docsUrl": "https://mixpanel.com/blog/mixpanel-mcp-server/", "serverUrl": "https://mcp.mixpanel.com/mcp", "authMode": "dcr", "prerequisite": "A Mixpanel account; the hosted MCP server is currently beta.", "riskTier": "S3" }, + { "slug": "postman", "name": "Postman", "wave": 2, "status": "self_serve", "docsUrl": "https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server", "serverUrl": "https://mcp.postman.com/minimal", "authMode": "dcr_or_api_key", "prerequisite": "A Postman account; OAuth is available for US endpoints and API keys are required for EU endpoints.", "riskTier": "S3" }, + { "slug": "razorpay", "name": "Razorpay", "wave": 2, "status": "self_serve", "docsUrl": "https://razorpay.com/docs/mcp-server/oauth/", "serverUrl": "https://mcp.razorpay.com/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Razorpay account; financial or destructive actions always require explicit approval.", "riskTier": "S4" }, + { "slug": "sanity", "name": "Sanity", "wave": 2, "status": "self_serve", "docsUrl": "https://www.sanity.io/docs/ai/mcp-server", "serverUrl": "https://mcp.sanity.io", "authMode": "dcr_or_api_key", "prerequisite": "A Sanity account with access to the relevant projects and datasets.", "riskTier": "S3" }, + { "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": "customer_oauth", "prerequisite": "Create an Asana MCP OAuth app and register Paperclip's callback URI; DCR is not supported.", "riskTier": "S3" }, + { "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" }, + { "slug": "mem0", "name": "Mem0", "wave": 3, "status": "self_serve", "docsUrl": "https://docs.mem0.ai/platform/mem0-mcp", "serverUrl": "https://mcp.mem0.ai/mcp/", "authMode": "api_key", "prerequisite": "A Mem0 API key; the live server currently requires the slash-normalized endpoint.", "riskTier": "S3" }, + { "slug": "pagerduty", "name": "PagerDuty", "wave": 3, "status": "self_serve", "docsUrl": "https://support.pagerduty.com/main/docs/pagerduty-mcp-server", "serverUrl": "https://mcp.pagerduty.com/mcp", "authMode": "api_key", "prerequisite": "A PagerDuty API token; choose the regional endpoint that hosts the account.", "riskTier": "S4" }, + { "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": "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" } + ] +} diff --git a/packages/shared/src/self-serve-mcp-research.ts b/packages/shared/src/self-serve-mcp-research.ts new file mode 100644 index 0000000000..312abfecb0 --- /dev/null +++ b/packages/shared/src/self-serve-mcp-research.ts @@ -0,0 +1,10 @@ +import manifest from "./self-serve-mcp-research.json" with { type: "json" }; +import type { SelfServeMcpResearchManifest } from "./types/app-definition.js"; + +export const SELF_SERVE_MCP_RESEARCH = manifest as SelfServeMcpResearchManifest; +export const SELF_SERVE_MCP_CANDIDATES = SELF_SERVE_MCP_RESEARCH.entries.filter( + (entry) => entry.status === "self_serve", +); +export const BLOCKED_MCP_PROVIDERS = SELF_SERVE_MCP_RESEARCH.entries.filter( + (entry) => entry.status === "blocked", +); diff --git a/packages/shared/src/types/app-definition.ts b/packages/shared/src/types/app-definition.ts index 514e161fca..831aabfb13 100644 --- a/packages/shared/src/types/app-definition.ts +++ b/packages/shared/src/types/app-definition.ts @@ -1,6 +1,33 @@ -import type { ConnectionGrantKind, ToolConnectionOwnership, ToolConnectionTransport } from "./tool-access.js"; +import type { ConnectionGrantKind, ToolConnectionOwnership, ToolConnectionTransport, VercelConnectPrincipalMode } from "./tool-access.js"; 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; 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 { key:string; label?:string; transport:ToolConnectionTransport; auth:"oauth"|"api_key"|"none"; oauthStrategy?:"paperclip_id_connector"; grantKinds?:ConnectionGrantKind[]; ownershipModes:ToolConnectionOwnership[]; whenToUse:string; defaults?:{serverUrl?:string;discoveryUrl?:string|null;serviceHost?:string;templateKey?:string;authorizationEndpoint?:string;tokenEndpoint?:string;metadataUrl?:string;scopesHint?:string[]}; tenantFields?:FieldDef[]; extensionFields?:FieldDef[]; configRequirements?:{atLeastOneOf?:string[]}; credentialFields?:FieldDef[]; keyPlacement?:{location:"header"|"query"|"body_json"|"env";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 { schemaVersion:1; slug:string; name:string; description:string; categories:AppCategory[]; featured?:boolean; branding:{logoUrl:string;darkLogoUrl?:string;backgroundColor?:string;accentColor?:string}; urlPatterns:string[]; docsUrl?:string; redirectConstraints?:OAuthRedirectConstraints; methods:ConnectionMethodDef[]; suggestable?:boolean; availability?:{available:boolean;reason?:string;robotEmail?:string}; ownershipAvailability?:Partial> } +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 { key:string; label?:string; transport:ToolConnectionTransport; auth:"oauth"|"api_key"|"none"; oauthStrategy?:"paperclip_id_connector"; connectorProfile?:string; 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"}}; 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 { schemaVersion:1; slug:string; name:string; description:string; categories:AppCategory[]; 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> } + +export type SelfServeMcpAuthMode = + | "dcr" + | "dcr_cimd" + | "dcr_or_api_key" + | "customer_oauth" + | "api_key" + | "generated_url" + | "provider_approval"; + +export interface SelfServeMcpResearchEntry { + slug: string; + name: string; + wave: 1 | 2 | 3 | "blocked"; + status: "self_serve" | "blocked"; + docsUrl: string; + serverUrl: string; + authMode: SelfServeMcpAuthMode; + prerequisite: string; + riskTier: "S1" | "S2" | "S3" | "S4"; +} + +export interface SelfServeMcpResearchManifest { + schemaVersion: 1; + verifiedAt: string; + entries: SelfServeMcpResearchEntry[]; +} diff --git a/packages/shared/src/types/tool-access.ts b/packages/shared/src/types/tool-access.ts index 0b461c7d04..0adac19c81 100644 --- a/packages/shared/src/types/tool-access.ts +++ b/packages/shared/src/types/tool-access.ts @@ -70,6 +70,7 @@ export type ToolActorType = "agent" | "user" | "system" | "plugin"; export type ToolConnectionTransport = "mcp_remote" | "rest_api" | "local_stdio"; export type ToolConnectionAuthKind = "oauth" | "api_key" | "none"; export type ToolConnectionOwnership = "platform_shared" | "platform_provisioned" | "customer" | "dcr"; +export type VercelConnectPrincipalMode = "app" | "user"; export type ToolConnectionStatus = "draft" | "active" | "disabled" | "archived"; export type ToolConnectionInstallTargetType = "company" | "agent"; export type ConnectionGrantKind = "organization" | "user"; diff --git a/packages/shared/src/validators/app-definition.ts b/packages/shared/src/validators/app-definition.ts index c4af1bbe15..c7ae89a165 100644 --- a/packages/shared/src/validators/app-definition.ts +++ b/packages/shared/src/validators/app-definition.ts @@ -1,6 +1,10 @@ import { z } from "zod"; import { connectionGrantKindSchema, toolConnectionOwnershipSchema, toolConnectionTransportSchema } from "./tool-access.js"; -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(),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"]})}); -export const connectionMethodDefSchema=z.object({key:z.string().min(1),label:z.string().min(1).optional(),transport:toolConnectionTransportSchema,auth:z.enum(["oauth","api_key","none"]),oauthStrategy:z.enum(["paperclip_id_connector"]).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(),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()}).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(),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.auth==="api_key"&&!v.keyPlacement)c.addIssue({code:"custom",message:"API-key methods require keyPlacement",path:["keyPlacement"]});if(v.oauthStrategy&&v.auth!=="oauth")c.addIssue({code:"custom",message:"OAuth strategies require OAuth auth",path:["oauthStrategy"]});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",message:"Config requirement references an unknown field",path:["configRequirements","atLeastOneOf"]})}); -export const appDefinitionSchema=z.object({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),featured:z.boolean().optional(),branding:z.object({logoUrl:z.string().url(),darkLogoUrl:z.string().url().optional(),backgroundColor:z.string().optional(),accentColor:z.string().optional()}),urlPatterns:z.array(z.string()),docsUrl:z.string().url().optional(),redirectConstraints:z.enum(["https-or-loopback-http"]).optional(),methods:z.array(connectionMethodDefSchema).min(1),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()}); +const appBrandAssetUrlSchema=z.string().refine((value)=>{ + if(/^\/brands\/apps\/[a-z0-9][a-z0-9._-]*\.(?:svg|png)$/i.test(value))return true; + 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(),transport:toolConnectionTransportSchema,auth:z.enum(["oauth","api_key","none"]),oauthStrategy:z.enum(["paperclip_id_connector"]).optional(),connectorProfile:z.string().regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/).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()}).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.auth==="api_key"&&!v.keyPlacement)c.addIssue({code:"custom",message:"API-key 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==="paperclip_id_connector"&&!v.connectorProfile)c.addIssue({code:"custom",message:"Paperclip ID connector methods require connectorProfile",path:["connectorProfile"]});if(v.connectorProfile&&v.oauthStrategy!=="paperclip_id_connector")c.addIssue({code:"custom",message:"connectorProfile requires the Paperclip ID 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",message:"Config requirement references an unknown field",path:["configRequirements","atLeastOneOf"]});if(v.defaults?.serverUrl&&v.defaults.serverUrlTemplate)c.addIssue({code:"custom",message:"Use either serverUrl or serverUrlTemplate",path:["defaults"]});for(const placeholder of v.defaults?.serverUrlTemplate?.matchAll(/\{([a-zA-Z0-9_-]+)\}/g)??[])if(!keys.has(placeholder[1]))c.addIssue({code:"custom",message:"Server URL template references an unknown field",path:["defaults","serverUrlTemplate"]})}); +export const appDefinitionSchema=z.object({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),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).min(1),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()}); export const appDefinitionsSchema=z.array(appDefinitionSchema).superRefine((v,c)=>{const s=new Set();v.forEach((a,i)=>{if(s.has(a.slug))c.addIssue({code:"custom",message:"Duplicate slug",path:[i,"slug"]});s.add(a.slug)})}); diff --git a/scripts/ingest-app-definitions.mjs b/scripts/ingest-app-definitions.mjs index 22f6e3fc46..948ea10627 100644 --- a/scripts/ingest-app-definitions.mjs +++ b/scripts/ingest-app-definitions.mjs @@ -1,33 +1,149 @@ import fs from "node:fs"; import path from "node:path"; -const root=process.cwd(); const corpus=process.env.PAPERCLIP_CONTENT_TEMPLATES??path.resolve(root,"../../../paperclip-content/research/connections/vercel/templates"); -const out=path.join(root,"packages/shared/src/app-definitions"); const favicon=d=>`https://www.google.com/s2/favicons?domain=${d}&sz=128`; +const root=process.cwd(); const corpus=process.env.PAPERCLIP_CONTENT_TEMPLATES??path.resolve(root,"../../paperclip-content/research/connections/vercel/templates"); +const out=path.join(root,"packages/shared/src/app-definitions"); +const brandingManifest=JSON.parse(fs.readFileSync(path.join(root,"ui/public/brands/apps/manifest.json"),"utf8")); +const brandingBySlug=new Map(brandingManifest.providers.map((entry)=>[entry.slug,entry])); +const brandingFor=(slug)=>{ + const entry=brandingBySlug.get(slug); + if(entry) return {logoUrl:entry.localAsset,...(entry.darkAsset?{darkLogoUrl:entry.darkAsset}:{})}; + if(slug==="oauth-generic"||slug==="api-key-generic") return {logoUrl:`/brands/apps/${slug}.svg`}; + throw new Error(`${slug}: missing local branding provenance`); +}; const field=(key,label,placeholder)=>({key,label,type:"password",required:true,placeholder,secret:true}); const method=(key,transport,auth,defaults,riskTier,guidanceMd,extra={})=>({key,transport,auth,ownershipModes:auth==="oauth"?["customer","dcr"]:["customer"],whenToUse:transport==="mcp_remote"?"Use the provider-hosted connection for the quickest setup.":"Use credentials from your provider account.",defaults,guidanceMd,riskTier,...extra}); +const vercelConnect=(serviceOrServices,principalMode,scopes,header={name:"Authorization",prefix:"Bearer "})=>({credentialSources:{vercelConnect:{services:Array.isArray(serviceOrServices)?serviceOrServices:[serviceOrServices],principalModes:[principalMode],scopes,header}}}); const posthogConfigFields=()=>[ - {key:"projectId",label:"Project ID",type:"text",required:true,placeholder:"12345",helperMd:"Find the numeric project ID in PostHog project settings.",validation:{pattern:"^[0-9]+$",maxLength:32},transport:{location:"header",name:"x-posthog-project-id"}}, - {key:"readOnly",label:"Read-only mode",type:"checkbox",defaultValue:false,helperMd:"Turn on to hide tools that can change PostHog data.",transport:{location:"query",name:"readonly",format:"boolean",omitFalse:true}}, + {key:"projectId",label:"Pin to project ID",type:"text",advanced:true,placeholder:"Optional numeric project ID",helperMd:"Optional. Pin this connection to one project and remove PostHog's project-switching tool.",validation:{pattern:"^[0-9]+$",maxLength:32},transport:{location:"header",name:"x-posthog-project-id"}}, + {key:"readOnly",label:"Read-only mode",type:"checkbox",advanced:true,defaultValue:false,helperMd:"Turn on to hide tools that can change PostHog data.",transport:{location:"query",name:"readonly",format:"boolean",omitFalse:true}}, {key:"features",label:"Feature groups",type:"textarea",advanced:true,placeholder:"Optional comma-separated feature groups",helperMd:"Leave blank to expose every feature group, or enter a comma-separated list to narrow access.",validation:{maxLength:500},transport:{location:"query",name:"features",format:"csv"}}, {key:"tools",label:"Individual tools",type:"textarea",advanced:true,placeholder:"Optional comma-separated tool names",helperMd:"Leave blank to expose all tools. Exact names here are combined with any feature groups.",validation:{maxLength:2000},transport:{location:"query",name:"tools",format:"csv"}}, - {key:"mode",label:"Tool response mode",type:"select",advanced:true,required:true,placeholder:"Individual tools",defaultValue:"tools",options:[{value:"tools",label:"Individual tools"}],helperMd:"Paperclip uses individual tools so every action can be governed. CLI mode remains unavailable until nested execution is governed.",transport:{location:"query",name:"mode"}}, + {key:"mode",label:"Tool response mode",type:"select",hidden:true,required:true,placeholder:"Individual tools",defaultValue:"tools",options:[{value:"tools",label:"Individual tools"}],helperMd:"Paperclip uses individual tools so every action can be governed. CLI mode remains unavailable until nested execution is governed.",transport:{location:"query",name:"mode"}}, ]; -const posthogMethod=(key,auth,extra={})=>method(key,"mcp_remote",auth,{serverUrl:"https://mcp.posthog.com/mcp"},"S3","Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.",{tenantFields:posthogConfigFields(),requiredResourceFilters:["project"],...extra}); +const posthogMethod=(key,auth,extra={})=>method(key,"mcp_remote",auth,{serverUrl:"https://mcp.posthog.com/mcp"},"S3","Connect with PostHog's recommended defaults. Project pinning, read-only access, and catalog filters are optional advanced controls.",{tenantFields:posthogConfigFields(),...extra}); const apps=[ -["zapier","Zapier","Reach thousands of apps through your Zapier account.","productivity","zapier.com",["https://mcp.zapier.com/*"],method("mcp-key","mcp_remote","api_key",{serverUrl:"https://mcp.zapier.com/api/mcp"},"S3","Create a Zapier MCP connection, then paste its token here.",{credentialFields:[field("authorization","Zapier MCP token","Paste your Zapier token")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "}})], +["zapier","Zapier","Reach thousands of apps through your Zapier account.","productivity","zapier.com",["https://mcp.zapier.com/*"],method("generated-url","mcp_remote","none",{},"S3","Create a Zapier MCP server, then paste the complete generated connection URL. The token remains embedded in that URL.",{label:"Paste generated MCP URL",whenToUse:"Use the complete provider-generated MCP URL from Zapier."})], ["github","GitHub","Read code and pull requests, and coordinate repository work.","developer","github.com",["https://api.githubcopilot.com/mcp/*"],method("mcp-key","mcp_remote","api_key",{serverUrl:"https://api.githubcopilot.com/mcp/"},"S3","Create a fine-grained token limited to the repositories agents should use.",{credentialFields:[field("authorization","GitHub token","github_pat_...")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "},requiredResourceFilters:["organization","repository"]})], -["slack","Slack","Search channels and coordinate team communication.","communication","slack.com",["https://mcp.slack.com/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.slack.com/mcp",authorizationEndpoint:"https://slack.com/oauth/v2/authorize",tokenEndpoint:"https://slack.com/api/oauth.v2.access",scopesHint:["channels:read","chat:write","search:read"]},"S3","Connect a Slack workspace and limit access to the channels agents need.",{requiredResourceFilters:["workspace","channel"]})], -["notion","Notion","Read and update pages in your Notion workspace.","content","notion.so",["https://mcp.notion.com/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.notion.com/mcp"},"S3","Connect Notion for workspace content. Share only the pages and databases agents should use.",{requiredResourceFilters:["workspace","page","database"]}),{redirectConstraints:"https-or-loopback-http"}], -["posthog","PostHog","Analyze product usage, errors, feature flags, and experiments in a pinned PostHog project.","analytics","posthog.com",["https://mcp.posthog.com/*"],[posthogMethod("mcp-oauth","oauth",{label:"Sign in with PostHog",ownershipModes:["customer","dcr"],whenToUse:"Sign in with PostHog in the browser. Recommended for hosted PostHog accounts.",consoleLinks:{docs:"https://posthog.com/docs/model-context-protocol"}}),posthogMethod("mcp-api-key","api_key",{label:"Use a personal API key",whenToUse:"Use a PostHog personal API key when browser sign-in is not suitable.",credentialFields:[field("authorization","PostHog personal API key","phx_...")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "},consoleLinks:{keys:"https://posthog.com/docs/model-context-protocol/faq",docs:"https://posthog.com/docs/model-context-protocol/faq"}})],{featured:true}], -["linear","Linear","Create, update, and read Linear issues.","productivity","linear.app",["https://mcp.linear.app/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.linear.app/mcp",authorizationEndpoint:"https://linear.app/oauth/authorize",tokenEndpoint:"https://api.linear.app/oauth/token",scopesHint:["read","write"]},"S2","Register a Linear OAuth app and add Paperclip's redirect URI before connecting.",{requiredResourceFilters:["workspace","team","project"]})], +["slack","Slack","Search channels and coordinate team communication.","communication","slack.com",["https://mcp.slack.com/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.slack.com/mcp",authorizationEndpoint:"https://slack.com/oauth/v2/authorize",tokenEndpoint:"https://slack.com/api/oauth.v2.access",scopesHint:["channels:read","chat:write","search:read"]},"S3","Connect a Slack workspace and limit access to the channels agents need.",{ownershipModes:["customer"],requiredResourceFilters:["workspace","channel"]})], +["notion","Notion","Read and update pages in your Notion workspace.","content","notion.so",["https://mcp.notion.com/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.notion.com/mcp"},"S3","Connect Notion for workspace content. Share only the pages and databases agents should use.",{requiredResourceFilters:["workspace","page","database"],...vercelConnect("notion","user",["*"])}),{redirectConstraints:"https-or-loopback-http"}], +["posthog","PostHog","Analyze product usage, errors, feature flags, and experiments with PostHog's hosted MCP server.","analytics","posthog.com",["https://mcp.posthog.com/*"],[posthogMethod("mcp-oauth","oauth",{label:"Sign in with PostHog",ownershipModes:["customer","dcr"],whenToUse:"Sign in with PostHog in the browser. Recommended for hosted PostHog accounts.",consoleLinks:{docs:"https://posthog.com/docs/model-context-protocol"},...vercelConnect(["posthog","mcp.posthog.com/mcp"],"user",["*"])}),posthogMethod("mcp-api-key","api_key",{label:"Use a personal API key",whenToUse:"Use a PostHog personal API key when browser sign-in is not suitable.",credentialFields:[field("authorization","PostHog personal API key","phx_...")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "},consoleLinks:{keys:"https://posthog.com/docs/model-context-protocol/faq",docs:"https://posthog.com/docs/model-context-protocol/faq"},...vercelConnect(["posthog","mcp.posthog.com/mcp"],"app",["*"])})],{featured:true}], +["linear","Linear","Create, update, and read Linear issues.","productivity","linear.app",["https://mcp.linear.app/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.linear.app/mcp",authorizationEndpoint:"https://linear.app/oauth/authorize",tokenEndpoint:"https://api.linear.app/oauth/token",scopesHint:["read","write"]},"S2","Register a Linear OAuth app and add Paperclip's redirect URI before connecting.",{ownershipModes:["customer"],requiredResourceFilters:["workspace","team","project"],...vercelConnect("linear","user",["read","write"])})], ["google-sheets","Google Sheets","Read and update selected spreadsheets.","data","sheets.google.com",["https://docs.google.com/spreadsheets/*","https://sheets.google.com/*"],method("local","local_stdio","none",{templateKey:"paperclip.google-sheets"},"S3","Share each spreadsheet with the Paperclip robot email, then paste the sheet links.",{requiredResourceFilters:["spreadsheet"]})], ["context7","Context7","Look up current documentation for software libraries.","developer","context7.com",["https://mcp.context7.com/*"],method("mcp","mcp_remote","none",{serverUrl:"https://mcp.context7.com/mcp"},"S1","Connect Context7 to give agents current library documentation.")], +["shopify","Shopify","Search a store's products and policies, and manage shopping carts.","commerce","shopify.com",["https://*.myshopify.com/api/mcp"],method("storefront-mcp","mcp_remote","none",{serverUrlTemplate:"https://{storeDomain}/api/mcp"},"S3","Connect Shopify's official Storefront MCP server for shopper-facing catalog, policy, and cart tools.",{label:"Connect a Shopify storefront",whenToUse:"Use a store's public myshopify.com domain. No Shopify app or OAuth registration is required.",tenantFields:[{key:"storeDomain",label:"Store domain",type:"text",required:true,placeholder:"your-store.myshopify.com",helperMd:"Enter the full myshopify.com domain without https://.",validation:{pattern:"^[A-Za-z0-9][A-Za-z0-9-]*\\.myshopify\\.com$",maxLength:255}}],consoleLinks:{docs:"https://shopify.dev/docs/apps/build/storefront-mcp/servers/storefront"},warnings:["This is Shopify's Storefront MCP, not Admin API access. It does not manage merchant products, orders, or customers.","The storefront must be publicly reachable. Password-protected or restricted trial stores can return HTTP 401."],requiredResourceFilters:["store"]})], ["composio","Composio","Connect Composio so Paperclip can discover and manage the toolkits in your project.","productivity","composio.dev",["https://backend.composio.dev/*"],method("api-key","rest_api","api_key",{serviceHost:"backend.composio.dev"},"S3","Create a scoped project API key in Composio. It needs read access to toolkits and auth configs; later service-connection phases also need connected-account and session access.",{whenToUse:"Use a project API key from the Composio project that owns the toolkits and connected accounts.",credentialFields:[field("apiKey","Composio project API key","Paste the Composio API key")],keyPlacement:{location:"header",name:"x-api-key"},consoleLinks:{keys:"https://app.composio.dev/",settings:"https://app.composio.dev/",docs:"https://docs.composio.dev/reference/authenticating-to-composio/project-api-key-permissions"}}),{featured:true}], ["oauth-generic","OAuth app","Connect a provider using your own OAuth client.","other","oauth.net",[],method("oauth","rest_api","oauth",{},"S3","Register an OAuth client with the provider and add Paperclip's redirect URI.",{credentialFields:[{...field("clientId","Client ID","Paste the client ID"),type:"text",secret:false},field("clientSecret","Client secret","Paste the client secret")]})], ["api-key-generic","API key app","Connect an API using a key from your provider.","other","openapis.org",[],method("api-key","rest_api","api_key",{},"S3","Create a restricted API key and paste it here.",{credentialFields:[field("apiKey","API key","Paste the API key")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "}})], ["sentry","Sentry","Investigate errors, releases, and production issues.","developer","sentry.io",["https://mcp.sentry.dev/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.sentry.dev/mcp",discoveryUrl:"https://sentry.io/.well-known/oauth-authorization-server"},"S2","Connect the Sentry organization and projects agents need for incident work.",{requiredResourceFilters:["organization","project","environment"]})], ["vercel","Vercel","Inspect projects, deployments, and runtime logs.","developer","vercel.com",["https://mcp.vercel.com/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.vercel.com/mcp"},"S3","Connect the Vercel team and projects agents should operate.",{requiredResourceFilters:["team","project","environment"]})], ["anthropic","Anthropic","Use Anthropic APIs with a restricted key.","ai","anthropic.com",["https://api.anthropic.com/*"],method("api-key","rest_api","api_key",{serviceHost:"api.anthropic.com"},"S3","Create a key in the Anthropic Console and rotate it if it has been exposed.",{credentialFields:[field("apiKey","API key","sk-ant-api03-...")],keyPlacement:{location:"header",name:"x-api-key"}})], -].map(([slug,name,description,category,domain,urlPatterns,m,extra={}])=>({schemaVersion:1,slug,name,description,categories:[category],featured:["zapier","github","slack","notion","posthog","linear"].includes(slug),branding:{logoUrl:favicon(domain)},urlPatterns,methods:Array.isArray(m)?m:[m],...extra})); -apps.push({schemaVersion:1,slug:"gmail",name:"Gmail",description:"Search and read Gmail messages and create drafts without enabling mail sending.",categories:["communication","productivity"],featured:true,branding:{logoUrl:favicon("gmail.com")},urlPatterns:["https://gmailmcp.googleapis.com/*"],docsUrl:"https://developers.google.com/workspace/guides/configure-mcp-servers",redirectConstraints:"https-or-loopback-http",methods:[{key:"paperclip-id-oauth",label:"Connect Gmail",transport:"mcp_remote",auth:"oauth",oauthStrategy:"paperclip_id_connector",grantKinds:["user"],ownershipModes:["customer"],whenToUse:"Use Paperclip ID for a personal Gmail connection with centrally registered Google OAuth.",defaults:{serverUrl:"https://gmailmcp.googleapis.com/mcp/v1",scopesHint:["https://www.googleapis.com/auth/gmail.readonly","https://www.googleapis.com/auth/gmail.compose"]},guidanceMd:"Connect your Gmail identity. Paperclip can search and read mail and create drafts. Sending mail is not enabled.",warnings:["This connection is personal. Agents need an explicit install, profile, and delegation before they can use it."],riskTier:"S3"}]}); +].map(([slug,name,description,category,_domain,urlPatterns,m,extra={}])=>({schemaVersion:1,slug,name,description,categories:[category],featured:["zapier","github","slack","notion","posthog","linear"].includes(slug),branding:brandingFor(slug),urlPatterns,methods:Array.isArray(m)?m:[m],...extra})); +apps.push({schemaVersion:1,slug:"gmail",name:"Gmail",description:"Search and read Gmail messages and create drafts without enabling mail sending.",categories:["communication","productivity"],featured:true,branding:brandingFor("gmail"),urlPatterns:["https://gmailmcp.googleapis.com/*"],docsUrl:"https://developers.google.com/workspace/guides/configure-mcp-servers",redirectConstraints:"https-or-loopback-http",methods:[{key:"paperclip-id-oauth",label:"Connect Gmail",transport:"mcp_remote",auth:"oauth",oauthStrategy:"paperclip_id_connector",grantKinds:["user"],ownershipModes:["customer"],whenToUse:"Use Paperclip ID for a personal Gmail connection with centrally registered Google OAuth.",defaults:{serverUrl:"https://gmailmcp.googleapis.com/mcp/v1",scopesHint:["https://www.googleapis.com/auth/gmail.readonly","https://www.googleapis.com/auth/gmail.compose"]},guidanceMd:"Connect your Gmail identity. Paperclip can search and read mail and create drafts. Sending mail is not enabled.",warnings:["This connection is personal. Agents need an explicit install, profile, and delegation before they can use it."],riskTier:"S3"}]}); + +// The reviewed MCP program is a durable input, not another hand-maintained +// allowlist. Runtime definitions are generated from the same 46-row evidence +// ledger that the tests and implementation checklist validate. +const researchManifest=JSON.parse(fs.readFileSync(path.join(root,"packages/shared/src/self-serve-mcp-research.json"),"utf8")); +const categoryBySlug={ + airtable:"data",asana:"productivity",beehiiv:"content",bitly:"analytics",box:"content",brex:"commerce",candid:"data",clickhouse:"data",cloudflare:"developer",cloudinary:"content",coda:"productivity",egnyte:"content",embat:"commerce","hugging-face":"ai",jira:"productivity",kernel:"developer","local-falcon":"analytics",make:"productivity",manufact:"productivity",mem0:"ai",miro:"productivity",mixpanel:"analytics",netlify:"developer",notion:"content",oreilly:"content",pagerduty:"developer",planetscale:"data",posthog:"analytics",postman:"developer",razorpay:"commerce",resend:"communication",sanity:"content",sentry:"developer",similarweb:"analytics",stripe:"commerce",supabase:"data","ticket-tailor":"commerce",ticktick:"productivity",todoist:"productivity",webflow:"content",wix:"content",xero:"commerce",zapier:"productivity", +}; +const oauthMethodFor=(entry,key="mcp-oauth",serverUrl=entry.serverUrl,extra={})=>method(key,"mcp_remote","oauth",{serverUrl},entry.riskTier,`Connect ${entry.name} in the browser. ${entry.prerequisite}`,{label:`Sign in with ${entry.name}`,ownershipModes:["dcr"],whenToUse:"Use browser sign-in for the provider-hosted MCP server.",consoleLinks:{docs:entry.docsUrl},warnings:[entry.prerequisite],...extra}); +const customerOAuthMethodFor=(entry)=>oauthMethodFor(entry,"mcp-own-oauth",entry.serverUrl,{label:"Use your own OAuth app",ownershipModes:["customer"],whenToUse:`Register an OAuth app with ${entry.name}, then enter its client ID and secret.`,consoleLinks:{register:entry.docsUrl,docs:entry.docsUrl}}); +const apiKeySpec={ + bitly:{name:"Authorization",prefix:"Bearer ",placeholder:"Paste your Bitly API token"}, + cloudflare:{name:"Authorization",prefix:"Bearer ",placeholder:"Paste your Cloudflare API token"}, + coda:{name:"Authorization",prefix:"Bearer ",placeholder:"Paste your Coda API token"}, + kernel:{name:"X-API-Key",prefix:null,placeholder:"Paste your Kernel API key"}, + mem0:{name:"Authorization",prefix:"Bearer ",placeholder:"m0sk_..."}, + oreilly:{name:"Authorization",prefix:"Bearer ",placeholder:"Paste your O'Reilly API token"}, + pagerduty:{name:"Authorization",prefix:"Token token=",placeholder:"Paste your PagerDuty user API token"}, + postman:{name:"X-API-Key",prefix:null,placeholder:"PMAK-..."}, + razorpay:{name:"Authorization",prefix:"Basic ",placeholder:"Paste the base64-encoded key ID and secret"}, + sanity:{name:"Authorization",prefix:"Bearer ",placeholder:"sk..."}, + similarweb:{name:"api-key",prefix:null,placeholder:"Paste your Similarweb API key"}, + stripe:{name:"Authorization",prefix:"Bearer ",placeholder:"sk_..."}, + supabase:{name:"Authorization",prefix:"Bearer ",placeholder:"sbp_..."}, +}; +const apiKeyMethodFor=(entry,key="mcp-api-key",serverUrl=entry.serverUrl,extra={})=>{ + const spec=apiKeySpec[entry.slug]??{name:"Authorization",prefix:"Bearer ",placeholder:`Paste your ${entry.name} API key`}; + return method(key,"mcp_remote","api_key",{serverUrl},entry.riskTier,`Use a customer-created ${entry.name} key. ${entry.prerequisite}`,{label:"Use an API key",whenToUse:"Use a restricted customer-owned key when browser sign-in is not suitable.",credentialFields:[field("authorization",`${entry.name} API key`,spec.placeholder)],keyPlacement:{location:"header",name:spec.name,prefix:spec.prefix},consoleLinks:{keys:entry.docsUrl,docs:entry.docsUrl},warnings:[entry.prerequisite],...extra}); +}; +const specialMethodsFor=(entry)=>{ + // Atlassian's /authv2 rollout only issues GA-tool-compatible tokens when the + // authorization request includes this reviewed protected-resource scope set. + // Omitting scope currently yields agent-interface scopes that its own Jira + // tools reject with HTTP 401. Users can still deselect write toolsets in the + // provider consent screen; never replace this allowlist with live discovery. + if(entry.slug==="jira") return [oauthMethodFor(entry,"mcp-oauth",entry.serverUrl,{defaults:{serverUrl:entry.serverUrl,scopesHint:["read:me","read:account","offline_access","email","read:jira-work","write:jira-work","search:confluence","read:confluence-user","read:page:confluence","write:page:confluence","read:comment:confluence","write:comment:confluence","read:space:confluence","read:hierarchical-content:confluence","write:component:compass","read:component:compass","read:scorecard:compass","write:scorecard:compass","read:event:compass","read:metric:compass","read:all:twg","write:all:twg"]}})]; + if(entry.slug==="hugging-face") return [oauthMethodFor(entry,"mcp-oauth",entry.serverUrl,{defaults:{serverUrl:entry.serverUrl,scopesHint:["read-mcp"]}})]; + if(entry.slug==="xero") return [oauthMethodFor(entry,"mcp-own-oauth",entry.serverUrl,{label:"Use your own OAuth app",ownershipModes:["customer"],whenToUse:`Register an OAuth app with ${entry.name}, then enter its client ID and secret.`,consoleLinks:{register:entry.docsUrl,docs:entry.docsUrl},defaults:{serverUrl:entry.serverUrl,scopesHint:["openid","profile","email","offline_access","accounting.settings","accounting.invoices.read","accounting.reports.aged.read","accounting.reports.balancesheet.read","accounting.reports.profitandloss.read"]}})]; + if(entry.slug==="clickhouse") return [oauthMethodFor(entry,"mcp-oauth",entry.serverUrl,{tenantFields:[{key:"serviceId",label:"ClickHouse Cloud service ID",type:"text",required:true,placeholder:"11e1031f-9a13-4cac-9bc7-d4ec9286ec17",helperMd:"Copy the service ID from ClickStack → Team Settings → API & Agents.",transport:{location:"header",name:"x-service-id"}}],requiredResourceFilters:["service"]})]; + if(entry.slug==="planetscale") return [ + oauthMethodFor(entry,"mcp-oauth",entry.serverUrl,{label:"Database access",tenantFields:[{key:"project",label:"Project or database",type:"text",advanced:true,placeholder:"Optional project or database name",helperMd:"Records the intended database boundary; final access is selected during PlanetScale authorization."},{key:"branch",label:"Branch",type:"text",advanced:true,placeholder:"Optional branch name",helperMd:"Records the intended branch boundary; final access is selected during PlanetScale authorization."}],requiredResourceFilters:["organization","database","branch"]}), + oauthMethodFor(entry,"mcp-insights-only","https://mcp.pscale.dev/mcp/planetscale-insights-only",{label:"Insights only",whenToUse:"Use query insights and schema recommendations without query execution tools.",requiredResourceFilters:["organization","database","branch"]}), + ]; + if(entry.slug==="postman") return [ + oauthMethodFor(entry,"mcp-oauth-minimal","https://mcp.postman.com/minimal",{label:"US · Minimal"}), + oauthMethodFor(entry,"mcp-oauth-code","https://mcp.postman.com/code",{label:"US · Code"}), + oauthMethodFor(entry,"mcp-oauth-full","https://mcp.postman.com/mcp",{label:"US · Full"}), + apiKeyMethodFor(entry,"mcp-eu-key-minimal","https://mcp.eu.postman.com/minimal",{label:"EU · Minimal"}), + apiKeyMethodFor(entry,"mcp-eu-key-code","https://mcp.eu.postman.com/code",{label:"EU · Code"}), + apiKeyMethodFor(entry,"mcp-eu-key-full","https://mcp.eu.postman.com/mcp",{label:"EU · Full"}), + ]; + if(entry.slug==="pagerduty") return [ + apiKeyMethodFor(entry,"mcp-api-key-us","https://mcp.pagerduty.com/mcp",{label:"US service region"}), + apiKeyMethodFor(entry,"mcp-api-key-eu","https://mcp.eu.pagerduty.com/mcp",{label:"EU service region"}), + ]; + if(entry.slug==="supabase") { + const tenantFields=[ + {key:"projectRef",label:"Project reference",type:"text",required:true,placeholder:"abcdefghijklmnopqrst",helperMd:"Scope the connection to one development project.",transport:{location:"query",name:"project_ref"}}, + {key:"readOnly",label:"Read-only mode",type:"checkbox",defaultValue:false,helperMd:"Enable this to prevent the connection from changing the database.",transport:{location:"query",name:"read_only",format:"boolean"}}, + {key:"features",label:"Feature groups",type:"textarea",advanced:true,placeholder:"database,docs",helperMd:"Optional comma-separated feature groups.",transport:{location:"query",name:"features",format:"csv"}}, + ]; + const warning="Do not connect production data unless you have reviewed Supabase's MCP security guidance."; + return [ + oauthMethodFor(entry,"mcp-oauth",entry.serverUrl,{guidanceMd:"Connect Supabase in the browser and scope the connection to one development project. Write tools start enabled and remain governed by Paperclip's action policies.",tenantFields,warnings:[entry.prerequisite,warning],requiredResourceFilters:["project"]}), + apiKeyMethodFor(entry,"mcp-api-key",entry.serverUrl,{guidanceMd:"Use a customer-created Supabase key scoped to one development project. Write tools start enabled and remain governed by Paperclip's action policies.",tenantFields,warnings:[entry.prerequisite,warning],requiredResourceFilters:["project"]}), + ]; + } + return null; +}; + +for(const entry of researchManifest.entries){ + const existing=apps.find((app)=>app.slug===entry.slug); + if(entry.status==="blocked"){ + if(existing) existing.availability={available:false,reason:entry.prerequisite}; + continue; + } + if(existing){ + existing.docsUrl=entry.docsUrl; + existing.redirectConstraints=existing.methods.some((entryMethod)=>entryMethod.auth==="oauth")?"https-or-loopback-http":existing.redirectConstraints; + if(entry.slug!=="zapier") for(const entryMethod of existing.methods) if(entryMethod.transport==="mcp_remote"&&entryMethod.defaults?.serverUrl) entryMethod.defaults.serverUrl=entry.serverUrl; + continue; + } + let methods=specialMethodsFor(entry); + if(!methods){ + if(entry.authMode==="customer_oauth") methods=[customerOAuthMethodFor(entry)]; + else if(entry.authMode==="api_key") methods=[apiKeyMethodFor(entry)]; + else { + methods=[oauthMethodFor(entry)]; + if(entry.authMode==="dcr_or_api_key") methods.push(apiKeyMethodFor(entry)); + } + } + const warnings=[]; + if(["coda","mixpanel"].includes(entry.slug)) warnings.push("This provider's hosted MCP server is currently beta or preview."); + if(["brex","razorpay","stripe"].includes(entry.slug)) warnings.push("Financial or destructive actions must be explicitly approved before execution."); + apps.push({schemaVersion:1,slug:entry.slug,name:entry.name,description:`Connect ${entry.name}'s provider-hosted MCP server.`,categories:[categoryBySlug[entry.slug]??"other"],featured:entry.slug==="jira",branding:brandingFor(entry.slug),urlPatterns:[`${new URL(entry.serverUrl).origin}/*`],docsUrl:entry.docsUrl,redirectConstraints:methods.some((entryMethod)=>entryMethod.auth==="oauth")?"https-or-loopback-http":undefined,methods:methods.map((entryMethod)=>warnings.length>0?{...entryMethod,warnings:[...(entryMethod.warnings??[]),...warnings]}:entryMethod)}); +} +// Google Workspace definitions are reviewed, first-class app entries rather +// than rows synthesized from the generic connection corpus. Keep each product +// independent in the generated manifest while sharing only backend OAuth +// infrastructure. +const reviewedGoogleSlugs=["gmail","google-drive","google-docs","google-sheets","google-slides","google-calendar","google-chat","google-people","google-workspace-search"]; +for(const slug of reviewedGoogleSlugs){ + const existingIndex=apps.findIndex((app)=>app.slug===slug); + if(existingIndex>=0) apps.splice(existingIndex,1); + apps.push(JSON.parse(fs.readFileSync(path.join(out,`${slug}.json`),"utf8"))); +} const parseTableRow=(line)=>line.slice(1,-1).split("|").map((cell)=>cell.trim()); const parseCapture=(fileName)=>{ const markdown=fs.readFileSync(path.join(corpus,fileName),"utf8"); @@ -49,10 +165,14 @@ const inferState=(slug,state)=>{ const transport=slug==="oauth-generic"||slug==="api-key-generic"||label.includes("path: api")||label.includes("api key form")?"rest_api":"mcp_remote"; const auth=slug==="oauth-generic"||label.includes("oauth")||fieldText.includes("client id")?"oauth":slug==="api-key-generic"||label.includes("api key")||fieldText.includes("api key")?"api_key":null; const ownershipModes=[]; - if(label.includes("managed")) ownershipModes.push("platform_shared"); + // A "Managed" state in Vercel describes credential custody, not ownership of + // a Paperclip connection. Keep those concepts separate: importing this review + // evidence must never silently turn an operator-owned connector into + // `platform_shared`. + const externalCredentialCustody=label.includes("managed")&&!label.includes("no managed")?"vercel_connect":null; if(label.includes("your own credentials")||label.includes("manual")||label.includes("api key")) ownershipModes.push("customer"); if(slug==="oauth-generic"&&!label.includes("manually")) ownershipModes.push("dcr"); - return {label:state.label,transport,auth,ownershipModes:[...new Set(ownershipModes)],fieldCount:state.fields.length,linkCount:state.links.length}; + return {label:state.label,transport,auth,ownershipModes:[...new Set(ownershipModes)],externalCredentialCustody,fieldCount:state.fields.length,linkCount:state.links.length}; }; const validateApp=(app)=>{ if(app.schemaVersion!==1||!app.slug||!app.name||!Array.isArray(app.methods)||app.methods.length===0) throw new Error(`${app.slug||"unknown"}: invalid AppDefinition`); diff --git a/server/src/__tests__/tool-access-service.test.ts b/server/src/__tests__/tool-access-service.test.ts index 944c1a90f2..3609ebea66 100644 --- a/server/src/__tests__/tool-access-service.test.ts +++ b/server/src/__tests__/tool-access-service.test.ts @@ -3403,23 +3403,32 @@ describeEmbeddedPostgres("tool access service", () => { canSetCompanyInstall: true, companyInstallReason: null, }); - expect(res.body.apps.map((app: { slug: string }) => app.slug)).toEqual([ - "zapier", - "github", - "slack", + expect(res.body.apps.map((app: { slug: string }) => app.slug)).toEqual(expect.arrayContaining([ + "jira", + "airtable", + "asana", "notion", "posthog", + "sentry", + "zapier", "linear", - "google-sheets", - "context7", - "composio", "gmail", - ]); - expect(res.body.apps.find((app: { slug: string }) => app.slug === "gmail").availability).toEqual({ - available: false, - reason: "Gmail is not available on this Paperclip instance yet.", + "google-drive", + "google-docs", + "google-sheets", + "google-slides", + "google-calendar", + "google-chat", + "google-people", + "google-workspace-search", + ])); + expect(res.body.apps).toHaveLength(58); + expect(res.body.apps.find((app: { slug: string }) => app.slug === "gmail").ownershipAvailability).toEqual({ + platform_shared: false, + platform_provisioned: false, + customer: true, + dcr: true, }); - expect(res.body.apps.map((app: { slug: string }) => app.slug)).not.toContain("google-drive"); expect(res.body.apps).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -3448,14 +3457,16 @@ describeEmbeddedPostgres("tool access service", () => { slug: "zapier", methods: expect.arrayContaining([ expect.objectContaining({ - credentialFields: [expect.objectContaining({ key: "authorization" })], - keyPlacement: expect.objectContaining({ location: "header", name: "Authorization" }), + key: "generated-url", + auth: "none", }), ]), }), expect.objectContaining({ slug: "google-sheets", - availability: expect.objectContaining({ available: false }), + methods: expect.arrayContaining([ + expect.objectContaining({ key: "local", transport: "local_stdio" }), + ]), }), ]), ); @@ -3908,12 +3919,14 @@ describeEmbeddedPostgres("tool access service", () => { await service.connectGalleryApp(companyB.id, { galleryKey: "google-sheets", + connectionMethodKey: "local", name: "Company B sheets", configValues: { allowedSpreadsheetIds: ["shared-sheet"] }, }, { actorType: "user", actorId: "board-b" }); await expect(service.connectGalleryApp(companyA.id, { galleryKey: "google-sheets", + connectionMethodKey: "local", name: "Company A sheets", configValues: { allowedSpreadsheetIds: ["shared-sheet"] }, }, { actorType: "user", actorId: "board-a" })).rejects.toMatchObject({ @@ -3936,6 +3949,7 @@ describeEmbeddedPostgres("tool access service", () => { const connect = await service.connectGalleryApp(company.id, { galleryKey: "google-sheets", + connectionMethodKey: "local", name: "Company sheets", configValues: { allowedSpreadsheetIds: ["sheet-with-inputs"] }, }, { actorType: "user", actorId: "board" }); @@ -4003,11 +4017,13 @@ describeEmbeddedPostgres("tool access service", () => { await service.connectGalleryApp(companyB.id, { galleryKey: "google-sheets", + connectionMethodKey: "local", name: "Company B sheets", configValues: { allowedSpreadsheetIds: ["company-b-sheet"] }, }, { actorType: "user", actorId: "board-b" }); const companyAConnection = await service.connectGalleryApp(companyA.id, { galleryKey: "google-sheets", + connectionMethodKey: "local", name: "Company A sheets", configValues: { allowedSpreadsheetIds: ["company-a-sheet"] }, }, { actorType: "user", actorId: "board-a" }); @@ -4088,11 +4104,13 @@ describeEmbeddedPostgres("tool access service", () => { const first = await service.connectGalleryApp(company.id, { galleryKey: "google-sheets", + connectionMethodKey: "local", name: "First sheets", configValues: { allowedSpreadsheetIds: ["same-company-sheet"] }, }, { actorType: "user", actorId: "board" }); const second = await service.connectGalleryApp(company.id, { galleryKey: "google-sheets", + connectionMethodKey: "local", name: "Second sheets", configValues: { allowedSpreadsheetIds: ["same-company-sheet"] }, }, { actorType: "user", actorId: "board" }); @@ -6264,10 +6282,10 @@ describeEmbeddedPostgres("tool access service", () => { runtimeConfig: {}, }).returning(); - const connect = await withGalleryServerUrl("zapier", PUBLIC_MCP_FIXTURE_URL, () => + const connect = await withGalleryServerUrl("github", PUBLIC_MCP_FIXTURE_URL, () => service.connectGalleryApp(company.id, { - galleryKey: "zapier", - name: "Zapier workspace", + galleryKey: "github", + name: "GitHub workspace", credentialValues: { "credentials.authorization": "zap-secret" }, }, { actorType: "user", actorId: "board" })); @@ -6281,11 +6299,11 @@ describeEmbeddedPostgres("tool access service", () => { expect(connect.connection).toMatchObject({ status: "draft", enabled: false, - config: expect.objectContaining({ sourceTemplateKey: "zapier", quarantineNewEntries: false }), + config: expect.objectContaining({ sourceTemplateKey: "github", quarantineNewEntries: false }), credentialSecretRefs: [ expect.objectContaining({ configPath: "credentials.authorization", - label: "Zapier MCP token", + label: "GitHub token", }), ], }); @@ -6655,11 +6673,12 @@ describeEmbeddedPostgres("tool access service", () => { runtimeConfig: {}, }).returning(); - const connect = await service.connectGalleryApp(company.id, { - galleryKey: "zapier", - name: "Zapier rollback", - credentialValues: { "credentials.authorization": "zap-secret" }, - }, { actorType: "user", actorId: "board" }); + const connect = await withGalleryServerUrl("github", PUBLIC_MCP_FIXTURE_URL, () => + service.connectGalleryApp(company.id, { + galleryKey: "github", + name: "GitHub rollback", + credentialValues: { "credentials.authorization": "github-secret" }, + }, { actorType: "user", actorId: "board" })); const listEntry = connect.catalog.find((entry) => entry.toolName === "list_zaps")!; const updateEntry = connect.catalog.find((entry) => entry.toolName === "update_zap")!; const firstFinish = await service.finishGalleryAppConnection(company.id, connect.connectionId, { @@ -6729,10 +6748,10 @@ describeEmbeddedPostgres("tool access service", () => { { name: "update_zap", description: "Update", inputSchema: { type: "object", properties: {} }, annotations: { readOnlyHint: false } }, ]); - const connect = await withGalleryServerUrl("zapier", PUBLIC_MCP_FIXTURE_URL, () => + const connect = await withGalleryServerUrl("github", PUBLIC_MCP_FIXTURE_URL, () => service.connectGalleryApp(company.id, { - galleryKey: "zapier", - name: "Zapier reconnect", + galleryKey: "github", + name: "GitHub reconnect", credentialValues: { "credentials.authorization": "old-secret" }, }, { actorType: "user", actorId: "board" })); @@ -8637,6 +8656,16 @@ describe("normalizeConnectionMethodConfig", () => { const posthog = getConnectableAppDefinition("posthog")!; const apiKeyMethod = posthog.methods.find((method) => method.key === "mcp-api-key")!; + it("builds a concrete Shopify endpoint from the validated store domain", () => { + const shopifyMethod = getConnectableAppDefinition("shopify")!.methods[0]!; + expect(normalizeConnectionMethodConfig(shopifyMethod, { + storeDomain: "paperclip-demo.myshopify.com", + })).toEqual({ + values: { storeDomain: "paperclip-demo.myshopify.com" }, + url: "https://paperclip-demo.myshopify.com/api/mcp", + }); + }); + it("uses the broad PostHog catalog when optional advanced filters are untouched", () => { expect(normalizeConnectionMethodConfig(apiKeyMethod, { projectId: "12345", @@ -8672,7 +8701,7 @@ describe("normalizeConnectionMethodConfig", () => { expect(() => normalizeConnectionMethodConfig(apiKeyMethod, { projectId: "not-a-project", features: "insights", - })).toThrow("Project ID has an invalid value"); + })).toThrow("Pin to project ID has an invalid value"); expect(() => normalizeConnectionMethodConfig(apiKeyMethod, { projectId: "12345", features: "insights", diff --git a/server/src/services/tool-access.ts b/server/src/services/tool-access.ts index 3bfbbf061f..9a55f2bf75 100644 --- a/server/src/services/tool-access.ts +++ b/server/src/services/tool-access.ts @@ -117,7 +117,7 @@ import type { UpdateToolProfileWithEntries, UnbindToolProfileBinding, } from "@paperclipai/shared"; -import { CLASS3_STATIC_LEASE_ALLOWLIST, credentialConfigPath, getAvailableConnectionMethod, getAvailableConnectionMethods, getConnectableAppDefinition, isToolConnectionAttentionHealth, recommendedDefaultsForApp } from "@paperclipai/shared"; +import { CLASS3_STATIC_LEASE_ALLOWLIST, credentialConfigPath, getAvailableConnectionMethod, getAvailableConnectionMethods, getConnectableAppDefinition, isToolConnectionAttentionHealth, recommendedDefaultsForApp, resolveConnectionMethodServerUrl } from "@paperclipai/shared"; import { checkMcpRemoteHeaderName, checkMcpRemoteHeaderValue, @@ -854,7 +854,11 @@ export function normalizeConnectionMethodConfig( } } - const endpoint = method.defaults?.serverUrl ? new URL(method.defaults.serverUrl) : null; + const resolvedServerUrl = resolveConnectionMethodServerUrl(method, values); + if (method.defaults?.serverUrlTemplate && !resolvedServerUrl) { + throw badRequest("Missing or invalid connection settings for the server URL"); + } + const endpoint = resolvedServerUrl ? new URL(resolvedServerUrl) : null; const headers: Record = {}; for (const field of fields) { const transport = field.transport; diff --git a/ui/src/components/OnboardingWizard.test.tsx b/ui/src/components/OnboardingWizard.test.tsx index 27bb045750..c83b1c24eb 100644 --- a/ui/src/components/OnboardingWizard.test.tsx +++ b/ui/src/components/OnboardingWizard.test.tsx @@ -1088,11 +1088,10 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", ( const deny = () => { throw new DOMException("The operation is insecure.", "SecurityError"); }; - const getItem = vi.spyOn(Storage.prototype, "getItem").mockImplementation(deny); - const removeItem = vi - .spyOn(Storage.prototype, "removeItem") - .mockImplementation(deny); - const setItem = vi.spyOn(Storage.prototype, "setItem").mockImplementation(deny); + // Deny the browser boundary itself. Spying on a Storage object or + // prototype is not portable: DOM implementations may return a fresh + // wrapper and Node exposes a separate experimental Storage global. + const localStorage = vi.spyOn(window, "localStorage", "get").mockImplementation(deny); mockCompany.companies = [{ id: "c1", name: "My Co", issuePrefix: "MC" }]; mockCompany.loading = false; @@ -1106,14 +1105,12 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", ( }); await flushReact(); - expect(getItem).toHaveBeenCalled(); + expect(localStorage).toHaveBeenCalled(); // It mounted: the wizard is open with no draft, rather than the render // throwing on the way in. expect(document.body.textContent).not.toBe(""); - getItem.mockRestore(); - removeItem.mockRestore(); - setItem.mockRestore(); + localStorage.mockRestore(); await act(async () => { root.unmount(); }); @@ -1171,9 +1168,7 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", ( const deny = () => { throw new DOMException("The operation is insecure.", "SecurityError"); }; - const getItem = vi.spyOn(Storage.prototype, "getItem").mockImplementation(deny); - const removeItem = vi.spyOn(Storage.prototype, "removeItem").mockImplementation(deny); - const setItem = vi.spyOn(Storage.prototype, "setItem").mockImplementation(deny); + const localStorage = vi.spyOn(window, "localStorage", "get").mockImplementation(deny); mockCompany.companies = [{ id: "c1", name: "My Co", issuePrefix: "MC" }]; mockCompany.loading = false; @@ -1198,9 +1193,7 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", ( expect(mockDialog.closeOnboarding).toHaveBeenCalled(); - getItem.mockRestore(); - removeItem.mockRestore(); - setItem.mockRestore(); + localStorage.mockRestore(); await act(async () => { root.unmount(); }); diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 5b5dd23dd7..9081a1a440 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -211,21 +211,21 @@ Propose, don't decide. Keep it conversational.`; const onboardingDraftStorage = { read(): string | null { try { - return localStorage.getItem(ONBOARDING_STORAGE_KEY); + return window.localStorage.getItem(ONBOARDING_STORAGE_KEY); } catch { return null; } }, write(value: string): void { try { - localStorage.setItem(ONBOARDING_STORAGE_KEY, value); + window.localStorage.setItem(ONBOARDING_STORAGE_KEY, value); } catch { // Storage unavailable: the draft is simply not resumable this session. } }, clear(): void { try { - localStorage.removeItem(ONBOARDING_STORAGE_KEY); + window.localStorage.removeItem(ONBOARDING_STORAGE_KEY); } catch { // Nothing to do. A draft that cannot be cleared is re-rejected on the // next load by the same ownership check that rejected it here. diff --git a/ui/src/pages/apps/AppsConnect.test.tsx b/ui/src/pages/apps/AppsConnect.test.tsx index c842bdf4ff..cc196b0712 100644 --- a/ui/src/pages/apps/AppsConnect.test.tsx +++ b/ui/src/pages/apps/AppsConnect.test.tsx @@ -1,7 +1,7 @@ // @vitest-environment jsdom -import { flushSync } from "react-dom"; -import { createRoot } from "react-dom/client"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { CONNECTABLE_APP_DEFINITIONS } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -23,6 +23,7 @@ const mockSearch = vi.hoisted(() => ({ value: "" })); const mockParams = vi.hoisted(() => ({ appKey: undefined as string | undefined })); const ZAPIER = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "zapier")!; +const MEM0 = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "mem0")!; const NOTION = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "notion")!; const POSTHOG = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "posthog")!; const GOOGLE_SHEETS = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "google-sheets")!; @@ -74,14 +75,6 @@ vi.mock("@/context/ToastContext", () => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; -async function act(callback: () => void | Promise) { - let result: void | Promise = undefined; - flushSync(() => { - result = callback(); - }); - await result; -} - async function flushReact() { await act(async () => { await Promise.resolve(); @@ -157,12 +150,14 @@ async function gotoLinkFrame(container: HTMLDivElement, url: string) { describe("AppsConnect — Connect with a link (M4 frame)", () => { let container: HTMLDivElement; + let mountedRoot: Root | null; beforeEach(() => { mockSearch.value = ""; mockParams.appKey = undefined; container = document.createElement("div"); document.body.appendChild(container); + mountedRoot = null; listGalleryMock.mockResolvedValue({ apps: [ ZAPIER, @@ -195,7 +190,10 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { ]); }); - afterEach(() => { + afterEach(async () => { + if (mountedRoot) { + await act(async () => mountedRoot?.unmount()); + } document.body.removeChild(container); document.body.innerHTML = ""; vi.clearAllMocks(); @@ -203,6 +201,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { async function render(queryClient?: QueryClient, byoOnly = false) { const root = createRoot(container); + mountedRoot = root; const client = queryClient ?? new QueryClient({ defaultOptions: { queries: { retry: false } } }); await act(async () => { root.render( @@ -259,14 +258,15 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { // ------------------------------------------------------------------------- it("asks both access questions before the credential and defaults per auth kind", async () => { - mockParams.appKey = "zapier"; + mockParams.appKey = "mem0"; + listGalleryMock.mockResolvedValueOnce({ apps: [MEM0] }); await render(); expect(container.textContent).toContain("Access"); expect(container.textContent).toContain("Who is this credential for?"); expect(container.textContent).toContain("Which agents can use this connection?"); // Nothing about the credential is on screen yet. - expect(container.textContent).not.toContain("Connect Zapier"); + expect(container.textContent).not.toContain("Connect Mem0"); const radios = Array.from(document.body.querySelectorAll('[role="radio"]')); const justMe = radios.find((r) => r.textContent?.includes("Just me")); @@ -282,7 +282,8 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); it("blocks Continue until Agents I pick has at least one agent", async () => { - mockParams.appKey = "zapier"; + mockParams.appKey = "mem0"; + listGalleryMock.mockResolvedValueOnce({ apps: [MEM0] }); await render(); // "Agents I pick" with nothing picked is not a usable connection, so the @@ -300,7 +301,8 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); it("keeps the access selections when the wizard moves backward", async () => { - mockParams.appKey = "zapier"; + mockParams.appKey = "mem0"; + listGalleryMock.mockResolvedValueOnce({ apps: [MEM0] }); await render(); await act(async () => { @@ -320,7 +322,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { buttonByText("Save and continue")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); await flushReact(); - expect(container.textContent).toContain("Connect Zapier"); + expect(container.textContent).toContain("Connect Mem0"); await act(async () => { buttonByText("Back")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); @@ -340,7 +342,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { * * A homogeneous fixture cannot tell those two apart: if every app on screen * is API-key-only, a blanket rule and a per-method predicate produce exactly - * the same DOM. So both live in one gallery here — Zapier, whose only method + * the same DOM. So both live in one gallery here — Mem0, whose only method * is an API key, and PostHog, whose methods are identity-bearing sign-in plus * a key its own label calls *personal*. The two mounts must disagree about * the default, which no blanket rule can do. @@ -350,7 +352,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { * grant, so disabling it would describe the product wrongly. */ it("decides the identity default per method, and keeps a personal key submittable", async () => { - listGalleryMock.mockResolvedValue({ apps: [ZAPIER, POSTHOG] }); + listGalleryMock.mockResolvedValue({ apps: [MEM0, POSTHOG] }); const identityChoices = () => { const radios = Array.from( @@ -365,22 +367,22 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { // --- API-key-only method: shared by default, personal still offered ------ let root = await render(); await act(async () => { - buttonContaining("Zapier")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + buttonContaining("Mem0")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); await flushReact(); - const zapier = identityChoices(); - expect(zapier.wholeOrg?.getAttribute("aria-checked")).toBe("true"); - expect(zapier.justMe?.getAttribute("aria-checked")).toBe("false"); + const mem0 = identityChoices(); + expect(mem0.wholeOrg?.getAttribute("aria-checked")).toBe("true"); + expect(mem0.justMe?.getAttribute("aria-checked")).toBe("false"); // Present, and genuinely selectable — not the disabled-with-reason state. - expect(zapier.justMe).toBeTruthy(); - expect(zapier.justMe?.disabled).toBe(false); + expect(mem0.justMe).toBeTruthy(); + expect(mem0.justMe?.disabled).toBe(false); expect(document.body.textContent).not.toContain( "This connection method supports a shared organization credential only.", ); await act(async () => { - zapier.justMe?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + mem0.justMe?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); await flushReact(); await act(async () => { @@ -395,7 +397,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { await flushReact(); const keyField = container.querySelector("input[type=password]"); - await act(async () => setInputValue(keyField!, "zapier-personal-token")); + await act(async () => setInputValue(keyField!, "mem0-personal-token")); await flushReact(); await act(async () => { buttonByText("Connect")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); @@ -406,7 +408,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { // a personal grant. A disabled "Just me" would make this unreachable. expect(connectAppMock).toHaveBeenCalledTimes(1); expect(connectAppMock.mock.calls[0]?.[1]).toMatchObject({ - galleryKey: "zapier", + galleryKey: "mem0", grantKind: "user", }); @@ -522,12 +524,12 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); await flushReact(); - const projectInput = container.querySelector('input[placeholder="12345"]'); const keyInput = container.querySelector('input[type="password"]'); const advanced = buttonByText("Advanced"); - expect(projectInput).toBeTruthy(); expect(keyInput).toBeTruthy(); - expect(container.querySelector('[role="switch"]')?.getAttribute("aria-checked")).toBe("false"); + expect(container.querySelector('input[placeholder="Optional numeric project ID"]')) + .toBeNull(); + expect(container.querySelector('[role="switch"]')).toBeNull(); expect(advanced?.getAttribute("aria-expanded")).toBe("false"); expect(container.textContent).not.toContain("Feature groups"); expect(container.textContent).not.toContain("Individual tools"); @@ -541,7 +543,12 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { expect(advanced?.getAttribute("aria-expanded")).toBe("true"); expect(container.textContent).toContain("Feature groups"); expect(container.textContent).toContain("Individual tools"); - expect(container.textContent).toContain("Tool response mode"); + expect(container.textContent).not.toContain("Tool response mode"); + const projectInput = container.querySelector( + 'input[placeholder="Optional numeric project ID"]', + ); + expect(projectInput).toBeTruthy(); + expect(container.querySelector('[role="switch"]')?.getAttribute("aria-checked")).toBe("false"); await act(async () => { setInputValue(projectInput!, "12345"); @@ -1222,10 +1229,11 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); it("leaving the default name connects with the app name", async () => { + listGalleryMock.mockResolvedValueOnce({ apps: [MEM0] }); await render(); await act(async () => { - buttonContaining("Zapier")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + buttonContaining("Mem0")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); await flushReact(); await passAccessStep(); @@ -1239,21 +1247,22 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { await flushReact(); expect(connectAppMock).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith("/apps/connect?byo=1&appKey=zapier&stage=access"); + expect(mockNavigate).toHaveBeenCalledWith("/apps/connect?byo=1&appKey=mem0&stage=access"); const [, input] = connectAppMock.mock.calls[0]; - expect(input).toMatchObject({ galleryKey: "zapier", name: "Zapier" }); + expect(input).toMatchObject({ galleryKey: "mem0", name: "Mem0" }); }); it("a custom name in the gallery step is sent to the connect mutation", async () => { + listGalleryMock.mockResolvedValueOnce({ apps: [MEM0] }); await render(); await act(async () => { - buttonContaining("Zapier")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + buttonContaining("Mem0")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); await flushReact(); await passAccessStep(); - await act(async () => setInputValue(nameInputFrom(container)!, "Zapier (stdio smoke)")); + await act(async () => setInputValue(nameInputFrom(container)!, "Mem0 (stdio smoke)")); const keyField = container.querySelector("input[type=password]"); await act(async () => setInputValue(keyField!, "secret-key")); await flushReact(); @@ -1264,7 +1273,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { expect(connectAppMock).toHaveBeenCalledTimes(1); const [, input] = connectAppMock.mock.calls[0]; - expect(input).toMatchObject({ galleryKey: "zapier", name: "Zapier (stdio smoke)" }); + expect(input).toMatchObject({ galleryKey: "mem0", name: "Mem0 (stdio smoke)" }); }); it("a custom name on the Google Sheets step is sent to the connect mutation", async () => { @@ -1346,12 +1355,14 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { */ describe("AppsConnect — guided generic MCP flow (PAP-17087)", () => { let container: HTMLDivElement; + let mountedRoot: Root | null; beforeEach(() => { mockSearch.value = ""; mockParams.appKey = undefined; container = document.createElement("div"); document.body.appendChild(container); + mountedRoot = null; listGalleryMock.mockResolvedValue({ apps: [ZAPIER] }); listApplicationsMock.mockResolvedValue({ applications: [] }); listConnectionsMock.mockResolvedValue({ connections: [] }); @@ -1373,7 +1384,10 @@ describe("AppsConnect — guided generic MCP flow (PAP-17087)", () => { }); }); - afterEach(() => { + afterEach(async () => { + if (mountedRoot) { + await act(async () => mountedRoot?.unmount()); + } document.body.removeChild(container); document.body.innerHTML = ""; vi.clearAllMocks(); @@ -1381,6 +1395,7 @@ describe("AppsConnect — guided generic MCP flow (PAP-17087)", () => { async function render() { const root = createRoot(container); + mountedRoot = root; const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); await act(async () => { root.render( diff --git a/ui/src/pages/apps/AppsConnect.tsx b/ui/src/pages/apps/AppsConnect.tsx index e08e8146e7..d5d94f4094 100644 --- a/ui/src/pages/apps/AppsConnect.tsx +++ b/ui/src/pages/apps/AppsConnect.tsx @@ -1903,8 +1903,12 @@ function KeyStep({ (f) => f.required === false || (values[f.configPath]?.trim().length ?? 0) > 0, ); const configFields = [...(method?.tenantFields ?? []), ...(method?.extensionFields ?? [])]; - const standardConfigFields = configFields.filter((field) => field.advanced !== true); - const advancedConfigFields = configFields.filter((field) => field.advanced === true); + const standardConfigFields = configFields.filter( + (field) => field.hidden !== true && field.advanced !== true, + ); + const advancedConfigFields = configFields.filter( + (field) => field.hidden !== true && field.advanced === true, + ); const [advancedOpen, setAdvancedOpen] = useState(false); const configFilled = configFields.every((field) => { if (!field.required) return true;