diff --git a/doc/connections/COMPOSIO-BROKER-RETIREMENT.md b/doc/connections/COMPOSIO-BROKER-RETIREMENT.md index 63499390d1..e05b7ef3c0 100644 --- a/doc/connections/COMPOSIO-BROKER-RETIREMENT.md +++ b/doc/connections/COMPOSIO-BROKER-RETIREMENT.md @@ -2,7 +2,7 @@ September 21, 2026 -Composio now has one Paperclip catalog method: direct MCP. Enable **Settings → Experimental → MCP aggregators**, then connect with Composio Connect or an externally configured MCP session URL and headers. The existing Access → Connect setup, OAuth, permissions, per-action Test, and gateway execution are unchanged. Connect underlying apps in Composio; Paperclip does not create per-app child connections. +Composio now has one Paperclip catalog method: direct MCP. Connect with Composio Connect or an externally configured MCP session URL and headers. The existing Access → Connect setup, OAuth, permissions, per-action Test, and gateway execution are unchanged. Connect underlying apps in Composio; Paperclip does not create per-app child connections. The project API-key method, toolkit-management API routes, Services tab, connected-account synchronization, and session-minting broker have been removed. There is no automatic credential or grant migration. diff --git a/doc/connections/CONNECTOR-PLAYBOOK.md b/doc/connections/CONNECTOR-PLAYBOOK.md index 5650069ed1..3b75e99e06 100644 --- a/doc/connections/CONNECTOR-PLAYBOOK.md +++ b/doc/connections/CONNECTOR-PLAYBOOK.md @@ -2211,8 +2211,8 @@ direct provider request only when a persisted message from the responsible human proves that choice. An agent-supplied query alone does not count. Unclear or missing message evidence falls back to a question naming that provider and None; alternatives remain a provider-choice question. New human consent must postdate any saved decline -or different choice. Native administrative restrictions still cannot be bypassed. With -`enableMcpAggregators` enabled, a missing built-in match can return eligible +or different choice. Native administrative restrictions still cannot be bypassed. A +missing built-in match can return eligible Composio, Arcade, Executor, and Zapier routes in that order. Search itself makes no provider requests and starts no authorization. diff --git a/doc/connections/REMOTE-MCP-BRAND-SOURCES.md b/doc/connections/REMOTE-MCP-BRAND-SOURCES.md index 7b7ee7d1eb..a9c9aa1809 100644 --- a/doc/connections/REMOTE-MCP-BRAND-SOURCES.md +++ b/doc/connections/REMOTE-MCP-BRAND-SOURCES.md @@ -2,7 +2,7 @@ Retrieved 2026-09-21 from the providers’ own sites for the independent connector design review. Brand-library membership does not publish a catalog connector. -The four connectors are available for setup only when the MCP aggregators experimental flag is enabled. +The four connectors are available for setup by default. No experimental flag is required. Zapier and Composio reuse the existing reviewed local marks. | File | Source | SHA-256 | diff --git a/doc/connections/REMOTE-MCP-LIVE-ACCEPTANCE.md b/doc/connections/REMOTE-MCP-LIVE-ACCEPTANCE.md index 44d8d873ca..cd8e4478c0 100644 --- a/doc/connections/REMOTE-MCP-LIVE-ACCEPTANCE.md +++ b/doc/connections/REMOTE-MCP-LIVE-ACCEPTANCE.md @@ -2,13 +2,15 @@ Test environment: an isolated development worktree with a fresh database and organization. No production data was cloned. The API served the compiled UI with browser-reachable OAuth callbacks. Detailed account and local-instance evidence remains in a private acceptance report. -**This PR delivers default-off experimental support at the maintainer's request. General provider acceptance remains incomplete: Zapier needs its generated credential entered before live validation can finish.** Three providers have real browser and real agent proof. Simulations are recorded separately below. The maintainer's subsequent delivery instruction was to put these connectors behind an experimental MCP aggregators flag and open a PR with passing checks. The Zapier gap is an explicit limitation of that experimental scope, not a claim that the connector playbook's full provider acceptance is complete. Complete that acceptance before promoting the feature out of experimental status. +The initial delivery was experimental. MCP aggregators are now available by default at the maintainer's request. The historical live evidence below is unchanged: three providers have real browser and real agent proof, while Zapier still lacks complete live acceptance. Removing the setup gate does not supply that missing proof. Simulations are recorded separately below. -## Experimental rollout +## Default-on rollout — 2026-09-24 -Setup is behind **Settings → Experimental → MCP aggregators** (`enableMcpAggregators`). It defaults off on self-hosted and managed instances. When off, all four fresh catalog entries are hidden and direct setup, reconnect setup, and OAuth-start requests are rejected server-side. Existing connections keep running and remain available for management. The legacy Composio API-key broker and child connections have since been retired; see [Composio broker retirement](COMPOSIO-BROKER-RETIREMENT.md). An in-progress OAuth callback may complete; the flag does not revoke already issued credentials or grants. +Zapier, Arcade, Composio Connect, and Executor are available by default on self-hosted and managed instances. No experimental setting is required for catalog discovery, setup, reconnect, OAuth, or agent connection search. The retired `enableMcpAggregators` setting is ignored, including saved `false` values. Connection authorization, agent access, tool permissions, and external-provider choice still apply. The legacy Composio API-key broker and child connections remain retired; see [Composio broker retirement](COMPOSIO-BROKER-RETIREMENT.md). -Focused regression tests cover flag defaults, persistence, managed metadata, cached catalog visibility, all four direct setup routes, and server-side rejection before network/credential writes. +The maintainer explicitly requested removal of the experimental gate for all four existing connectors. This rollout follows that request as a scoped exception to the playbook's live-acceptance prerequisite; it does not change the general requirement for new connectors. Zapier's missing live proof remains recorded below and is not a passing acceptance result. + +Focused regression tests cover default-on settings, ignored stored and managed opt-outs, cached catalog visibility, all four direct and inline setup routes, successful server setup and discovery, agent fallback with provider choice, and removal of the Settings toggle. The former flag-off rejection test was replaced; there is no longer a flag-based setup rejection before network or credential writes. ## Live results diff --git a/packages/shared/src/feature-catalog.ts b/packages/shared/src/feature-catalog.ts index d914fe0ce8..9660136a2e 100644 --- a/packages/shared/src/feature-catalog.ts +++ b/packages/shared/src/feature-catalog.ts @@ -124,12 +124,11 @@ export const INSTANCE_FEATURE_CATALOG: Record { - 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 }); + it("defaults the retired compatibility setting on and accepts older configs", () => { + expect(instanceExperimentalSettingsSchema.parse({}).enableMcpAggregators).toBe(true); + expect(patchInstanceExperimentalSettingsSchema.parse({ enableMcpAggregators: false })).toEqual({ enableMcpAggregators: false }); expect(patchInstanceExperimentalSettingsSchema.parse({})).not.toHaveProperty("enableMcpAggregators"); - expect(INSTANCE_FEATURE_CATALOG.enableMcpAggregators).toMatchObject({ tier: "managed", cloudDefault: false, selfHostedDefault: false }); + expect(INSTANCE_FEATURE_CATALOG.enableMcpAggregators).toMatchObject({ tier: "managed", cloudDefault: true, selfHostedDefault: true }); }); for (const [provider, methodKey] of Object.entries(REMOTE_MCP_CONNECTOR_METHODS)) { it(`${provider} has its own catalog and accepts URL plus explicit credentials`, () => { diff --git a/packages/shared/src/types/instance.ts b/packages/shared/src/types/instance.ts index 5c32253a99..8bb10f117e 100644 --- a/packages/shared/src/types/instance.ts +++ b/packages/shared/src/types/instance.ts @@ -71,7 +71,7 @@ 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. */ + /** @deprecated Compatibility key only. MCP aggregators are always enabled. */ enableMcpAggregators: boolean; /** Show experimental memory connection setup. Existing connections remain usable. */ enableMemoryConnectors: boolean; diff --git a/packages/shared/src/validators/instance.ts b/packages/shared/src/validators/instance.ts index 6131a487ca..85cf6d076a 100644 --- a/packages/shared/src/validators/instance.ts +++ b/packages/shared/src/validators/instance.ts @@ -52,7 +52,8 @@ 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), + // Compatibility only: old stored and managed values must still parse. + enableMcpAggregators: z.boolean().default(true), enableMemoryConnectors: z.boolean().default(false), enablePipelines: z.boolean().default(false), enableCases: z.boolean().default(false), diff --git a/server/src/__tests__/connection-aggregator-fallback.test.ts b/server/src/__tests__/connection-aggregator-fallback.test.ts index 7fcb11ba41..80c905336e 100644 --- a/server/src/__tests__/connection-aggregator-fallback.test.ts +++ b/server/src/__tests__/connection-aggregator-fallback.test.ts @@ -22,7 +22,6 @@ import { import type { RuntimeToolsTokenClaims } from "../runtime-tools-token.js"; import { connectionIntentService } from "../services/connection-intents.js"; import { issueThreadInteractionService } from "../services/issue-thread-interactions.js"; -import { instanceSettingsService } from "../services/instance-settings.js"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, @@ -142,9 +141,6 @@ const support = await getEmbeddedPostgresTestSupport(); await db .delete(toolApplications) .where(eq(toolApplications.companyId, claims.company_id)); - await instanceSettingsService(db).updateExperimental({ - enableMcpAggregators: true, - }); } async function selectProvider( option: string, @@ -439,22 +435,12 @@ const support = await getEmbeddedPostgresTestSupport(); }), ).rejects.toMatchObject({ status: 403 }); }); - it("gates fallback and direct aggregator requests behind the experiment", async () => { + it("offers fallback and direct aggregator requests without an experimental opt-in", async () => { await resetQuestions(); - await instanceSettingsService(db).updateExperimental({ - enableMcpAggregators: false, - }); - const result = await connectionIntentService(db).search( - claims, - "hubspot", - ); - expect(result.results.some((item) => item.source === "aggregator")).toBe( - false, - ); - expect(result.providerQuestion).toBeUndefined(); - await expect( - connectionIntentService(db).request(claims, "composio"), - ).rejects.toMatchObject({ status: 422 }); + const result = await connectionIntentService(db).search(claims, "hubspot"); + expect(result.results.some((item) => item.source === "aggregator")).toBe(true); + expect(result.providerQuestion).toBeDefined(); + await expect(connectionIntentService(db).request(claims, "composio")).resolves.toMatchObject({ state: "needs_user_action" }); }); it("reuses an allowed selected provider without claiming the underlying app is ready", async () => { await resetQuestions(); diff --git a/server/src/__tests__/connection-intents-service.test.ts b/server/src/__tests__/connection-intents-service.test.ts index b1a87b00ce..9c661e0744 100644 --- a/server/src/__tests__/connection-intents-service.test.ts +++ b/server/src/__tests__/connection-intents-service.test.ts @@ -33,7 +33,6 @@ import { issueThreadInteractionService } from "../services/issue-thread-interact import { PaperclipRunnerToolAuthority } from "../services/native-runtime/paperclip-runner-tool-authority.js"; import { materializeNativeInteractionResponses } from "../services/native-runtime/native-interaction-bridge.js"; import { connectionIntentService } from "../services/connection-intents.js"; -import { instanceSettingsService } from "../services/instance-settings.js"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, @@ -680,7 +679,6 @@ describeEmbeddedPostgres("connectionIntentService", () => { }); it("preserves an authorizing card through comments and later runs", async () => { - await instanceSettingsService(db).updateExperimental({ enableMcpAggregators: true }); const service = connectionIntentService(db); const first = await service.request(claims, "zapier"); await service.updatePhase(first.interactionId!, "authorizing", claims.responsible_user_id); diff --git a/server/src/__tests__/generic-mcp-connection.test.ts b/server/src/__tests__/generic-mcp-connection.test.ts index 648cfc9413..a25aefede6 100644 --- a/server/src/__tests__/generic-mcp-connection.test.ts +++ b/server/src/__tests__/generic-mcp-connection.test.ts @@ -42,7 +42,6 @@ import { startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { toolAccessService } from "../services/tool-access.js"; -import { instanceSettingsService } from "../services/instance-settings.js"; import { ComposioApiError, type ComposioClient } from "../services/composio.js"; import { createComposioSessionManager } from "../services/composio-session-manager.js"; import { createToolGatewayService } from "../services/tool-gateway.js"; @@ -565,7 +564,6 @@ describeEmbeddedPostgres("generic remote MCP connections", () => { }); it("keeps a generated Zapier URL attached to the curated Zapier identity", async () => { - await instanceSettingsService(db).updateExperimental({ enableMcpAggregators: true }); const secretUrl = "https://mcp.zapier.com/api/v1/connect?token=zapier-secret"; const publicUrl = "https://mcp.zapier.com/api/v1/connect"; const company = await createCompany(db); diff --git a/server/src/__tests__/instance-settings-managed-overlay.test.ts b/server/src/__tests__/instance-settings-managed-overlay.test.ts index 6b9b56116d..e6d149cdc1 100644 --- a/server/src/__tests__/instance-settings-managed-overlay.test.ts +++ b/server/src/__tests__/instance-settings-managed-overlay.test.ts @@ -82,6 +82,15 @@ describe("applyManagedExperimentalOverlay", () => { }); describe("instanceSettingsService managed overlay", () => { + it.each([{}, { enableMcpAggregators: false }])("keeps aggregators on with legacy stored and managed values: %j", async (stored) => { + const { db } = stubDb(settingsRow({ ...stored, enableChatConnectors: true })); + const service = instanceSettingsService(db, { runtimeEnv: managedEnv(JSON.stringify({ + v: 1, mode: "cloud", catalogVersion: "legacy", features: { enableMcpAggregators: false }, plugins: { autoInstall: [] }, + })) }); + expect(await service.getExperimental()).toMatchObject({ enableMcpAggregators: true, enableChatConnectors: true, managedKeys: {} }); + expect(await service.updateExperimental({ enableMcpAggregators: false })).toMatchObject({ experimental: { enableMcpAggregators: true, enableChatConnectors: true } }); + }); + it("persists chat connector opt-in and reads it back after service reconstruction", async () => { const row = settingsRow({}); const { db, persistedSets } = stubDb(row); diff --git a/server/src/__tests__/instance-settings-service.test.ts b/server/src/__tests__/instance-settings-service.test.ts index 98157ca653..31b6d1751b 100644 --- a/server/src/__tests__/instance-settings-service.test.ts +++ b/server/src/__tests__/instance-settings-service.test.ts @@ -41,9 +41,9 @@ describe("instance settings service", () => { enableStreamlinedLeftNavigation: true, enableStreamlinedUi: true, enableApps: true, + enableMcpAggregators: true, enableAgentChat: false, enableChatConnectors: false, - enableMcpAggregators: false, enableMemoryConnectors: false, enableConferenceRoomChat: false, enableClassicTaskInterface: false, diff --git a/server/src/__tests__/remote-mcp-connectors.test.ts b/server/src/__tests__/remote-mcp-connectors.test.ts index abc62ab1d5..1004790d83 100644 --- a/server/src/__tests__/remote-mcp-connectors.test.ts +++ b/server/src/__tests__/remote-mcp-connectors.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; -import { agents, heartbeatRuns, issues, toolCatalogEntries, toolConnectionInstalls, toolInvocations, companies, companyMemberships, createDb, toolConnections, toolPolicies, toolProfileEntries } from "@paperclipai/db"; +import { agents, heartbeatRuns, issues, toolCatalogEntries, toolConnectionInstalls, toolInvocations, companies, companyMemberships, instanceSettings, createDb, toolConnections, toolPolicies, toolProfileEntries } from "@paperclipai/db"; import { eq } from "drizzle-orm"; import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js"; import { toolAccessService } from "../services/tool-access.js"; @@ -23,7 +23,6 @@ describe("remote connector lifecycle", () => { vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY_FILE", join(keyDir, "key")); fixture = await startEmbeddedPostgresTestDatabase("mcp-connectors-test-"); db = createDb(fixture.connectionString); - await instanceSettingsService(db).updateExperimental({ enableMcpAggregators: true }); }); afterAll(async () => { await fixture?.cleanup(); vi.unstubAllEnvs(); if (keyDir) await rm(keyDir, { recursive: true, force: true }); }); async function company() { @@ -51,28 +50,28 @@ describe("remote connector lifecycle", () => { }); return { service, requests, add: () => { added = true; }, remove: () => { removed = true; }, restore: () => { removed = false; } }; } - it("defaults MCP aggregators off and rejects direct setup without provider requests or credential writes", async () => { - expect(normalizeExperimentalSettings({}).enableMcpAggregators).toBe(false); + it.each([undefined, false])("allows all aggregator setup with stored setting %s", async (legacyValue) => { + await db.delete(instanceSettings); + if (legacyValue !== undefined) await db.insert(instanceSettings).values({ experimental: { enableMcpAggregators: legacyValue } }); + expect(normalizeExperimentalSettings({ enableMcpAggregators: legacyValue }).enableMcpAggregators).toBe(true); + expect((await instanceSettingsService(db).getExperimental()).enableMcpAggregators).toBe(true); const org = await company(); const remote = remoteFixture(); - await instanceSettingsService(db).updateExperimental({ enableMcpAggregators: false }); - try { - const app = express(); - app.use((req, _res, next) => { req.actor = { type: "board", userId: actor.actorId, source: "local_implicit", isInstanceAdmin: true }; next(); }); - app.use("/api", toolAccessRoutes(db, { paperclipCloudConnector: null })); - const gallery = await request(app).get(`/api/companies/${org.id}/tools/gallery`); - expect(gallery.status).toBe(200); - expect(gallery.body.apps.some((entry: { slug: string }) => ["zapier", "arcade", "composio", "executor"].includes(entry.slug))).toBe(false); - expect(gallery.body.apps.some((entry: { slug: string }) => entry.slug === "notion")).toBe(true); - for (const [galleryKey, connectionMethodKey] of [["zapier", "generated-url"], ["arcade", "mcp"], ["composio", "mcp"], ["executor", "mcp"]]) { - await expect(remote.service.connectGalleryApp(org.id, { galleryKey, connectionMethodKey, saveDraft: true }, actor)) - .rejects.toMatchObject({ status: 403, details: { code: "mcp_aggregators_disabled" } }); - } - await expect(remote.service.preflightGalleryAppMetadata("composio", "mcp")) - .rejects.toMatchObject({ status: 403 }); - expect(remote.requests).toHaveLength(0); - expect(await db.select().from(toolConnections).where(eq(toolConnections.companyId, org.id))).toHaveLength(0); - } finally { - await instanceSettingsService(db).updateExperimental({ enableMcpAggregators: true }); + const app = express(); + app.use((req, _res, next) => { req.actor = { type: "board", userId: actor.actorId, source: "local_implicit", isInstanceAdmin: true }; next(); }); + app.use("/api", toolAccessRoutes(db, { paperclipCloudConnector: null })); + const gallery = await request(app).get(`/api/companies/${org.id}/tools/gallery`); + expect(gallery.status).toBe(200); + expect(gallery.body.apps.map((entry: { slug: string }) => entry.slug)).toEqual(expect.arrayContaining(["zapier", "arcade", "composio", "executor", "notion"])); + expect(gallery.body.apps.some((entry: { slug: string }) => entry.slug === "mem0")).toBe(false); + for (const [galleryKey, connectionMethodKey, link] of [ + ["zapier", "generated-url", "https://mcp.zapier.com/api/v1/connect?token=fixture-secret"], + ["arcade", "mcp", "https://api.arcade.dev/mcp/fixture"], + ["composio", "mcp", "https://connect.composio.dev/mcp"], + ["executor", "mcp", "https://example.com/mcp"], + ]) { + const connected = await remote.service.connectGalleryApp(org.id, { galleryKey, connectionMethodKey, link, authMode: "none" }, actor); + expect(connected.catalog).toHaveLength(3); + expect(connected.connectionId).toBeTruthy(); } }); it.each([ diff --git a/server/src/__tests__/tool-access-connector-telemetry.test.ts b/server/src/__tests__/tool-access-connector-telemetry.test.ts index fd41786598..1577791538 100644 --- a/server/src/__tests__/tool-access-connector-telemetry.test.ts +++ b/server/src/__tests__/tool-access-connector-telemetry.test.ts @@ -176,9 +176,6 @@ describeEmbeddedPostgres("connector lifecycle telemetry (tool-access)", () => { "paperclip-lifecycle-telemetry-", ); db = createDb(tempDb.connectionString); - await instanceSettingsService(db).updateExperimental({ - enableMcpAggregators: true, - }); }, 30_000); afterEach(async () => { diff --git a/server/src/__tests__/tool-access-service.test.ts b/server/src/__tests__/tool-access-service.test.ts index f6bbc6c92b..5260b0c42c 100644 --- a/server/src/__tests__/tool-access-service.test.ts +++ b/server/src/__tests__/tool-access-service.test.ts @@ -811,14 +811,14 @@ describeEmbeddedPostgres("tool access service", () => { process.env.PAPERCLIP_TOOL_ACCESS_TEST_DATABASE_URL?.trim(); if (externalDatabaseUrl) { db = createDb(externalDatabaseUrl); - await instanceSettingsService(db).updateExperimental({ enableMcpAggregators: true, enableMemoryConnectors: true }); + await instanceSettingsService(db).updateExperimental({ enableMemoryConnectors: true }); return; } tempDb = await startEmbeddedPostgresTestDatabase( "paperclip-tool-access-service-", ); db = createDb(tempDb.connectionString); - await instanceSettingsService(db).updateExperimental({ enableMcpAggregators: true, enableMemoryConnectors: true }); + await instanceSettingsService(db).updateExperimental({ enableMemoryConnectors: true }); }, 20_000); afterEach(async () => { diff --git a/server/src/routes/tool-access.ts b/server/src/routes/tool-access.ts index 4bfff80054..0f4ad7d8b0 100644 --- a/server/src/routes/tool-access.ts +++ b/server/src/routes/tool-access.ts @@ -4,7 +4,6 @@ import { agents, companies, connectionGrants, issueThreadInteractions, toolConne import { and, eq, or } from "drizzle-orm"; import { APP_STORE_DEFINITIONS, - isRemoteMcpConnectorId, isMemoryConnectorId, GITHUB_CONNECTOR_PROFILES, GOOGLE_WORKSPACE_CONNECTOR_PROFILES, @@ -816,7 +815,7 @@ function connectorEnrollmentPrincipal(req: Request): string { ? await options.paperclipCloudConnector.getCapabilities() : []; const vercelConnect = vercelConnectIntegrationStatus(); - const { enableMcpAggregators, enableMemoryConnectors } = await instanceSettingsService(db).getExperimental(); + const { enableMemoryConnectors } = await instanceSettingsService(db).getExperimental(); res.json({ capabilities: await describeConnectionCreateCapabilities(req, companyId), credentialSources: { @@ -832,7 +831,7 @@ function connectorEnrollmentPrincipal(req: Request): string { : "Vercel Connect setup is disabled on this Paperclip instance.", }, }, - apps: APP_STORE_DEFINITIONS.filter((app) => (enableMcpAggregators || !isRemoteMcpConnectorId(app.slug)) && (enableMemoryConnectors || !isMemoryConnectorId(app.slug))).map((app) => + apps: APP_STORE_DEFINITIONS.filter((app) => (enableMemoryConnectors || !isMemoryConnectorId(app.slug))).map((app) => appWithPaperclipCloudConnectorAvailability(app, advertisedProfiles) ), }); diff --git a/server/src/services/connection-intents.ts b/server/src/services/connection-intents.ts index 01be2af2f0..66137faaeb 100644 --- a/server/src/services/connection-intents.ts +++ b/server/src/services/connection-intents.ts @@ -35,7 +35,6 @@ import { conflict, forbidden, notFound, unprocessable } from "../errors.js"; import type { RuntimeToolsTokenClaims } from "../runtime-tools-token.js"; import { issueThreadInteractionService } from "./issue-thread-interactions.js"; import { toolAccessService } from "./tool-access.js"; -import { instanceSettingsService } from "./instance-settings.js"; import { captureRunIdentity } from "./run-identity.js"; import { resolveManagedGitHubIdentitySelection } from "./git-credentials.js"; @@ -343,7 +342,6 @@ export function connectionIntentService(db: Db) { async function search(claims: ConnectionRunClaims, query: string, options: { retryProviderChoice?: boolean } = {}): Promise { const { run, agent, issue } = await loadRunContext(claims); - const aggregatorsEnabled = (await instanceSettingsService(db).getExperimental()).enableMcpAggregators; const normalized = query.trim().toLocaleLowerCase(); const tokens = normalized.split(/[^\p{L}\p{N}]+/u).filter(Boolean); const inventory = await connectionInventory(run.companyId); @@ -353,7 +351,6 @@ export function connectionIntentService(db: Db) { sourceSlugForConnection(connection, inventory.applicationsById)?.startsWith("connection:") && connection.status !== "archived").map((connection) => `connection:${connection.id}`)]; for (const service of services) { - if (!aggregatorsEnabled && isRemoteMcpConnectorId(service)) continue; let app; try { app = await resolveService(service, run.companyId, run.responsibleUserId!, agent.id); } catch (error) { if (service.startsWith("connection:") && (error as { status?: number }).status === 404) continue; throw error; } @@ -401,7 +398,7 @@ export function connectionIntentService(db: Db) { const explicitConsent = explicit && await hasExplicitProviderRequest(run.companyId, issue.id, run.responsibleUserId!, targetService, explicit.provider, previous[0]); if (exact.length && (!explicitConsent || exact.some(({ item }) => item.state === "unavailable"))) return directSearchResult(query, exact.map(({ item }) => item)); const alternatives: ConnectionSearchResultItem[] = []; - if (aggregatorsEnabled && /^[a-z0-9][a-z0-9-]{0,79}$/.test(targetService)) { + if (/^[a-z0-9][a-z0-9-]{0,79}$/.test(targetService)) { for (const provider of AGGREGATOR_PRIORITY) { // Broad execute/search descriptions are not evidence of app support. Only a // namespaced action (or an explicitly listed Executor integration) qualifies. @@ -563,7 +560,6 @@ export function connectionIntentService(db: Db) { upstreamService = { slug: route.targetService, name: selected.aggregator.targetName, selectionInteractionId: options.selectionInteractionId }; serviceSlug = route.provider; } - if (isRemoteMcpConnectorId(serviceSlug) && !(await instanceSettingsService(db).getExperimental()).enableMcpAggregators) throw unprocessable("Experimental MCP aggregators are disabled"); const app = await resolveService(serviceSlug, context.run.companyId, context.run.responsibleUserId!, context.agent.id, options.purpose); if (!app.available || app.methods.length === 0) { throw unprocessable(`Connection service ${serviceSlug} is not available`); diff --git a/server/src/services/instance-settings.ts b/server/src/services/instance-settings.ts index 2676126721..fa88c0e256 100644 --- a/server/src/services/instance-settings.ts +++ b/server/src/services/instance-settings.ts @@ -233,8 +233,8 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta // Apps graduated from Experimental. Ignore historical off values while // continuing to accept the compatibility key in stored settings. enableApps: true, + enableMcpAggregators: true, enableChatConnectors: parsed.data.enableChatConnectors ?? false, - enableMcpAggregators: parsed.data.enableMcpAggregators ?? false, enableMemoryConnectors: parsed.data.enableMemoryConnectors ?? false, enablePipelines: parsed.data.enablePipelines ?? false, enableCases: parsed.data.enableCases ?? false, @@ -276,8 +276,8 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta enableStreamlinedLeftNavigation: true, enableStreamlinedUi: true, enableApps: true, + enableMcpAggregators: true, enableChatConnectors: false, - enableMcpAggregators: false, enableMemoryConnectors: false, enablePipelines: false, enableCases: false, @@ -334,9 +334,9 @@ export function applyManagedExperimentalOverlay( for (const [key, value] of Object.entries(managedConfig.features) as Array< [ManagedExperimentalFeatureKey, boolean] >) { - // Existing Cloud stack configs may still carry enableApps. Accept the - // document during rollout, but never let the retired flag disable Apps. - if (key === "enableApps") continue; + // Existing Cloud stack configs may still carry retired flags. Accept the + // document during rollout, but never let retired flags disable Apps or MCP aggregators. + if (key === "enableApps" || key === "enableMcpAggregators") continue; next[key] = value; managedKeys[key] = { managed: true, managedBy: PAPERCLIP_CLOUD_MANAGED_BY }; } diff --git a/server/src/services/tool-access.ts b/server/src/services/tool-access.ts index f2f2f062b6..50962c9ba5 100644 --- a/server/src/services/tool-access.ts +++ b/server/src/services/tool-access.ts @@ -11869,17 +11869,11 @@ export function toolAccessService( return `${base.slice(0, 151).trimEnd()} (${randomUUID().slice(0, 6)})`; } - async function assertExperimentalConnectorSetupEnabled(provider: unknown, method: unknown, existing = false) { + async function assertExperimentalConnectorSetupEnabled(provider: unknown, existing = false) { if (!existing && isMemoryConnectorId(provider) && !(await instanceSettingsService(db).getExperimental()).enableMemoryConnectors) { throw forbidden("Enable memory connectors in Settings → Experimental to set up this connection", { code: "memory_connectors_disabled" }); } - if (isRemoteMcpConnectorMethod(provider, method) - && !(await instanceSettingsService(db).getExperimental()).enableMcpAggregators) { - throw forbidden("Enable MCP aggregators in Settings → Experimental to set up this connection", { - code: "mcp_aggregators_disabled", - }); - } } async function connectGalleryApp( @@ -12030,7 +12024,7 @@ export function toolAccessService( ? connectionMethodFor(galleryEntry, inferredMethodKey) : null; const remoteMcpConnector = isRemoteMcpConnectorMethod(galleryEntry?.slug, method?.key); - await assertExperimentalConnectorSetupEnabled(galleryEntry?.slug, method?.key, Boolean( + await assertExperimentalConnectorSetupEnabled(galleryEntry?.slug, Boolean( input.reconnectConnectionId && requestedResumeConnection?.status !== "draft" && requestedResumeConnection?.config.sourceTemplateKey === galleryEntry?.slug, )); @@ -13960,7 +13954,7 @@ export function toolAccessService( ): Promise { const connection = await getConnectionRow(connectionId, companyId); assertSupportedConnection(connection); - await assertExperimentalConnectorSetupEnabled(connection.config.sourceTemplateKey, connection.config.connectionMethodKey, connection.status !== "draft"); + await assertExperimentalConnectorSetupEnabled(connection.config.sourceTemplateKey, connection.status !== "draft"); if (connection.status === "archived") throw conflict("Archived app connections cannot be reconnected"); if (connection.credentialSource === "vercel_connect") { @@ -14139,7 +14133,7 @@ export function toolAccessService( ): Promise { let connection = await getConnectionRow(connectionId, companyId); assertSupportedConnection(connection); - await assertExperimentalConnectorSetupEnabled(connection.config.sourceTemplateKey, connection.config.connectionMethodKey, connection.status !== "draft"); + await assertExperimentalConnectorSetupEnabled(connection.config.sourceTemplateKey, connection.status !== "draft"); if (connection.status === "archived") throw conflict("Archived app connections cannot start sign in"); const sourceTemplateKey = @@ -16636,7 +16630,7 @@ export function toolAccessService( if (!app || app.availability?.available === false) throw notFound("App not found"); const method = connectionMethodFor(app, methodKey); - await assertExperimentalConnectorSetupEnabled(app.slug, method.key); + await assertExperimentalConnectorSetupEnabled(app.slug); if (method.transport !== "mcp_remote" || !method.defaults?.serverUrl) { throw unprocessable( "This app method does not use a hosted remote MCP endpoint", diff --git a/tests/runner-e2e/everyday-flow.ts b/tests/runner-e2e/everyday-flow.ts index 9909d98353..635bcd96a4 100644 --- a/tests/runner-e2e/everyday-flow.ts +++ b/tests/runner-e2e/everyday-flow.ts @@ -646,7 +646,6 @@ export async function runEverydayFlow(input: Input) { authenticated: true, }); if (providerChoice || nativeProviderCase) { - await api.patch("/api/instance/settings/experimental", {enableMcpAggregators:true}); if (caseId === "provider-second") aggregatorFixture = await setupAggregatorFixture(api, fixtures.company.id, fixtures.agent.id, `CONTACTS_${nonce}`); const state = await api.get<{connections:Row[]}>(`/api/companies/${fixtures.company.id}/tools/connections`); initialConnections = state.connections.map(c=>c.id); diff --git a/ui/src/features/connections/ConnectionSetupFlow.tsx b/ui/src/features/connections/ConnectionSetupFlow.tsx index cb4d58ebcb..e190796387 100644 --- a/ui/src/features/connections/ConnectionSetupFlow.tsx +++ b/ui/src/features/connections/ConnectionSetupFlow.tsx @@ -1,6 +1,5 @@ import { RemoteMcpProductionSetup } from "./remote-mcp/RemoteMcpProductionSetup"; import { useMemoryConnectorsEnabled } from "@/hooks/useMemoryConnectorsEnabled"; -import { useMcpAggregatorsEnabled } from "@/hooks/useMcpAggregatorsEnabled"; import { AiConnectionCredentialStep } from "@/components/ai-connections/AiConnectionCredentialStep"; import { ConnectionChoiceList } from "./ConnectionChoiceList"; import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode, type Ref } from "react"; @@ -539,7 +538,6 @@ export function ConnectionSetupFlow(props: ConnectionSetupFlowProps = {}) { const [searchParams] = useSearchParams(); const params = useParams<{ appKey?: string }>(); const { selectedCompanyId } = useCompany(); - const aggregators = useMcpAggregatorsEnabled(); const memory = useMemoryConnectorsEnabled(); const interactionId = props.interactionId || searchParams.get("intent") || undefined; const source = props.serviceSlug || searchParams.get("source") || params.appKey || searchParams.get("appKey"); @@ -562,8 +560,6 @@ export function ConnectionSetupFlow(props: ConnectionSetupFlowProps = {}) { } if (!props.byoOnly && (props.credentialSource ?? "paperclip_vault") === "paperclip_vault" && isRemoteMcpConnectorId(provider) && (!method || isRemoteMcpConnectorMethod(provider, method))) { - if (!aggregators.loaded) return

Loading connection settings…

; - if (!aggregators.enabled) return

Enable MCP aggregators in Settings → Experimental to set up this connection.

; return ; } return ; diff --git a/ui/src/hooks/useMcpAggregatorsEnabled.ts b/ui/src/hooks/useMcpAggregatorsEnabled.ts deleted file mode 100644 index 5cb163f35d..0000000000 --- a/ui/src/hooks/useMcpAggregatorsEnabled.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { instanceSettingsApi } from "@/api/instanceSettings"; -import { queryKeys } from "@/lib/queryKeys"; - -/** Default-off setup gate. Existing connections and their grants remain usable. */ -export function useMcpAggregatorsEnabled() { - const query = useQuery({ - queryKey: queryKeys.instance.experimentalSettings, - queryFn: () => instanceSettingsApi.getExperimental(), - }); - return { enabled: !query.isError && query.data?.enableMcpAggregators === true, loaded: query.isFetched }; -} diff --git a/ui/src/pages/InstanceExperimentalSettings.test.tsx b/ui/src/pages/InstanceExperimentalSettings.test.tsx index b8a433c97c..8e7d7d9654 100644 --- a/ui/src/pages/InstanceExperimentalSettings.test.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.test.tsx @@ -78,8 +78,8 @@ function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload { enableStreamlinedLeftNavigation: true, enableStreamlinedUi: true, enableApps: true, + enableMcpAggregators: true, enableChatConnectors: false, - enableMcpAggregators: false, enableMemoryConnectors: false, enablePipelines: false, enableCases: false, @@ -225,17 +225,10 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233) } }); - it("defaults MCP aggregators off and persists an explicit toggle in both directions", async () => { + it("does not offer a retired MCP aggregators toggle", async () => { await renderPage(); - const selector = 'button[aria-label="Toggle MCP aggregators experimental setting"]'; - expect(container.querySelector(selector)?.getAttribute("aria-checked")).toBe("false"); - expect(container.textContent).toContain("Existing MCP connections keep running."); - for (const enabled of [true, false]) { - await act(() => container.querySelector(selector)!.click()); - await flushReact(); - expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenLastCalledWith({ enableMcpAggregators: enabled }); - expect(container.querySelector(selector)?.getAttribute("aria-checked")).toBe(String(enabled)); - } + expect(container.querySelector('button[aria-label="Toggle MCP aggregators experimental setting"]')).toBeNull(); + expect(container.textContent).not.toContain("MCP aggregators"); }); it("defaults chat connectors off and persists an explicit toggle in both directions", async () => { @@ -1062,7 +1055,7 @@ describe("InstanceExperimentalSettings — operator-hidden cards", () => { it("keeps only permitted controls in alphabetical order, including when hidden features are enabled", async () => { setWorktreeRuntimeMeta(true); - const visible = new Set(["enableExternalObjects", "enableMcpAggregators", "enableSimplifiedEnglishInteractions"]); + const visible = new Set(["enableExternalObjects", "enableMemoryConnectors", "enableSimplifiedEnglishInteractions"]); await renderPage( INSTANCE_FEATURE_KEYS.filter((key) => !visible.has(key)).map((key) => `instance.experimental.${key}`), { @@ -1078,7 +1071,7 @@ describe("InstanceExperimentalSettings — operator-hidden cards", () => { expect([...container.querySelectorAll("h3")].map((heading) => heading.textContent)).toEqual([ "Enable External Objects", - "MCP aggregators", + "Memory connectors", "Simplified English Interactions", ]); expect([...container.querySelectorAll("section h2")].map((heading) => heading.textContent)).toEqual([ diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index a179b09a3b..8fc3a2c641 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -432,18 +432,6 @@ export function InstanceExperimentalSettings() { ariaLabel="Toggle first task plan proposal experimental setting" /> - toggleMutation.mutate({ enableMcpAggregators: checked })} - disabled={toggleMutation.isPending} - settingKey="enableMcpAggregators" - managed={managedKeys.enableMcpAggregators} - ariaLabel="Toggle MCP aggregators experimental setting" - /> - { } it.each(["zapier", "arcade", "composio", "executor"])("inline aggregator %s collects the endpoint and completes only for the requester", async (provider) => { - experimentalMock.mockResolvedValue({ enableMcpAggregators: true }); const onComplete = vi.fn(); const popup = vi.spyOn(window, "open").mockReturnValue(null); const connection = { id: "conn-inline", status: "draft", credentialPolicy: "per_user", authKind: "api_key" }; @@ -359,7 +358,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); it.each(["arcade", "composio", "executor"])("inline aggregator %s binds OAuth to the task and retries the same draft", async (provider) => { - experimentalMock.mockResolvedValue({ enableMcpAggregators: true }); const onComplete = vi.fn(); const onPhaseChange = vi.fn(); const popup = { closed: false, location: { assign: vi.fn() }, focus: vi.fn(), close: vi.fn() }; @@ -394,7 +392,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); it("inline aggregator saves and resumes a draft without storing credentials in browser storage", async () => { - experimentalMock.mockResolvedValue({ enableMcpAggregators: true }); const onCancel = vi.fn(); const connection = { id: "conn-inline-draft", status: "draft", credentialPolicy: "per_user", authKind: "none", config: { url: "https://provider.example/mcp", sourceTemplateKey: "zapier", connectionMethodKey: "generated-url" } }; connectAppMock.mockResolvedValue({ connectionId: connection.id, connection, catalog: [] }); @@ -419,7 +416,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); it("inline aggregator reuses an eligible account without changing its access", async () => { - experimentalMock.mockResolvedValue({ enableMcpAggregators: true }); const onUseExisting = vi.fn().mockResolvedValue(undefined); await render(undefined, false, ); await act(async () => buttonContaining("Existing Composio")!.click()); @@ -445,10 +441,12 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { expect(container.textContent).not.toContain("Enable memory connectors"); }); - it.each(["zapier", "arcade", "composio", "executor"])("blocks direct %s setup while MCP aggregators are off", async (provider) => { + it.each(["zapier", "arcade", "composio", "executor"])("opens direct %s setup with default settings", async (provider) => { mockSearch.value = `source=${provider}`; await render(); - expect(container.textContent).toContain("Enable MCP aggregators"); + expect(container.textContent).not.toContain("Enable MCP aggregators"); + await passAccessStep(); + expect(container.textContent).toContain("MCP server URL"); expect(connectAppMock).not.toHaveBeenCalled(); expect(startOAuthMock).not.toHaveBeenCalled(); }); @@ -456,7 +454,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { it.each(["arcade", "composio", "executor"])("explains a failed %s OAuth return and retries the same saved draft", async (provider) => { const draft = { id: "conn-oauth-draft", companyId: "company-1", status: "draft", authKind: "oauth", credentialPolicy: "shared", config: { sourceTemplateKey: provider, connectionMethodKey: "mcp", url: "https://example.com/mcp" } }; mockSearch.value = `source=${provider}&resume=${draft.id}&oauth=failed&code=oauth_callback_failed&error_description=untrusted-provider-message`; - experimentalMock.mockResolvedValue({ enableMcpAggregators: true }); getConnectionMock.mockResolvedValue(draft); connectAppMock.mockResolvedValue({ connectionId: draft.id, connection: draft, catalog: [], auth: { kind: "oauth" } }); await render(); @@ -471,7 +468,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { it("explains a declined Composio OAuth return without discarding the draft", async () => { mockSearch.value = "source=composio&resume=conn-oauth-draft&oauth=denied&code=oauth_authorization_denied"; - experimentalMock.mockResolvedValue({ enableMcpAggregators: true }); getConnectionMock.mockResolvedValue({ id: "conn-oauth-draft", status: "draft", authKind: "oauth", config: { sourceTemplateKey: "composio", connectionMethodKey: "mcp", url: "https://connect.composio.dev/mcp" } }); await render(); await vi.waitFor(() => expect(container.textContent).toContain("Connection cancelled. Your setup details are preserved")); @@ -485,7 +481,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { ])("retains the %s identity when returning to Access after an OAuth return", async (credentialPolicy, identityLabel, asCurrentUser) => { const draft = { id: "conn-oauth-draft", status: "draft", authKind: "oauth", credentialPolicy, config: { sourceTemplateKey: "composio", connectionMethodKey: "mcp", url: "https://connect.composio.dev/mcp" } }; mockSearch.value = `source=composio&resume=${draft.id}&oauth=denied`; - experimentalMock.mockResolvedValue({ enableMcpAggregators: true }); getConnectionMock.mockResolvedValue(draft); connectAppMock.mockResolvedValue({ connectionId: draft.id, connection: draft, catalog: [], auth: { kind: "oauth" } }); await render(); @@ -2830,7 +2825,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); it("gives Zapier the shared credential and agent access opener", async () => { - experimentalMock.mockResolvedValue({ enableMcpAggregators: true }); mockSearch.value = "source=zapier"; listGalleryMock.mockResolvedValueOnce({ apps: [ diff --git a/ui/src/pages/apps/Browse.test.tsx b/ui/src/pages/apps/Browse.test.tsx index 14cdce1326..b81ae392b2 100644 --- a/ui/src/pages/apps/Browse.test.tsx +++ b/ui/src/pages/apps/Browse.test.tsx @@ -219,23 +219,14 @@ describe("Connectors landing page", () => { for (const slug of ["zep", "supermemory", "cognee", "honcho"]) expect(container.querySelector(`[data-app-slug="${slug}"]`)).toBeNull(); }); - it("hides cached MCP aggregators until enabled and preserves saved MCP connections", async () => { + it("shows all MCP aggregators by default and ignores cached legacy opt-outs", async () => { const providers = ["zapier", "arcade", "composio", "executor"]; listGalleryMock.mockResolvedValue({ apps: [...providers, "notion"].map(getAppStoreDefinition) }); const client = await renderBrowse(); - for (const slug of providers) expect(container.querySelector(`[data-app-slug="${slug}"]`)).toBeNull(); - expect(container.querySelector('[data-app-slug="notion"]')).not.toBeNull(); - await act(() => { client.setQueryData(queryKeys.instance.experimentalSettings, { enableMcpAggregators: true }); }); + for (const slug of providers) expect(container.querySelector(`[data-app-slug="${slug}"]`)).not.toBeNull(); + await act(() => { client.setQueryData(queryKeys.instance.experimentalSettings, { enableMcpAggregators: false }); }); await flushReact(); for (const slug of providers) expect(container.querySelector(`[data-app-slug="${slug}"]`)).not.toBeNull(); - await act(() => { - client.setQueryData(queryKeys.tools.connections("company-1"), { connections: [connection({ id: "saved", applicationId: "saved-app", config: { sourceTemplateKey: "composio", connectionMethodKey: "mcp" }, transport: "mcp_remote" })] }); - client.setQueryData(queryKeys.tools.applications("company-1"), { applications: [application({ id: "saved-app", name: "Composio", metadata: { sourceTemplateKey: "composio" } })] }); - client.setQueryData(queryKeys.instance.experimentalSettings, { enableMcpAggregators: false }); - }); - await flushReact(); - expect(container.textContent).toContain("Composio"); - for (const slug of ["zapier", "arcade", "executor"]) expect(container.querySelector(`[data-app-slug="${slug}"]`)).toBeNull(); }); it("defaults to tools-only GitHub and hides chat-only catalog and existing chat accounts", async () => { diff --git a/ui/src/pages/apps/Browse.tsx b/ui/src/pages/apps/Browse.tsx index 945935648d..e6aa890438 100644 --- a/ui/src/pages/apps/Browse.tsx +++ b/ui/src/pages/apps/Browse.tsx @@ -19,7 +19,6 @@ import { import type { ToolApplication, ToolConnection } from "@paperclipai/shared"; import { getAppDefinitionForUrl, - isRemoteMcpConnectorId, isMemoryConnectorId, getAppStoreDefinition, isToolConnectionAttentionHealth, @@ -28,7 +27,6 @@ import { import { useNavigate } from "@/lib/router"; import { useChatConnectorsEnabled } from "@/hooks/useChatConnectorsEnabled"; import { useMemoryConnectorsEnabled } from "@/hooks/useMemoryConnectorsEnabled"; -import { useMcpAggregatorsEnabled } from "@/hooks/useMcpAggregatorsEnabled"; import { appCopyFor } from "@/lib/app-gallery-copy"; import { useCompany } from "@/context/CompanyContext"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; @@ -284,7 +282,6 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne const { pushToast } = useToast(); const { selectedCompanyId } = useCompany(); const { enabled: chatConnectorsEnabled } = useChatConnectorsEnabled(); - const { enabled: mcpAggregatorsEnabled } = useMcpAggregatorsEnabled(); const { enabled: memoryConnectorsEnabled } = useMemoryConnectorsEnabled(); const { setBreadcrumbs } = useBreadcrumbs(); const [query, setQuery] = useState(""); @@ -368,7 +365,6 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne (galleryQuery.data?.apps ?? []) as AppGalleryDisplayEntry[] ).filter((entry) => { if (!memoryConnectorsEnabled && isMemoryConnectorId(appDefinitionSlug(entry))) return false; - if (!mcpAggregatorsEnabled && isRemoteMcpConnectorId(appDefinitionSlug(entry))) return false; const definition = getAppStoreDefinition(appDefinitionSlug(entry)); return ( chatConnectorsEnabled ||