Skip to content

Propose: filter MCP logs and surface token activity stats#47

Merged
tobias-gp merged 8 commits intomainfrom
proposal/add-mcp-log-filters
Apr 29, 2026
Merged

Propose: filter MCP logs and surface token activity stats#47
tobias-gp merged 8 commits intomainfrom
proposal/add-mcp-log-filters

Conversation

@tobias-gp
Copy link
Copy Markdown
Contributor

Summary

OpenSpec proposal add-mcp-log-filters — no implementation yet.

  • Adds a filter bar to /:projectId/monitoring (tool, status, token, date range using shadcn Calendar + Popover).
  • Adds GET /api/projects/:projectId/mcp-logs/tools so the tool dropdown is sourced from real traffic.
  • Extends GET /api/projects/:projectId/mcp-tokens with eventCount30d and updates the MCP Access page to render relative Last Used (with absolute tooltip) plus a new Events (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 include eventCount30d; MODIFIED MCP Access UI for relative time + event count column.

Validated with openspec validate add-mcp-log-filters --strict → passes.

Test plan

  • Review proposal.md, tasks.md, and the two spec deltas
  • Confirm error-type interpretation (3-way Status: All/Success/Error) and 30-day event window match expectations
  • Approve to start Stage 2 implementation following tasks.md

Made with Cursor

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
@railway-app
Copy link
Copy Markdown

railway-app Bot commented Apr 29, 2026

🚅 Deployed to the archmax-pr-47 environment in archmax SemLayer

Service Status Web Updated (UTC)
archmax_standalone ✅ Success (View Logs) Apr 29, 2026 at 7:53 am
archmax_external_dbs ✅ Success (View Logs) Apr 29, 2026 at 7:52 am
archmax_standalone_with_volume ✅ Success (View Logs) Apr 29, 2026 at 7:51 am

@railway-app railway-app Bot temporarily deployed to archmax SemLayer / archmax-pr-47 April 29, 2026 04:32 Destroyed
@github-actions
Copy link
Copy Markdown

Docker image ready

docker 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
@railway-app railway-app Bot temporarily deployed to archmax SemLayer / archmax-pr-47 April 29, 2026 06:18 Destroyed
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
@railway-app railway-app Bot temporarily deployed to archmax SemLayer / archmax-pr-47 April 29, 2026 06:30 Destroyed
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
@railway-app railway-app Bot temporarily deployed to archmax SemLayer / archmax-pr-47 April 29, 2026 06:45 Destroyed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Stale comment

Security review findings against the requested threat surfaces:

  1. apps/api/src/mcp/archmax-route.ts:108-130 and apps/api/src/mcp/archmax-route.ts:118-126: resumed MCP requests with mcp-session-id do 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 returning jsonrpc: "2.0", error: { code, message }, id for missing/invalid/expired credentials.

  2. packages/core/src/services/sql-validation.ts:1-16: execute_query allows SHOW and PRAGMA as 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 removing SHOW/PRAGMA from ALLOWED_FIRST_KEYWORD, updating the tests that currently allow them, and only permitting metadata reads through explicit scoped helper tools if needed.

  3. 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_query sandboxing, 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.local secrets. pnpm audit --prod reports existing transitive advisories under markitdown-ts/deepagents; I did not see the newly added date picker packages as the source of those audit findings.

Open in Web View Automation 

Sent by Cursor Automation: archmax Security Review

Comment thread apps/api/src/routes/mcp-tokens.ts
…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
@railway-app railway-app Bot temporarily deployed to archmax SemLayer / archmax-pr-47 April 29, 2026 07:23 Destroyed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

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 through markitdown-ts/deepagents, but the newly added date-fns and react-day-picker packages were not implicated.

Open in Web View Automation 

Sent by Cursor Automation: archmax Security Review

Comment thread apps/api/src/middleware/csrf.ts Outdated
Comment thread packages/core/src/services/sql-validation.ts
Comment thread packages/core/src/services/sql-validation.ts
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
@railway-app railway-app Bot temporarily deployed to archmax SemLayer / archmax-pr-47 April 29, 2026 07:39 Destroyed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

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 under markitdown-ts/deepagents, but I did not see audit findings tied to the newly added UI date-picker dependencies.

Open in Web View Automation 

Sent by Cursor Automation: archmax Security Review

Comment thread packages/core/src/services/sql-validation.ts
Comment thread apps/api/src/middleware/csrf.ts
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
@railway-app railway-app Bot temporarily deployed to archmax SemLayer / archmax-pr-47 April 29, 2026 07:50 Destroyed
…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
@railway-app railway-app Bot temporarily deployed to archmax SemLayer / archmax-pr-47 April 29, 2026 07:51 Destroyed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

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 --prod still reports existing transitive advisories under markitdown-ts/deepagents, but the newly added date-fns and react-day-picker were not the reported paths.

Open in Web View Automation 

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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

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.

Open in Web View Automation 

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

must be solved at a different level

@tobias-gp tobias-gp merged commit 14baf0b into main Apr 29, 2026
8 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