Skip to content

feat: add the MCP approval management API - #5072

Open
daviddanialy wants to merge 6 commits into
daviddanialy/ais-468-feat-assemble-mcp-server-evidencefrom
daviddanialy/ais-470-feat-mcp-approval-management-api
Open

feat: add the MCP approval management API#5072
daviddanialy wants to merge 6 commits into
daviddanialy/ais-468-feat-assemble-mcp-server-evidencefrom
daviddanialy/ais-470-feat-mcp-approval-management-api

Conversation

@daviddanialy

@daviddanialy daviddanialy commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Adds the management API behind the MCP approval page (AIS-470). Stacked on the evidence-package PR; review that one first.

What it does

Three endpoints, all project-scoped:

  • mcpApproval.listRequests — the review queue, filterable by status, with a requester count per row so demand is visible without a second call.
  • mcpApproval.getRequest — one request with its evidence, everyone who asked, and every decision made on it. A repeat request starts from the last rationale rather than from zero.
  • mcpApproval.recordDecision — approve or deny, with a rationale and the principals the approval covers.

Notes for review

Evidence is frozen onto the decision. The request's current_evidence is a cache that gets overwritten on re-gather; the copy a reviewer actually saw is snapshotted onto the decision, and evidence_version is copied from the request rather than defaulted, so a v2 payload cannot later be read as v1. Covered by a test that re-gathers and asserts the decision is unchanged.

A denial grants nobody anything, whatever the caller sent in granted_principal_urns.

Nothing here adjudicates. Every field is a declaration by the server or its registry, and the API descriptions say so — the surface has to present them as claims, not findings.

Tenancy

A request id appears in dashboard URLs and is not a secret, so id-alone lookups would be an IDOR. Every one of the nine queries in this service is bounded by project_id, no tenant id is ever read from the payload, and recordDecision resolves the request under the project before writing rather than relying on the insert's predicate alone. organization_id on a decision is derived from the resolved request, not from the auth context, because the composite FK pins project but not organisation.

Tests cover the cross-project path for all three handlers: another project's request must not list, must 404 on read, and must 404 on decide with nothing written.

RBAC

Two new scopes. mcp_approval:read to review the queue, mcp_approval:decide to commit the organisation to a server; decide expands to satisfy read, matching how skill:write carries skill:read. Tested in both directions.

Not in this PR

Request intake (createRequest, and promotion from a shadow-MCP block) lands with AIS-469. Pagination is capped at 200 rather than paged — next_cursor is always null.

Testing

23 tests, one file per handler plus a shared harness. mise run lint:server, mise run test:server ./internal/mcpapproval/, mise run gen:sdk, and pnpm -F dashboard type-check all pass.

AIS-470


Summary by cubic

Adds the mcpApproval dashboard API to manage MCP server approvals: list requests, view evidence, research reports, and history, and record approve/deny decisions with a required rationale (and optional research report citation). Delivers AIS-470 and generates the TypeScript SDK, React Query hooks, and CLI bindings for the new endpoints.

  • New Features

    • Endpoints: mcpApproval.listRequests (filter by status; includes requester count), mcpApproval.getRequest (evidence, requesters, prior decisions, and research-agent runs), mcpApproval.recordDecision (approve/deny with rationale, covered principals, and an optional cited research_report_id validated to the same request/project).
    • Review rules and hardening: decisions snapshot evidence with evidence_version; JSON is parsed strictly (rejects trailing content) and numerics preserve precision; concurrent decisions serialize so status reflects the latest; re-requests reopen denied reviews.
    • Tenancy and platform: all queries bind to project_id and cross-project reads/decides 404; gated by the mcp_approval product feature; decisions emit audit log events on the mcp_approval_request subject (new URN and outbox event); scopes (mcp_approval:read, mcp_approval:decide and their blocked_ pairs) are registered across the role editor (project-selectable “MCP Approvals”), access request form, and selector (resource_kind: mcp_approval); OpenAPI/CLI and dashboard sdk/React Query hooks added for /rpc/mcpApproval.*.
  • Migration

    • Grant mcp_approval:read to reviewers; grant mcp_approval:decide only to approvers (decide implies read; both have blocked_ pairs).
    • Enable the org-level mcp_approval product feature.

Written for commit be5fde6. Summary will update on new commits.

Review in cubic

@daviddanialy
daviddanialy requested review from a team as code owners August 7, 2026 18:20
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

AIS-470

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: be5fde6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
server Minor
dashboard Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cubic analysis

All reported issues were addressed across 53 files

Linked issue analysis

Linked issue: AIS-470: feat: MCP approval management API

Status Acceptance criteria Notes
Expose a Goa service mcpApproval with endpoints mcpApproval.listRequests, mcpApproval.getRequest, and mcpApproval.recordDecision and generate OpenAPI/SDK/React Query hooks. Design/service added and goa-generated HTTP server + client + CLI + SDK/react-query artifacts were added. Paths and OpenAPI entries for /rpc/mcpApproval.* are present.
All SQL queries and handlers are tenant-scoped (every query bounded by project_id) and handlers resolve the request under the project to prevent IDOR; cross-project access 404s. Queries file and generated repo code bind queries to project_id, and handler resolves request under the project. Tests cover cross-project access returning 404 and no writes.
recordDecision snapshots the evidence (evidence_snapshot) and copies evidence_version from the request; a denial grants no principals regardless of caller payload. DB model and sqlc-generated create approval decision persist evidence_snapshot and evidence_version; implementation and tests assert snapshot behaviour and that denials do not grant principals.
Introduce RBAC scopes mcp_approval:read and mcp_approval:decide (and paired blocked_ scopes) and wire them into the grant surface/admin scopes, with tests exercising scope behaviour. New scopes and grant-surface entries were added and the PR notes tests exercising read/decide relationships in both directions.
Provide automated tests and generated SDK/tooling; CI/lint/type-check steps run successfully per PR description. Multiple test files added (one per handler + harness), sqlc-generated repo code present, and SDK + OpenAPI generation artifacts added. PR description states lint/test/gen:sdk and pnpm type-check passed.

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread server/internal/mcpapproval/impl.go Outdated
Comment thread server/design/mcpapproval/design.go
Comment thread server/internal/authz/scopes.go
Comment thread server/internal/mcpapproval/views.go Outdated
Comment thread server/internal/mcpapproval/queries.sql
Comment thread server/design/mcpapproval/design.go
Comment thread server/internal/authz/grant_surface.go
Comment thread server/internal/mcpapproval/get_request_test.go Outdated
Comment thread server/internal/mcpapproval/list_requests_test.go
Comment thread server/internal/mcpapproval/impl.go

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 49 files (changes from recent commits).

Tip: instead of fixing issues one by one fix them all with cubic
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread client/dashboard/src/pages/access/types.ts
Comment thread server/internal/mcpapproval/queries.sql
Comment thread server/internal/mcpapproval/get_request_test.go
Comment thread server/internal/urn/mcp_approval_request.go
Comment thread server/internal/urn/mcp_approval_request.go Outdated
Comment thread server/internal/mcpapproval/impl.go
Comment thread server/design/mcpapproval/design.go
Comment thread server/design/mcpapproval/design.go
Comment thread server/internal/mcpapproval/views.go

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 26 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread server/internal/mcpapproval/impl.go Outdated
Comment thread server/internal/mcpapproval/views.go Outdated
Comment thread server/internal/mcpapproval/setup_test.go Outdated
Comment thread client/dashboard/src/hooks/useRBAC.ts
daviddanialy and others added 6 commits August 7, 2026 14:15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the service test harness and one file per handler, covering the happy
paths, the RBAC boundary between reviewing the queue and deciding on a
server, and the cross-project cases: a request id read or decided from
another project must not resolve, since the id appears in dashboard URLs
and is not a secret.

Seeding goes through three new SQLc queries rather than raw SQL, per the
notestingrawsql analyzer. All three are intake and evidence-gather writes
the feature needs regardless, and all are bounded by project id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes what the first cut of the management API left half-wired, from
adversarial review and cubic findings:

- The four mcp_approval scopes now exist everywhere a scope must be
  registered, not just in the authz engine: the role-grant slug enum,
  the listScopes catalogue, the full-access grant set, the access
  request form, the selector resource-kind enum, and the dashboard role
  editor's resource types. Before this, the generated validator
  rejected the scopes before any handler ran, so no role could carry
  them.
- ResourceKindForScope maps mcp_approval to its own resource kind
  instead of falling through to the wildcard, which had made
  project-narrowed grants unrepresentable: selector validation demanded
  resource_kind="*", so the scopes could only ever be granted org-wide.
- The service is gated by the mcp_approval product feature, per the
  ticket. The gate is independent of RBAC: a grant says who may use the
  surface, the feature says whether the organization has it at all.
- recordDecision writes an audit entry in the same transaction as the
  decision, with approve and deny as distinct filterable actions, on a
  new mcp_approval_request audit subject with its own URN type and
  outbox event.
- Each decision in the API now carries the evidence frozen at its own
  decision time plus its version, so after a re-gather the surface can
  still show what an older decision actually rested on.
- Evidence numerics decode with UseNumber so a large integer re-encodes
  exactly as stored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Concurrent decisions on one request now serialise: the decision
  transaction resolves the request through a locking read, so its
  status always matches the newest decision rather than whichever
  transaction happened to commit last.
- A re-request reopens a denied review — the denial stays in the
  decision history and the request returns to the queue — while an
  approved or still-pending request keeps its status. The upsert's
  conflict branch previously only bumped updated_at, contradicting the
  documented re-request behaviour.
- A rationale is now required at the payload boundary and blank ones
  are rejected: it is the artifact cited when explaining the decision
  to the requester, and a decision without one cannot be explained.
- getRequest returns the request's research-agent runs, newest first,
  completing the ticket's read surface. Reports are project-bounded
  like every other child read.
- Test honesty from review: the page-size test now seeds past the cap
  so the clamp is what bounds the result; the cross-project child test
  exercises the repo predicates directly instead of asserting on a
  fresh request the parent 404 already covered; the grant-surface
  success case covers the four new scopes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The role editor actually shows the new permissions now: the dashboard
  group list is hard-coded and anything unlisted is silently dropped,
  so mcp_approval gets a group, joins the project-selectable resource
  types, and is registered in the client-side scope mirrors
  (resourceKindForScope, exclusion sets, sub-scope expansion) that
  previously fell through to the wildcard.
- A decision can cite the research report that informed it. The report
  is resolved against the request being decided and the caller's
  project before anything is written, so a decision can never attribute
  research about one server to another; the citation rides on the
  decision in the API.
- Research reports expose prompt_version, and failed runs can record
  their failure reason.
- The URN type validates its current ID on every call instead of
  caching the constructor's verdict, and a zero UUID is rejected at
  parse rather than deferred to a later marshal.
- RBAC runs before the feature lookup, so an unauthorized caller costs
  no feature-store work and a lookup failure never masks a denial.
- rawEvidence rejects trailing content after the decoded value,
  restoring the strict no-partial-document contract UseNumber's decoder
  had relaxed.
- The cross-project child test now seeds a decision in the other
  project, so its empty result proves the predicate filters rather than
  the data being absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- rawEvidence verifies end-of-input with a Token read rather than
  More(): More reports false for a trailing `]` or `}`, so `{"a":1}]`
  slipped past the guard — confirmed empirically before fixing.
- A malformed research_report_id is rejected before any database work,
  so it costs no transaction and never locks the request row.
- The mcp_approval mappings in useRBAC get unit coverage matching the
  other scope families, and the orphaned ptrString test helper is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@daviddanialy
daviddanialy force-pushed the daviddanialy/ais-470-feat-mcp-approval-management-api branch from f0a7ad2 to be5fde6 Compare August 7, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant