From 5b8b2b38ca38eef9f418f92bad1006e0e0b7139e Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Fri, 2 Oct 2026 15:04:52 -0700 Subject: [PATCH] feat(apps): add Neon connection (#14980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 wires a provider's hosted MCP server into Paperclip's shared vault, grants, policies, gateway, and audit trail. > - Neon is a widely used serverless Postgres provider with an official hosted MCP server, but it is not in the catalog. Teams that run their databases on Neon must use the generic "connect your own MCP server" path, which has no branding, no guidance, and no project or read-only controls. > - The connector playbook requires a catalog entry for a provider like this: the hosted server supports dynamic client registration and bearer API keys, and the common definition fields can express every option Paperclip can serialize. > - This pull request adds the Neon definition, its official artwork, the research and permission-review ledger rows, documentation, and deterministic tests, without any provider-specific runtime code. > - The benefit is a one-click, governed Neon connection with optional project pinning and read-only mode, and a documented path to live qualification. ## Linked Issues or Issue Description **Problem or motivation** Neon is a common Postgres host for the applications agents work on, but Paperclip's Apps catalog has no Neon entry. Operators who want agents to inspect schemas, run SQL, or manage branches must paste the MCP URL into the generic remote-MCP flow, which gives no branding, no provider guidance, no project boundary, and no read-only switch. **Proposed solution** Add a catalog-only Neon connection built from the connector playbook: browser sign-in through Neon's dynamic client registration with the reviewed `read` and `write` scopes, or a customer API key sent as an Authorization bearer header. Both methods expose Neon's documented `projectId` pin and `readonly` switch as optional Advanced fields. Every discovered tool stays governed by the normal per-action policies. **Alternatives considered** A plugin was not needed because no custom UI, tables, workers, or webhooks are involved. A separate read-only method was not added because the playbook treats read-only switches as advanced fields rather than methods. Neon's repeatable `category` query filter was left out because tenant fields serialize lists as one comma-joined value, so it cannot be sent correctly without new runtime code; per-action policies cover catalog narrowing instead. **Roadmap alignment** This extends the existing self-serve remote-MCP connection catalog and does not overlap planned core work. ## What Changed - Added the `neon` provider to `scripts/ingest-app-definitions.mjs` (category, API-key placement, methods, tenant fields, guidance, warnings) and regenerated `packages/shared/src/app-definitions/neon.json` plus the generated registry. - Added the Neon row to the self-serve MCP research ledger with `dcr_or_api_key` auth and risk tier S4. - Added permission reviews for `neon/mcp-oauth` (explicit scopes `read`, `write`, taken from Neon's live authorization-server metadata) and `neon/mcp-api-key` (provider key), with evidence links. - Added Neon's official tile icon (`ui/public/brands/apps/neon.png`, copied byte-for-byte from the icon linked by neon.com) and the brand manifest entry. - Added prosumer gallery copy for the Neon card. - Added `doc/connections/NEON.md` (service involvement, endpoints, administrator setup, capabilities and policy, manifest, brand provenance, validation hook) and linked it from the connections README and the permission audit. - Tests: Neon definition shape, store visibility and artwork, URL recognition, reviewed scopes with scope-widening rejection, URL projection of the project pin and read-only flag, invalid project ID rejection, the connect form's API-key gating, 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` — 34 passed. - `pnpm exec vitest run server/src/__tests__/tool-access-service.test.ts` — 367 passed. - `pnpm exec vitest run 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` — all 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`, `pnpm --filter @paperclipai/ui typecheck`, `pnpm check:token-gates` — clean. - Manual: in a local instance, open Apps → Browse, confirm the Neon card and icon, open `/apps/connect?source=neon`, confirm both methods, the Advanced project pin and read-only toggle, and that Connect enables after an API key is entered. The operator completed a live connection against a Neon account on this build. - Live metadata probed on 2026-10-02: both `.well-known` documents at `mcp.neon.tech` return the recorded endpoints and scopes; an unauthenticated `initialize` returns 401 with `resource_metadata`. ## Risks - Low risk to existing providers: the change is additive catalog data plus tests. The generated registry only gains one import. - Neon's hosted server grants broad project and database management. The definition carries two warnings, recommends a development project, and keeps every write under the normal action policies; the read-only switch is enforced by Neon's server, not locally. - The permission-review ledger records live proof for both methods as not run; the full lifecycle checklist in `doc/connections/NEON.md` still needs a documented pass before the entry is considered 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 Fable 5.1 (`claude-fable-5-1`) in Claude Code, with extended thinking and tool use (shell, file editing, browser verification). ## 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 --- doc/connections/CONNECTOR-PERMISSION-AUDIT.md | 4 + doc/connections/NEON.md | 237 ++++++++++++++++++ doc/connections/README.md | 2 +- .../tool-method-permission-reviews.json | 36 +++ .../shared/src/app-definitions-url.test.ts | 1 + .../shared/src/app-definitions.generated.ts | 27 +- packages/shared/src/app-definitions.test.ts | 85 ++++++- packages/shared/src/app-definitions/neon.json | 155 ++++++++++++ .../shared/src/self-serve-mcp-research.json | 3 +- scripts/ingest-app-definitions.mjs | 63 ++++- .../src/__tests__/tool-access-service.test.ts | 50 +++- ui/public/brands/apps/manifest.json | 6 + ui/public/brands/apps/neon.png | Bin 0 -> 1319 bytes ui/src/lib/app-gallery-copy.ts | 4 + ui/src/pages/apps/AppsConnect.test.tsx | 43 ++++ 15 files changed, 693 insertions(+), 23 deletions(-) create mode 100644 doc/connections/NEON.md create mode 100644 packages/shared/src/app-definitions/neon.json create mode 100644 ui/public/brands/apps/neon.png diff --git a/doc/connections/CONNECTOR-PERMISSION-AUDIT.md b/doc/connections/CONNECTOR-PERMISSION-AUDIT.md index 07942d7504..af6b592458 100644 --- a/doc/connections/CONNECTOR-PERMISSION-AUDIT.md +++ b/doc/connections/CONNECTOR-PERMISSION-AUDIT.md @@ -6,6 +6,10 @@ method's exact requested scopes, supported actions, restrictions, sources and verification limits. AI runtime authentication and chat/channel setup are 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). + ## Shared credential failure Zapier's secret URL exposed a shared ownership/resolution problem. The same diff --git a/doc/connections/NEON.md b/doc/connections/NEON.md new file mode 100644 index 0000000000..253745f41f --- /dev/null +++ b/doc/connections/NEON.md @@ -0,0 +1,237 @@ +# Neon + +Updated: 2026-10-02. Status: catalog definition reviewed against official +documentation and live provider metadata; live account qualification +outstanding. + +Neon appears in Apps and uses Paperclip's shared remote-MCP OAuth connection, +vault, catalog, grants, policies, gateway, and audit trail. It is a resource +connection, not Paperclip sign-in. No plugin, provider-specific runtime code, +or database migration is required. + +Paperclip connects to Neon's hosted MCP server at `https://mcp.neon.tech/mcp`. +The connection supports two explicit methods: + +- browser OAuth, recommended for Neon accounts; or +- a Neon API key stored as a Paperclip secret and sent as an + `Authorization: Bearer ...` header. + +Paperclip does not silently fall back from OAuth to an API key. The selected +method is saved on the connection and reused for reconnects. + +This curated connection is the polished route: it provides branding, optional +project pinning and read-only controls, field validation, and tailored +guidance. None of it is *required* to reach Neon's server. Neon can also be +connected generically from **Connect your own MCP server** by pasting +`https://mcp.neon.tech/mcp`, with no Paperclip-specific code involved. See +[Connecting any remote MCP server](./GENERIC-REMOTE-MCP.md). + +## Service involvement + +Neon hosts both the MCP resource and its OAuth authorization service on the +same origin. Paperclip discovers the OAuth endpoints, dynamically registers the +client, stores returned credentials as secret references, and handles the +callback at `/api/tools/oauth/callback`. No Paperclip-operated vendor relay is +involved; cloud and self-hosted instances use the same path. + +```mermaid +sequenceDiagram + actor A as Administrator + participant P as Paperclip + participant M as mcp.neon.tech + + A->>P: Choose Sign in with Neon + P->>M: GET /.well-known/oauth-protected-resource/mcp + M-->>P: Authorization server: https://mcp.neon.tech + P->>M: GET /.well-known/oauth-authorization-server + M-->>P: authorize, token, register, revoke endpoints; scopes read, write + P->>M: POST /api/register (dynamic client registration) + M-->>P: Client registration + P-->>A: Open browser authorization (scope: read write, PKCE S256) + A->>M: Approve access + M-->>P: Redirect to /api/tools/oauth/callback + P->>M: POST /api/token (authorization code) + M-->>P: Access and refresh tokens + P->>M: tools/list on /mcp with optional projectId and readonly query + M-->>P: Neon tool catalog +``` + +The hosted endpoints retrieved on 2026-10-02: + +| Purpose | Endpoint | +| --- | --- | +| MCP resource (Streamable HTTP) | `https://mcp.neon.tech/mcp` | +| Protected-resource metadata | `https://mcp.neon.tech/.well-known/oauth-protected-resource/mcp` | +| Authorization-server metadata | `https://mcp.neon.tech/.well-known/oauth-authorization-server` | +| Authorize | `https://mcp.neon.tech/api/authorize` | +| Token | `https://mcp.neon.tech/api/token` | +| Dynamic client registration | `https://mcp.neon.tech/api/register` | +| Revoke | `https://mcp.neon.tech/api/revoke` | +| Paperclip callback | `/api/tools/oauth/callback` | + +The authorization server advertises `code` responses, PKCE `S256`, +`authorization_code` and `refresh_token` grants, `none` client authentication +for registered public clients, and exactly two scopes: `read` and `write`. +Paperclip requests both so the connection has the write surface described +below; the read-only switch narrows the server itself rather than the token. +Caller widening beyond the reviewed scopes is rejected. + +Neon's older SSE endpoint (`https://mcp.neon.tech/sse`) is deprecated and +returns `410 Gone` on or after 2026-10-01. Paperclip does not offer it. + +## Administrator setup + +1. In **Apps → Browse**, choose **Neon**. +2. Explicitly choose **Sign in with Neon** or **Use an API key**. +3. Continue directly with Neon's defaults. No project ID is required. +4. Open **Advanced** only when you need to pin the connection to one + **project ID** or force **Read-only mode**. +5. For OAuth, continue through browser consent. For API-key setup, create a + key in Neon Console → **Settings → API keys** and paste it into Paperclip. + Prefer a project-scoped key, which Neon limits to one project with Editor + rights; personal and organization keys reach every project the account can + access. Never put the key in connection configuration or a URL. +6. Review discovered actions on the connection's **Permissions** screen. Every + discovered action starts **Allowed**, including writes and destructive + actions. Set deletion, branch reset, and compute mutations to **Ask first** + where operator review is wanted. + +Use a public HTTPS Paperclip origin or a loopback HTTP origin such as +`http://localhost:3100`. A plain HTTP tailnet hostname is not loopback; use +HTTPS or change the local canonical origin before connecting. + +When configured, Paperclip appends `projectId=` and `readonly=true` as +query parameters on the server URL, exactly as Neon documents. The same URL is +used for catalog discovery and tool execution, and a caller cannot override it. +Neon documents a repeatable `category=` filter as well; Paperclip's +tenant fields serialize lists as one comma-joined value, so that filter is not +offered. Use the per-action Off / Ask first / Allowed controls to narrow the +catalog instead. + +## Capabilities and policy + +The catalog is discovered live from the actual provider schemas. Neon groups +its tools into these categories: + +| Category | What the tools do | Classification | +| --- | --- | --- | +| `docs` | Look up Neon documentation | Read | +| `schema`, `observability` | Inspect tables and columns, compare schemas, query logs, check availability | Read; may expose application data | +| `projects`, `branches`, `endpoints` | List, create, describe, delete projects and branches; manage roles, databases and computes | Write or destructive | +| `snapshots` | Create, restore and schedule snapshots | Write or destructive | +| `querying` | Execute SQL, apply schema changes, run diagnostics | Write; read-only mode limits SQL to `SELECT` | +| `neon_auth`, `data_api` | Provision Neon Auth, manage OAuth providers, enable or disable the Data API | Write | +| `functions`, `storage` | Deploy functions, manage buckets and objects | Write or destructive | + +Neon enforces the account, organization, and project permissions behind the +credential. The optional project pin and read-only switch are enforced by +Neon's server, not by Paperclip. `requiredResourceFilters: ["project"]` is +reviewed policy metadata that names the boundary operators should set; it is +not a local allowlist. + +Neon's own guidance: the hosted server grants broad database management +capabilities, so always review and authorize actions before execution, and +prefer development or testing projects over production data. + +## Vendor + +- App key: `neon` +- App name: Neon +- Reuse classification: MCP-direct +- Reason for classification: official hosted Streamable HTTP server with + RFC 7591 dynamic registration and documented bearer API keys; common fields + represent every documented option Paperclip can serialize. +- Security tier: S4 +- Plugin needed? No. + +## Transport and auth + +- Transport: `mcp_remote` +- Endpoint: `https://mcp.neon.tech/mcp` +- Auth modes: OAuth (DCR, PKCE) or API key +- OAuth scopes: `read`, `write` (explicit, reviewed) +- Key scope: whatever the Neon key carries; Paperclip cannot widen it +- Credential owner: company or user grant through the standard access screen +- Secret storage: `company_secrets` refs only +- Revocation: Neon publishes `/api/revoke`; Paperclip removal revokes the + grant and deletes stored secrets + +## Resource filters + +- Required filters: none on the default path +- Optional filters: `projectId` (query `projectId`), `readOnly` (query + `readonly=true`, omitted when off) +- Write-enabling filters: none; writes depend on the credential and on + read-only mode being off +- Filters enforced by: Neon's hosted server + +## Manifest + +- slug: `neon`; name: Neon; categories: `data` +- branding: `/brands/apps/neon.png` (official touch icon, both themes) +- docsUrl: `https://neon.com/docs/ai/neon-mcp-server` +- methods: `mcp-oauth` (Sign in with Neon, `dcr`), `mcp-api-key` (Use an API + key, `customer`, `Authorization: Bearer` header) +- tenantFields: `projectId` (text, advanced, `^[a-z0-9-]+$`, max 64), + `readOnly` (checkbox, advanced, default off) +- riskTier: S4; requiredResourceFilters: `project` +- urlPatterns: `https://mcp.neon.tech/*`; redirectConstraints: + `https-or-loopback-http` + +Source of truth: the `neon` block in `scripts/ingest-app-definitions.mjs`, the +ledger row in `packages/shared/src/self-serve-mcp-research.json`, and the two +reviews in `doc/connections/tool-method-permission-reviews.json`. Regenerate +with `pnpm connections:ingest-app-definitions` (or `--definitions-only` +without the research corpus). + +## Wizard path + +- User path: Apps → Browse → Neon → method → Access → Connect. +- Configuration steps: none required; Advanced holds project pin and + read-only mode. +- Error states: discovery or OAuth failures stay inline on Connect with a + retry of the same saved connection; an invalid project ID is rejected before + the provider is contacted. +- Redacted metadata shown: method, pinned project ID, read-only flag. Tokens + and keys are never shown. + +## Governance defaults + +- Default profile and bindings: the standard connection profile; every + discovered action starts Allowed under the current product default. +- Policies: operators narrow destructive categories to Ask first or Off on the + Permissions screen. +- Quarantine rules: the shared defaults; no Neon-specific exceptions. + +## Brand provenance + +Neon's own app icon, the black tile with the green logomark, is used because the +bare logomark from the brand kit is a thin outline that reads weakly at 24–36px +on the light frame. The file is Neon's official touch icon, copied byte-for-byte +on 2026-10-02 and used unchanged in both themes (Neon's brand colours are +black and `#34D59A`). + +| File | Source | SHA-256 | +| --- | --- | --- | +| `ui/public/brands/apps/neon.png` (180×180) | `https://neon.com/apple-touch-icon.png`, the icon linked from `https://neon.com/` | `a6cf4b0772b06a5a64ccfefbfb8b7a1af56e0876eb10c9052f43c8624f4a0b61` | + +The brand kit at `https://neon.com/brand` publishes the bare logomark as SVG +(light and dark colour variants) but no vector of the tile; the raster official +icon is preferred over a locally composed SVG so the artwork stays the +vendor's own. + +## Validation hook + +- Environment: definition review on 2026-10-02 against `origin/master`; + no Neon account was used. +- Metadata probe: both `.well-known` documents above returned HTTP 200 with the + endpoints and scopes recorded here. An unauthenticated `initialize` on + `/mcp` returned HTTP 401 with + `WWW-Authenticate: Bearer ... resource_metadata="https://mcp.neon.tech/.well-known/oauth-protected-resource/mcp"`. +- Deterministic tests: manifest shape, store visibility, artwork, reviewed + scopes, scope-widening rejection, URL projection of the project pin and + read-only flag, and invalid project ID rejection. +- Connect evidence, catalog evidence, allowed read, governed write, + denied case, revoke, audit: not run. Each exposed method still needs the + full live lifecycle with a Neon development project before this connection + is considered qualified. diff --git a/doc/connections/README.md b/doc/connections/README.md index 7e1bfe8f05..9677c400bd 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), +[Gmail](./GMAIL.md), [Asana](./ASANA.md), [PostHog](./POSTHOG.md), [Neon](./NEON.md), [AgentMail](./AGENTMAIL.md), and [iMessage Photon](./IMESSAGE-PHOTON.md). Optional credential custody: [Vercel Connect](./VERCEL-CONNECT.md). diff --git a/doc/connections/tool-method-permission-reviews.json b/doc/connections/tool-method-permission-reviews.json index 2ef0bc115f..1a7f785211 100644 --- a/doc/connections/tool-method-permission-reviews.json +++ b/doc/connections/tool-method-permission-reviews.json @@ -1368,6 +1368,42 @@ "reviewedAt": "2026-09-30", "liveProof": "not-run" }, + { + "app": "neon", + "method": "mcp-oauth", + "auth": "oauth", + "policy": "explicit", + "requestedScopes": [ + "read", + "write" + ], + "capability": "write", + "supportedActions": "Create, describe and delete projects and branches; manage computes, roles, databases and snapshots; run SQL and apply schema changes; configure Neon Auth and the Data API; read logs. Optional project pinning and read-only mode narrow the hosted server.", + "evidence": [ + "https://neon.com/docs/ai/neon-mcp-server", + "https://mcp.neon.tech/.well-known/oauth-protected-resource/mcp", + "https://mcp.neon.tech/.well-known/oauth-authorization-server" + ], + "reviewedAt": "2026-10-02", + "liveProof": "not-run" + }, + { + "app": "neon", + "method": "mcp-api-key", + "auth": "api_key", + "policy": "provider-key", + "requestedScopes": [], + "capability": "provider-controlled", + "supportedActions": "Create, describe and delete projects and branches; manage computes, roles, databases and snapshots; run SQL and apply schema changes; configure Neon Auth and the Data API; read logs. Optional project pinning and read-only mode narrow the hosted server.", + "evidence": [ + "https://neon.com/docs/ai/neon-mcp-server", + "https://neon.com/docs/manage/api-keys", + "https://mcp.neon.tech/.well-known/oauth-protected-resource/mcp" + ], + "reviewedAt": "2026-10-02", + "liveProof": "not-run", + "keyPermissions": "Project, branch, compute, snapshot, SQL and schema changes within the key\u2019s reach. A project-scoped key limits access to one project with Editor rights; personal and organization keys reach every project they can access. Paperclip cannot increase an existing key\u2019s permissions." + }, { "app": "notion", "method": "mcp-oauth", diff --git a/packages/shared/src/app-definitions-url.test.ts b/packages/shared/src/app-definitions-url.test.ts index c6a5d1a548..5c93e8c2f8 100644 --- a/packages/shared/src/app-definitions-url.test.ts +++ b/packages/shared/src/app-definitions-url.test.ts @@ -11,6 +11,7 @@ describe("tool app gallery URL matching", () => { expect(getAppDefinitionForUrl("https://github.com/paperclipai/paperclip/pull/1")?.slug).toBe("github"); 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"); }); 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 bfe14f543e..d85f7b9b36 100644 --- a/packages/shared/src/app-definitions.generated.ts +++ b/packages/shared/src/app-definitions.generated.ts @@ -67,17 +67,18 @@ import a65 from "./app-definitions/fireflies.json" with { type: "json" }; import a66 from "./app-definitions/zep.json" with { type: "json" }; import a67 from "./app-definitions/supermemory.json" with { type: "json" }; import a68 from "./app-definitions/honcho.json" with { type: "json" }; -import a69 from "./app-definitions/gmail.json" with { type: "json" }; -import a70 from "./app-definitions/google-drive.json" with { type: "json" }; -import a71 from "./app-definitions/google-docs.json" with { type: "json" }; -import a72 from "./app-definitions/google-sheets.json" with { type: "json" }; -import a73 from "./app-definitions/google-slides.json" with { type: "json" }; -import a74 from "./app-definitions/google-calendar.json" with { type: "json" }; -import a75 from "./app-definitions/google-chat.json" with { type: "json" }; -import a76 from "./app-definitions/google-people.json" with { type: "json" }; -import a77 from "./app-definitions/google-workspace-search.json" with { type: "json" }; -import a78 from "./app-definitions/openai.json" with { type: "json" }; -import a79 from "./app-definitions/openrouter.json" with { type: "json" }; -import a80 from "./app-definitions/xai.json" with { type: "json" }; +import a69 from "./app-definitions/neon.json" with { type: "json" }; +import a70 from "./app-definitions/gmail.json" with { type: "json" }; +import a71 from "./app-definitions/google-drive.json" with { type: "json" }; +import a72 from "./app-definitions/google-docs.json" with { type: "json" }; +import a73 from "./app-definitions/google-sheets.json" with { type: "json" }; +import a74 from "./app-definitions/google-slides.json" with { type: "json" }; +import a75 from "./app-definitions/google-calendar.json" with { type: "json" }; +import a76 from "./app-definitions/google-chat.json" with { type: "json" }; +import a77 from "./app-definitions/google-people.json" with { type: "json" }; +import a78 from "./app-definitions/google-workspace-search.json" with { type: "json" }; +import a79 from "./app-definitions/openai.json" with { type: "json" }; +import a80 from "./app-definitions/openrouter.json" with { type: "json" }; +import a81 from "./app-definitions/xai.json" with { type: "json" }; import type { AppDefinition } from "./types/app-definition.js"; -export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80] 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] as AppDefinition[]; diff --git a/packages/shared/src/app-definitions.test.ts b/packages/shared/src/app-definitions.test.ts index da32aedf63..aa038c32f6 100644 --- a/packages/shared/src/app-definitions.test.ts +++ b/packages/shared/src/app-definitions.test.ts @@ -262,7 +262,7 @@ describe("AppDefinition catalog", () => { "google-workspace-search", ]), ); - expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(49); + expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(50); expect(BLOCKED_MCP_PROVIDERS.map((entry) => entry.slug)).toEqual([ "g2", "vercel", @@ -426,15 +426,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 52 researched MCP providers", () => { + it("keeps a complete, unique, dated evidence ledger for all 53 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(52); + expect(SELF_SERVE_MCP_RESEARCH.entries).toHaveLength(53); expect( new Set(SELF_SERVE_MCP_RESEARCH.entries.map((entry) => entry.slug)), - ).toHaveProperty("size", 52); + ).toHaveProperty("size", 53); 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:"); @@ -728,7 +728,7 @@ describe("AppDefinition catalog", () => { "ticktick", "xero", ]); - expect(APP_STORE_DEFINITIONS).toHaveLength(58); + expect(APP_STORE_DEFINITIONS).toHaveLength(59); const connectableSlugs = new Set( CONNECTABLE_APP_DEFINITIONS.map((entry) => entry.slug), ); @@ -938,6 +938,81 @@ describe("AppDefinition catalog", () => { expect(method.guidanceMd).toContain("optional advanced controls"); } }); + it("connects Neon's hosted server with optional project pinning and read-only mode", () => { + const neon = APP_DEFINITIONS.find((app) => app.slug === "neon")!; + expect(neon).toMatchObject({ + name: "Neon", + categories: ["data"], + urlPatterns: ["https://mcp.neon.tech/*"], + docsUrl: "https://neon.com/docs/ai/neon-mcp-server", + redirectConstraints: "https-or-loopback-http", + branding: { logoUrl: "/brands/apps/neon.png" }, + }); + expect(neon.branding.darkLogoUrl).toBeUndefined(); + expect(APP_STORE_DEFINITIONS.some((app) => app.slug === "neon")).toBe(true); + expect(neon.methods.map((candidate) => candidate.key)).toEqual([ + "mcp-oauth", + "mcp-api-key", + ]); + const [oauth, apiKey] = neon.methods; + expect(oauth).toMatchObject({ + auth: "oauth", + ownershipModes: ["dcr"], + riskTier: "S4", + requiredResourceFilters: ["project"], + defaults: { + serverUrl: "https://mcp.neon.tech/mcp", + scopesHint: ["read", "write"], + }, + }); + expect(apiKey).toMatchObject({ + auth: "api_key", + ownershipModes: ["customer"], + riskTier: "S4", + defaults: { serverUrl: "https://mcp.neon.tech/mcp" }, + keyPlacement: { + location: "header", + name: "Authorization", + prefix: "Bearer ", + }, + consoleLinks: { + keys: "https://console.neon.tech/app/settings/api-keys", + }, + }); + expect(apiKey!.credentialFields).toEqual([ + expect.objectContaining({ + key: "authorization", + type: "password", + secret: true, + required: true, + }), + ]); + for (const method of neon.methods) { + // Nothing is required on the default path: both narrowing controls are + // optional and folded under Advanced, and the provider enforces them. + expect(method.tenantFields?.map((field) => field.key)).toEqual([ + "projectId", + "readOnly", + ]); + expect(method.tenantFields?.every((field) => field.advanced && !field.required)).toBe(true); + expect(method.tenantFields?.[0]).toMatchObject({ + type: "text", + validation: { pattern: "^[a-z0-9-]+$", maxLength: 64 }, + transport: { location: "query", name: "projectId" }, + }); + expect(method.tenantFields?.[1]).toMatchObject({ + type: "checkbox", + defaultValue: false, + transport: { + location: "query", + name: "readonly", + format: "boolean", + omitFalse: true, + }, + }); + expect(method.warnings?.length).toBe(2); + } + }); 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/neon.json b/packages/shared/src/app-definitions/neon.json new file mode 100644 index 0000000000..8e9f20eaa1 --- /dev/null +++ b/packages/shared/src/app-definitions/neon.json @@ -0,0 +1,155 @@ +{ + "schemaVersion": 1, + "slug": "neon", + "name": "Neon", + "description": "Manage Postgres projects and branches, run SQL, and inspect schemas in Neon.", + "categories": [ + "data" + ], + "featured": false, + "branding": { + "logoUrl": "/brands/apps/neon.png" + }, + "urlPatterns": [ + "https://mcp.neon.tech/*" + ], + "docsUrl": "https://neon.com/docs/ai/neon-mcp-server", + "redirectConstraints": "https-or-loopback-http", + "methods": [ + { + "key": "mcp-oauth", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "dcr" + ], + "whenToUse": "Use browser sign-in for the provider-hosted MCP server.", + "defaults": { + "serverUrl": "https://mcp.neon.tech/mcp", + "scopesHint": [ + "read", + "write" + ] + }, + "guidanceMd": "Connect Neon in the browser. Open Advanced to pin one project or enable read-only mode. Write tools start enabled and remain governed by Paperclip's action policies.", + "riskTier": "S4", + "label": "Sign in with Neon", + "consoleLinks": { + "docs": "https://neon.com/docs/ai/neon-mcp-server" + }, + "warnings": [ + "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.", + "Neon recommends its hosted server for development and testing. Review write and destructive actions before execution." + ], + "tenantFields": [ + { + "key": "projectId", + "label": "Pin to project ID", + "type": "text", + "advanced": true, + "placeholder": "Optional Neon project ID", + "helperMd": "Optional. Restrict this connection to one project. Copy the project ID from Neon Console → Project settings → General.", + "validation": { + "pattern": "^[a-z0-9-]+$", + "maxLength": 64 + }, + "transport": { + "location": "query", + "name": "projectId" + } + }, + { + "key": "readOnly", + "label": "Read-only mode", + "type": "checkbox", + "defaultValue": false, + "helperMd": "Enable this to limit SQL to SELECT queries and schema inspection.", + "transport": { + "location": "query", + "name": "readonly", + "format": "boolean", + "omitFalse": true + }, + "advanced": true + } + ], + "requiredResourceFilters": [ + "project" + ] + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a restricted customer-owned key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.neon.tech/mcp" + }, + "guidanceMd": "Use a customer-created Neon API key. Prefer a project-scoped key for one development project; personal and organization keys reach every project they can access. Write tools start enabled and remain governed by Paperclip's action policies.", + "riskTier": "S4", + "label": "Use an API key", + "credentialFields": [ + { + "key": "authorization", + "label": "Neon API key", + "type": "password", + "required": true, + "placeholder": "napi_... or neon_project_key_...", + "secret": true, + "helperMd": "Project, branch, compute, snapshot, SQL and schema changes within the key’s reach. A project-scoped key limits access to one project with Editor rights; personal and organization keys reach every project they can access. Paperclip cannot increase an existing key’s permissions." + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://console.neon.tech/app/settings/api-keys", + "docs": "https://neon.com/docs/ai/neon-mcp-server" + }, + "warnings": [ + "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.", + "Neon recommends its hosted server for development and testing. Review write and destructive actions before execution." + ], + "tenantFields": [ + { + "key": "projectId", + "label": "Pin to project ID", + "type": "text", + "advanced": true, + "placeholder": "Optional Neon project ID", + "helperMd": "Optional. Restrict this connection to one project. Copy the project ID from Neon Console → Project settings → General.", + "validation": { + "pattern": "^[a-z0-9-]+$", + "maxLength": 64 + }, + "transport": { + "location": "query", + "name": "projectId" + } + }, + { + "key": "readOnly", + "label": "Read-only mode", + "type": "checkbox", + "defaultValue": false, + "helperMd": "Enable this to limit SQL to SELECT queries and schema inspection.", + "transport": { + "location": "query", + "name": "readonly", + "format": "boolean", + "omitFalse": true + }, + "advanced": true + } + ], + "requiredResourceFilters": [ + "project" + ] + } + ] +} diff --git a/packages/shared/src/self-serve-mcp-research.json b/packages/shared/src/self-serve-mcp-research.json index 366d6d763f..ff278480b8 100644 --- a/packages/shared/src/self-serve-mcp-research.json +++ b/packages/shared/src/self-serve-mcp-research.json @@ -57,6 +57,7 @@ { "slug": "zomato", "name": "Zomato", "wave": "blocked", "status": "blocked", "docsUrl": "https://github.com/Zomato/mcp-server-manifest", "serverUrl": "https://mcp-server.zomato.com/mcp", "authMode": "provider_approval", "prerequisite": "Zomato currently limits third-party clients and requires redirect-URI allowlisting.", "riskTier": "S3" }, {"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": "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" } ] } diff --git a/scripts/ingest-app-definitions.mjs b/scripts/ingest-app-definitions.mjs index 1ecfeb2e19..928085347b 100644 --- a/scripts/ingest-app-definitions.mjs +++ b/scripts/ingest-app-definitions.mjs @@ -945,6 +945,7 @@ const categoryBySlug = { miro: "productivity", mixpanel: "analytics", netlify: "developer", + neon: "data", notion: "content", oreilly: "content", pagerduty: "developer", @@ -1018,6 +1019,11 @@ const apiKeySpec = { placeholder: "Paste your Kernel API key", }, mem0: { name: "Authorization", prefix: "Bearer ", placeholder: "Paste your Mem0 API key" }, + neon: { + name: "Authorization", + prefix: "Bearer ", + placeholder: "napi_... or neon_project_key_...", + }, oreilly: { name: "Authorization", prefix: "Bearer ", @@ -1414,6 +1420,61 @@ const specialMethodsFor = (entry) => { }), ]; } + if (entry.slug === "neon") { + // Neon's hosted server narrows itself with documented query options: + // `projectId` pins one project and `readonly=true` limits SQL to SELECT + // and schema inspection. Its repeatable `category` filter has no + // comma-joined form, so catalog narrowing stays with per-action policies. + const tenantFields = [ + { + key: "projectId", + label: "Pin to project ID", + type: "text", + advanced: true, + placeholder: "Optional Neon project ID", + helperMd: + "Optional. Restrict this connection to one project. Copy the project ID from Neon Console → Project settings → General.", + validation: { pattern: "^[a-z0-9-]+$", maxLength: 64 }, + transport: { location: "query", name: "projectId" }, + }, + { + key: "readOnly", + label: "Read-only mode", + type: "checkbox", + defaultValue: false, + helperMd: + "Enable this to limit SQL to SELECT queries and schema inspection.", + transport: { + location: "query", + name: "readonly", + format: "boolean", + omitFalse: true, + }, + }, + ]; + const warning = + "Neon recommends its hosted server for development and testing. Review write and destructive actions before execution."; + return [ + oauthMethodFor(entry, "mcp-oauth", entry.serverUrl, { + guidanceMd: + "Connect Neon in the browser. Open Advanced to pin one project or enable read-only mode. Write tools start enabled and remain governed by Paperclip's action policies.", + tenantFields, + warnings: [entry.prerequisite, warning], + requiredResourceFilters: ["project"], + }), + apiKeyMethodFor(entry, "mcp-api-key", entry.serverUrl, { + guidanceMd: + "Use a customer-created Neon API key. Prefer a project-scoped key for one development project; personal and organization keys reach every project they can access. Write tools start enabled and remain governed by Paperclip's action policies.", + consoleLinks: { + keys: "https://console.neon.tech/app/settings/api-keys", + docs: entry.docsUrl, + }, + tenantFields, + warnings: [entry.prerequisite, warning], + requiredResourceFilters: ["project"], + }), + ]; + } if (entry.slug === "youcom") { // You.com also serves a documented keyless profile at ?profile=free with a // reduced read-only tool set. That is a real user choice: try web search @@ -1515,7 +1576,7 @@ for (const entry of researchManifest.entries) { schemaVersion: 1, slug: entry.slug, name: entry.name, - description: ({ 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.", 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 6aee6ee0ec..4d14318cc5 100644 --- a/server/src/__tests__/tool-access-service.test.ts +++ b/server/src/__tests__/tool-access-service.test.ts @@ -2441,7 +2441,7 @@ describeEmbeddedPostgres("tool access service", () => { } }); - it.each(["airtable", "beehiiv", "miro", "netlify", "sentry", "supabase", "todoist", "ticktick", "hugging-face"])( + it.each(["airtable", "beehiiv", "miro", "neon", "netlify", "sentry", "supabase", "todoist", "ticktick", "hugging-face"])( "requests the reviewed read/write scopes for %s without adopting advertised admin scopes", async (slug) => { const company = await createCompany(db); @@ -5123,7 +5123,7 @@ describeEmbeddedPostgres("tool access service", () => { "youcom", ]), ); - expect(res.body.apps).toHaveLength(58); + expect(res.body.apps).toHaveLength(59); expect( res.body.apps.find((app: { slug: string }) => app.slug === "gmail") .ownershipAvailability, @@ -6132,6 +6132,52 @@ describeEmbeddedPostgres("tool access service", () => { ).rejects.toMatchObject({ status: 400 }); }); + 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); + + const pinned = await service.connectGalleryApp( + company.id, + { + galleryKey: "neon", + connectionMethodKey: "mcp-oauth", + name: "Neon pinned", + configValues: { projectId: "shy-sun-12345678", readOnly: true }, + }, + { actorType: "user", actorId: "board" }, + ); + expect(pinned.connection.config).toMatchObject({ + url: "https://mcp.neon.tech/mcp?projectId=shy-sun-12345678&readonly=true", + sourceTemplateKey: "neon", + connectionMethodKey: "mcp-oauth", + methodConfig: { projectId: "shy-sun-12345678", readOnly: true }, + }); + + // The default path sends Neon's own defaults: no pin, no readonly flag. + const unpinned = await service.connectGalleryApp( + company.id, + { galleryKey: "neon", connectionMethodKey: "mcp-oauth", name: "Neon unpinned" }, + { actorType: "user", actorId: "board" }, + ); + expect(unpinned.connection.config).toMatchObject({ + url: "https://mcp.neon.tech/mcp", + methodConfig: { readOnly: false }, + }); + + await expect( + service.connectGalleryApp( + company.id, + { + galleryKey: "neon", + connectionMethodKey: "mcp-oauth", + name: "Neon invalid", + configValues: { projectId: "Shy Sun!" }, + }, + { actorType: "user", actorId: "board" }, + ), + ).rejects.toMatchObject({ status: 400 }); + }); + it("resumes an interrupted configured OAuth draft instead of conflicting on its generated name", async () => { const company = await createCompany(db); const service = createTestToolAccessService(db); diff --git a/ui/public/brands/apps/manifest.json b/ui/public/brands/apps/manifest.json index 6b55457f13..ed490bfc21 100644 --- a/ui/public/brands/apps/manifest.json +++ b/ui/public/brands/apps/manifest.json @@ -288,6 +288,12 @@ "localAsset": "/brands/apps/netlify.svg", "darkAsset": "/brands/apps/netlify-dark.svg" }, + { + "slug": "neon", + "provider": "Neon", + "catalogVisible": true, + "localAsset": "/brands/apps/neon.png" + }, { "slug": "notion", "provider": "Notion", diff --git a/ui/public/brands/apps/neon.png b/ui/public/brands/apps/neon.png new file mode 100644 index 0000000000000000000000000000000000000000..f2eb817ad48f8bd53c5d88d8c23e8c901cd82623 GIT binary patch literal 1319 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD4M^IaWitX&oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBK3|DzL`iUdT1k0gQ7VI5W_oVoyp7Y6feIIS zx;TbZFuuKezB@Ek=J?0sGF+<80bC}^jzL+;-C{e>Y*i86p;a<7P`Y)|CRZhqEm~Vr zo!53XC2YI7ski%U1Yup<_X(QpO^VSsG1hU9@HjFJ+wF|3>$9*?SG0`7@J)Q=h#Ed1-zBf{UO@ zAd9bvqp&MFb5_Ndy#e8AdSdVLlFqK$RCM;$BsHTK#`*r!POC5DF5mWWYi#P_f3d2) z%Ir8yz-}&XQTUO{2{pR8H@@cyB-{@=0pV~cpCqK(xo@;j7_vZY4lTexP_}8Hd&Z?Pf z?tOcq@rrqooUx<+;@{@$W_MS8-R9f+QQ(Uakol=0^6LzaUF^@&uO6$q%el+_RfPYQ zyBxdH&#c-lSi*lb^6Rye&1qM^J6y2c`fR59so;2dqifL$j^A(m0V@1Ie?j%RHC2f$ zcN?U>PQEXr^}77M#+|tfq(A+bxc6Aqf5!{GhO75kJDRU>+xzpxzC&wS?ye}^JzsY2 z>ifowKgAu*&v3m0idk%aHJj7wK<~zTyB9C-jy`>X)5;~*ac%Wg=lyz)7ZTg*Vw)CJ zKl{}AvZ(Y~(#xb1o3k9hpSfDRf9lJ<@>6dH@1LfZb;WrPP^oRn(>q6tJ{|pjx;4LE zBbMjmE*79mXO^p-K67#9rK1I#n~UazgU`<%+?Xuizt?`Q6I_s_55y~MwA zO!BzEyQ{3S?A`2Nf1V%bHWw~Ae6H?C>T^yHDM#~leh%mAK7>AGb7Z&pf3$qDdUw0^ z{|Fz27Xq7?pX*Cyx@6_ZK2!d6X>3l`%ms(fxZf?^xH7bfFXVXDEEmoK^MZf%j89 zMI=jV4X^y4d?#*h!zq zc?*6X{}b{4(#QE;t69$azKVGNMf&)xxeXk%ZfBJORo@Bh?)IJN&m}p3*A0`s@12al wKUynMyH1?r;R#1FVdQ&MBb@00P-VKmY&$ literal 0 HcmV?d00001 diff --git a/ui/src/lib/app-gallery-copy.ts b/ui/src/lib/app-gallery-copy.ts index fb253b23c0..cfb964f727 100644 --- a/ui/src/lib/app-gallery-copy.ts +++ b/ui/src/lib/app-gallery-copy.ts @@ -74,6 +74,10 @@ const APP_COPY: Record = { tagline: "Explore product usage, errors, flags, and experiments.", short: "Sign in with PostHog. Project pinning and access controls are optional.", }, + neon: { + tagline: "Manage Postgres projects, branches, and queries.", + short: "Sign in with Neon. Project pinning and read-only mode are optional.", + }, 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 82309bf53e..2d306338dd 100644 --- a/ui/src/pages/apps/AppsConnect.test.tsx +++ b/ui/src/pages/apps/AppsConnect.test.tsx @@ -59,6 +59,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 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")!; @@ -1842,6 +1843,48 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { expect(container.textContent).not.toContain("Pick the app you want your agents to use."); }); + 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] }); + await render(); + await openAccessAdvanced(); + + expect(radioContaining("Sign in with Neon")?.getAttribute("aria-checked")).toBe("true"); + expect(buttonByText("Continue to sign in")?.disabled).toBe(false); + + await act(async () => { + radioContaining("Use an API key")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const keyInput = container.querySelector('input[type="password"]'); + expect(keyInput).toBeTruthy(); + expect(container.textContent).toContain("Pin to project ID"); + expect(container.querySelector('input[placeholder="Optional Neon project ID"]')).toBeTruthy(); + expect(container.querySelector('[role="switch"]')?.getAttribute("aria-checked")).toBe("false"); + // The key is the only required input on this method: Connect waits for it + // and for nothing else, since both narrowing controls are optional. + expect(buttonByText("Connect")?.disabled).toBe(true); + + await act(async () => { + setInputValue(keyInput!, "napi_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: "neon", + connectionMethodKey: "mcp-api-key", + credentialValues: { "credentials.authorization": "napi_test-key" }, + configValues: { readOnly: false }, + })); + }); + it("connects PostHog without a project ID and keeps optional controls advanced", async () => { mockParams.appKey = "posthog"; listGalleryMock.mockResolvedValueOnce({ apps: [POSTHOG] });