mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Connections give agents governed access to external services. > - Native connections should remain the first choice for a supported app. > - Other apps may be available through an external MCP provider. > - The user must know which external provider handles the connection and choose it before setup. > - This pull request adds ranked alternatives and server-authored instructions to connection search. > - Agents can follow the returned instructions while Paperclip validates saved choices and access. ## Linked Issues or Issue Description Related: #13879, which fixed inline MCP provider setup. This PR adds discovery and provider selection on top of that work. **Subsystem affected** Cross-cutting: shared connection contracts, server search and intent services, native runtime, CLI, inline setup UI, and evals. **Problem or motivation** An agent cannot offer a clear external-provider choice when Paperclip has no native connection for an app. Adding provider-specific branches to the core prompt would make those instructions harder to maintain. **Proposed solution** Prefer a native connection. Otherwise return verified alternatives in Composio, Arcade, Executor, Zapier order. Include an external-service disclosure, a question with None, and the next instruction in the search result. Validate the saved human choice before creating a selected fallback setup card. Reuse existing provider accounts and verify underlying app access separately. **Alternatives considered** Do not silently choose a provider. Do not claim that broad execution tools prove support for every app. Reuse existing questions and connection intents rather than add another connection model. **Roadmap alignment** Extends the existing MCP Tool Gateway & Apps and Agent evals & feedback capabilities. The MCP aggregators experiment remains the gate. No duplicate provider-routing PR was found in the public search. ## What Changed - Add a dated support index and authorized cached-tool evidence for external routes. - Return provider questions and next-step instructions from `connections_search`. - Preserve pending choices and declines across continuation. Validate company, task, agent, human, app, and current route eligibility. - Carry the selected app into new setup and account reuse, validate explicit provider requests against persisted human messages, and distinguish provider readiness from app authorization. - Sync native, MCP, REST, and CLI contracts. Keep core agent instructions provider-neutral. - Add production-component Storybooks, focused database tests, and three real-agent browser eval cases. - Record the plan, observed failures, fixes, passing evidence, and acceptance limits. ## Verification - Latest head `586f0e6cd`: 54 checks passed, 2 skipped; Greptile 5/5 and all review threads resolved. - After rebasing on master `18dac1e1e`: 64 focused shared, validator, route-contract, and database tests passed; server typecheck passed. - Embedded-browser test drive on the rebased head: native Jira card, HubSpot external-provider question, Arcade account reuse, one actual MCP read against a local synthetic fixture, reload persistence, and None preventing further calls. A real OpenAI-backed agent performed discovery and continuation. - UX observation: the agent initially combined mutually exclusive request fields; the server rejected it and the agent recovered without changing access. This extra retry remains visible in the transcript. - After rebase: 23 focused eval grader/catalog tests, affected TypeScript checks, token gates, production UI build, and Storybook build passed. Full local tests are intentionally excluded at the maintainer's request. - Before rebase: four browser/real-agent attempts passed: native Jira, None, and reuse of the second provider on two Codex profiles. - Browser evals used an isolated deterministic MCP fixture through the real Paperclip gateway. They do not prove production compatibility with all four providers. - Review `Apps / Connections / Provider choice` in Storybook. Choose Arcade, continue through Access, and verify the app name, external-service disclosure, and URL configuration. - The detailed verification report is `doc/connections/2026-09-23-aggregator-routing-verification.md`. ## Risks - The public support index is finite and can age. Account capability and app authorization still require verification after selection. - Existing installed-tool permissions remain in effect. Provider choice is not a new execution permission boundary. An early Mini attempt skipped search; clearer provider-neutral instructions made the targeted rerun pass. This is not a measured reliability rate. - Explicit requests skip provider confirmation only when a clear persisted human message or saved provider choice supports them. Other phrasing falls back to confirmation; the agent query alone is not consent. These routes do not add tool permissions. - No database migration or legacy Composio broker is added. Real-provider acceptance remains separate from fixture proof. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, code execution, and browser tools. The exact deployment variant and context-window size are not exposed in this session. Product evals separately used the repository's primary Codex and Codex Mini profiles; those agents supplied test behavior, not independent provider compatibility proof. ## 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>
302 lines
8.6 KiB
TypeScript
302 lines
8.6 KiB
TypeScript
import type {
|
|
AskUserQuestionsQuestion,
|
|
ConnectionSearchResultItem,
|
|
} from "./types/index.js";
|
|
import {
|
|
isRemoteMcpConnectorId,
|
|
type RemoteMcpConnectorId,
|
|
} from "./remote-mcp-connectors.js";
|
|
|
|
export const AGGREGATOR_PRIORITY = [
|
|
"composio",
|
|
"arcade",
|
|
"executor",
|
|
"zapier",
|
|
] as const;
|
|
export const AGGREGATOR_NAMES: Record<RemoteMcpConnectorId, string> = {
|
|
composio: "Composio",
|
|
arcade: "Arcade",
|
|
executor: "Executor",
|
|
zapier: "Zapier",
|
|
};
|
|
|
|
/** Reviewed public catalog snapshot, not a claim that an account has authorized an app.
|
|
* Add only services confirmed in the cited official catalog. No provider credentials.
|
|
* Executor has no universal app catalog: use the authorized workspace's indexed tools.
|
|
*/
|
|
export const AGGREGATOR_SUPPORT_INDEX = [
|
|
{
|
|
slug: "hubspot",
|
|
name: "HubSpot",
|
|
aliases: ["hub spot"],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "salesforce",
|
|
name: "Salesforce",
|
|
aliases: [],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "ashby",
|
|
name: "Ashby",
|
|
aliases: [],
|
|
providers: { composio: "2026-09-23", arcade: "2026-09-23" },
|
|
},
|
|
{
|
|
slug: "pipedrive",
|
|
name: "Pipedrive",
|
|
aliases: [],
|
|
providers: { composio: "2026-09-23", zapier: "2026-09-23" },
|
|
},
|
|
{
|
|
slug: "gong",
|
|
name: "Gong",
|
|
aliases: [],
|
|
providers: { composio: "2026-09-23" },
|
|
},
|
|
{
|
|
slug: "outlook",
|
|
name: "Outlook",
|
|
aliases: ["microsoft outlook"],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "microsoft-teams",
|
|
name: "Microsoft Teams",
|
|
aliases: ["teams"],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "firecrawl",
|
|
name: "Firecrawl",
|
|
aliases: [],
|
|
providers: { composio: "2026-09-23", arcade: "2026-09-23" },
|
|
},
|
|
{
|
|
slug: "tavily",
|
|
name: "Tavily",
|
|
aliases: [],
|
|
providers: { composio: "2026-09-23", arcade: "2026-09-23" },
|
|
},
|
|
{
|
|
slug: "exa",
|
|
name: "Exa",
|
|
aliases: [],
|
|
providers: { composio: "2026-09-23", arcade: "2026-09-23" },
|
|
},
|
|
{
|
|
slug: "jira",
|
|
name: "Jira",
|
|
aliases: ["atlassian jira"],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "notion",
|
|
name: "Notion",
|
|
aliases: [],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "github",
|
|
name: "GitHub",
|
|
aliases: ["git hub"],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "gmail",
|
|
name: "Gmail",
|
|
aliases: ["google mail"],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "slack",
|
|
name: "Slack",
|
|
aliases: [],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "airtable",
|
|
name: "Airtable",
|
|
aliases: [],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "asana",
|
|
name: "Asana",
|
|
aliases: [],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
{
|
|
slug: "linear",
|
|
name: "Linear",
|
|
aliases: [],
|
|
providers: {
|
|
composio: "2026-09-23",
|
|
arcade: "2026-09-23",
|
|
zapier: "2026-09-23",
|
|
},
|
|
},
|
|
] as const;
|
|
export const AGGREGATOR_CATALOG_SOURCES: Partial<
|
|
Record<RemoteMcpConnectorId, string>
|
|
> = {
|
|
composio: "https://composio.dev/toolkits",
|
|
arcade: "https://docs.arcade.dev/en/resources/integrations",
|
|
zapier: "https://zapier.com/apps",
|
|
};
|
|
|
|
export function normalizeConnectionQuery(value: string): string {
|
|
return value
|
|
.toLocaleLowerCase()
|
|
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
.trim();
|
|
}
|
|
|
|
export function findAggregatorService(query: string) {
|
|
const normalized = normalizeConnectionQuery(query);
|
|
const exact = AGGREGATOR_SUPPORT_INDEX.find((entry) =>
|
|
[entry.slug, entry.name, ...entry.aliases].some(
|
|
(name) => normalizeConnectionQuery(name) === normalized,
|
|
),
|
|
);
|
|
if (exact) return exact;
|
|
// Agents often include the desired capability ("HubSpot recent contacts").
|
|
// Whole phrases avoid substring guesses; multiple named services need clarification.
|
|
const matches = AGGREGATOR_SUPPORT_INDEX.filter((entry) =>
|
|
[entry.slug, entry.name, ...entry.aliases].some((name) =>
|
|
` ${normalized} `.includes(` ${normalizeConnectionQuery(name)} `),
|
|
),
|
|
);
|
|
return matches.length === 1 ? matches[0] : undefined;
|
|
}
|
|
|
|
/** Preserve a provider explicitly named by the user instead of applying default ranking. */
|
|
export function explicitAggregatorQuery(query: string) {
|
|
// A trailing exclusion is not a second proposed provider. Keep this grammar
|
|
// narrow; alternatives and contradictory choices still require confirmation.
|
|
const exclusion = query.match(/\s*,?\s+not\s+(composio|arcade|executor|zapier)[.!]?\s*$/i);
|
|
if (exclusion) query = query.slice(0, exclusion.index).trim();
|
|
const matches = [
|
|
...query.matchAll(
|
|
/\b(?:through|via|using)\s+(composio|arcade|executor|zapier)\b/gi,
|
|
),
|
|
];
|
|
const namedProviders = new Set(
|
|
(query.match(/\b(?:composio|arcade|executor|zapier)\b/gi) ?? []).map(
|
|
(name) => name.toLowerCase(),
|
|
),
|
|
);
|
|
if (matches.length !== 1 || namedProviders.size !== 1) return null;
|
|
const match = matches[0]!;
|
|
if (exclusion?.[1]?.toLowerCase() === match[1]!.toLowerCase()) return null;
|
|
return {
|
|
provider: match[1]!.toLowerCase() as RemoteMcpConnectorId,
|
|
serviceQuery: (
|
|
query.slice(0, match.index) + query.slice(match.index! + match[0].length)
|
|
).trim(),
|
|
};
|
|
}
|
|
|
|
export function parseAggregatorRoute(service: string) {
|
|
const parts = service.split(":");
|
|
if (
|
|
parts.length !== 3 ||
|
|
parts[0] !== "via" ||
|
|
!isRemoteMcpConnectorId(parts[1]) ||
|
|
!/^[a-z0-9][a-z0-9-]{0,79}$/.test(parts[2]!)
|
|
)
|
|
return null;
|
|
return { provider: parts[1], targetService: parts[2]! };
|
|
}
|
|
|
|
export function aggregatorProviderQuestion(
|
|
targetService: string,
|
|
targetName: string,
|
|
results: ConnectionSearchResultItem[],
|
|
): AskUserQuestionsQuestion {
|
|
return {
|
|
id: `connection-provider:${targetService}`,
|
|
// Keep required disclosure in the prompt: all question transports preserve it.
|
|
prompt: `Connect ${targetName} through an external service? These services handle the connection and requests to ${targetName}. Connecting a provider does not yet authorize ${targetName}.`,
|
|
selectionMode: "single",
|
|
required: true,
|
|
allowOther: false,
|
|
options: [
|
|
...results.map((result, index) => ({
|
|
id: result.service,
|
|
label: `${AGGREGATOR_NAMES[result.aggregator!.provider]}${index === 0 ? " — Recommended" : ""}`,
|
|
description:
|
|
result.reason ?? "Verify the app in this external provider.",
|
|
})),
|
|
{
|
|
id: "none",
|
|
label: "None for now",
|
|
description: "Do not connect through an external service.",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function aggregatorContinuationInstruction(
|
|
provider: RemoteMcpConnectorId,
|
|
targetName: string,
|
|
) {
|
|
const name = AGGREGATOR_NAMES[provider];
|
|
const discovery = {
|
|
composio:
|
|
"Use COMPOSIO_SEARCH_TOOLS to verify the requested app and capability, then COMPOSIO_MANAGE_CONNECTIONS if app authorization is needed.",
|
|
arcade:
|
|
"Inspect the gateway tools for the requested app. If absent, ask the user to add the app's tools in the Arcade gateway and refresh the connection. Follow tool authorization links when required.",
|
|
executor:
|
|
"Inspect the Executor workspace's actual integrations using its documented discovery tools. Do not assume arbitrary apps exist or register integrations merely to search. Follow provider authorization and resume instructions when needed.",
|
|
zapier:
|
|
"In Agentic mode, use discover_zapier_actions and the documented app connection flow. In Managed mode, ask the user to add the required actions in Zapier and refresh the catalog.",
|
|
}[provider];
|
|
return `${name} is an external service. Its connection is available to this agent, but ${targetName} access is not yet verified. ${discovery} Use only the requested capability and effective permissions. If support is absent, explain that result and ask before switching providers. Claim success only after verifying the underlying app; never repeat an uncertain write.`;
|
|
}
|