Propose: filter MCP logs and surface token activity stats#47
Conversation
Adds an OpenSpec change proposal for filtering MCP logs by tool, status (success/error), token, and date range; and surfacing per-token last-used relative time + 30-day event count on the MCP Access page. No implementation yet — proposal stage only. Made-with: Cursor
|
🚅 Deployed to the archmax-pr-47 environment in archmax SemLayer
|
Docker image readydocker pull ghcr.io/archmaxai/archmax:pr-47 |
API: - Add GET /api/projects/:projectId/mcp-logs/tools returning distinct, sorted toolName values for a project so the frontend tool filter is sourced from real traffic. - Extend GET /api/projects/:projectId/mcp-tokens with eventCount30d, computed via a single McpCallLog aggregation grouped by tokenId for the trailing 30 days. Frontend: - Add a filter bar to /:projectId/monitoring (Tool, Status, Token, DateRangePicker) with a clear-all X button. All filters reset pagination on change. Empty state distinguishes "no logs yet" from "no logs match the current filters". - Render Last Used as a relative time (with absolute tooltip) and add an Events (30d) column on the MCP Access page. - Date range serializes the picker's local-day boundaries (00:00:00 of the start date through 23:59:59.999 of the end date) to UTC ISO so ranges match user intent regardless of timezone. UI package: - Add Calendar (react-day-picker v9, shadcn-style classes) and a small DateRangePicker built on Popover + Calendar mode="range". Tests / docs: - Vitest integration coverage for both new API endpoints. - E2E coverage for token activity stats and the monitoring tool / status filters. - Update apps/docs MCP integration guide with the new monitoring filters and the Events (30d) / Last Used columns. Implements OpenSpec change add-mcp-log-filters (now archived). Made-with: Cursor
Move the change to changes/archive/2026-04-29-bundle-dependabot-prs/ and fold its spec deltas into openspec/specs/dependency-automation/spec.md. Made-with: Cursor
Address review findings on PR #47: - mcp-logs.ts: replace permissive query schema with z.coerce.number().int() for page/limit (limit max=200), z.string().datetime() for from/to, ObjectId regex for tokenId, and z.enum(["true","false"]) for errorOnly. Validate the projectId path param against the same ObjectId regex before any DB call so malformed input returns a clean 400 instead of leaking a CastError 500. - mcp-tokens.ts: validate projectId on every handler and tokenId on the delete handler against the ObjectId regex up front; previously new mongoose.Types.ObjectId(projectId) and findOne({ _id: tokenId }) would throw on malformed input. Add integration coverage for each rejected-input path (invalid projectId, tokenId, page, date, limit) so future regressions surface as 400s. Made-with: Cursor
There was a problem hiding this comment.
Stale comment
Security review findings against the requested threat surfaces:
apps/api/src/mcp/archmax-route.ts:108-130andapps/api/src/mcp/archmax-route.ts:118-126: resumed MCP requests withmcp-session-iddo not validate the bearer token or the route slug/project before handing the request to the existing transport, and invalid/expired tokens return plain{ error: ... }JSON instead of a JSON-RPC error envelope. Risk: a captured session id can continue driving a tool session without presenting the bearer token, and clients get non-protocol error responses. Fix by authenticating every MCP request, binding sessions to{ projectId, tokenId }, checking the current slug resolves to that project, and returningjsonrpc: "2.0", error: { code, message }, idfor missing/invalid/expired credentials.
packages/core/src/services/sql-validation.ts:1-16:execute_queryallowsSHOWandPRAGMAas first keywords even though the MCP contract says queries are scoped to semantic model views. Risk: callers can inspect DuckDB metadata/settings and potentially enumerate attached catalogs or schemas outside the intended VIEW-only surface. Fix by removingSHOW/PRAGMAfromALLOWED_FIRST_KEYWORD, updating the tests that currently allow them, and only permitting metadata reads through explicit scoped helper tools if needed.
apps/api/src/routes/mcp-tokens.ts:75/apps/api/src/routes/mcp-tokens.ts:114: MCP token create/revoke are state-changing admin routes protected by session cookies but I did not find a CSRF token or origin check on these Hono mutation routes. Risk: token creation/revocation depends on SameSite/CORS/content-type behavior instead of an explicit CSRF control. Fix by adding shared CSRF/origin enforcement for non-GET/api/*routes, or by requiring a Better Auth CSRF token/header from the frontend mutations.Checked: MCP endpoint auth,
execute_querysandboxing, Better Auth startup/cookie settings, API Zod validation for the changed MCP log/token routes, environment secret exposure, and dependency exposure. The new MCP log filter inputs are Zod-constrained before reaching MongoDB filters, and I did not find committed.env.localsecrets.pnpm audit --prodreports existing transitive advisories undermarkitdown-ts/deepagents; I did not see the newly added date picker packages as the source of those audit findings.Sent by Cursor Automation: archmax Security Review
…SRF origin check
Security review findings:
1. MCP session resume bypassed bearer auth and slug binding. Resumed
`mcp-session-id` requests now re-authenticate the bearer token and
verify it resolves to the same {projectId, tokenId} that opened the
session and that the URL slug still maps to that project. All
auth/session protocol errors now return JSON-RPC error envelopes
(`{jsonrpc:"2.0", error:{code,message}, id:null}`) instead of plain
`{error}` JSON.
2. `validateReadOnlySQL` no longer allows `SHOW` / `PRAGMA` as the first
keyword. The public MCP `execute_query` contract is scoped to
semantic-model views, and DuckDB metadata reads must not be exposed
through this surface. Internal callers that legitimately need
metadata (data-browser, connection bootstrap) execute those statements
directly without going through the validator.
3. Added an Origin/Referer enforcement middleware for cookie-authed
`/api/*` mutation routes. Browsers always attach `Origin` on
credentialed non-GET requests, so requiring it to match
`corsOrigins` blocks browser-driven CSRF on token create/revoke and
every other state-changing API route. `/api/auth/*` is exempt
(Better Auth runs first and applies its own protections); requests
without `Origin` or `Referer` are allowed since they cannot originate
from a browser CSRF context.
Made-with: Cursor
There was a problem hiding this comment.
Stale comment
I reviewed the changed files against the requested threat surfaces. I found issues in CSRF enforcement and the MCP SQL sandbox. I also ran
pnpm audit --prod; it reports existing advisories throughmarkitdown-ts/deepagents, but the newly addeddate-fnsandreact-day-pickerpackages were not implicated.Sent by Cursor Automation: archmax Security Review
The MCP route now returns `{jsonrpc:"2.0", error:{code,message}, id:null}`
for missing/invalid bearer tokens (commit 555b04a). Update the api-auth
and mcp e2e specs to match the new shape and assert the JSON-RPC
unauthorized envelope (`code: -32001`).
Made-with: Cursor
There was a problem hiding this comment.
Stale comment
Security review against the requested threat surfaces found two concrete issues in the changed files. I also ran
pnpm audit --prod; it reports existing transitive advisories undermarkitdown-ts/deepagents, but I did not see audit findings tied to the newly added UI date-picker dependencies.Sent by Cursor Automation: archmax Security Review
Address review findings on top of 555b04a: CSRF middleware now rejects state-changing /api/* requests when both Origin and Referer are missing instead of falling through. Cookie- authenticated mutation routes were otherwise accepting any caller that suppressed those headers; non-browser API integrations should use the bearer-token MCP surface or set Origin explicitly. The E2E \`apiHeaders\` helper now sets Origin to BASE_URL since Playwright's APIRequestContext doesn't add it automatically. SQL validator hardening: - EXPLAIN ANALYZE in DuckDB executes the wrapped statement, so unrestricted EXPLAIN is not actually read-only. Only \`EXPLAIN SELECT\` and \`EXPLAIN WITH\` are now accepted; \`EXPLAIN ANALYZE\` and \`EXPLAIN <DDL/DML>\` are rejected. - validateScopedSQL now denies DuckDB metadata table functions (\`duckdb_tables()\`, \`duckdb_columns()\`, \`duckdb_secrets()\`, \`duckdb_settings()\`, ...), the pg_catalog/sqlite_master metadata catalogs, references to the main/temp/system schemas, and external file readers (\`read_csv\`, \`read_parquet\`, \`read_json\`, \`read_blob\`). \`enable_external_access=false\` already gates the readers in the default sandbox, but Iceberg-enabled projects keep external access on, so this denylist provides defence-in-depth. Tests cover EXPLAIN ANALYZE, EXPLAIN-of-DDL, the new denylist patterns, and confirm legitimate identifiers that contain forbidden substrings (\`order_main.id\`, \`duckdb_user\`) still pass. Note on positive allowlisting: validating SELECT references against the exact set of semantic-model views requires a SQL parser/AST and is left as a follow-up. The denylist closes the immediately reachable bypasses. Made-with: Cursor
…calls Three call sites in the scope-enforcement tests were still invoking mcpToolCall without the new required token argument, causing 401s after the MCP route began re-authenticating every resumed request. Made-with: Cursor
There was a problem hiding this comment.
Stale comment
I found one query-sandboxing issue in the changed files. I also checked MCP bearer/session re-auth, token scope enforcement, admin CSRF/cookie/env-secret handling, Hono input validation on the changed routes, and the new UI dependencies.
pnpm audit --prodstill reports existing transitive advisories undermarkitdown-ts/deepagents, but the newly addeddate-fnsandreact-day-pickerwere not the reported paths.Sent by Cursor Automation: archmax Security Review
| * read_csv/parquet/json/blob) | ||
| * 4. No explicit _scope_ schema references — search_path handles resolution | ||
| */ | ||
| export function validateScopedSQL(sql: string, catalogSlugs: string[]): string | null { |
There was a problem hiding this comment.
The catalog-prefix deny check only matches unquoted catalog. (\b${escaped}\.). DuckDB accepts quoted identifiers (and whitespace around dots), so an MCP caller can still bypass the scoped semantic-model views with SQL like SELECT * FROM "shopify"."orders" or SELECT * FROM "main"."some_table". Because executeScopedQuery sets search_path to the scoped view schema but does not otherwise prevent fully-qualified raw catalog references, this can expose underlying tables/columns outside the token-scoped model views.
Fix by parsing identifiers or extending the validation to reject quoted/escaped catalog and system-schema references with optional whitespace around ., then add regression tests for "<catalog>"."<table>", "main"."...", and quoted _scope_ prefixes.
There was a problem hiding this comment.
Security review found two concrete issues in the MCP query sandbox changes. I also checked MCP bearer/session binding, admin CSRF/cookie/secret handling, Hono input validation on the changed admin routes, environment secret exposure, and the new UI dependencies. pnpm audit --prod reports existing advisories through markitdown-ts/deepagents, but not from the newly added date-fns or react-day-picker packages.
Sent by Cursor Automation: archmax Security Review
| // not be exposed through this surface. Internal callers that need metadata | ||
| // (e.g. data-browser routes) execute those statements directly without going | ||
| // through this validator. | ||
| const ALLOWED_FIRST_KEYWORD = /^\s*(SELECT|WITH|EXPLAIN|DESCRIBE)\b/i; |
There was a problem hiding this comment.
Risk: this first-keyword allowlist still lets CTE-prefixed DML through. For example, DuckDB supports WITH c AS (...) INSERT INTO ..., UPDATE ..., and DELETE ...; those statements start with WITH, so validateReadOnlySQL() returns null before execute_query prepares and runs them. That violates the read-only guarantee even if some writes later fail against scoped views. Fix by parsing the statement type with a SQL parser/DuckDB parser, or at minimum require WITH queries to end in a read-only top-level SELECT/DESCRIBE and add tests for WITH ... INSERT/UPDATE/DELETE.
| * read_csv/parquet/json/blob) | ||
| * 4. No explicit _scope_ schema references — search_path handles resolution | ||
| */ | ||
| export function validateScopedSQL(sql: string, catalogSlugs: string[]): string | null { |
There was a problem hiding this comment.
Risk: raw catalog and system-schema blocking can be bypassed with quoted identifiers. SELECT * FROM "shopify".public.orders does not match this ${catalog}. regex, and the main/temp/system pattern similarly misses forms like main."foo" or "main".foo. That lets callers address raw attached catalogs or DuckDB schemas instead of only the semantic-model views. Fix by normalizing/parsing identifiers before validation, or extend tests and checks to reject quoted catalog/schema references as well as unquoted ones.
There was a problem hiding this comment.
must be solved at a different level


Summary
OpenSpec proposal
add-mcp-log-filters— no implementation yet./:projectId/monitoring(tool, status, token, date range using shadcnCalendar+Popover).GET /api/projects/:projectId/mcp-logs/toolsso the tool dropdown is sourced from real traffic.GET /api/projects/:projectId/mcp-tokenswitheventCount30dand updates the MCP Access page to render relativeLast Used(with absolute tooltip) plus a newEvents (30d)column.Spec deltas
openspec/changes/add-mcp-log-filters/specs/mcp-monitoring/spec.md— ADDED distinct-tools endpoint; MODIFIED Call Log UI for filter bar.openspec/changes/add-mcp-log-filters/specs/mcp-token-management/spec.md— MODIFIED Token CRUD API to includeeventCount30d; MODIFIED MCP Access UI for relative time + event count column.Validated with
openspec validate add-mcp-log-filters --strict→ passes.Test plan
tasks.mdMade with Cursor