diff --git a/doc/connections/CONNECTOR-PERMISSION-AUDIT.md b/doc/connections/CONNECTOR-PERMISSION-AUDIT.md index 8b41d07f228..57f72332831 100644 --- a/doc/connections/CONNECTOR-PERMISSION-AUDIT.md +++ b/doc/connections/CONNECTOR-PERMISSION-AUDIT.md @@ -8,7 +8,8 @@ separate contracts and were not changed. Methods reviewed after this audit carry their own `reviewedAt` date in the same ledger; the counts above are not restated. Later additions: [Neon](./NEON.md) -(`mcp-oauth`, `mcp-api-key`; 2026-10-02). +(`mcp-oauth`, `mcp-api-key`; 2026-10-02), [Superagent](./SUPERAGENT.md) +(`mcp-api-key`; 2026-10-06). ## Shared credential failure diff --git a/doc/connections/README.md b/doc/connections/README.md index 8046e3d8e4c..2b7f9b33878 100644 --- a/doc/connections/README.md +++ b/doc/connections/README.md @@ -13,7 +13,7 @@ Runtime authentication: [AI Connections](./AI-CONNECTIONS.md). Long-term memory: [Experimental memory connectors](./MEMORY.md). Provider notes: [Google Workspace](./GOOGLE-WORKSPACE.md), -[Gmail](./GMAIL.md), [Asana](./ASANA.md), [PostHog](./POSTHOG.md), [Neon](./NEON.md), +[Gmail](./GMAIL.md), [Asana](./ASANA.md), [PostHog](./POSTHOG.md), [Neon](./NEON.md), [Superagent](./SUPERAGENT.md), [AgentMail](./AGENTMAIL.md), [iMessage Photon](./IMESSAGE-PHOTON.md), and [Enterpret](./ENTERPRET.md). Optional credential custody: [Vercel Connect](./VERCEL-CONNECT.md). diff --git a/doc/connections/SUPERAGENT.md b/doc/connections/SUPERAGENT.md new file mode 100644 index 00000000000..4353bd5d34a --- /dev/null +++ b/doc/connections/SUPERAGENT.md @@ -0,0 +1,162 @@ +# Superagent + +Updated: 2026-10-06. Status: catalog definition reviewed against official +documentation and live provider metadata; live account qualification +outstanding. + +Superagent (`superagent.sh`) is a security platform: PR security scans, +dependency updates, red-team reports, context guardrails that score content +before agents consume it, and runtime guardrails for coding agents. It appears +in Apps and uses Paperclip's shared remote-MCP connection, vault, catalog, +grants, policies, gateway, and audit trail. It is a resource connection, not +Paperclip sign-in. No plugin or database migration is required. The only +provider-specific runtime code is a reviewed risk-classification rule. + +Paperclip connects to Superagent's hosted MCP server at +`https://www.superagent.sh/mcp` (Streamable HTTP) with one method: an +organization API key stored as a Paperclip secret and sent as an +`Authorization: Bearer sk_live_...` header. + +The `www` host is pinned deliberately. The bare `superagent.sh` domain +redirects to `www`, and Superagent documents that some MCP clients fail on +that redirect for POST requests. + +This curated connection is the polished route: it provides branding, key +guidance, and a billable/destructive-tool warning. None of it is *required* to +reach Superagent's server. It can also be connected generically from +**Connect your own MCP server** with the same URL and header. See +[Connecting any remote MCP server](./GENERIC-REMOTE-MCP.md). + +## Service involvement + +Superagent hosts the MCP resource. Paperclip stores the key as a secret +reference, discovers tools with `tools/list`, and sends the key on every +governed call. No Paperclip-operated vendor relay is involved; cloud and +self-hosted instances use the same path. + +## Administrator setup + +1. In Superagent, open **Settings → API keys** (`/app/settings#api-keys`) and + create a key for Paperclip. Use a separate key so it can be revoked + independently. +2. In Paperclip, open **Apps → Browse → Superagent**, paste the key, and click + **Connect**. +3. On the connection's **Permissions** screen, set the billable and + destructive actions listed below to **Ask first** or **Off** if agents run + unattended. + +Revoke the key in Superagent, then remove the connection in Paperclip, to +disconnect. + +## Capabilities and policy + +The server exposes roughly 70 tools that mirror the Superagent REST API: +findings and triage, red-team reports, Contributor Trust scans, dependency +update runs, context-guardrail scoring (web pages, email, messages, files, +skills, MCP repositories, packages), Applications, Agents and endpoint +clients, runtime guardrail groups, rules and alerts, OpenTelemetry +destinations, and the structured agent report return path. + +Superagent's documentation calls out these tools: + +- Billable: `create_repository_report`, `create_web_app_report`, + `create_agent_report`, `create_package_report`, `triage_finding`. +- Permanent deletes: `delete_finding`, `delete_telemetry_endpoint`. The + `delete_agent_group` and `delete_agent_rule` tools also delete. +- Immediate endpoint-policy changes: `set_agent_builtin_rule_mode`, + `restore_agent_builtin_rule`. + +Paperclip applies a reviewed Superagent risk rule in `classifyRisk`, because +several mutations use names the generic classifier reads as reads +(`triage_finding`, `scan_*`, `restore_agent_builtin_rule`). Only `list_*` and +`get_*` tools, and tools Superagent marks `readOnlyHint: true`, are reads. +`delete_*` and `revoke_agent_client` are destructive. Everything else is a +write. The API-key helper text tells operators to put billable and +destructive tools behind Ask first. + +## Vendor + +- Product: Superagent, `https://www.superagent.sh`. +- MCP documentation: `https://www.superagent.sh/docs/mcp`. +- API keys: `https://www.superagent.sh/docs/reference/settings`. +- Rate limits: per key per minute; HTTP 429 with `Retry-After`. + +## Transport and auth + +- Transport: `mcp_remote`, Streamable HTTP. +- Auth: `api_key`, `Authorization: Bearer `. Keys use the `sk_live_` + prefix. +- Key permissions: not scoped. A key reaches everything in its organization. + Paperclip cannot narrow an issued key. +- OAuth: not offered. The server returns + `WWW-Authenticate: Bearer ... resource_metadata="https://www.superagent.sh/.well-known/oauth-protected-resource"`, + and that document names `https://superagent.sh` as its authorization + server, but neither `/.well-known/oauth-authorization-server` nor + `/.well-known/openid-configuration` exists there, so browser sign-in + cannot be discovered. Add an OAuth method if Superagent publishes + authorization-server metadata. + +## Resource filters + +None. Superagent documents no query or header options that narrow the hosted +server. Narrowing is done with per-action policies. + +## Manifest + +| Field | Value | +| --- | --- | +| Slug | `superagent` | +| Category | `developer` | +| Method | `mcp-api-key` (`customer` ownership, risk tier S4) | +| Server URL | `https://www.superagent.sh/mcp` | +| Credential | `authorization`, password, required, placeholder `sk_live_...` | +| Key placement | header `Authorization`, prefix `Bearer ` | +| Console links | keys `https://www.superagent.sh/app/settings#api-keys`, docs `https://www.superagent.sh/docs/mcp` | + +The definition is generated from the `superagent` row in +`packages/shared/src/self-serve-mcp-research.json` and the `superagent` branch +of `specialMethodsFor` in `scripts/ingest-app-definitions.mjs`. + +## Wizard path + +`/apps/connect?source=superagent` opens a single-method form: the API key +field and **Connect**, which stays disabled until a key is entered. A +successful key check and tool discovery lead to the connection's +Permissions screen. + +## Governance defaults + +- Default profile and bindings: the standard connection profile; every + discovered action starts Allowed under the current product default. +- Policies: operators narrow billable and destructive tools to Ask first or + Off on the Permissions screen. +- Risk: the reviewed Superagent rule in `classifyRisk` described above. +- Quarantine rules: the shared defaults; no Superagent-specific exceptions. + +## Brand provenance + +Superagent's site publishes its pyramid mark only as 32×32 WebP favicons +(light and dark), below the catalog's 128px minimum. The same mark is the +avatar of Superagent's official GitHub organization (`superagent-ai`, whose +profile links `https://superagent.sh`), published as a 460×460 transparent +PNG. That file is used unchanged in both themes. + +| File | Source | SHA-256 | +| --- | --- | --- | +| `ui/public/brands/apps/superagent.png` (460×460) | `https://avatars.githubusercontent.com/u/152537519?v=4&s=512` | `2c731c7a4cdaabe2ed341b141dc75608e23b362b9a6e5ee81d1c7a373154fbcf` | + +## Validation hook + +- Environment: definition review on 2026-10-06 against `origin/master`; no + Superagent account was used. +- Metadata probe: an unauthenticated `initialize` on `/mcp` returned HTTP 401 + with the `resource_metadata` header above; the protected-resource document + returned HTTP 200; no authorization-server metadata was found. +- Deterministic tests: manifest shape, store visibility, artwork, URL + recognition, bearer-header projection with the key kept out of connection + config, risk classification of read, write and destructive fixture tools, + and the connect form's API-key gating. +- Connect evidence, catalog evidence, allowed read, governed write, denied + case, revoke, audit: not run. The API-key method still needs the full live + lifecycle with a Superagent organization before this connection is + considered qualified. diff --git a/doc/connections/tool-method-permission-reviews.json b/doc/connections/tool-method-permission-reviews.json index 9d6f6cae72b..2c96d442544 100644 --- a/doc/connections/tool-method-permission-reviews.json +++ b/doc/connections/tool-method-permission-reviews.json @@ -2005,6 +2005,23 @@ "liveProof": "not-run", "keyPermissions": "Project/database/environment/storage changes and Edge Function deployment. Project selection and optional read-only configuration remain authoritative. Create a key with read and write permissions for these actions; Paperclip cannot increase an existing key\u2019s permissions." }, + { + "app": "superagent", + "method": "mcp-api-key", + "auth": "api_key", + "policy": "provider-key", + "requestedScopes": [], + "capability": "provider-controlled", + "supportedActions": "List, read, triage and permanently delete findings; start billable repository, application, agent and package red-team reports; run Contributor Trust scans and dependency update runs; score web pages, email, messages, files, skills, MCP repositories and packages; manage Applications, Agents, endpoint clients, groups, runtime guardrail rules and alerts; manage OpenTelemetry destinations; return structured agent reports.", + "evidence": [ + "https://www.superagent.sh/docs/mcp", + "https://www.superagent.sh/docs/reference/settings", + "https://www.superagent.sh/.well-known/oauth-protected-resource" + ], + "reviewedAt": "2026-10-06", + "liveProof": "not-run", + "keyPermissions": "Superagent organization keys are not scoped: a key reaches everything in its organization, including permanently deleting findings and starting billable report and triage work. Create a separate key for Paperclip so it can be revoked independently, and set billable and destructive actions to Ask first before agents run unattended." + }, { "app": "supermemory", "method": "mcp-oauth", diff --git a/packages/shared/src/app-definitions-url.test.ts b/packages/shared/src/app-definitions-url.test.ts index eec26f63dd3..153a509f844 100644 --- a/packages/shared/src/app-definitions-url.test.ts +++ b/packages/shared/src/app-definitions-url.test.ts @@ -12,6 +12,7 @@ describe("tool app gallery URL matching", () => { expect(getAppDefinitionForUrl("https://docs.google.com/spreadsheets/d/sheet_123/edit")?.slug).toBe("google-sheets"); expect(getAppDefinitionForUrl("https://gmailmcp.googleapis.com/mcp/v1")?.slug).toBe("gmail"); expect(getAppDefinitionForUrl("https://mcp.neon.tech/mcp")?.slug).toBe("neon"); + expect(getAppDefinitionForUrl("https://www.superagent.sh/mcp")?.slug).toBe("superagent"); }); it("returns null for invalid or unknown links", () => { diff --git a/packages/shared/src/app-definitions.generated.ts b/packages/shared/src/app-definitions.generated.ts index e29716053dd..6e729faa890 100644 --- a/packages/shared/src/app-definitions.generated.ts +++ b/packages/shared/src/app-definitions.generated.ts @@ -69,23 +69,24 @@ import a67 from "./app-definitions/zep.json" with { type: "json" }; import a68 from "./app-definitions/supermemory.json" with { type: "json" }; import a69 from "./app-definitions/honcho.json" with { type: "json" }; import a70 from "./app-definitions/neon.json" with { type: "json" }; -import a71 from "./app-definitions/gmail.json" with { type: "json" }; -import a72 from "./app-definitions/google-drive.json" with { type: "json" }; -import a73 from "./app-definitions/google-docs.json" with { type: "json" }; -import a74 from "./app-definitions/google-sheets.json" with { type: "json" }; -import a75 from "./app-definitions/google-slides.json" with { type: "json" }; -import a76 from "./app-definitions/google-calendar.json" with { type: "json" }; -import a77 from "./app-definitions/google-chat.json" with { type: "json" }; -import a78 from "./app-definitions/google-people.json" with { type: "json" }; -import a79 from "./app-definitions/google-workspace-search.json" with { type: "json" }; -import a80 from "./app-definitions/openai.json" with { type: "json" }; -import a81 from "./app-definitions/openrouter.json" with { type: "json" }; -import a82 from "./app-definitions/xai.json" with { type: "json" }; -import a83 from "./app-definitions/google.json" with { type: "json" }; -import a84 from "./app-definitions/bedrock.json" with { type: "json" }; -import a85 from "./app-definitions/responses-api.json" with { type: "json" }; -import a86 from "./app-definitions/messages-api.json" with { type: "json" }; -import a87 from "./app-definitions/chat-completions-api.json" with { type: "json" }; -import a88 from "./app-definitions/local.json" with { type: "json" }; +import a71 from "./app-definitions/superagent.json" with { type: "json" }; +import a72 from "./app-definitions/gmail.json" with { type: "json" }; +import a73 from "./app-definitions/google-drive.json" with { type: "json" }; +import a74 from "./app-definitions/google-docs.json" with { type: "json" }; +import a75 from "./app-definitions/google-sheets.json" with { type: "json" }; +import a76 from "./app-definitions/google-slides.json" with { type: "json" }; +import a77 from "./app-definitions/google-calendar.json" with { type: "json" }; +import a78 from "./app-definitions/google-chat.json" with { type: "json" }; +import a79 from "./app-definitions/google-people.json" with { type: "json" }; +import a80 from "./app-definitions/google-workspace-search.json" with { type: "json" }; +import a81 from "./app-definitions/openai.json" with { type: "json" }; +import a82 from "./app-definitions/openrouter.json" with { type: "json" }; +import a83 from "./app-definitions/xai.json" with { type: "json" }; +import a84 from "./app-definitions/google.json" with { type: "json" }; +import a85 from "./app-definitions/bedrock.json" with { type: "json" }; +import a86 from "./app-definitions/responses-api.json" with { type: "json" }; +import a87 from "./app-definitions/messages-api.json" with { type: "json" }; +import a88 from "./app-definitions/chat-completions-api.json" with { type: "json" }; +import a89 from "./app-definitions/local.json" with { type: "json" }; import type { AppDefinition } from "./types/app-definition.js"; -export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80,a81,a82,a83,a84,a85,a86,a87,a88] as AppDefinition[]; +export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80,a81,a82,a83,a84,a85,a86,a87,a88,a89] as AppDefinition[]; diff --git a/packages/shared/src/app-definitions.test.ts b/packages/shared/src/app-definitions.test.ts index cff22a088fa..24ac23bdb49 100644 --- a/packages/shared/src/app-definitions.test.ts +++ b/packages/shared/src/app-definitions.test.ts @@ -273,7 +273,7 @@ describe("AppDefinition catalog", () => { "google-workspace-search", ]), ); - expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(50); + expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(51); expect(BLOCKED_MCP_PROVIDERS.map((entry) => entry.slug)).toEqual([ "g2", "vercel", @@ -437,15 +437,15 @@ describe("AppDefinition catalog", () => { expect(channel("slack")?.guidanceMd).toContain("reactions"); expect(channel("slack")?.guidanceMd).toContain("direct messages"); }); - it("keeps a complete, unique, dated evidence ledger for all 53 researched MCP providers", () => { + it("keeps a complete, unique, dated evidence ledger for all 54 researched MCP providers", () => { // Ledger-wide date reflects the last full re-verification (2026-08-26); // later provider additions carry their own research evidence, but // bumping the shared date would overstate freshness for the other providers. expect(SELF_SERVE_MCP_RESEARCH.verifiedAt).toBe("2026-08-26"); - expect(SELF_SERVE_MCP_RESEARCH.entries).toHaveLength(53); + expect(SELF_SERVE_MCP_RESEARCH.entries).toHaveLength(54); expect( new Set(SELF_SERVE_MCP_RESEARCH.entries.map((entry) => entry.slug)), - ).toHaveProperty("size", 53); + ).toHaveProperty("size", 54); for (const entry of SELF_SERVE_MCP_RESEARCH.entries) { expect(new URL(entry.docsUrl).protocol).toBe("https:"); expect(new URL(entry.serverUrl).protocol).toBe("https:"); @@ -817,7 +817,7 @@ describe("AppDefinition catalog", () => { "ticktick", "xero", ]); - expect(APP_STORE_DEFINITIONS).toHaveLength(66); + expect(APP_STORE_DEFINITIONS).toHaveLength(67); const connectableSlugs = new Set( CONNECTABLE_APP_DEFINITIONS.map((entry) => entry.slug), ); @@ -1102,6 +1102,56 @@ describe("AppDefinition catalog", () => { expect(method.warnings?.length).toBe(2); } }); + it("connects Superagent's hosted server with an organization API key only", () => { + const superagent = APP_DEFINITIONS.find((app) => app.slug === "superagent")!; + expect(superagent).toMatchObject({ + name: "Superagent", + categories: ["developer"], + urlPatterns: ["https://www.superagent.sh/*"], + docsUrl: "https://www.superagent.sh/docs/mcp", + branding: { logoUrl: "/brands/apps/superagent.png" }, + }); + // The hosted server publishes no OAuth authorization-server metadata, so + // there is no browser sign-in method and no OAuth redirect constraint. + expect(superagent.redirectConstraints).toBeUndefined(); + expect(superagent.branding.darkLogoUrl).toBeUndefined(); + expect(APP_STORE_DEFINITIONS.some((app) => app.slug === "superagent")).toBe(true); + expect(superagent.methods).toHaveLength(1); + const [apiKey] = superagent.methods; + expect(apiKey).toMatchObject({ + key: "mcp-api-key", + transport: "mcp_remote", + auth: "api_key", + ownershipModes: ["customer"], + riskTier: "S4", + // The bare superagent.sh domain redirects, and some clients drop POST + // bodies on redirect, so the www host is pinned. + defaults: { serverUrl: "https://www.superagent.sh/mcp" }, + keyPlacement: { + location: "header", + name: "Authorization", + prefix: "Bearer ", + }, + consoleLinks: { + keys: "https://www.superagent.sh/app/settings#api-keys", + docs: "https://www.superagent.sh/docs/mcp", + }, + }); + expect(apiKey!.tenantFields ?? []).toEqual([]); + expect(apiKey!.credentialFields).toEqual([ + expect.objectContaining({ + key: "authorization", + type: "password", + placeholder: "sk_live_...", + secret: true, + required: true, + }), + ]); + expect(apiKey!.credentialFields?.[0]?.helperMd).toContain("not scoped"); + expect(apiKey!.warnings).toEqual([ + expect.stringContaining("consume organization credits"), + ]); + }); it("requires only reviewed provider or safety-boundary configuration on the default path", () => { const required = APP_DEFINITIONS.flatMap((app) => app.methods.flatMap((method) => diff --git a/packages/shared/src/app-definitions/superagent.json b/packages/shared/src/app-definitions/superagent.json new file mode 100644 index 00000000000..6e1a847cc0c --- /dev/null +++ b/packages/shared/src/app-definitions/superagent.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 1, + "slug": "superagent", + "name": "Superagent", + "description": "Review security findings, start red-team reports, and score content and packages before agents trust them.", + "categories": [ + "developer" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/superagent.png" + }, + "urlPatterns": [ + "https://www.superagent.sh/*" + ], + "docsUrl": "https://www.superagent.sh/docs/mcp", + "methods": [ + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Connect with a Superagent organization API key.", + "defaults": { + "serverUrl": "https://www.superagent.sh/mcp" + }, + "guidanceMd": "Open Superagent Settings → API keys, create a separate key for Paperclip, and paste it below.", + "riskTier": "S4", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Superagent API key", + "type": "password", + "required": true, + "placeholder": "sk_live_...", + "secret": true, + "helperMd": "Superagent organization keys are not scoped: a key reaches everything in its organization, including permanently deleting findings and starting billable report and triage work. Create a separate key for Paperclip so it can be revoked independently, and set billable and destructive actions to Ask first before agents run unattended." + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://www.superagent.sh/app/settings#api-keys", + "docs": "https://www.superagent.sh/docs/mcp" + }, + "warnings": [ + "Superagent report and triage tools consume organization credits, and deleting a finding is permanent. Set those actions to Ask first before agents run unattended." + ] + } + ] +} diff --git a/packages/shared/src/self-serve-mcp-research.json b/packages/shared/src/self-serve-mcp-research.json index ff278480b80..a60fefce38a 100644 --- a/packages/shared/src/self-serve-mcp-research.json +++ b/packages/shared/src/self-serve-mcp-research.json @@ -58,6 +58,7 @@ {"slug": "zep", "name": "Zep", "wave": 3, "status": "self_serve", "docsUrl": "https://help.getzep.com/memory-mcp-server", "serverUrl": "https://api.getzep.com/mcp", "authMode": "dcr_cimd", "prerequisite": "A Zep project with Memory MCP enabled, available MCP seats, and Google Workspace or enterprise OIDC configured by its administrator. Project API keys do not authenticate Memory MCP.", "riskTier": "S3"}, {"slug": "supermemory", "name": "Supermemory", "wave": 3, "status": "self_serve", "docsUrl": "https://supermemory.ai/docs/supermemory-mcp/mcp", "serverUrl": "https://mcp.supermemory.ai/mcp", "authMode": "dcr", "prerequisite": "Sign in to Supermemory and select the spaces this connection may access. The hosted MCP uses OAuth, not a developer API key.", "riskTier": "S3"}, {"slug": "honcho", "name": "Honcho", "wave": 3, "status": "self_serve", "docsUrl": "https://honcho.dev/docs/v3/guides/integrations/mcp", "serverUrl": "https://mcp.honcho.dev", "authMode": "api_key", "prerequisite": "An API key from the Honcho dashboard. Memory is stored in your Honcho account; select workspace and peer identifiers when calling tools.", "riskTier": "S3"}, - { "slug": "neon", "name": "Neon", "wave": 4, "status": "self_serve", "docsUrl": "https://neon.com/docs/ai/neon-mcp-server", "serverUrl": "https://mcp.neon.tech/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Neon account. The hosted server grants broad project and database management, so use a development project and review write actions before connecting production data.", "riskTier": "S4" } + { "slug": "neon", "name": "Neon", "wave": 4, "status": "self_serve", "docsUrl": "https://neon.com/docs/ai/neon-mcp-server", "serverUrl": "https://mcp.neon.tech/mcp", "authMode": "dcr_or_api_key", "prerequisite": "A Neon account. The hosted server grants broad project and database management, so use a development project and review write actions before connecting production data.", "riskTier": "S4" }, + { "slug": "superagent", "name": "Superagent", "wave": 4, "status": "self_serve", "docsUrl": "https://www.superagent.sh/docs/mcp", "serverUrl": "https://www.superagent.sh/mcp", "authMode": "api_key", "prerequisite": "Superagent report and triage tools consume organization credits, and deleting a finding is permanent. Set those actions to Ask first before agents run unattended.", "riskTier": "S4" } ] } diff --git a/scripts/ingest-app-definitions.mjs b/scripts/ingest-app-definitions.mjs index 65cff958116..ec81b7ea61e 100644 --- a/scripts/ingest-app-definitions.mjs +++ b/scripts/ingest-app-definitions.mjs @@ -1043,6 +1043,7 @@ const categoryBySlug = { sentry: "developer", similarweb: "analytics", stripe: "commerce", + superagent: "developer", supabase: "data", "ticket-tailor": "commerce", ticktick: "productivity", @@ -1143,6 +1144,7 @@ const apiKeySpec = { prefix: "Bearer ", placeholder: "sbp_...", }, + superagent: { name: "Authorization", prefix: "Bearer ", placeholder: "sk_live_..." }, youcom: { name: "Authorization", prefix: "Bearer ", @@ -1221,6 +1223,16 @@ const specialMethodsFor = (entry) => { }] } : {}), }), ]; + // Superagent's hosted server advertises protected-resource metadata, but its + // authorization server publishes no OAuth metadata, so organization API keys + // are the only working credential. + if (entry.slug === "superagent") return [ + apiKeyMethodFor(entry, "mcp-api-key", entry.serverUrl, { + whenToUse: "Connect with a Superagent organization API key.", + guidanceMd: "Open Superagent Settings → API keys, create a separate key for Paperclip, and paste it below.", + consoleLinks: { keys: "https://www.superagent.sh/app/settings#api-keys", docs: entry.docsUrl }, + }), + ]; if (entry.slug === "zep") return [oauthMethodFor(entry, "mcp-oauth", entry.serverUrl, { grantKinds: ["user"], defaults: { serverUrl: entry.serverUrl, scopesHint: ["graph:read", "graph:write"] }, @@ -1665,7 +1677,7 @@ for (const entry of researchManifest.entries) { schemaVersion: 1, slug: entry.slug, name: entry.name, - description: ({ neon: "Manage Postgres projects and branches, run SQL, and inspect schemas in Neon.", mem0: "Remember preferences, conversations, events, and agent state.", zep: "Retrieve temporal graph memory and authorized business context.", supermemory: "Search and save shared memories, documents, and profiles.", honcho: "Remember conversations and retrieve context about peers." })[entry.slug] ?? (entry.slug === "fireflies" + description: ({ neon: "Manage Postgres projects and branches, run SQL, and inspect schemas in Neon.", superagent: "Review security findings, start red-team reports, and score content and packages before agents trust them.", mem0: "Remember preferences, conversations, events, and agent state.", zep: "Retrieve temporal graph memory and authorized business context.", supermemory: "Search and save shared memories, documents, and profiles.", honcho: "Remember conversations and retrieve context about peers." })[entry.slug] ?? (entry.slug === "fireflies" ? "Search meeting transcripts, read summaries and action items, and connect meeting-ready routines." : `Connect ${entry.name}'s provider-hosted MCP server.`), categories: [categoryBySlug[entry.slug] ?? "other"], diff --git a/server/src/__tests__/tool-access-service.test.ts b/server/src/__tests__/tool-access-service.test.ts index ac7f1c845d7..21f60559df0 100644 --- a/server/src/__tests__/tool-access-service.test.ts +++ b/server/src/__tests__/tool-access-service.test.ts @@ -5183,7 +5183,7 @@ describeEmbeddedPostgres("tool access service", () => { "local", ]), ); - expect(res.body.apps).toHaveLength(66); + expect(res.body.apps).toHaveLength(67); for (const slug of ["openrouter", "bedrock", "responses-api", "messages-api", "chat-completions-api", "local"]) { expect(res.body.apps.find((app: { slug: string }) => app.slug === slug).tags).toContain("model-provider"); } @@ -6309,6 +6309,52 @@ describeEmbeddedPostgres("tool access service", () => { ).rejects.toMatchObject({ status: 400 }); }); + it("connects Superagent with an organization API key sent as a bearer header", async () => { + const company = await createCompany(db); + const service = createTestToolAccessService(db); + const fetchMock = mockToolsList([ + { name: "list_findings", annotations: { readOnlyHint: true } }, + { name: "create_repository_report" }, + { name: "delete_finding", annotations: { destructiveHint: true } }, + ]); + + const result = await service.connectGalleryApp( + company.id, + { + galleryKey: "superagent", + credentialValues: { "credentials.authorization": "sk_live_test-secret" }, + }, + { actorType: "user", actorId: "board" }, + ); + + expect(fetchMock).toHaveBeenCalledWith( + "https://www.superagent.sh/mcp", + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: "Bearer sk_live_test-secret", + }), + }), + ); + expect(result.connection).toMatchObject({ + authKind: "api_key", + config: { + url: "https://www.superagent.sh/mcp", + sourceTemplateKey: "superagent", + connectionMethodKey: "mcp-api-key", + }, + }); + expect(JSON.stringify(result.connection.config)).not.toContain( + "sk_live_test-secret", + ); + expect(result.catalog).toEqual( + expect.arrayContaining([ + expect.objectContaining({ toolName: "list_findings", riskLevel: "read", status: "active" }), + expect.objectContaining({ toolName: "create_repository_report", riskLevel: "write", status: "active" }), + expect.objectContaining({ toolName: "delete_finding", riskLevel: "destructive", status: "active" }), + ]), + ); + }); + it("projects Neon's optional project pin and read-only mode into the hosted server URL", async () => { const company = await createCompany(db); const service = createTestToolAccessService(db); @@ -19191,6 +19237,30 @@ describe("classifyRisk", () => { expect(classifyRisk({ name: "fireflies_share_meeting", annotations: { destructiveHint: true } }, "fireflies")).toBe("destructive"); }); + it("classifies Superagent mutations whose names read like reads as writes", () => { + for (const name of ["list_findings", "get_finding", "get_context_score", "list_agent_alerts"]) + expect(classifyRisk({ name }, "superagent")).toBe("read"); + for (const name of [ + "triage_finding", + "restore_agent_builtin_rule", + "scan_package", + "discover_application_inventory", + "trigger_dependency_updates", + "test_telemetry_endpoint", + "create_repository_report", + "set_agent_builtin_rule_mode", + ]) + expect(classifyRisk({ name }, "superagent")).toBe("write"); + for (const name of ["delete_finding", "delete_agent_rule", "revoke_agent_client"]) + expect(classifyRisk({ name }, "superagent")).toBe("destructive"); + // A provider write hint wins over a read-looking name; a read-only hint + // opts a tool outside list/get into reads. + expect(classifyRisk({ name: "get_finding", annotations: { readOnlyHint: false } }, "superagent")).toBe("write"); + expect(classifyRisk({ name: "validate_agent_rule", annotations: { readOnlyHint: true } }, "superagent")).toBe("read"); + // Without the provider rule, the generic classifier treats these as reads. + expect(classifyRisk({ name: "triage_finding" })).toBe("read"); + }); + it("classifies Enterpret run_graph_query as write despite readOnlyHint", () => { expect( classifyRisk( diff --git a/server/src/services/tool-access.ts b/server/src/services/tool-access.ts index 24229ef17f4..86475ecf27d 100644 --- a/server/src/services/tool-access.ts +++ b/server/src/services/tool-access.ts @@ -2406,6 +2406,15 @@ export function classifyRisk( return "write"; if (sourceTemplateKey === "posthog" && normalizedToolName === "exec") return "destructive"; + // Superagent mirrors its REST API, and several mutations use verbs the + // generic classifier reads as reads: billable triage_finding, scans, + // restore_agent_builtin_rule, and credential-revoking revoke_agent_client. + // Only list/get tools and tools the provider marks read-only are reads. + if (sourceTemplateKey === "superagent") { + if (verbMatches(tool.name, "delete|remove|destroy|revoke")) return "destructive"; + if (annotations.readOnlyHint === false || annotations.writeHint === true) return "write"; + return /^(list|get)-/.test(normalizedToolName) || annotations.readOnlyHint === true ? "read" : "write"; + } if ( sourceTemplateKey === "shopify" && SHOPIFY_DESTRUCTIVE_TOOLS.has(normalizedToolName) diff --git a/ui/public/brands/apps/manifest.json b/ui/public/brands/apps/manifest.json index 1355cd95545..48771802c04 100644 --- a/ui/public/brands/apps/manifest.json +++ b/ui/public/brands/apps/manifest.json @@ -409,6 +409,12 @@ "catalogVisible": true, "localAsset": "/brands/apps/supabase.svg" }, + { + "slug": "superagent", + "provider": "Superagent", + "catalogVisible": true, + "localAsset": "/brands/apps/superagent.png" + }, { "slug": "telegram", "provider": "Telegram", diff --git a/ui/public/brands/apps/superagent.png b/ui/public/brands/apps/superagent.png new file mode 100644 index 00000000000..f8e3b7b4576 Binary files /dev/null and b/ui/public/brands/apps/superagent.png differ diff --git a/ui/src/lib/app-gallery-copy.ts b/ui/src/lib/app-gallery-copy.ts index cfb964f727d..33ae068ac07 100644 --- a/ui/src/lib/app-gallery-copy.ts +++ b/ui/src/lib/app-gallery-copy.ts @@ -78,6 +78,10 @@ const APP_COPY: Record = { tagline: "Manage Postgres projects, branches, and queries.", short: "Sign in with Neon. Project pinning and read-only mode are optional.", }, + superagent: { + tagline: "Review security findings and run red-team checks.", + short: "Connect with a Superagent API key. Billable and destructive tools stay under action policies.", + }, linear: { tagline: "Create, update and read tickets.", short: "Create, update and read tickets.", diff --git a/ui/src/pages/apps/AppsConnect.test.tsx b/ui/src/pages/apps/AppsConnect.test.tsx index 4d8b0dd1cc2..eea8a1fc6fc 100644 --- a/ui/src/pages/apps/AppsConnect.test.tsx +++ b/ui/src/pages/apps/AppsConnect.test.tsx @@ -64,6 +64,7 @@ const ASANA_MANAGED = { const BOX = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "box")!; const POSTHOG = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "posthog")!; const NEON = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "neon")!; +const SUPERAGENT = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "superagent")!; const POSTMAN = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "postman")!; const SHOPIFY = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "shopify")!; const GOOGLE_SHEETS = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "google-sheets")!; @@ -2024,6 +2025,37 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { expect(container.textContent).not.toContain("Pick the app you want your agents to use."); }); + it("connects Superagent with only an organization API key", async () => { + mockParams.appKey = "superagent"; + listGalleryMock.mockResolvedValueOnce({ apps: [SUPERAGENT] }); + await render(); + + expect(radioContaining("Sign in with")).toBeFalsy(); + // The Ask-first advice must be visible on the key form, which renders the + // credential helper text rather than method warnings. + expect(container.textContent).toContain("set billable and destructive actions to Ask first"); + const keyInput = container.querySelector('input[type="password"]'); + expect(keyInput).toBeTruthy(); + expect(buttonByText("Connect")?.disabled).toBe(true); + + await act(async () => { + setInputValue(keyInput!, "sk_live_test-key"); + }); + await flushReact(); + const submit = buttonByText("Connect"); + expect(submit?.disabled).toBe(false); + await act(async () => { + submit?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(connectAppMock).toHaveBeenCalledWith("company-1", expect.objectContaining({ + galleryKey: "superagent", + connectionMethodKey: "mcp-api-key", + credentialValues: { "credentials.authorization": "sk_live_test-key" }, + })); + }); + it("enables Neon's Connect button only once the API key is entered, with pin and read-only optional", async () => { mockParams.appKey = "neon"; listGalleryMock.mockResolvedValueOnce({ apps: [NEON] });