feat(apps): add Superagent connection (#15394)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents reach external services through the Apps catalog. Each
catalog entry is a reviewed `AppDefinition` that connects a provider's
hosted MCP server to Paperclip's shared vault, grants, policies,
gateway, and audit trail.
> - Superagent (`superagent.sh`) is a security platform. Its hosted MCP
server lets agents read and triage security findings, start red-team
reports, run Contributor Trust and dependency update jobs, and score web
pages, email, files, skills, MCP repositories, and packages before an
agent trusts them.
> - Superagent is not in the catalog. Operators must use the generic
"Connect your own MCP server" flow. That flow has no branding, no key
guidance, and no warning about billable or destructive tools.
> - The connector playbook supports this provider with existing
definition fields. The server accepts an organization API key as a
bearer header. It publishes no OAuth authorization-server metadata, so
browser sign-in is not possible.
> - This pull request adds the Superagent definition, its artwork, the
research and permission-review ledger rows, documentation, deterministic
tests, and one reviewed risk-classification rule.
> - The benefit is a branded, governed Superagent connection with clear
key guidance and a warning about tools that cost credits or delete data.

## Linked Issues or Issue Description

**Problem or motivation**

Teams that use Superagent for PR security, red teaming, and agent
guardrails want their Paperclip agents to read findings, return
structured reports, and score content before they use it. Superagent is
not in the Apps catalog. Operators must paste the MCP URL and an
`Authorization` header into the generic remote-MCP flow. That flow gives
no branding and no provider guidance. It also does not tell the operator
that the key reaches the whole organization, or that some tools consume
credits or permanently delete findings.

**Proposed solution**

Add a catalog-only Superagent connection that follows the connector
playbook. It has one method: a customer organization API key
(`sk_live_...`), sent as an `Authorization: Bearer` header to
`https://www.superagent.sh/mcp`. The field helper text explains that
Superagent keys are not scoped. The method warning tells operators to
set billable and destructive actions to Ask first before agents run
unattended. All discovered tools stay governed by the normal per-action
policies.

**Alternatives considered**

A browser sign-in method was not added. The server's protected-resource
metadata names `https://superagent.sh` as its authorization server, but
that origin publishes no `oauth-authorization-server` or
`openid-configuration` document, so Paperclip cannot discover OAuth
endpoints. A plugin was not needed because the connection needs no
custom UI, tables, workers, or webhooks. Relying on the generic risk
classifier was not enough. Several Superagent mutations
(`triage_finding`, `scan_*`, `restore_agent_builtin_rule`) use names
that it reads as reads, so a narrow reviewed Superagent rule was added
instead.

**Roadmap alignment**

This extends the existing self-serve remote-MCP connection catalog. It
does not overlap planned core work.

## What Changed

- Added the `superagent` row to
`packages/shared/src/self-serve-mcp-research.json` (API-key auth, risk
tier S4).
- Added the `superagent` provider to
`scripts/ingest-app-definitions.mjs` (category, key placement and
placeholder, console links, guidance, description). Regenerated
`packages/shared/src/app-definitions/superagent.json` and the generated
registry.
- Added the `superagent/mcp-api-key` permission review to
`doc/connections/tool-method-permission-reviews.json`, with
key-permission text and evidence links.
- Added Superagent's official mark
(`ui/public/brands/apps/superagent.png`, the 460×460 avatar of the
official `superagent-ai` GitHub organization) and the brand manifest
entry.
- Added gallery copy for the Superagent card.
- Added a reviewed Superagent rule to `classifyRisk` in
`server/src/services/tool-access.ts`. Only `list_*` and `get_*` tools,
and tools that Superagent marks read-only, are reads. `delete_*` and
`revoke_agent_client` are destructive. All other tools are writes, so
billable and rule-changing tools can be set to Ask first.
- Put the Ask-first advice in the API-key helper text, because the key
form shows helper text and not method warnings.
- Added `doc/connections/SUPERAGENT.md` (transport and auth, why there
is no OAuth, administrator setup, capabilities and policy, manifest,
brand provenance, validation hook). Linked it from the connections
README and the permission audit.
- Tests: definition shape, store visibility and artwork, URL
recognition, the bearer header on discovery with the key kept out of
connection config, read/write/destructive classification of fixture
tools, the Superagent risk rule (including `triage_finding` and
`restore_agent_builtin_rule`), the visible Ask-first advice and API-key
gating of the connect form, and the pinned catalog counts.

## Verification

- `pnpm exec vitest run packages/shared/src/app-definitions.test.ts
packages/shared/src/app-definitions-url.test.ts
ui/src/pages/apps/AppsConnect.test.tsx ui/src/pages/apps/Browse.test.tsx
ui/src/lib/app-brand-assets.test.ts
ui/src/pages/apps/AppLogo.brand-assets.test.tsx`: 317 passed.
- `pnpm exec vitest run
server/src/__tests__/tool-access-service.test.ts`: 385 passed.
- `node scripts/check-app-brand-assets.mjs` and `node --test
scripts/app-brand-validation.test.mjs`: passed.
- `pnpm --filter @paperclipai/shared typecheck`, `pnpm --filter
@paperclipai/server typecheck`, and `pnpm --filter @paperclipai/ui
typecheck`: clean.
- Manual: in a local instance, open Apps → Browse and confirm the
Superagent card and icon. Open `/apps/connect?source=superagent`.
Confirm the single API-key method, and confirm that Connect enables only
after a key is entered.
- Live metadata probe on 2026-10-06: an unauthenticated `initialize` on
`https://www.superagent.sh/mcp` returns 401 with
`resource_metadata="https://www.superagent.sh/.well-known/oauth-protected-resource"`.
That document returns 200. No authorization-server metadata exists at
the named issuer.

## Risks

- Low risk to existing providers. The change is additive catalog data
plus tests. The generated registry only gains one import. The new risk
rule runs only for Superagent connections.
- A Superagent key reaches its whole organization. Some tools consume
credits (`create_*_report`, `triage_finding`) or delete data permanently
(`delete_finding`). Every action starts Allowed under the current
product default. The key helper text tells operators to set these
actions to Ask first.
- The permission-review ledger records live proof as not run. No
Superagent account was used. The lifecycle checklist in
`doc/connections/SUPERAGENT.md` needs a documented pass before the entry
is fully qualified.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`, 1M context) in Claude Code, with
extended thinking and tool use (shell, file editing, web fetch, browser
checks).

## 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
This commit is contained in:
Devin Foley authored and GitHub committed 2026-10-06 15:57:16 -07:00
1 parent 5c15be3715
commit 8cbd21b3e7
16 files changed
+452 -29

No files matched your search

@@ -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
+1 -1
View File
@@ -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).
+162
View File
@@ -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 <key>`. 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.
@@ -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",
@@ -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", () => {
@@ -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[];
+55 -5
View File
@@ -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) =>
@@ -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."
]
}
]
}
@@ -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" }
]
}
+13 -1
View File
@@ -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"],
@@ -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(
+9
View File
@@ -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)
+6
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

+4
View File
@@ -78,6 +78,10 @@ const APP_COPY: Record<string, AppCopy> = {
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.",
+32
View File
@@ -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<HTMLInputElement>('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] });