diff --git a/README.md b/README.md
index b149439c..c31f26bc 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,15 @@ Launch the transport:
npx @sentry/mcp-server@latest --access-token=sentry-user-token
```
+If you've already authenticated with the [Sentry CLI](https://cli.sentry.dev/),
+you can omit the token and the stdio server will reuse the token from
+`~/.sentry/cli.db`:
+
+```shell
+sentry auth
+npx @sentry/mcp-server@latest
+```
+
Need to connect to a self-hosted deployment? Add --host (hostname
only, e.g. --host=sentry.example.com) when you run the command.
@@ -65,7 +74,7 @@ npx @sentry/mcp-server@latest --access-token=TOKEN --host=sentry.example.com --d
#### Environment Variables
```shell
-SENTRY_ACCESS_TOKEN= # Required: Your Sentry auth token
+SENTRY_ACCESS_TOKEN= # Optional: explicit auth token override
# LLM Provider Configuration (required for AI-powered search tools)
EMBEDDED_AGENT_PROVIDER= # Required: 'openai' or 'anthropic'
@@ -213,7 +222,7 @@ pnpm -w run cli --agent "who am I?"
# Test against production
pnpm -w run cli --mcp-host=https://mcp.sentry.dev "query"
-# Test with local stdio mode (requires SENTRY_ACCESS_TOKEN)
+# Test with local stdio mode (explicit token or Sentry CLI auth)
pnpm -w run cli --access-token=TOKEN "query"
```
diff --git a/docs/releases/stdio.md b/docs/releases/stdio.md
index 7cb958e4..3570947a 100644
--- a/docs/releases/stdio.md
+++ b/docs/releases/stdio.md
@@ -128,8 +128,14 @@ Add to `.cursor/mcp.json`:
## Environment Variables
-Required:
-- `SENTRY_ACCESS_TOKEN` - Sentry API access token
+Authentication:
+- `SENTRY_ACCESS_TOKEN` - Explicit Sentry API access token
+
+If the user has already authenticated with the
+[Sentry CLI](https://cli.sentry.dev/commands/auth/), the stdio server can reuse
+the token stored in `~/.sentry/cli.db` without requiring `SENTRY_ACCESS_TOKEN`.
+
+Host:
- `SENTRY_HOST` - Sentry instance hostname (default: `sentry.io`)
Optional:
@@ -162,7 +168,7 @@ npm pack
npm install -g ./sentry-mcp-server-1.2.3.tgz
# Run stdio server
-SENTRY_ACCESS_TOKEN=... @sentry/mcp-server
+@sentry/mcp-server
```
### Beta Releases
diff --git a/docs/testing-stdio.md b/docs/testing-stdio.md
index ff060e8a..dc399987 100644
--- a/docs/testing-stdio.md
+++ b/docs/testing-stdio.md
@@ -91,6 +91,11 @@ cd packages/mcp-server
pnpm start --access-token=YOUR_TOKEN
```
+If you've already authenticated with the
+[Sentry CLI](https://cli.sentry.dev/commands/auth/), you can omit
+`--access-token` and the stdio server will reuse the token stored in
+`~/.sentry/cli.db`.
+
This uses `tsx` to run TypeScript directly without building.
### Option 2: Using the Built Package (Production-like)
@@ -103,6 +108,9 @@ node packages/mcp-server/dist/index.js --access-token=YOUR_TOKEN
# Or use the workspace command
pnpm -w run mcp-server --access-token=YOUR_TOKEN
+
+# Or reuse stored Sentry CLI auth
+node packages/mcp-server/dist/index.js
```
### Option 3: Using npx (End-user Experience)
@@ -117,6 +125,9 @@ npx @sentry/mcp-server@latest --access-token=YOUR_TOKEN
cd packages/mcp-server
pnpm pack
npx ./sentry-mcp-server-*.tgz --access-token=YOUR_TOKEN
+
+# Or reuse stored Sentry CLI auth
+npx @sentry/mcp-server@latest
```
## Testing with MCP Inspector
@@ -206,7 +217,8 @@ The `mcp-test-client` package provides a CLI-based way to test the stdio transpo
The CLI client automatically selects the transport based on flags:
-- **Stdio transport**: `--access-token` flag provided
+- **Stdio transport**: explicit token provided or
+ [Sentry CLI](https://cli.sentry.dev/commands/auth/)-managed auth state available
- **Remote HTTP transport**: `--mcp-host` flag or no access token
### Basic Usage
@@ -374,7 +386,7 @@ Add to `.vscode/settings.json`:
```bash
# Basic usage
---access-token=TOKEN # Sentry access token (required)
+--access-token=TOKEN # Optional explicit Sentry access token
# Host configuration
--host=sentry.example.com # Self-hosted Sentry (hostname only)
@@ -497,18 +509,26 @@ pnpm build
node dist/index.js --access-token=TOKEN
```
-### "Missing required parameter: access-token"
+### "No access token was provided"
**Cause:** No authentication provided.
+Reuse stored auth by running
+[Sentry CLI auth](https://cli.sentry.dev/commands/auth/) first:
+
**Solution:**
```bash
-# Option 1: CLI flag
+# Option 1: Reuse stored Sentry CLI auth
+sentry auth
+pnpm start
+
+# Option 2: CLI flag
pnpm start --access-token=YOUR_TOKEN
-# Option 2: Environment variable
+# Option 3: Environment variable
export SENTRY_ACCESS_TOKEN=YOUR_TOKEN
pnpm start
+
```
### "AI-powered search tools unavailable"
diff --git a/packages/mcp-cloudflare/src/client/components/fragments/stdio-setup.tsx b/packages/mcp-cloudflare/src/client/components/fragments/stdio-setup.tsx
index f232d008..6aec8b3c 100644
--- a/packages/mcp-cloudflare/src/client/components/fragments/stdio-setup.tsx
+++ b/packages/mcp-cloudflare/src/client/components/fragments/stdio-setup.tsx
@@ -43,6 +43,12 @@ export default function StdioSetup() {
Create a User Auth Token in your account settings with the following
scopes:
+
+ If you've already authenticated with the{" "}
+ Sentry CLI,
+ you can skip the explicit token and the stdio server will reuse the
+ token stored in ~/.sentry/cli.db.
+
AI-powered search: If you want the
search_events and search_issues tools to
@@ -89,7 +95,11 @@ export default function StdioSetup() {
--access-token / SENTRY_ACCESS_TOKEN
- Required user auth token.
+
+ Explicit user auth token override. If omitted, the stdio server
+ falls back to Sentry CLI auth from ~/.sentry/cli.db
+ when available.
+
--host / SENTRY_HOST
diff --git a/packages/mcp-core/package.json b/packages/mcp-core/package.json
index fa478a67..1d52612c 100644
--- a/packages/mcp-core/package.json
+++ b/packages/mcp-core/package.json
@@ -121,6 +121,10 @@
"types": "./dist/internal/agents/types.ts",
"default": "./dist/internal/agents/types.js"
},
+ "./internal/sentry-cli-auth": {
+ "types": "./dist/internal/sentry-cli-auth.ts",
+ "default": "./dist/internal/sentry-cli-auth.js"
+ },
"./utils/url-utils": {
"types": "./dist/utils/url-utils.ts",
"default": "./dist/utils/url-utils.js"
@@ -157,6 +161,7 @@
"@logtape/sentry": "^1.1.1",
"@modelcontextprotocol/sdk": "catalog:",
"@sentry/core": "catalog:",
+ "better-sqlite3": "catalog:",
"ai": "catalog:",
"dotenv": "catalog:",
"zod": "catalog:"
diff --git a/packages/mcp-core/src/internal/sentry-cli-auth.ts b/packages/mcp-core/src/internal/sentry-cli-auth.ts
new file mode 100644
index 00000000..6fe6e15b
--- /dev/null
+++ b/packages/mcp-core/src/internal/sentry-cli-auth.ts
@@ -0,0 +1,64 @@
+import Database from "better-sqlite3";
+import { homedir } from "node:os";
+import { join } from "node:path";
+
+const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
+
+export type SentryCliAccessTokenSource = "sentry_cli_db";
+
+type AuthRow = {
+ token: string | null;
+ expires_at: number | null;
+};
+
+export function normalizeAccessToken(
+ token?: string | null,
+): string | undefined {
+ const trimmed = token?.trim();
+ return trimmed ? trimmed : undefined;
+}
+
+export function getSentryCliDbPath(homeDir = homedir()): string {
+ return join(homeDir, ".sentry", "cli.db");
+}
+
+export function readAccessTokenFromSentryCliDb({
+ nowMs,
+ homeDir,
+}: {
+ nowMs: number;
+ homeDir?: string;
+}): string | undefined {
+ const cliDbPath = getSentryCliDbPath(homeDir);
+
+ try {
+ const db = new Database(cliDbPath, {
+ readonly: true,
+ fileMustExist: true,
+ });
+
+ try {
+ const row = db
+ .prepare("SELECT token, expires_at FROM auth WHERE id = 1")
+ .get() as AuthRow | undefined;
+ const token = normalizeAccessToken(row?.token);
+
+ if (!token) {
+ return undefined;
+ }
+
+ if (
+ typeof row?.expires_at === "number" &&
+ nowMs + TOKEN_EXPIRY_BUFFER_MS >= row.expires_at
+ ) {
+ return undefined;
+ }
+
+ return token;
+ } finally {
+ db.close();
+ }
+ } catch {
+ return undefined;
+ }
+}
diff --git a/packages/mcp-core/tsconfig.json b/packages/mcp-core/tsconfig.json
index d4fe4fe1..1984152b 100644
--- a/packages/mcp-core/tsconfig.json
+++ b/packages/mcp-core/tsconfig.json
@@ -5,5 +5,6 @@
"outDir": "dist",
"rootDir": "src"
},
+ "files": ["../mcp-server-tsconfig/better-sqlite3.d.ts"],
"include": ["src"]
}
diff --git a/packages/mcp-core/tsdown.config.ts b/packages/mcp-core/tsdown.config.ts
index 3cdb3e43..ce1c78b2 100644
--- a/packages/mcp-core/tsdown.config.ts
+++ b/packages/mcp-core/tsdown.config.ts
@@ -14,6 +14,7 @@ export default defineConfig({
external: [
// Keep workspace dependencies external (don't bundle them)
"@sentry/mcp-server-mocks",
+ "better-sqlite3",
],
env: {
DEFAULT_SENTRY_DSN:
diff --git a/packages/mcp-server-tsconfig/better-sqlite3.d.ts b/packages/mcp-server-tsconfig/better-sqlite3.d.ts
new file mode 100644
index 00000000..4c8e01dc
--- /dev/null
+++ b/packages/mcp-server-tsconfig/better-sqlite3.d.ts
@@ -0,0 +1,20 @@
+declare module "better-sqlite3" {
+ export type DatabaseOptions = {
+ readonly?: boolean;
+ fileMustExist?: boolean;
+ };
+
+ export type Statement> = {
+ get(...params: unknown[]): Result | undefined;
+ run(...params: unknown[]): unknown;
+ };
+
+ export default class Database {
+ constructor(filename: string, options?: DatabaseOptions);
+ prepare>(
+ source: string,
+ ): Statement;
+ exec(source: string): this;
+ close(): this;
+ }
+}
diff --git a/packages/mcp-server-tsconfig/package.json b/packages/mcp-server-tsconfig/package.json
index ab5d8e96..505db09c 100644
--- a/packages/mcp-server-tsconfig/package.json
+++ b/packages/mcp-server-tsconfig/package.json
@@ -2,8 +2,5 @@
"name": "@sentry/mcp-server-tsconfig",
"version": "0.29.0",
"private": true,
- "files": [
- "tsconfig.base.json",
- "tsconfig.vite.json"
- ]
+ "files": ["better-sqlite3.d.ts", "tsconfig.base.json", "tsconfig.vite.json"]
}
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index ac7b0efe..230f0450 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -13,11 +13,7 @@
"license": "FSL-1.1-ALv2",
"author": "Sentry",
"homepage": "https://github.com/getsentry/sentry-mcp",
- "keywords": [
- "sentry",
- "mcp",
- "model-context-protocol"
- ],
+ "keywords": ["sentry", "mcp", "model-context-protocol"],
"bugs": {
"url": "https://github.com/getsentry/sentry-mcp/issues"
},
@@ -28,9 +24,7 @@
"bin": {
"sentry-mcp": "./dist/index.js"
},
- "files": [
- "./dist/*"
- ],
+ "files": ["./dist/*"],
"exports": {
".": {
"types": "./dist/index.ts",
@@ -53,6 +47,7 @@
"@modelcontextprotocol/sdk": "catalog:",
"@sentry/node": "catalog:",
"@sentry/core": "catalog:",
+ "better-sqlite3": "catalog:",
"dotenv": "catalog:",
"zod": "catalog:"
},
diff --git a/packages/mcp-server/src/cli/auth.test.ts b/packages/mcp-server/src/cli/auth.test.ts
new file mode 100644
index 00000000..5a9d5a6f
--- /dev/null
+++ b/packages/mcp-server/src/cli/auth.test.ts
@@ -0,0 +1,118 @@
+import Database from "better-sqlite3";
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import { afterEach, describe, expect, it } from "vitest";
+import { resolveAccessToken } from "./auth";
+
+function createCliDb({
+ token,
+ expiresAt,
+}: {
+ token?: string | null;
+ expiresAt?: number | null;
+}) {
+ const homeDir = mkdtempSync(join(tmpdir(), "sentry-mcp-auth-"));
+ const configDir = join(homeDir, ".sentry");
+ mkdirSync(configDir);
+ const dbPath = join(configDir, "cli.db");
+ const db = new Database(dbPath);
+
+ db.exec(`
+ CREATE TABLE auth (
+ id INTEGER PRIMARY KEY CHECK (id = 1),
+ token TEXT,
+ refresh_token TEXT,
+ expires_at INTEGER,
+ issued_at INTEGER,
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
+ );
+ `);
+
+ db.prepare("INSERT INTO auth (id, token, expires_at) VALUES (1, ?, ?)").run(
+ token ?? null,
+ expiresAt ?? null,
+ );
+ db.close();
+
+ return homeDir;
+}
+
+const tempDirs: string[] = [];
+
+afterEach(() => {
+ for (const tempDir of tempDirs.splice(0)) {
+ rmSync(tempDir, { recursive: true, force: true });
+ }
+});
+
+describe("resolveAccessToken", () => {
+ it("prefers explicit access token over env and cli db", () => {
+ const nowMs = Date.now();
+ const configDir = createCliDb({
+ token: "db-token",
+ expiresAt: nowMs + 10 * 60_000,
+ });
+ tempDirs.push(configDir);
+
+ const result = resolveAccessToken({
+ accessToken: "flag-token",
+ nowMs,
+ homeDir: configDir,
+ });
+
+ expect(result).toEqual({
+ accessToken: "flag-token",
+ source: "flag-or-env",
+ });
+ });
+
+ it("uses cli.db when explicit token is not provided", () => {
+ const nowMs = Date.now();
+ const configDir = createCliDb({
+ token: "db-token",
+ expiresAt: nowMs + 10 * 60_000,
+ });
+ tempDirs.push(configDir);
+
+ const result = resolveAccessToken({
+ nowMs,
+ homeDir: configDir,
+ });
+
+ expect(result).toEqual({
+ accessToken: "db-token",
+ source: "sentry_cli_db",
+ });
+ });
+
+ it("ignores expired cli.db tokens", () => {
+ const nowMs = Date.now();
+ const configDir = createCliDb({
+ token: "db-token",
+ expiresAt: nowMs + 4 * 60_000,
+ });
+ tempDirs.push(configDir);
+
+ const result = resolveAccessToken({
+ nowMs,
+ homeDir: configDir,
+ });
+
+ expect(result).toEqual({});
+ });
+
+ it("ignores missing or unreadable cli.db state", () => {
+ const homeDir = mkdtempSync(join(tmpdir(), "sentry-mcp-auth-"));
+ const configDir = join(homeDir, ".sentry");
+ mkdirSync(configDir);
+ tempDirs.push(homeDir);
+ writeFileSync(join(configDir, "cli.db"), "not-a-sqlite-db");
+
+ const result = resolveAccessToken({
+ homeDir,
+ });
+
+ expect(result).toEqual({});
+ });
+});
diff --git a/packages/mcp-server/src/cli/auth.ts b/packages/mcp-server/src/cli/auth.ts
new file mode 100644
index 00000000..e9605776
--- /dev/null
+++ b/packages/mcp-server/src/cli/auth.ts
@@ -0,0 +1,45 @@
+import {
+ getSentryCliDbPath,
+ normalizeAccessToken,
+ readAccessTokenFromSentryCliDb,
+ type SentryCliAccessTokenSource,
+} from "@sentry/mcp-core/internal/sentry-cli-auth";
+
+export type AccessTokenSource = "flag-or-env" | SentryCliAccessTokenSource;
+
+export type ResolvedAccessToken = {
+ accessToken?: string;
+ source?: AccessTokenSource;
+};
+
+export function getCliDbPath(homeDir?: string): string {
+ return getSentryCliDbPath(homeDir);
+}
+
+export function resolveAccessToken({
+ accessToken,
+ nowMs = Date.now(),
+ homeDir,
+}: {
+ accessToken?: string;
+ nowMs?: number;
+ homeDir?: string;
+}): ResolvedAccessToken {
+ const normalizedAccessToken = normalizeAccessToken(accessToken);
+ if (normalizedAccessToken) {
+ return {
+ accessToken: normalizedAccessToken,
+ source: "flag-or-env",
+ };
+ }
+
+ const cliDbToken = readAccessTokenFromSentryCliDb({ nowMs, homeDir });
+ if (cliDbToken) {
+ return {
+ accessToken: cliDbToken,
+ source: "sentry_cli_db",
+ };
+ }
+
+ return {};
+}
diff --git a/packages/mcp-server/src/cli/resolve.ts b/packages/mcp-server/src/cli/resolve.ts
index dbf934f9..7bdda586 100644
--- a/packages/mcp-server/src/cli/resolve.ts
+++ b/packages/mcp-server/src/cli/resolve.ts
@@ -18,7 +18,7 @@ export function finalize(input: MergedArgs): ResolvedConfig {
// Access token required
if (!input.accessToken) {
throw new Error(
- "Error: No access token was provided. Pass one with `--access-token` or via `SENTRY_ACCESS_TOKEN`.",
+ "Error: No access token was provided. Pass one with `--access-token`, via `SENTRY_ACCESS_TOKEN`, or authenticate with `sentry auth`.",
);
}
diff --git a/packages/mcp-server/src/cli/usage.ts b/packages/mcp-server/src/cli/usage.ts
index d7887f85..da891125 100644
--- a/packages/mcp-server/src/cli/usage.ts
+++ b/packages/mcp-server/src/cli/usage.ts
@@ -4,10 +4,12 @@ export function buildUsage(
packageName: string,
allSkills: ReadonlyArray,
): string {
- return `Usage: ${packageName} --access-token= [--host=]
+ return `Usage: ${packageName} [--access-token=] [--host=]
-Required:
+Authentication:
--access-token Sentry User Auth Token with API access
+ If omitted, falls back to SENTRY_ACCESS_TOKEN,
+ or \`sentry auth\` state.
Common optional flags:
--host Change Sentry host (self-hosted)
@@ -34,12 +36,14 @@ All skills: ${allSkills.join(", ")}
Environment variables:
SENTRY_ACCESS_TOKEN Sentry auth token (alternative to --access-token)
+ SENTRY_URL Full Sentry HTTPS URL (alternative to SENTRY_HOST)
OPENAI_API_KEY OpenAI API key for AI-powered search tools
ANTHROPIC_API_KEY Anthropic API key for AI-powered search tools
EMBEDDED_AGENT_PROVIDER Provider override: openai or anthropic
MCP_DISABLE_SKILLS Disable specific skills (comma-separated)
Examples:
+ ${packageName}
${packageName} --access-token=TOKEN
${packageName} --access-token=TOKEN --skills=inspect,triage
${packageName} --access-token=TOKEN --host=sentry.example.com
diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts
index 049edd14..16010a1b 100644
--- a/packages/mcp-server/src/index.ts
+++ b/packages/mcp-server/src/index.ts
@@ -21,6 +21,7 @@ import { LIB_VERSION } from "@sentry/mcp-core/version";
import { buildUsage } from "./cli/usage";
import { parseArgv, parseEnv, merge } from "./cli/parse";
import { finalize } from "./cli/resolve";
+import { getCliDbPath, resolveAccessToken } from "./cli/auth";
import { sentryBeforeSend } from "@sentry/mcp-core/telem/sentry";
import { SKILLS } from "@sentry/mcp-core/skills";
import {
@@ -58,7 +59,22 @@ if (cli.unknownArgs.length > 0) {
const env = parseEnv(process.env);
const cfg = (() => {
try {
- return finalize(merge(cli, env));
+ const merged = merge(cli, env);
+ const resolvedAccessToken = resolveAccessToken({
+ accessToken: merged.accessToken,
+ });
+
+ if (resolvedAccessToken.source === "sentry_cli_db") {
+ console.warn(
+ `Using access token from Sentry CLI auth state in ${getCliDbPath()}.`,
+ );
+ console.warn("");
+ }
+
+ return finalize({
+ ...merged,
+ accessToken: resolvedAccessToken.accessToken,
+ });
} catch (err) {
die(err instanceof Error ? err.message : String(err));
}
diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json
index 07eb7e9f..c2bfcc30 100644
--- a/packages/mcp-server/tsconfig.json
+++ b/packages/mcp-server/tsconfig.json
@@ -4,6 +4,7 @@
"outDir": "./dist",
"rootDir": "./src"
},
+ "files": ["../mcp-server-tsconfig/better-sqlite3.d.ts"],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
diff --git a/packages/mcp-server/tsdown.config.ts b/packages/mcp-server/tsdown.config.ts
index e52edf49..15e32415 100644
--- a/packages/mcp-server/tsdown.config.ts
+++ b/packages/mcp-server/tsdown.config.ts
@@ -9,6 +9,7 @@ export default defineConfig({
external: [
// Only mark test-only packages as external
"@sentry/mcp-server-mocks",
+ "better-sqlite3",
// Everything else (including @sentry/mcp-core) will be bundled
],
env: {
diff --git a/packages/mcp-test-client/README.md b/packages/mcp-test-client/README.md
index 3072d768..514584fa 100644
--- a/packages/mcp-test-client/README.md
+++ b/packages/mcp-test-client/README.md
@@ -54,7 +54,7 @@ Create a `.env` file in the package directory:
# Required
OPENAI_API_KEY=your_openai_api_key
-# Required - Sentry access token with appropriate permissions
+# Optional - explicit auth token override
SENTRY_ACCESS_TOKEN=your_sentry_access_token
# Optional (self-hosted only)
@@ -84,7 +84,8 @@ The client automatically determines the connection mode:
1. Command-line flag (`--access-token`)
2. Environment variable (`SENTRY_ACCESS_TOKEN`)
-3. `.env` file
+3. [Sentry CLI](https://cli.sentry.dev/) auth state in `~/.sentry/cli.db`
+4. `.env` file
**Remote Mode (HTTP streaming)**: Used when no access token is provided, prompts for OAuth authentication
@@ -101,7 +102,7 @@ Your Sentry access token needs the following scopes:
## Usage
-### Remote Mode (Default)
+### Remote Mode
Connect to the remote MCP server via HTTP streaming (uses OAuth for authentication):
@@ -117,7 +118,9 @@ pnpm mcp-test-client --mcp-host http://localhost:8787
### Local Mode
-Use the local stdio transport by providing a Sentry access token:
+Use the local stdio transport by providing a Sentry access token, or by
+authenticating with the [Sentry CLI](https://cli.sentry.dev/commands/auth/)
+first:
```bash
# Using environment variable
@@ -125,6 +128,10 @@ SENTRY_ACCESS_TOKEN=your_token pnpm mcp-test-client
# Using command line flag
pnpm mcp-test-client --access-token your_token
+
+# Using stored Sentry CLI auth state
+sentry auth
+pnpm mcp-test-client
```
### Interactive Mode (Default)
@@ -206,8 +213,8 @@ If you see "Failed to connect to MCP server":
If you get authentication errors:
1. Verify your OPENAI_API_KEY is set correctly
-2. Check that your SENTRY_ACCESS_TOKEN has the required permissions
-3. For self-hosted Sentry, ensure SENTRY_HOST is set
+2. Check that your token source (`SENTRY_ACCESS_TOKEN` or [Sentry CLI](https://cli.sentry.dev/commands/auth/) auth) has the required permissions
+3. For self-hosted Sentry, ensure `SENTRY_HOST` is set
### Tool Errors
diff --git a/packages/mcp-test-client/package.json b/packages/mcp-test-client/package.json
index 180ac410..b80d443a 100644
--- a/packages/mcp-test-client/package.json
+++ b/packages/mcp-test-client/package.json
@@ -24,6 +24,7 @@
"@sentry/mcp-core": "workspace:*",
"@sentry/node": "catalog:",
"ai": "catalog:",
+ "better-sqlite3": "catalog:",
"chalk": "catalog:",
"commander": "catalog:",
"dotenv": "catalog:",
diff --git a/packages/mcp-test-client/src/index.ts b/packages/mcp-test-client/src/index.ts
index 626f7eab..177be195 100644
--- a/packages/mcp-test-client/src/index.ts
+++ b/packages/mcp-test-client/src/index.ts
@@ -13,6 +13,7 @@ import { connectToRemoteMCPServer } from "./mcp-test-client-remote.js";
import { runAgent } from "./agent.js";
import { logError, logInfo } from "./logger.js";
import { sentryBeforeSend } from "@sentry/mcp-core/telem/sentry";
+import { resolveLocalAuth } from "./stdio-auth.js";
import type { MCPConnection } from "./types.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -80,17 +81,17 @@ program
: "production"),
});
- // Check for access token in priority order
- const accessToken =
- options.accessToken || process.env.SENTRY_ACCESS_TOKEN;
- const sentryHost = process.env.SENTRY_HOST;
+ const localAuth = resolveLocalAuth({
+ accessToken: options.accessToken || process.env.SENTRY_ACCESS_TOKEN,
+ env: process.env,
+ });
const openaiKey = process.env.OPENAI_API_KEY;
// Determine mode based on access token availability
// Local mode (stdio transport) when access token is provided
// Remote mode (SSE transport with OAuth) when no access token
- const useLocalMode = !!accessToken;
+ const useLocalMode = !!localAuth.accessToken;
if (!openaiKey) {
logError("OPENAI_API_KEY environment variable is required");
@@ -105,9 +106,13 @@ program
let connection: MCPConnection;
if (useLocalMode) {
// Use local stdio transport when access token is provided
+ if (localAuth.accessTokenSource === "sentry_cli_db") {
+ logInfo("Authenticated with Sentry", "using Sentry CLI auth state");
+ }
+
connection = await connectToMCPServer({
- accessToken,
- host: sentryHost || process.env.SENTRY_HOST,
+ accessToken: localAuth.accessToken,
+ host: localAuth.host,
sentryDsn: sentryDsn,
useAgentEndpoint: options.agent,
useExperimental: options.experimental,
@@ -116,7 +121,7 @@ program
// Use remote SSE transport when no access token
connection = await connectToRemoteMCPServer({
mcpHost: options.mcpHost,
- accessToken: accessToken,
+ accessToken: localAuth.accessToken,
useAgentEndpoint: options.agent,
useExperimental: options.experimental,
});
diff --git a/packages/mcp-test-client/src/stdio-auth.test.ts b/packages/mcp-test-client/src/stdio-auth.test.ts
new file mode 100644
index 00000000..202d907b
--- /dev/null
+++ b/packages/mcp-test-client/src/stdio-auth.test.ts
@@ -0,0 +1,116 @@
+import Database from "better-sqlite3";
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import { afterEach, describe, expect, it } from "vitest";
+import { resolveLocalAuth, resolveSentryHost } from "./stdio-auth.js";
+
+function createCliDb({
+ token,
+ expiresAt,
+}: {
+ token?: string | null;
+ expiresAt?: number | null;
+}) {
+ const homeDir = mkdtempSync(join(tmpdir(), "sentry-mcp-client-auth-"));
+ const configDir = join(homeDir, ".sentry");
+ mkdirSync(configDir);
+ const dbPath = join(configDir, "cli.db");
+ const db = new Database(dbPath);
+
+ db.exec(`
+ CREATE TABLE auth (
+ id INTEGER PRIMARY KEY CHECK (id = 1),
+ token TEXT,
+ refresh_token TEXT,
+ expires_at INTEGER,
+ issued_at INTEGER,
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
+ );
+ `);
+
+ db.prepare("INSERT INTO auth (id, token, expires_at) VALUES (1, ?, ?)").run(
+ token ?? null,
+ expiresAt ?? null,
+ );
+ db.close();
+
+ return homeDir;
+}
+
+const tempDirs: string[] = [];
+
+afterEach(() => {
+ for (const tempDir of tempDirs.splice(0)) {
+ rmSync(tempDir, { recursive: true, force: true });
+ }
+});
+
+describe("resolveSentryHost", () => {
+ it("falls back to SENTRY_HOST", () => {
+ expect(
+ resolveSentryHost({
+ SENTRY_HOST: "sentry.example.com",
+ }),
+ ).toBe("sentry.example.com");
+ });
+});
+
+describe("resolveLocalAuth", () => {
+ it("uses cli.db when explicit token is missing", () => {
+ const nowMs = Date.now();
+ const configDir = createCliDb({
+ token: "db-token",
+ expiresAt: nowMs + 10 * 60_000,
+ });
+ tempDirs.push(configDir);
+
+ const result = resolveLocalAuth({
+ env: {
+ SENTRY_HOST: "sentry.example.com",
+ },
+ nowMs,
+ homeDir: configDir,
+ });
+
+ expect(result).toEqual({
+ accessToken: "db-token",
+ accessTokenSource: "sentry_cli_db",
+ host: "sentry.example.com",
+ });
+ });
+
+ it("stays in remote mode when cli.db token is expired", () => {
+ const nowMs = Date.now();
+ const configDir = createCliDb({
+ token: "db-token",
+ expiresAt: nowMs + 4 * 60_000,
+ });
+ tempDirs.push(configDir);
+
+ const result = resolveLocalAuth({
+ env: {
+ SENTRY_HOST: "sentry.example.com",
+ },
+ nowMs,
+ homeDir: configDir,
+ });
+
+ expect(result).toEqual({});
+ });
+
+ it("ignores unreadable cli.db state", () => {
+ const homeDir = mkdtempSync(join(tmpdir(), "sentry-mcp-client-auth-"));
+ const configDir = join(homeDir, ".sentry");
+ mkdirSync(configDir);
+ tempDirs.push(homeDir);
+ writeFileSync(join(configDir, "cli.db"), "not-a-sqlite-db");
+
+ const result = resolveLocalAuth({
+ env: {},
+ homeDir,
+ });
+
+ expect(result).toEqual({});
+ });
+});
diff --git a/packages/mcp-test-client/src/stdio-auth.ts b/packages/mcp-test-client/src/stdio-auth.ts
new file mode 100644
index 00000000..07c87529
--- /dev/null
+++ b/packages/mcp-test-client/src/stdio-auth.ts
@@ -0,0 +1,56 @@
+import {
+ normalizeAccessToken,
+ readAccessTokenFromSentryCliDb,
+ type SentryCliAccessTokenSource,
+} from "@sentry/mcp-core/internal/sentry-cli-auth";
+import { validateSentryHostThrows } from "@sentry/mcp-core/utils/url-utils";
+
+export type AccessTokenSource = "flag-or-env" | SentryCliAccessTokenSource;
+
+export type ResolvedLocalAuth = {
+ accessToken?: string;
+ accessTokenSource?: AccessTokenSource;
+ host?: string;
+};
+
+export function resolveSentryHost(env: NodeJS.ProcessEnv): string | undefined {
+ const sentryHost = env.SENTRY_HOST?.trim();
+ if (sentryHost) {
+ validateSentryHostThrows(sentryHost);
+ return sentryHost;
+ }
+
+ return undefined;
+}
+
+export function resolveLocalAuth({
+ accessToken,
+ env,
+ nowMs = Date.now(),
+ homeDir,
+}: {
+ accessToken?: string;
+ env: NodeJS.ProcessEnv;
+ nowMs?: number;
+ homeDir?: string;
+}): ResolvedLocalAuth {
+ const normalizedAccessToken = normalizeAccessToken(accessToken);
+ if (normalizedAccessToken) {
+ return {
+ accessToken: normalizedAccessToken,
+ accessTokenSource: "flag-or-env",
+ host: resolveSentryHost(env),
+ };
+ }
+
+ const cliDbToken = readAccessTokenFromSentryCliDb({ nowMs, homeDir });
+ if (cliDbToken) {
+ return {
+ accessToken: cliDbToken,
+ accessTokenSource: "sentry_cli_db",
+ host: resolveSentryHost(env),
+ };
+ }
+
+ return {};
+}
diff --git a/packages/mcp-test-client/tsconfig.json b/packages/mcp-test-client/tsconfig.json
index 4c1f2efa..0c68e0f9 100644
--- a/packages/mcp-test-client/tsconfig.json
+++ b/packages/mcp-test-client/tsconfig.json
@@ -4,6 +4,7 @@
"outDir": "./dist",
"rootDir": "./src"
},
+ "files": ["../mcp-server-tsconfig/better-sqlite3.d.ts"],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
-}
\ No newline at end of file
+}
diff --git a/packages/mcp-test-client/tsdown.config.ts b/packages/mcp-test-client/tsdown.config.ts
index 75457e93..cdf3837c 100644
--- a/packages/mcp-test-client/tsdown.config.ts
+++ b/packages/mcp-test-client/tsdown.config.ts
@@ -7,6 +7,7 @@ const packageVersion =
export default defineConfig({
entry: ["src/index.ts"],
+ external: ["better-sqlite3"],
format: ["esm"],
clean: true,
platform: "node",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3005c8bc..43c0295f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -379,6 +379,9 @@ importers:
ai:
specifier: 'catalog:'
version: 6.0.64(zod@3.25.76)
+ better-sqlite3:
+ specifier: 'catalog:'
+ version: 11.10.0
dotenv:
specifier: 'catalog:'
version: 16.6.1
@@ -422,6 +425,9 @@ importers:
'@sentry/node':
specifier: 'catalog:'
version: 10.35.0
+ better-sqlite3:
+ specifier: 'catalog:'
+ version: 11.10.0
dotenv:
specifier: 'catalog:'
version: 16.6.1
@@ -537,6 +543,9 @@ importers:
ai:
specifier: 'catalog:'
version: 6.0.64(zod@3.25.76)
+ better-sqlite3:
+ specifier: 'catalog:'
+ version: 11.10.0
chalk:
specifier: 'catalog:'
version: 5.4.1
@@ -4006,6 +4015,7 @@ packages:
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
+ deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
hasBin: true
prettier@3.6.2: