mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
feat(apps): add experimental MCP aggregator connectors (#13755)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Its tool gateway applies company access rules and approval controls to connected apps. > - MCP aggregators expose many apps through one provider endpoint. > - Each aggregator needs its own credential, catalog, grants, and lifecycle in Paperclip. > - This pull request adds independent Zapier, Arcade, Composio Connect, and Executor setup with a common Access → Connect layout. > - A default-off MCP aggregators flag lets operators opt in while we complete provider acceptance tests. > - Agents use the normal Paperclip permissions, Test screen, and gateway after setup. ## Linked Issues or Issue Description **Subsystem affected** Apps, connection setup, shared contracts, and the remote MCP gateway. **Problem or motivation** Aggregator endpoints need clear provider setup and correct MCP sessions. Generic setup does not explain each provider's authentication or broad execution tools. Provider approval must preserve the original execution instead of replaying a write. **Proposed solution** Add four separate connectors behind Settings → Experimental → MCP aggregators. Start with human and agent access, then connect the endpoint and read its tools. Enable tools by default. Use the existing Permissions and Test screens after setup. Keep legacy Composio API-key and child connections intact. **Alternatives considered** A shared connection for all providers would mix credentials and access rules. Separate provider-specific permission and test screens would duplicate existing controls. Vercel Connect is outside this change. **Roadmap alignment** Extends the existing MCP Tool Gateway & Apps capability and the Connected Apps roadmap area. This work was requested and reviewed by the maintainer. Related work: #11894, #12630, #12632, #12634, and #12906 concern the legacy Composio broker. #13102 also covers remote MCP pagination. This change preserves the broker path and adds initialized sessions, response matching, and provider resume handling alongside pagination. ## What Changed - Add branded setup and interactive Storybooks for Zapier, Arcade, Composio Connect, and Executor. Use the existing access controls and normal action tests. Do not request a connection name or action choices during setup. - Add the default-off `enableMcpAggregators` flag to settings, managed feature metadata, the catalog, and setup guards. Hidden connections keep running. Legacy Composio connections remain unchanged. - Reuse the vault, grants, policy, and catalog models. Support OAuth discovery, bearer tokens, custom headers, and credential-bearing URLs. Add no database tables or migrations. - Initialize and retain Streamable HTTP sessions by connection and effective credentials. Read paginated catalogs and match streaming responses to request IDs. - Classify unfamiliar aggregator tools as writes despite upstream read-only hints; only exact reviewed read capabilities enter the read-only allowlist. Legacy Composio child behavior is preserved. - Preserve provider authorization links and execution IDs. Support Executor approve/resume, decline, and cancel without automatic replay of uncertain writes. - Preserve Off and Ask first choices during refresh and reconnect. Allow new tools and retire removed tools. Keep agent access updates atomic and preserve an empty agent selection. - Document connector UX rules, provider branding sources, and live acceptance results. - Stabilize the existing Sentry release fixture after its repeated CI failure by reusing one module mock; production Sentry behavior is unchanged. ## Verification - Final head `d11781970`: [CI run](https://github.com/paperclipai/paperclip/actions/runs/35633534900) passed, including broad typecheck, test shards, build, and E2E. All 54 checks pass; 2 optional checks are skipped. Greptile is 5/5, Security Scan passes, and all review threads are resolved. - Passed 27 focused connector Vitest checks and 18 connector-only Storybook browser checks before the flag change. All 85 stories rendered at desktop and narrow widths. - Passed 5 connector lifecycle/server checks and 7 selected flag checks after adding the flag. The latter cover settings, managed defaults, cached catalog visibility, and all four setup routes. - Review fixes passed 13 risk/handoff/lifecycle checks, dedicated session-expiration and transport regressions, 13 selected connector/gateway CI cases, and 10 selected setup/reconnect UI cases. A real Composio connection-list call also succeeded through the refreshed UI on `9ab115f71`. - UI and server TypeScript checks passed. UI build, Storybook build, token gates, and diff whitespace checks passed during implementation. - Real browser and real Paperclip agent tests passed for Arcade, Composio, and Executor. Tested action permissions, denied agent access, reconnect, disconnect, and isolation. Tested Arcade catalog additions/removal and Executor provider approve/resume, decline, and cancel. - Zapier live acceptance is incomplete. Its dedicated provider server is configured, but its credential-copy dialog returned an empty clipboard through browser automation. No live Zapier action is claimed. - The three isolated Sentry release cases pass after the CI fixture fix. - Local verification is deliberately narrow at the maintainer's request. The full local suite, recursive typecheck, and repository-wide build were not run. CI provides the broader checks. ## Risks - Shared MCP transport changes affect other remote MCP servers. Protocol fixtures cover initialized sessions, streaming response matching, pagination, and isolation. - Broad execution tools remain broad permissions. The provider governs actions inside those tools. - Provider handoff links are retained briefly in memory. After a server restart, a one-time link may require reopening the provider dashboard. Paperclip does not replay the original call. - Zapier remains unproven live. Custom-header imports and self-hosted endpoints have fixture coverage rather than a separate live account for every variant. - Turning the experimental flag off hides setup; it does not revoke existing credentials or stop existing connections. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, shell execution, and browser automation. The exact runtime model ID and context-window size are not exposed in this session. A separate Anthropic-backed Paperclip agent performed live gateway acceptance tasks. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [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 <noreply@paperclip.ing>
This commit is contained in:
1 parent
3790ca2f13
commit
e8c8ba3c19
74 files changed
+2988
-240
No files matched your search
@@ -33,9 +33,10 @@ describe("tool app gallery URL matching", () => {
|
||||
expect(getAppDefinitionForUrl("https://drivemcp.googleapis.com/mcp/v1")?.slug).toBe("google-drive");
|
||||
});
|
||||
|
||||
it("lists Composio as a connectable API-key app", () => {
|
||||
it("lists Composio Connect alongside the legacy API-key method", () => {
|
||||
const composio = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "composio");
|
||||
expect(composio?.methods).toEqual([
|
||||
expect.objectContaining({ key: "mcp", transport: "mcp_remote", ownershipModes: ["dcr", "customer"] }),
|
||||
expect.objectContaining({ key: "api-key", transport: "rest_api", auth: "api_key" }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
import a0 from "./app-definitions/agentmail.json" with { type: "json" };
|
||||
import a1 from "./app-definitions/zapier.json" with { type: "json" };
|
||||
import a2 from "./app-definitions/railway.json" with { type: "json" };
|
||||
import a3 from "./app-definitions/github.json" with { type: "json" };
|
||||
import a4 from "./app-definitions/slack.json" with { type: "json" };
|
||||
import a5 from "./app-definitions/microsoft-teams.json" with { type: "json" };
|
||||
import a6 from "./app-definitions/imessage-photon.json" with { type: "json" };
|
||||
import a7 from "./app-definitions/telegram.json" with { type: "json" };
|
||||
import a8 from "./app-definitions/discord.json" with { type: "json" };
|
||||
import a9 from "./app-definitions/notion.json" with { type: "json" };
|
||||
import a10 from "./app-definitions/posthog.json" with { type: "json" };
|
||||
import a11 from "./app-definitions/linear.json" with { type: "json" };
|
||||
import a12 from "./app-definitions/context7.json" with { type: "json" };
|
||||
import a13 from "./app-definitions/shopify.json" with { type: "json" };
|
||||
import a14 from "./app-definitions/composio.json" with { type: "json" };
|
||||
import a15 from "./app-definitions/oauth-generic.json" with { type: "json" };
|
||||
import a16 from "./app-definitions/api-key-generic.json" with { type: "json" };
|
||||
import a17 from "./app-definitions/sentry.json" with { type: "json" };
|
||||
import a18 from "./app-definitions/vercel.json" with { type: "json" };
|
||||
import a19 from "./app-definitions/anthropic.json" with { type: "json" };
|
||||
import a20 from "./app-definitions/jira.json" with { type: "json" };
|
||||
import a21 from "./app-definitions/airtable.json" with { type: "json" };
|
||||
import a22 from "./app-definitions/beehiiv.json" with { type: "json" };
|
||||
import a23 from "./app-definitions/bitly.json" with { type: "json" };
|
||||
import a24 from "./app-definitions/candid.json" with { type: "json" };
|
||||
import a25 from "./app-definitions/cloudflare.json" with { type: "json" };
|
||||
import a26 from "./app-definitions/cloudinary.json" with { type: "json" };
|
||||
import a27 from "./app-definitions/coda.json" with { type: "json" };
|
||||
import a28 from "./app-definitions/hugging-face.json" with { type: "json" };
|
||||
import a29 from "./app-definitions/kernel.json" with { type: "json" };
|
||||
import a30 from "./app-definitions/local-falcon.json" with { type: "json" };
|
||||
import a31 from "./app-definitions/make.json" with { type: "json" };
|
||||
import a32 from "./app-definitions/manufact.json" with { type: "json" };
|
||||
import a33 from "./app-definitions/miro.json" with { type: "json" };
|
||||
import a34 from "./app-definitions/netlify.json" with { type: "json" };
|
||||
import a35 from "./app-definitions/oreilly.json" with { type: "json" };
|
||||
import a36 from "./app-definitions/planetscale.json" with { type: "json" };
|
||||
import a37 from "./app-definitions/resend.json" with { type: "json" };
|
||||
import a38 from "./app-definitions/ticktick.json" with { type: "json" };
|
||||
import a39 from "./app-definitions/todoist.json" with { type: "json" };
|
||||
import a40 from "./app-definitions/webflow.json" with { type: "json" };
|
||||
import a41 from "./app-definitions/wix.json" with { type: "json" };
|
||||
import a42 from "./app-definitions/brex.json" with { type: "json" };
|
||||
import a43 from "./app-definitions/clickhouse.json" with { type: "json" };
|
||||
import a44 from "./app-definitions/egnyte.json" with { type: "json" };
|
||||
import a45 from "./app-definitions/embat.json" with { type: "json" };
|
||||
import a46 from "./app-definitions/mixpanel.json" with { type: "json" };
|
||||
import a47 from "./app-definitions/postman.json" with { type: "json" };
|
||||
import a48 from "./app-definitions/razorpay.json" with { type: "json" };
|
||||
import a49 from "./app-definitions/sanity.json" with { type: "json" };
|
||||
import a50 from "./app-definitions/stripe.json" with { type: "json" };
|
||||
import a51 from "./app-definitions/supabase.json" with { type: "json" };
|
||||
import a52 from "./app-definitions/ticket-tailor.json" with { type: "json" };
|
||||
import a53 from "./app-definitions/asana.json" with { type: "json" };
|
||||
import a54 from "./app-definitions/box.json" with { type: "json" };
|
||||
import a55 from "./app-definitions/mem0.json" with { type: "json" };
|
||||
import a56 from "./app-definitions/pagerduty.json" with { type: "json" };
|
||||
import a57 from "./app-definitions/similarweb.json" with { type: "json" };
|
||||
import a58 from "./app-definitions/xero.json" with { type: "json" };
|
||||
import a59 from "./app-definitions/youcom.json" with { type: "json" };
|
||||
import a60 from "./app-definitions/gmail.json" with { type: "json" };
|
||||
import a61 from "./app-definitions/google-drive.json" with { type: "json" };
|
||||
import a62 from "./app-definitions/google-docs.json" with { type: "json" };
|
||||
import a63 from "./app-definitions/google-sheets.json" with { type: "json" };
|
||||
import a64 from "./app-definitions/google-slides.json" with { type: "json" };
|
||||
import a65 from "./app-definitions/google-calendar.json" with { type: "json" };
|
||||
import a66 from "./app-definitions/google-chat.json" with { type: "json" };
|
||||
import a67 from "./app-definitions/google-people.json" with { type: "json" };
|
||||
import a68 from "./app-definitions/google-workspace-search.json" with { type: "json" };
|
||||
import a69 from "./app-definitions/openai.json" with { type: "json" };
|
||||
import a70 from "./app-definitions/openrouter.json" with { type: "json" };
|
||||
import a71 from "./app-definitions/xai.json" with { type: "json" };
|
||||
import a2 from "./app-definitions/arcade.json" with { type: "json" };
|
||||
import a3 from "./app-definitions/executor.json" with { type: "json" };
|
||||
import a4 from "./app-definitions/railway.json" with { type: "json" };
|
||||
import a5 from "./app-definitions/github.json" with { type: "json" };
|
||||
import a6 from "./app-definitions/slack.json" with { type: "json" };
|
||||
import a7 from "./app-definitions/microsoft-teams.json" with { type: "json" };
|
||||
import a8 from "./app-definitions/imessage-photon.json" with { type: "json" };
|
||||
import a9 from "./app-definitions/telegram.json" with { type: "json" };
|
||||
import a10 from "./app-definitions/discord.json" with { type: "json" };
|
||||
import a11 from "./app-definitions/notion.json" with { type: "json" };
|
||||
import a12 from "./app-definitions/posthog.json" with { type: "json" };
|
||||
import a13 from "./app-definitions/linear.json" with { type: "json" };
|
||||
import a14 from "./app-definitions/context7.json" with { type: "json" };
|
||||
import a15 from "./app-definitions/shopify.json" with { type: "json" };
|
||||
import a16 from "./app-definitions/composio.json" with { type: "json" };
|
||||
import a17 from "./app-definitions/oauth-generic.json" with { type: "json" };
|
||||
import a18 from "./app-definitions/api-key-generic.json" with { type: "json" };
|
||||
import a19 from "./app-definitions/sentry.json" with { type: "json" };
|
||||
import a20 from "./app-definitions/vercel.json" with { type: "json" };
|
||||
import a21 from "./app-definitions/anthropic.json" with { type: "json" };
|
||||
import a22 from "./app-definitions/jira.json" with { type: "json" };
|
||||
import a23 from "./app-definitions/airtable.json" with { type: "json" };
|
||||
import a24 from "./app-definitions/beehiiv.json" with { type: "json" };
|
||||
import a25 from "./app-definitions/bitly.json" with { type: "json" };
|
||||
import a26 from "./app-definitions/candid.json" with { type: "json" };
|
||||
import a27 from "./app-definitions/cloudflare.json" with { type: "json" };
|
||||
import a28 from "./app-definitions/cloudinary.json" with { type: "json" };
|
||||
import a29 from "./app-definitions/coda.json" with { type: "json" };
|
||||
import a30 from "./app-definitions/hugging-face.json" with { type: "json" };
|
||||
import a31 from "./app-definitions/kernel.json" with { type: "json" };
|
||||
import a32 from "./app-definitions/local-falcon.json" with { type: "json" };
|
||||
import a33 from "./app-definitions/make.json" with { type: "json" };
|
||||
import a34 from "./app-definitions/manufact.json" with { type: "json" };
|
||||
import a35 from "./app-definitions/miro.json" with { type: "json" };
|
||||
import a36 from "./app-definitions/netlify.json" with { type: "json" };
|
||||
import a37 from "./app-definitions/oreilly.json" with { type: "json" };
|
||||
import a38 from "./app-definitions/planetscale.json" with { type: "json" };
|
||||
import a39 from "./app-definitions/resend.json" with { type: "json" };
|
||||
import a40 from "./app-definitions/ticktick.json" with { type: "json" };
|
||||
import a41 from "./app-definitions/todoist.json" with { type: "json" };
|
||||
import a42 from "./app-definitions/webflow.json" with { type: "json" };
|
||||
import a43 from "./app-definitions/wix.json" with { type: "json" };
|
||||
import a44 from "./app-definitions/brex.json" with { type: "json" };
|
||||
import a45 from "./app-definitions/clickhouse.json" with { type: "json" };
|
||||
import a46 from "./app-definitions/egnyte.json" with { type: "json" };
|
||||
import a47 from "./app-definitions/embat.json" with { type: "json" };
|
||||
import a48 from "./app-definitions/mixpanel.json" with { type: "json" };
|
||||
import a49 from "./app-definitions/postman.json" with { type: "json" };
|
||||
import a50 from "./app-definitions/razorpay.json" with { type: "json" };
|
||||
import a51 from "./app-definitions/sanity.json" with { type: "json" };
|
||||
import a52 from "./app-definitions/stripe.json" with { type: "json" };
|
||||
import a53 from "./app-definitions/supabase.json" with { type: "json" };
|
||||
import a54 from "./app-definitions/ticket-tailor.json" with { type: "json" };
|
||||
import a55 from "./app-definitions/asana.json" with { type: "json" };
|
||||
import a56 from "./app-definitions/box.json" with { type: "json" };
|
||||
import a57 from "./app-definitions/mem0.json" with { type: "json" };
|
||||
import a58 from "./app-definitions/pagerduty.json" with { type: "json" };
|
||||
import a59 from "./app-definitions/similarweb.json" with { type: "json" };
|
||||
import a60 from "./app-definitions/xero.json" with { type: "json" };
|
||||
import a61 from "./app-definitions/youcom.json" with { type: "json" };
|
||||
import a62 from "./app-definitions/gmail.json" with { type: "json" };
|
||||
import a63 from "./app-definitions/google-drive.json" with { type: "json" };
|
||||
import a64 from "./app-definitions/google-docs.json" with { type: "json" };
|
||||
import a65 from "./app-definitions/google-sheets.json" with { type: "json" };
|
||||
import a66 from "./app-definitions/google-slides.json" with { type: "json" };
|
||||
import a67 from "./app-definitions/google-calendar.json" with { type: "json" };
|
||||
import a68 from "./app-definitions/google-chat.json" with { type: "json" };
|
||||
import a69 from "./app-definitions/google-people.json" with { type: "json" };
|
||||
import a70 from "./app-definitions/google-workspace-search.json" with { type: "json" };
|
||||
import a71 from "./app-definitions/openai.json" with { type: "json" };
|
||||
import a72 from "./app-definitions/openrouter.json" with { type: "json" };
|
||||
import a73 from "./app-definitions/xai.json" with { type: "json" };
|
||||
import type { AppDefinition } from "./types/app-definition.js";
|
||||
export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71] as AppDefinition[];
|
||||
export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73] as AppDefinition[];
|
||||
@@ -692,7 +692,6 @@ describe("AppDefinition catalog", () => {
|
||||
"brex",
|
||||
"candid",
|
||||
"coda",
|
||||
"composio",
|
||||
"context7",
|
||||
"egnyte",
|
||||
"embat",
|
||||
@@ -709,7 +708,7 @@ describe("AppDefinition catalog", () => {
|
||||
"ticktick",
|
||||
"xero",
|
||||
]);
|
||||
expect(APP_STORE_DEFINITIONS).toHaveLength(48);
|
||||
expect(APP_STORE_DEFINITIONS).toHaveLength(51);
|
||||
const connectableSlugs = new Set(
|
||||
CONNECTABLE_APP_DEFINITIONS.map((entry) => entry.slug),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@ export const CONNECTABLE_APP_SLUGS = new Set([
|
||||
"agentmail",
|
||||
...SELF_SERVE_MCP_CANDIDATES.map((entry) => entry.slug),
|
||||
"zapier",
|
||||
"arcade",
|
||||
"executor",
|
||||
"slack",
|
||||
"notion",
|
||||
"railway",
|
||||
@@ -48,7 +50,6 @@ export const APP_STORE_HIDDEN_SLUGS = new Set([
|
||||
"brex",
|
||||
"candid",
|
||||
"coda",
|
||||
"composio",
|
||||
"context7",
|
||||
"egnyte",
|
||||
"embat",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "arcade",
|
||||
"name": "Arcade",
|
||||
"description": "Use the tools exposed by your Arcade MCP connection.",
|
||||
"categories": [
|
||||
"productivity"
|
||||
],
|
||||
"featured": true,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/arcade.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://api.arcade.dev/*"
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"dcr",
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use the provider-hosted connection for the quickest setup.",
|
||||
"defaults": {},
|
||||
"guidanceMd": "Paste your Arcade MCP URL. Sign in if required, or add a token or headers under Advanced authentication.",
|
||||
"riskTier": "S3",
|
||||
"label": "Connect MCP server",
|
||||
"consoleLinks": {
|
||||
"docs": "https://docs.arcade.dev/en/operate/governance/mcp-gateways"
|
||||
}
|
||||
}
|
||||
],
|
||||
"docsUrl": "https://docs.arcade.dev/en/operate/governance/mcp-gateways"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"slug": "composio",
|
||||
"name": "Composio",
|
||||
"description": "Connect Composio so Paperclip can discover and manage the toolkits in your project.",
|
||||
"description": "Discover and use connected apps through Composio Connect.",
|
||||
"categories": [
|
||||
"productivity"
|
||||
],
|
||||
@@ -12,9 +12,28 @@
|
||||
"darkLogoUrl": "/brands/apps/composio-dark.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://backend.composio.dev/*"
|
||||
"https://backend.composio.dev/*",
|
||||
"https://connect.composio.dev/*",
|
||||
"https://mcp.composio.dev/*",
|
||||
"https://*.composio.dev/*"
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"dcr",
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use the provider-hosted connection for the quickest setup.",
|
||||
"defaults": {
|
||||
"serverUrl": "https://connect.composio.dev/mcp"
|
||||
},
|
||||
"guidanceMd": "Sign in to Composio Connect, or paste an externally configured MCP session URL and headers.",
|
||||
"riskTier": "S3",
|
||||
"label": "Composio Connect"
|
||||
},
|
||||
{
|
||||
"key": "api-key",
|
||||
"transport": "rest_api",
|
||||
@@ -48,5 +67,6 @@
|
||||
"docs": "https://docs.composio.dev/reference/authenticating-to-composio/project-api-key-permissions"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"docsUrl": "https://docs.composio.dev/docs/composio-connect"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "executor",
|
||||
"name": "Executor",
|
||||
"description": "Use the tools exposed by your Executor MCP connection.",
|
||||
"categories": [
|
||||
"productivity"
|
||||
],
|
||||
"featured": true,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/executor.png"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://executor.sh/*"
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"key": "mcp",
|
||||
"transport": "mcp_remote",
|
||||
"auth": "none",
|
||||
"ownershipModes": [
|
||||
"dcr",
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use the provider-hosted connection for the quickest setup.",
|
||||
"defaults": {},
|
||||
"guidanceMd": "Paste your Executor MCP URL. Sign in if required, or add a token or headers under Advanced authentication.",
|
||||
"riskTier": "S3",
|
||||
"label": "Connect MCP server",
|
||||
"consoleLinks": {
|
||||
"docs": "https://executor.sh/docs/mcp-proxy"
|
||||
}
|
||||
}
|
||||
],
|
||||
"docsUrl": "https://executor.sh/docs/mcp-proxy"
|
||||
}
|
||||
@@ -116,6 +116,14 @@ export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalog
|
||||
cloudDefault: false,
|
||||
selfHostedDefault: false,
|
||||
},
|
||||
enableMcpAggregators: {
|
||||
title: "MCP aggregators",
|
||||
description:
|
||||
"Show experimental Zapier, Arcade, Composio Connect, and Executor setup. Existing connections keep running when hidden; legacy Composio connections are unchanged.",
|
||||
tier: "managed",
|
||||
cloudDefault: false,
|
||||
selfHostedDefault: false,
|
||||
},
|
||||
enablePipelines: {
|
||||
title: "Pipelines",
|
||||
description: "Enable pipeline definitions and pipeline-driven case production surfaces.",
|
||||
|
||||
@@ -1466,6 +1466,7 @@ export type {
|
||||
ToolConnectionTestAgentAccessResponse,
|
||||
ToolConnectionTestAgentsResponse,
|
||||
ToolConnectionTestCallResult,
|
||||
ToolUpstreamPending,
|
||||
ToolConnectionTestCallStatus,
|
||||
ToolConnectionTestCallStatusPhase,
|
||||
ToolConnectionLifecycleEvent,
|
||||
@@ -2767,3 +2768,5 @@ export * from "./types/email.js";
|
||||
export * from "./validators/email.js";
|
||||
export { configureRailwaySshSchema, type ConfigureRailwaySsh, type RailwaySshSetup } from "./railway-connection.js";
|
||||
export * from "./announcements.js";
|
||||
|
||||
export { REMOTE_MCP_CONNECTOR_METHODS, isRemoteMcpConnectorId, isRemoteMcpConnectorMethod, type RemoteMcpConnectorId } from "./remote-mcp-connectors.js";
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getConnectableAppDefinition } from "./app-definitions.js";
|
||||
import { isRemoteMcpConnectorMethod, REMOTE_MCP_CONNECTOR_METHODS } from "./remote-mcp-connectors.js";
|
||||
import { connectToolAppSchema, finishToolAppSchema } from "./validators/tool-access.js";
|
||||
import { instanceExperimentalSettingsSchema, patchInstanceExperimentalSettingsSchema } from "./validators/instance.js";
|
||||
import { INSTANCE_FEATURE_CATALOG } from "./feature-catalog.js";
|
||||
|
||||
describe("independent remote MCP connectors", () => {
|
||||
it("requires an explicit MCP aggregators opt-in for self-hosted and managed instances", () => {
|
||||
expect(instanceExperimentalSettingsSchema.parse({}).enableMcpAggregators).toBe(false);
|
||||
expect(patchInstanceExperimentalSettingsSchema.parse({ enableMcpAggregators: true })).toEqual({ enableMcpAggregators: true });
|
||||
expect(patchInstanceExperimentalSettingsSchema.parse({})).not.toHaveProperty("enableMcpAggregators");
|
||||
expect(INSTANCE_FEATURE_CATALOG.enableMcpAggregators).toMatchObject({ tier: "managed", cloudDefault: false, selfHostedDefault: false });
|
||||
});
|
||||
for (const [provider, methodKey] of Object.entries(REMOTE_MCP_CONNECTOR_METHODS)) {
|
||||
it(`${provider} has its own catalog and accepts URL plus explicit credentials`, () => {
|
||||
const definition = getConnectableAppDefinition(provider)!;
|
||||
expect(definition.slug).toBe(provider);
|
||||
const method = definition.methods.find((method) => method.key === methodKey)!;
|
||||
expect(method.transport).toBe("mcp_remote");
|
||||
if (provider !== "zapier") expect(method.ownershipModes).toContain("dcr");
|
||||
expect(connectToolAppSchema.safeParse({ galleryKey: provider, connectionMethodKey: methodKey, link: "https://example.com/mcp", authMode: "bearer", credentialValues: { "credentials.authorization": "test-secret", "headers.X-User-ID": "test-user" } }).success).toBe(true);
|
||||
});
|
||||
}
|
||||
it("keeps the legacy Composio API-key broker distinct", () => {
|
||||
expect(isRemoteMcpConnectorMethod("composio", "api-key")).toBe(false);
|
||||
expect(getConnectableAppDefinition("composio")?.methods.find((method) => method.key === "api-key")?.transport).toBe("rest_api");
|
||||
expect(connectToolAppSchema.safeParse({ galleryKey: "composio", connectionMethodKey: "api-key", authMode: "bearer" }).success).toBe(false);
|
||||
});
|
||||
it("allows removing all agent access without changing the tool choices", () => {
|
||||
expect(finishToolAppSchema.safeParse({ access: { agentIds: [] } }).success).toBe(true);
|
||||
expect(finishToolAppSchema.safeParse({ access: { agentIds: ["not-an-agent-id"] } }).success).toBe(false);
|
||||
});
|
||||
it("rejects unsafe header overrides and draft saving on unrelated connectors", () => {
|
||||
expect(connectToolAppSchema.safeParse({ galleryKey: "arcade", connectionMethodKey: "mcp", credentialValues: { "headers.Host": "evil.example" } }).success).toBe(false);
|
||||
expect(connectToolAppSchema.safeParse({ galleryKey: "github", saveDraft: true }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/** Providers whose own MCP endpoint defines the catalog and authentication. */
|
||||
export const REMOTE_MCP_CONNECTOR_METHODS = {
|
||||
zapier: "generated-url",
|
||||
arcade: "mcp",
|
||||
composio: "mcp",
|
||||
executor: "mcp",
|
||||
} as const;
|
||||
|
||||
export type RemoteMcpConnectorId = keyof typeof REMOTE_MCP_CONNECTOR_METHODS;
|
||||
export function isRemoteMcpConnectorId(value: unknown): value is RemoteMcpConnectorId {
|
||||
return typeof value === "string" && Object.hasOwn(REMOTE_MCP_CONNECTOR_METHODS, value);
|
||||
}
|
||||
export function isRemoteMcpConnectorMethod(provider: unknown, method: unknown): boolean {
|
||||
return isRemoteMcpConnectorId(provider) && method === REMOTE_MCP_CONNECTOR_METHODS[provider];
|
||||
}
|
||||
@@ -608,6 +608,7 @@ export type {
|
||||
ToolConnectionTestAgentAccessResponse,
|
||||
ToolConnectionTestAgentsResponse,
|
||||
ToolConnectionTestCallResult,
|
||||
ToolUpstreamPending,
|
||||
ToolConnectionTestCallStatus,
|
||||
ToolConnectionTestCallStatusPhase,
|
||||
} from "./tool-access.js";
|
||||
|
||||
@@ -71,6 +71,8 @@ export interface InstanceExperimentalSettings {
|
||||
enableApps: boolean;
|
||||
/** Exposes chat connector setup and Board surfaces; existing delivery continues when hidden. */
|
||||
enableChatConnectors: boolean;
|
||||
/** Exposes MCP aggregator setup; existing connections keep running when hidden. */
|
||||
enableMcpAggregators: boolean;
|
||||
enablePipelines: boolean;
|
||||
enableCases: boolean;
|
||||
enableAgentChat: boolean;
|
||||
|
||||
@@ -1657,6 +1657,17 @@ export interface ToolConnectionTestAgentAccessResponse {
|
||||
access: ToolConnectionAccessSummary;
|
||||
}
|
||||
|
||||
export interface ToolUpstreamPending {
|
||||
kind: "authorization" | "approval";
|
||||
links: Array<{ url: string; host: string; elicitationId?: string }>;
|
||||
executionId?: string;
|
||||
elicitationId?: string;
|
||||
resumeTool?: string;
|
||||
expiresAt?: string;
|
||||
message?: string;
|
||||
requestedSchema?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Result of `POST /tool-connections/:id/test-calls`. */
|
||||
export interface ToolConnectionTestCallResult {
|
||||
decision: ToolConnectionTestDecision;
|
||||
@@ -1667,6 +1678,8 @@ export interface ToolConnectionTestCallResult {
|
||||
error?: { message: string; reasonCode: ToolAccessReasonCode | string | null };
|
||||
/** Present (with `decision: "ask_first"`) — the parked approval request. */
|
||||
actionRequestId?: string;
|
||||
/** Provider handoff, distinct from a Paperclip permission approval. */
|
||||
upstreamPending?: ToolUpstreamPending;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1692,6 +1705,7 @@ export interface ToolConnectionTestCallStatus {
|
||||
parameters?: Record<string, unknown> | null;
|
||||
/** Present once `phase === "done"` and the tool succeeded. */
|
||||
result?: unknown;
|
||||
upstreamPending?: ToolUpstreamPending;
|
||||
/** Present once `phase === "done"` and the tool failed, or when the request was denied/expired. */
|
||||
error?: { message: string; reasonCode: ToolAccessReasonCode | string | null };
|
||||
/** Wall-clock duration of the executed call in ms, when known. */
|
||||
|
||||
@@ -52,6 +52,7 @@ export const instanceExperimentalSettingsSchema = z.object({
|
||||
// configs continue to load during upgrades.
|
||||
enableApps: z.boolean().default(true),
|
||||
enableChatConnectors: z.boolean().default(false),
|
||||
enableMcpAggregators: z.boolean().default(false),
|
||||
enablePipelines: z.boolean().default(false),
|
||||
enableCases: z.boolean().default(false),
|
||||
enableAgentChat: z.boolean().default(false),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isRemoteMcpConnectorMethod } from "../remote-mcp-connectors.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CONNECTION_TOKEN_ISSUANCE_PATHS,
|
||||
@@ -419,6 +420,7 @@ export const connectToolAppSchema = z.object({
|
||||
resumeConnectionId: z.string().guid().optional(),
|
||||
/** Exact configured connection to reauthorize without replacing its identity. */
|
||||
reconnectConnectionId: z.string().guid().optional(),
|
||||
saveDraft: z.boolean().optional(),
|
||||
authMode: genericMcpAuthModeSchema.optional(),
|
||||
oauthClient: genericMcpOAuthClientSchema.optional(),
|
||||
credentialSource: z.enum(["paperclip_vault", "vercel_connect"]).optional(),
|
||||
@@ -438,7 +440,10 @@ export const connectToolAppSchema = z.object({
|
||||
if ((value.grantKind === "agent") !== Boolean(value.subjectAgentId)) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["subjectAgentId"], message: "subjectAgentId is required exactly for an agent grant" });
|
||||
}
|
||||
if (value.authMode && value.galleryKey) {
|
||||
if (value.saveDraft && !isRemoteMcpConnectorMethod(value.galleryKey, value.connectionMethodKey)) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["saveDraft"], message: "Draft saving requires a remote MCP connector" });
|
||||
}
|
||||
if (value.authMode && value.galleryKey && !isRemoteMcpConnectorMethod(value.galleryKey, value.connectionMethodKey)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["authMode"],
|
||||
@@ -490,7 +495,8 @@ export const finishToolAppSchema = z.object({
|
||||
reviewedCatalogEntryIds: z.array(z.string().guid()).max(500).optional(),
|
||||
access: z.union([
|
||||
z.literal("all_agents"),
|
||||
z.object({ agentIds: z.array(z.string().guid()).min(1).max(250) }),
|
||||
// Choosing specific agents may intentionally leave the connection unassigned.
|
||||
z.object({ agentIds: z.array(z.string().guid()).max(250) }),
|
||||
]),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user