From 34f2bb78ee1fc42e197001fc886d6e94d47b17e4 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 11 Nov 2026 09:26:24 -0600 Subject: [PATCH] feat(connections): connect Sentry with a long-lived user auth token (#20823) 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 use app connections to call provider tools, such as the Sentry MCP server > - The Sentry connection has only one method: browser sign-in through the hosted MCP OAuth server > - That grant expires. An unattended triage agent loses Sentry access until a person signs in again > - During that time, a scheduled triage routine completes each run but reads no Sentry data > - This pull request adds a second Sentry method that uses a user auth token > - The benefit is a Sentry connection that stays valid until a person revokes the token ## Linked Issues or Issue Description **Subsystem affected** App connections: the Sentry entry in the connection catalog. **Problem or motivation** A Sentry connection that was created with browser sign-in on 2026-10-11 changed to "OAuth authorization expired. Reconnect this app to continue." at 04:01 UTC on 2026-11-10. The agent that used it had no Sentry tools after that time. Each hourly triage run reported the blocker but could not continue. Unattended agents need a Sentry connection that does not need a person to sign in again each month. **Proposed solution** The hosted Sentry MCP server accepts a Sentry user auth token in the `Authorization: Sentry-Bearer ` header. It reserves `Bearer` for its own OAuth tokens. Paperclip API-key methods already support a custom header prefix. Add a Sentry API-key method that uses this header. **Alternatives considered** Reconnect with browser sign-in again. That restores access only until the next grant expiry. ## What Changed - Add the `mcp-user-token` method ("Use a user auth token") to Sentry in `scripts/ingest-app-definitions.mjs`. It uses the same server URL and sends the token as `Authorization: Sentry-Bearer `. - Regenerate `packages/shared/src/app-definitions/sentry.json`. I did not include unrelated output that the content corpus drift caused for other providers. - Add a permission review entry for the new method in `doc/connections/tool-method-permission-reviews.json`. - Add a test for the new method and its header prefix. - Document the token method, its required scopes, and its ownership risk in `doc/connections/SENTRY.md`. ## Verification - `pnpm exec vitest run packages/shared/src/app-definitions.test.ts`: 40 passed. The new assertion fails on `master` because Sentry has only one method there. - `pnpm exec vitest run src/__tests__/tool-access-service.test.ts -t Sentry` (server): 2 passed. - `pnpm exec vitest run ui/src/pages/apps/app-connect-policy.test.ts server/src/__tests__/satisfied-connection-intents.test.ts`: 64 passed. - `pnpm verify` returned exit 2 (a doc is a global CI input). Required CI supplies the broad coverage. - Live probe of `https://mcp.sentry.dev/mcp` with an `initialize` request: `Authorization: Sentry-Bearer ` starts an MCP session (the token is checked on tool calls). `Authorization: Bearer ` returns `401 invalid_token`. This confirms the server accepts the header scheme. - Not done yet: a real agent tool call with a real token after deployment. ## Risks - Low risk. This adds a method. The existing OAuth method and saved connections do not change. - A user auth token acts as its creator and lasts until revoked. The method guidance and docs say this. Rotate the token when that person leaves. - If Sentry changes the `Sentry-Bearer` scheme, connections that use this method fail on tool calls. The OAuth method still works. > 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`) in Claude Code, with tool use (shell, browser, web search). ## 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- doc/connections/SENTRY.md | 19 ++++++ .../tool-method-permission-reviews.json | 16 +++++ packages/shared/src/app-definitions.test.ts | 9 +++ .../shared/src/app-definitions/sentry.json | 40 +++++++++++++ scripts/ingest-app-definitions.mjs | 58 ++++++++++++++----- .../runner-e2e/source-sdk-public-inputs.json | 4 +- 6 files changed, 131 insertions(+), 15 deletions(-) diff --git a/doc/connections/SENTRY.md b/doc/connections/SENTRY.md index 8a05839ee3d..b689834bd57 100644 --- a/doc/connections/SENTRY.md +++ b/doc/connections/SENTRY.md @@ -4,6 +4,25 @@ Paperclip connects to Sentry's [official MCP server](https://mcp.sentry.dev/) at `https://mcp.sentry.dev/mcp`. Browser sign-in uses the MCP authorization server. A normal connection does not require a customer-created OAuth app. +## User auth token + +Browser sign-in grants expire. A staging company connected on 2026-10-11 +needed reauthorization at 04:01 UTC on 2026-11-10, and its agents lost Sentry +access until a person signed in again. For unattended agents such as a triage +routine, use **Use a user auth token** instead. + +Create a [user auth token](https://sentry.io/settings/account/api/auth-tokens/) +with `org:read`, `project:read`, `project:write`, `team:read`, `team:write` and +`event:write`. Paperclip sends it to the same hosted server as +`Authorization: Sentry-Bearer `. The hosted server reserves `Bearer` for +its own OAuth tokens and forwards a `Sentry-Bearer` token to Sentry without +storing or refreshing it. See the +[Sentry MCP README](https://github.com/getsentry/sentry-mcp#remote-server). + +The token acts as the person who created it and is limited by that person's +organization role. It lasts until revoked, so rotate it if that person leaves +the organization. + ## Discovery The method pins the [MCP protected-resource metadata](https://mcp.sentry.dev/.well-known/oauth-protected-resource/mcp). diff --git a/doc/connections/tool-method-permission-reviews.json b/doc/connections/tool-method-permission-reviews.json index 3ee97832ed1..405ad058330 100644 --- a/doc/connections/tool-method-permission-reviews.json +++ b/doc/connections/tool-method-permission-reviews.json @@ -2016,6 +2016,22 @@ "liveProof": "not-run", "evidenceLimit": "Provider documentation was unavailable or not readable during this review; metadata/registered source only. Account-bound proof remains outstanding." }, + { + "app": "sentry", + "method": "mcp-user-token", + "auth": "api_key", + "policy": "provider-key", + "requestedScopes": [], + "capability": "provider-controlled", + "supportedActions": "Sentry API actions allowed by the token's scopes and its creator's organization role, including issue triage and project/team changes.", + "evidence": [ + "https://github.com/getsentry/sentry-mcp#remote-server", + "https://docs.sentry.io/account/auth-tokens/" + ], + "reviewedAt": "2026-11-11", + "liveProof": "not-run", + "keyPermissions": "Create a user auth token with org:read, project:read, project:write, team:read, team:write and event:write. The token acts as its creator; Paperclip cannot increase an existing token's permissions." + }, { "app": "shopify", "method": "ucp-commerce", diff --git a/packages/shared/src/app-definitions.test.ts b/packages/shared/src/app-definitions.test.ts index e0584a8d9eb..3c048c63a6d 100644 --- a/packages/shared/src/app-definitions.test.ts +++ b/packages/shared/src/app-definitions.test.ts @@ -717,6 +717,15 @@ describe("AppDefinition catalog", () => { discoveryUrl: "https://mcp.sentry.dev/.well-known/oauth-protected-resource/mcp", }, }); + // A user auth token does not expire with the 30-day browser grant. The + // hosted server reserves `Bearer` for its own OAuth tokens. + expect(APP_DEFINITIONS.find((app) => app.slug === "sentry")?.methods[1]).toMatchObject({ + key: "mcp-user-token", + transport: "mcp_remote", + auth: "api_key", + defaults: { serverUrl: "https://mcp.sentry.dev/mcp" }, + keyPlacement: { location: "header", name: "Authorization", prefix: "Sentry-Bearer " }, + }); expect(method("zapier")).toMatchObject({ key: "generated-url", auth: "none", diff --git a/packages/shared/src/app-definitions/sentry.json b/packages/shared/src/app-definitions/sentry.json index 5a85dd024f1..0afeca8c0e7 100644 --- a/packages/shared/src/app-definitions/sentry.json +++ b/packages/shared/src/app-definitions/sentry.json @@ -42,6 +42,46 @@ "project", "environment" ] + }, + { + "key": "mcp-user-token", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a user auth token when an unattended agent needs Sentry access that does not expire with a browser sign-in grant.", + "defaults": { + "serverUrl": "https://mcp.sentry.dev/mcp" + }, + "guidanceMd": "Connect with a Sentry user auth token for unattended agents. Browser sign-in grants expire and need the owner to sign in again; a user auth token lasts until you revoke it.", + "riskTier": "S2", + "label": "Use a user auth token", + "credentialFields": [ + { + "key": "authorization", + "label": "Sentry user auth token", + "type": "password", + "required": true, + "placeholder": "sntryu_...", + "secret": true, + "helperMd": "Create a user auth token with org:read, project:read, project:write, team:read, team:write and event:write. The token acts as its creator; Paperclip cannot increase an existing token's permissions." + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Sentry-Bearer " + }, + "consoleLinks": { + "keys": "https://sentry.io/settings/account/api/auth-tokens/", + "docs": "https://github.com/getsentry/sentry-mcp#remote-server" + }, + "requiredResourceFilters": [ + "organization", + "project", + "environment" + ] } ], "docsUrl": "https://mcp.sentry.dev/.well-known/oauth-authorization-server", diff --git a/scripts/ingest-app-definitions.mjs b/scripts/ingest-app-definitions.mjs index 6eb77ca0ca9..782ad7e03f6 100644 --- a/scripts/ingest-app-definitions.mjs +++ b/scripts/ingest-app-definitions.mjs @@ -776,19 +776,51 @@ const apps = [ "developer", "sentry.io", ["https://mcp.sentry.dev/*"], - method( - "mcp-oauth", - "mcp_remote", - "oauth", - { - serverUrl: "https://mcp.sentry.dev/mcp", - discoveryUrl: - "https://mcp.sentry.dev/.well-known/oauth-protected-resource/mcp", - }, - "S2", - "Connect the Sentry organization and projects agents need for incident work.", - { requiredResourceFilters: ["organization", "project", "environment"] }, - ), + [ + method( + "mcp-oauth", + "mcp_remote", + "oauth", + { + serverUrl: "https://mcp.sentry.dev/mcp", + discoveryUrl: + "https://mcp.sentry.dev/.well-known/oauth-protected-resource/mcp", + }, + "S2", + "Connect the Sentry organization and projects agents need for incident work.", + { requiredResourceFilters: ["organization", "project", "environment"] }, + ), + method( + "mcp-user-token", + "mcp_remote", + "api_key", + { serverUrl: "https://mcp.sentry.dev/mcp" }, + "S2", + "Connect with a Sentry user auth token for unattended agents. Browser sign-in grants expire and need the owner to sign in again; a user auth token lasts until you revoke it.", + { + label: "Use a user auth token", + whenToUse: + "Use a user auth token when an unattended agent needs Sentry access that does not expire with a browser sign-in grant.", + credentialFields: [ + { + ...field("authorization", "Sentry user auth token", "sntryu_..."), + helperMd: + "Create a user auth token with org:read, project:read, project:write, team:read, team:write and event:write. The token acts as its creator, limited to that person's organization role.", + }, + ], + keyPlacement: { + location: "header", + name: "Authorization", + prefix: "Sentry-Bearer ", + }, + consoleLinks: { + keys: "https://sentry.io/settings/account/api/auth-tokens/", + docs: "https://github.com/getsentry/sentry-mcp#remote-server", + }, + requiredResourceFilters: ["organization", "project", "environment"], + }, + ), + ], ], [ "vercel", diff --git a/tests/runner-e2e/source-sdk-public-inputs.json b/tests/runner-e2e/source-sdk-public-inputs.json index b452ede9dde..ca7eb258c48 100644 --- a/tests/runner-e2e/source-sdk-public-inputs.json +++ b/tests/runner-e2e/source-sdk-public-inputs.json @@ -72,7 +72,7 @@ "packages/shared/src/app-definitions-url.test.ts": "7378d29fd8428e4807dd234b495aa9043f40e6d7", "packages/shared/src/app-definitions.generated.ts": "d74319a5c9684633b4a0a23c8bfa682e0d3bf267", "packages/shared/src/app-definitions.ingestion-report.json": "a7b60c7c16a484a411989315e9f08d48258fa647", - "packages/shared/src/app-definitions.test.ts": "e0584a8d9eb648d3cc42d594a2e5617240475dab", + "packages/shared/src/app-definitions.test.ts": "3c048c63a6d2c630a984f6f10e1dbdd9bf7c4f68", "packages/shared/src/app-definitions.ts": "eaadd6b7831cc23998a1a3ec6fd1d3a5032a503b", "packages/shared/src/app-definitions/1password.json": "c11a4777de2c78805b2d391da5eb075a9511e0f8", "packages/shared/src/app-definitions/agentmail.json": "edd0799b1980d0f5f43b89c8f7a1173dc623f7fa", @@ -159,7 +159,7 @@ "packages/shared/src/app-definitions/resend.json": "38f7595265f9ff6afdc84b9bce18602cd2c6ebfa", "packages/shared/src/app-definitions/responses-api.json": "29ad92e4c41a0dc0e3a3b07f518200bdc8e2e6ff", "packages/shared/src/app-definitions/sanity.json": "15968d52eb75008b170f45a82e79e0b77516b50d", - "packages/shared/src/app-definitions/sentry.json": "5a85dd024f11382a802557b0c5374b856e29b56a", + "packages/shared/src/app-definitions/sentry.json": "0afeca8c0e76be5d8324fa6d18fb970a38bb2b88", "packages/shared/src/app-definitions/shopify.json": "f6bff7db55083d96060d8bd46a08aa38b33c3080", "packages/shared/src/app-definitions/similarweb.json": "37a912287b7ad323cf783eb32d01b943a93e83fd", "packages/shared/src/app-definitions/slack.json": "733de01cdeec0dc66d732639925c9bb9fb7dc53d",