Skip to content

Fix MCP env aliasing, explicit missing-env errors, and add memory smoke diagnostics - #121

Merged
besfeng23 merged 1 commit into
mainfrom
codex/fix-memory-capture-and-retrieval-failures
Jul 2, 2026
Merged

Fix MCP env aliasing, explicit missing-env errors, and add memory smoke diagnostics#121
besfeng23 merged 1 commit into
mainfrom
codex/fix-memory-capture-and-retrieval-failures

Conversation

@besfeng23

@besfeng23 besfeng23 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Motivation

  • MCP/API calls were producing opaque Invalid API key failures because the MCP route and auth expected specific env names and would construct a Supabase client with empty strings when keys were missing.
  • Capture/distillation failures returned generic messages and made it hard to determine whether gates or missing env vars caused the blockage.
  • A safe diagnostics script and clearer env resolution were needed to verify gating, provider config, and Supabase access without exposing secrets.

Description

  • Added lib/services/pandora-mcp-env.ts to centralize env alias resolution for MCP bearer token, Supabase URL, and DB/service-role key and to return explicit Missing server env: <KEY_NAME> style messages.
  • Updated lib/services/mcp-auth.ts to use the new env resolver and to return explicit errors for missing token/user/db-key and to replace generic gate messages with capture_disabled: PANDORA_ENABLE_MCP_CAPTURE is not true and distillation_disabled: PANDORA_ENABLE_MCP_DISTILLATION is not true.
  • Updated app/api/mcp/route.ts to validate Supabase URL and DB key aliases before creating the Supabase client so a missing config no longer surfaces as a Supabase Invalid API key.
  • Added a non-secret smoke/diagnostics script at scripts/memory-smoke.ts and npm scripts memory:smoke/memory:diagnostics to check env presence, gates, Supabase connectivity, candidate analysis/capture, context retrieval, distillation, and open-loops using a neutral seed text.
  • Added unit tests tests/unit/pandora-mcp-env.test.ts to cover alias resolution and the new explicit error/gate messages.
  • Small package.json script entries added for convenience.

Testing

  • Ran npm run typecheck and npm run lint (lint had pre-existing warnings only). — passed.
  • Ran focused unit tests tests/unit/pandora-mcp-auth.test.ts and tests/unit/pandora-mcp-tools.test.ts — passed.
  • Added and ran tests/unit/pandora-mcp-env.test.ts covering alias acceptance and explicit missing-env/gate messages — passed.
  • Ran full test suite npm run test — all tests passed (85 files, 550 tests in CI run here).
  • Built the app with npm run build and ran npm run env:policy — both succeeded (build emitted Next.js warnings only).
  • Ran the new smoke script npm run memory:smoke in this environment and it failed safely due to intentionally-missing deployment secrets, producing explicit missing-env messages (Missing server env: NEXT_PUBLIC_SUPABASE_URL, Missing server env: PANDORA_MCP_DB_KEY, Missing server env: PANDORA_MCP_USER_ID or PANDORA_MEMORY_BRIDGE_USER_ID) rather than Invalid API key; this is expected in a non-secret local environment.

Files changed or added (key): lib/services/pandora-mcp-env.ts, lib/services/mcp-auth.ts, app/api/mcp/route.ts, scripts/memory-smoke.ts, tests/unit/pandora-mcp-env.test.ts, package.json.


Codex Task

Summary by CodeRabbit

  • New Features

    • Added a new memory smoke/diagnostics command to help verify memory-related flows and environment setup.
    • Improved environment variable handling so the app can recognize supported configuration aliases automatically.
  • Bug Fixes

    • Made authentication and feature-availability errors clearer and more specific.
    • Improved startup behavior when required connection or secret settings are missing.
  • Tests

    • Added coverage for environment aliases, missing token handling, and disabled feature messaging.

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
memory Ready Ready Preview, Comment Jul 2, 2026 2:49pm

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new environment-variable alias resolution module (pandora-mcp-env.ts) used by the MCP route and mcp-auth service to resolve Supabase URL, DB key, and bearer secret, updating error codes/messages. A new memory smoke-test script and npm scripts are added, along with unit tests for the env helpers.

Changes

Pandora MCP env alias helpers and consumers

Layer / File(s) Summary
Env alias helper module
lib/services/pandora-mcp-env.ts
New PandoraMcpEnvStatus type and firstPresent-based helpers (getPandoraMcpBearerSecret, getPandoraMcpDbKey, getPandoraSupabaseUrl, presentEnvName) resolve env var aliases.
MCP auth principal resolution
lib/services/mcp-auth.ts
resolvePandoraMcpPrincipal now uses the env helpers, distinguishing mcp_token_env_missing, mcp_token_missing, and mcp_db_key_missing; capture/distillation disabled messages gain explicit prefixes.
MCP route Supabase client
app/api/mcp/route.ts
createMcpClient derives Supabase URL/DB key via the new helpers, throws on failure, and sets x-pandora-db-key-env header.
Memory smoke-test script
scripts/memory-smoke.ts, package.json
New diagnostic script exercises memory-related MCP tool calls end-to-end; wired via memory:smoke and memory:diagnostics npm scripts.
Unit tests
tests/unit/pandora-mcp-env.test.ts
Tests cover alias-based env resolution, missing-token principal errors, and capture/distillation disabled messages.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant McpRoute as MCP Route
  participant McpAuth as mcp-auth
  participant EnvHelpers as pandora-mcp-env

  Client->>McpRoute: request with Authorization Bearer token
  McpRoute->>McpAuth: resolvePandoraMcpPrincipal(env)
  McpAuth->>EnvHelpers: getPandoraMcpBearerSecret(env)
  EnvHelpers-->>McpAuth: status ok or missing
  McpAuth->>EnvHelpers: getPandoraMcpDbKey(env)
  EnvHelpers-->>McpAuth: status ok or missing
  McpAuth-->>McpRoute: principal or error code
  McpRoute->>EnvHelpers: getPandoraSupabaseUrl(env)
  EnvHelpers-->>McpRoute: status ok or missing
  McpRoute-->>Client: Supabase client created or error thrown
Loading

Possibly related PRs

  • besfeng23/Memory#101: Both PRs modify the Phase 4B MCP server auth/env wiring, with this PR building directly on the route/auth logic introduced there.
  • besfeng23/Memory#102: Both PRs modify resolvePandoraMcpPrincipal credential parsing/validation logic in lib/services/mcp-auth.ts.
  • besfeng23/Memory#105: Depends on the same requireMcpCaptureEnabled gating and disabled-message behavior refactored in this PR.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the template's checklist section or answer the env-var/public-safe questions. Add the required Checklist section and explicitly answer the env-var, Env Broker, server-only, and NEXT_PUBLIC safety items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: MCP env aliasing, clearer missing-env errors, and the new memory smoke diagnostics.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-memory-capture-and-retrieval-failures

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9115389c13

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/memory-smoke.ts
Comment on lines +26 to +29
await step("analyze_memory_candidates", async () => analyzeMemoryCandidatesTool(client, principal, { namespace: ns, text: seed, source: "memory_smoke", mode: "candidate_only" }, env));
await step("capture_adaptive_memory candidate_only", async () => captureAdaptiveMemoryTool(client, principal, { namespace: ns, text: seed, source: "memory_smoke", mode: "candidate_only" }, env));
await step("capture_adaptive_memory auto_capture_allowed", async () => captureAdaptiveMemoryTool(client, principal, { namespace: ns, text: seed, source: "memory_smoke", mode: "auto_capture_allowed" }, env));
await step("capture_memory_event", async () => captureMemoryEventTool(client, principal, { namespace: ns, raw_text: seed, source: "memory_smoke", sensitivity: "low" }, env), env.PANDORA_ENABLE_MCP_CAPTURE !== "true");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make memory smoke diagnostics dry-run only

When npm run memory:smoke is run against a configured environment with PANDORA_ENABLE_MCP_CAPTURE=true, these diagnostics use the service-role client and default to the real_life namespace, but the called tools are not read-only: analyzeMemoryCandidatesTool/captureAdaptiveMemoryTool insert candidate rows and captureMemoryEventTool inserts a captured memory_events row. A smoke/diagnostics command can therefore contaminate production user memory with the seed text instead of only reporting readiness; keep this path dry-run/read-only or require an explicit non-dry-run opt-in.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/mcp/route.ts (1)

30-42: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Uncaught throw from createMcpClient() yields an opaque 500 instead of the intended explicit error.

createMcpClient() throws a plain Error when Supabase URL/DB key resolution fails, but the call at Line 42 (inside handle) isn't wrapped in try/catch. In Next.js App Router, an uncaught throw inside a route handler results in a generic, unhelpful 500 response — not the structured { ok: false, code, message } shape produced by jsonError for other failure paths in this same file. This defeats the PR's goal of surfacing clear "Missing server env" errors for this route.

Wrap the client construction in try/catch and return a jsonError-style response.

🔧 Proposed fix
 async function handle(request: Request) {
   if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders(request) });
   const principal = resolvePandoraMcpPrincipal(request);
   if (!principal.ok) return jsonError(principal, request);
-  const server = createPandoraMcpServer({ client: createMcpClient(), principal });
+  let client: MemoryBridgeDbClient;
+  try {
+    client = createMcpClient();
+  } catch (e) {
+    const headers = new Headers(corsHeaders(request));
+    return NextResponse.json({ ok: false, code: "mcp_env_missing", message: e instanceof Error ? e.message : String(e) }, { status: 500, headers });
+  }
+  const server = createPandoraMcpServer({ client, principal });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/mcp/route.ts` around lines 30 - 42, The `handle` flow is letting
`createMcpClient()` throw uncaught, which turns missing Supabase env resolution
into a generic 500 instead of the structured `jsonError` response used
elsewhere. Wrap the `createMcpClient()` call in `handle` with try/catch, and on
failure return a `jsonError`-style response that preserves the explicit
env-missing message from `getPandoraSupabaseUrl` and `getPandoraMcpDbKey` so the
route returns a clear client error instead of an opaque server error.
🧹 Nitpick comments (3)
tests/unit/pandora-mcp-env.test.ts (1)

19-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant dynamic import.

requireMcpCaptureEnabled/requireMcpDistillationEnabled are re-imported dynamically here, but mcp-auth is already statically imported at Line 3. Add these to the existing static import instead.

♻️ Proposed fix
-import { resolvePandoraMcpPrincipal } from "`@/lib/services/mcp-auth`";
+import { resolvePandoraMcpPrincipal, requireMcpCaptureEnabled, requireMcpDistillationEnabled } from "`@/lib/services/mcp-auth`";
-  it("reports disabled capture and distillation with explicit env names", async () => {
-    const { requireMcpCaptureEnabled, requireMcpDistillationEnabled } = await import("`@/lib/services/mcp-auth`");
+  it("reports disabled capture and distillation with explicit env names", () => {
     expect(requireMcpCaptureEnabled({ PANDORA_ENABLE_MCP_CAPTURE: "false" }).message).toBe("capture_disabled: PANDORA_ENABLE_MCP_CAPTURE is not true");
     expect(requireMcpDistillationEnabled({ PANDORA_ENABLE_MCP_DISTILLATION: "false" }).message).toBe("distillation_disabled: PANDORA_ENABLE_MCP_DISTILLATION is not true");
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/pandora-mcp-env.test.ts` around lines 19 - 23, The test re-imports
requireMcpCaptureEnabled and requireMcpDistillationEnabled dynamically even
though mcp-auth is already statically imported; move these symbols into the
existing top-level import in pandora-mcp-env.test.ts and remove the await
import() inside the test. Keep the test body using the same function names so it
still verifies the explicit env-name error messages.
scripts/memory-smoke.ts (2)

11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Alias lists duplicated from pandora-mcp-env.ts.

aliases hardcodes the same env-var lists already defined in getPandoraMcpBearerSecret/getPandoraMcpDbKey/getPandoraSupabaseUrl. If those source lists change, this diagnostic output silently drifts and misreports which env var is active.

Export the alias arrays from pandora-mcp-env.ts and import them here instead of re-declaring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/memory-smoke.ts` at line 11, The alias lists in the memory smoke
script are duplicated from the env helpers, so the diagnostic output can drift
from the real source of truth. Export the alias arrays from the functions in
pandora-mcp-env.ts (the ones used by getPandoraMcpBearerSecret,
getPandoraMcpDbKey, and getPandoraSupabaseUrl) and import them into
memory-smoke.ts instead of hardcoding aliases there, so the active env-var
reporting always stays in sync.

14-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fragile failure detection via string matching on serialized JSON.

step() determines failure by checking whether the stringified result contains '"ok":false' or 'Invalid API key' rather than checking the actual ok property on the parsed result. If a tool's failure shape doesn't literally serialize to those substrings, a real failure could be reported as PASS, undermining the diagnostic's purpose.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/memory-smoke.ts` at line 14, The failure check in step() is relying
on string matching over JSON.stringify(r), which can miss real tool failures.
Update step() to inspect the returned value directly from the parsed result,
using the ok field (and any other explicit failure indicators already present in
the step output flow) instead of searching for substrings like "ok":false or
"Invalid API key". Keep the existing out() reporting path in step() so the name,
optional flag, and summary text still flow through unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/services/mcp-auth.ts`:
- Around line 25-40: `resolvePandoraMcpPrincipal` currently returns
`mcp_token_env_missing` before it can try OAuth, which blocks OAuth-only
deployments. Update the auth flow so `getPandoraMcpBearerSecret` is only
required for the static bearer-token path in `resolvePandoraMcpPrincipal`, then
always fall through to `verifyPandoraMcpOAuthAccessToken` when the configured
token is missing or doesn’t match. Keep the existing `authType` and `userId`
checks, and make `mcp_token_env_missing` the final fallback only after both
bearer-token and OAuth validation fail.

In `@lib/services/pandora-mcp-env.ts`:
- Around line 13-15: The new fallback in getPandoraMcpDbKey silently promotes
the MCP route to use SUPABASE_SERVICE_ROLE_KEY, which broadens access instead of
failing closed. Update getPandoraMcpDbKey so it only returns a dedicated
PANDORA_MCP_DB_KEY or PANDORA_MEMORY_BRIDGE_DB_KEY by default, and make any use
of SUPABASE_SERVICE_ROLE_KEY explicit and opt-in via a separate flag or
configuration path. Ensure the app/api/mcp/route.ts flow surfaces a clear
missing-key error when no dedicated key is configured rather than silently using
the service-role secret.

In `@scripts/memory-smoke.ts`:
- Line 7: The namespace selection in memory-smoke.ts defaults to the
production-like real_life namespace, which can leak or pollute real data when
the smoke/diagnostics flow runs. Update the ns initialization logic so the safe
test namespace (au) is the default, and only switch to real_life when there is
an explicit opt-in via PANDORA_MEMORY_SMOKE_NAMESPACE; make sure the same guard
applies to the tool sequence in step() and the write/read calls like
captureAdaptiveMemoryTool, captureMemoryEventTool, createSessionDigestTool,
getMemoryContextTool, getAdaptiveContextTool, and distillContextPackTool.

---

Outside diff comments:
In `@app/api/mcp/route.ts`:
- Around line 30-42: The `handle` flow is letting `createMcpClient()` throw
uncaught, which turns missing Supabase env resolution into a generic 500 instead
of the structured `jsonError` response used elsewhere. Wrap the
`createMcpClient()` call in `handle` with try/catch, and on failure return a
`jsonError`-style response that preserves the explicit env-missing message from
`getPandoraSupabaseUrl` and `getPandoraMcpDbKey` so the route returns a clear
client error instead of an opaque server error.

---

Nitpick comments:
In `@scripts/memory-smoke.ts`:
- Line 11: The alias lists in the memory smoke script are duplicated from the
env helpers, so the diagnostic output can drift from the real source of truth.
Export the alias arrays from the functions in pandora-mcp-env.ts (the ones used
by getPandoraMcpBearerSecret, getPandoraMcpDbKey, and getPandoraSupabaseUrl) and
import them into memory-smoke.ts instead of hardcoding aliases there, so the
active env-var reporting always stays in sync.
- Line 14: The failure check in step() is relying on string matching over
JSON.stringify(r), which can miss real tool failures. Update step() to inspect
the returned value directly from the parsed result, using the ok field (and any
other explicit failure indicators already present in the step output flow)
instead of searching for substrings like "ok":false or "Invalid API key". Keep
the existing out() reporting path in step() so the name, optional flag, and
summary text still flow through unchanged.

In `@tests/unit/pandora-mcp-env.test.ts`:
- Around line 19-23: The test re-imports requireMcpCaptureEnabled and
requireMcpDistillationEnabled dynamically even though mcp-auth is already
statically imported; move these symbols into the existing top-level import in
pandora-mcp-env.test.ts and remove the await import() inside the test. Keep the
test body using the same function names so it still verifies the explicit
env-name error messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6af5922d-4a29-43a3-a41a-a1d8f8d93f8b

📥 Commits

Reviewing files that changed from the base of the PR and between 48b5cfb and 9115389.

📒 Files selected for processing (6)
  • app/api/mcp/route.ts
  • lib/services/mcp-auth.ts
  • lib/services/pandora-mcp-env.ts
  • package.json
  • scripts/memory-smoke.ts
  • tests/unit/pandora-mcp-env.test.ts

Comment thread lib/services/mcp-auth.ts
Comment on lines +25 to 40
const configuredToken = getPandoraMcpBearerSecret(env);
const suppliedToken = bearerToken(request);
if (!configuredToken || !suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (safeEqual(suppliedToken, configuredToken)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Pandora MCP user id is not configured." };
if (!env.PANDORA_MCP_DB_KEY) return { ok: false, status: 403, code: "mcp_db_key_missing", message: "Pandora MCP database key is not configured." };
if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
if (!suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (safeEqual(suppliedToken, configuredToken.value)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_bearer_token", userId: env.PANDORA_MCP_USER_ID };
}
const oauth = verifyPandoraMcpOAuthAccessToken(suppliedToken, env);
if (!oauth.ok) return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Pandora MCP user id is not configured." };
if (!env.PANDORA_MCP_DB_KEY) return { ok: false, status: 403, code: "mcp_db_key_missing", message: "Pandora MCP database key is not configured." };
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing bearer-secret env blocks OAuth authentication entirely.

resolvePandoraMcpPrincipal returns mcp_token_env_missing at Line 27 as soon as getPandoraMcpBearerSecret fails, before ever checking suppliedToken or reaching the OAuth branch (Line 35). Since OAuth (verifyPandoraMcpOAuthAccessToken) verifies against a completely independent secret, a deployment that intentionally relies solely on OAuth (no PANDORA_MCP_TOKEN/PANDORA_MCP_API_KEY/PANDORA_API_KEY/MEMORY_API_KEY set) will have every request — including ones bearing a perfectly valid OAuth access token — rejected with a "missing server env" error. OAuth-only auth becomes impossible.

Reorder so OAuth is attempted whenever the static secret doesn't match/isn't configured, and only report mcp_token_env_missing as the final fallback when both mechanisms fail.

🔧 Proposed fix
 export function resolvePandoraMcpPrincipal(request: Request, env: Partial<NodeJS.ProcessEnv> = process.env): PandoraMcpPrincipal {
   if (env.PANDORA_ENABLE_MCP !== "true") return { ok: false, status: 403, code: "mcp_disabled", message: "Pandora MCP is disabled." };
   const configuredToken = getPandoraMcpBearerSecret(env);
   const suppliedToken = bearerToken(request);
-  if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
   if (!suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
-  if (safeEqual(suppliedToken, configuredToken.value)) {
+  if (configuredToken.ok && safeEqual(suppliedToken, configuredToken.value)) {
     if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
     const dbKey = getPandoraMcpDbKey(env);
     if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
     return { ok: true, authType: "mcp_bearer_token", userId: env.PANDORA_MCP_USER_ID };
   }
   const oauth = verifyPandoraMcpOAuthAccessToken(suppliedToken, env);
-  if (!oauth.ok) return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
-  if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
-  const dbKey = getPandoraMcpDbKey(env);
-  if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
-  return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
+  if (oauth.ok) {
+    if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
+    const dbKey = getPandoraMcpDbKey(env);
+    if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
+    return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
+  }
+  if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
+  return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const configuredToken = getPandoraMcpBearerSecret(env);
const suppliedToken = bearerToken(request);
if (!configuredToken || !suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (safeEqual(suppliedToken, configuredToken)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Pandora MCP user id is not configured." };
if (!env.PANDORA_MCP_DB_KEY) return { ok: false, status: 403, code: "mcp_db_key_missing", message: "Pandora MCP database key is not configured." };
if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
if (!suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (safeEqual(suppliedToken, configuredToken.value)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_bearer_token", userId: env.PANDORA_MCP_USER_ID };
}
const oauth = verifyPandoraMcpOAuthAccessToken(suppliedToken, env);
if (!oauth.ok) return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Pandora MCP user id is not configured." };
if (!env.PANDORA_MCP_DB_KEY) return { ok: false, status: 403, code: "mcp_db_key_missing", message: "Pandora MCP database key is not configured." };
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
const configuredToken = getPandoraMcpBearerSecret(env);
const suppliedToken = bearerToken(request);
if (!suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (configuredToken.ok && safeEqual(suppliedToken, configuredToken.value)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_bearer_token", userId: env.PANDORA_MCP_USER_ID };
}
const oauth = verifyPandoraMcpOAuthAccessToken(suppliedToken, env);
if (oauth.ok) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
}
if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/mcp-auth.ts` around lines 25 - 40, `resolvePandoraMcpPrincipal`
currently returns `mcp_token_env_missing` before it can try OAuth, which blocks
OAuth-only deployments. Update the auth flow so `getPandoraMcpBearerSecret` is
only required for the static bearer-token path in `resolvePandoraMcpPrincipal`,
then always fall through to `verifyPandoraMcpOAuthAccessToken` when the
configured token is missing or doesn’t match. Keep the existing `authType` and
`userId` checks, and make `mcp_token_env_missing` the final fallback only after
both bearer-token and OAuth validation fail.

Comment on lines +13 to +15
export function getPandoraMcpDbKey(env: Partial<NodeJS.ProcessEnv> = process.env) {
return firstPresent(env, ["PANDORA_MCP_DB_KEY", "PANDORA_MEMORY_BRIDGE_DB_KEY", "SUPABASE_SERVICE_ROLE_KEY"]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

DB key fallback widens blast radius to full service-role privileges.

getPandoraMcpDbKey now silently falls back to SUPABASE_SERVICE_ROLE_KEY when no dedicated PANDORA_MCP_DB_KEY/PANDORA_MEMORY_BRIDGE_DB_KEY is set. Per the line-range change details, the prior code read env.PANDORA_MCP_DB_KEY directly with no such fallback — this is new exposure. The Supabase service-role key always bypasses Row Level Security, so any deployment that hasn't set a dedicated scoped key will have the public-facing MCP route (app/api/mcp/route.ts) operate with full, RLS-bypassing database privileges instead of failing closed with a clear "missing dedicated key" error.

Consider making the service-role fallback opt-in (e.g., a separate explicit flag) rather than a silent alias, so misconfigured deployments fail loudly instead of quietly running with elevated privileges.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/pandora-mcp-env.ts` around lines 13 - 15, The new fallback in
getPandoraMcpDbKey silently promotes the MCP route to use
SUPABASE_SERVICE_ROLE_KEY, which broadens access instead of failing closed.
Update getPandoraMcpDbKey so it only returns a dedicated PANDORA_MCP_DB_KEY or
PANDORA_MEMORY_BRIDGE_DB_KEY by default, and make any use of
SUPABASE_SERVICE_ROLE_KEY explicit and opt-in via a separate flag or
configuration path. Ensure the app/api/mcp/route.ts flow surfaces a clear
missing-key error when no dedicated key is configured rather than silently using
the service-role secret.

Comment thread scripts/memory-smoke.ts
import type { MemoryBridgeDbClient } from "../lib/services/memory-bridge-service";

const seed = "Creative workflow preference: preserve continuity and user feedback for future writing.";
const ns = (process.env.PANDORA_MEMORY_SMOKE_NAMESPACE === "au" ? "au" : "real_life") as "real_life" | "au";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Default namespace risks polluting/leaking real data.

ns defaults to "real_life" unless PANDORA_MEMORY_SMOKE_NAMESPACE is explicitly set to "au" (Line 7). Since this script performs real writes (captureAdaptiveMemoryTool with auto_capture_allowed, captureMemoryEventTool, createSessionDigestTool) and reads (getMemoryContextTool, getAdaptiveContextTool, distillContextPackTool) against whatever Supabase instance the env points to, running npm run memory:smoke/memory:diagnostics without remembering to set the namespace env var will inject synthetic seed memories into the real_life namespace, and step() logs up to 500 chars of each tool's JSON response (Line 14) — which could include genuine pre-existing personal memory content pulled from real_life — straight to stdout/CI logs.

Default to the safe test namespace and require explicit opt-in to touch real_life.

🔧 Proposed fix
-const ns = (process.env.PANDORA_MEMORY_SMOKE_NAMESPACE === "au" ? "au" : "real_life") as "real_life" | "au";
+const ns = (process.env.PANDORA_MEMORY_SMOKE_NAMESPACE === "real_life" ? "real_life" : "au") as "real_life" | "au";

Also applies to: 21-35

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/memory-smoke.ts` at line 7, The namespace selection in
memory-smoke.ts defaults to the production-like real_life namespace, which can
leak or pollute real data when the smoke/diagnostics flow runs. Update the ns
initialization logic so the safe test namespace (au) is the default, and only
switch to real_life when there is an explicit opt-in via
PANDORA_MEMORY_SMOKE_NAMESPACE; make sure the same guard applies to the tool
sequence in step() and the write/read calls like captureAdaptiveMemoryTool,
captureMemoryEventTool, createSessionDigestTool, getMemoryContextTool,
getAdaptiveContextTool, and distillContextPackTool.

@besfeng23
besfeng23 merged commit 9115389 into main Jul 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant