Files
accounted/supabase/migrations/20260729094000_create_mcp_tasks.sql
T
Jakob Wennberg ef25a87d75 feat(mcp): Tasks extension (io.modelcontextprotocol/tasks) (#1283)
* feat(mcp): speak spec revision 2026-07-28 (stateless core)

Adopt the 2026-07-28 MCP spec revision on the connector endpoint while
keeping every handshake-era client (2025-06-18 and earlier) byte-identical:

- Accept per-request _meta protocol negotiation
  (io.modelcontextprotocol/protocolVersion); unsupported versions return
  UnsupportedProtocolVersionError (-32022) with the supported list.
- Implement server/discover (spec MUST): supported revisions, capabilities
  including the extensions field, identity, instructions, freshness hints.
- Decorate results for stateless clients: required resultType, serverInfo
  in _meta, and CacheableResult ttlMs/cacheScope on tools/list,
  prompts/list, resources/list, resources/read.
- Validate the standard Mcp-Method/Mcp-Name request headers when present
  (HeaderMismatchError -32020); absence stays accepted.
- Declare the ratified MCP Apps extension (io.modelcontextprotocol/ui) in
  capabilities; the widgets already use the ratified mime type and
  _meta.ui.resourceUri shape, so no widget changes are needed.
- OAuth: include the RFC 9207 iss parameter on every authorization
  response (success and error) and advertise
  authorization_response_iss_parameter_supported in RFC 8414 metadata.

Resource-not-found already used -32602 and tools/list ordering was already
deterministic; both are covered by the new test file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(mcp): Tasks extension (io.modelcontextprotocol/tasks)

Durable handles for long-running MCP tool calls, per the official Tasks
extension. A client that declares the extension in its per-request
capabilities gets a CreateTaskResult (resultType: "task") immediately;
the work completes after the response via after() and lands in the new
mcp_tasks table for tasks/get polling. Clients that did not declare the
extension are never handed a task (spec MUST).

- New mcp_tasks table (migration 20260729094000): company-scoped SELECT
  RLS, service-role-only writes (mirrors pending_operations), 1-hour
  expiry, status lifecycle CHECK. pg-real coverage included; triaged as
  excluded in the full-archive backup contract (transient state).
- tasks/get (creator-scoped), tasks/cancel (cooperative, working-only
  flip), tasks/update (ack no-op: no input_required flows yet).
- Tool opt-in via shouldRunAsTask predicate; first producer is
  gnubok_audit_package, the one genuinely long-running blocking call
  (multi-minute ZIP generation). estimate_only stays synchronous.
- Tool failures complete the task with the standard isError envelope,
  exactly what the synchronous call would have returned; the failed
  status stays reserved for infrastructure errors.
- server/discover and initialize now advertise the tasks extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): creator-only task RLS, enforced expiry sweep, RoPA entry

Compliance-swarm follow-ups on the mcp_tasks migration (editing the
migration is safe: it has not shipped beyond the ephemeral PR preview):

- SELECT RLS tightened from company-wide to auth.uid() = user_id so the
  DB grant matches the creator-scoped tasks/get contract; task results
  carry raw tool output (Art. 5(1)(c)). pg test now proves a same-company
  colleague cannot read the row.
- The 1-hour retention is now enforced, not aspirational: createMcpTask
  opportunistically deletes expired rows on every creation
  (idx_mcp_tasks_expires), best-effort (Art. 5(1)(e)).
- RoPA entry mcp.async_task_handles added to .compliance/ropa.yaml
  (Art. 30).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): literal terminal-update payload for the phantom-column guard

The conditional spreads in resolveMcpTask made the payload unresolvable
for the no-phantom-columns guard (362 > 360 ceiling). A literal payload
writing null for absent terminal fields is equivalent here: the terminal
transition sets the complete terminal state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:15:06 +02:00

60 lines
2.9 KiB
SQL

-- Migration: MCP Tasks (io.modelcontextprotocol/tasks extension)
-- Durable handles for long-running MCP tool calls: the tool call returns a
-- task handle immediately and the work completes after the response; clients
-- poll tasks/get until a terminal status. Writes go through the service-role
-- MCP handler only (mirrors pending_operations); only the creating user may
-- read. Retention (1 hour via expires_at) is enforced by an opportunistic
-- sweep in the MCP handler: createMcpTask deletes expired rows on every
-- task creation (GDPR Art. 5(1)(e)).
CREATE TABLE public.mcp_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company_id UUID NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- Attribution only (which API key created the task); deliberately no FK so
-- key rotation or deletion never breaks task history.
api_key_id UUID,
tool_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'working' CHECK (status IN (
'working', 'input_required', 'completed', 'failed', 'cancelled'
)),
status_message TEXT,
-- Terminal payloads: `result` holds exactly what the synchronous tool call
-- would have returned (a CallToolResult, including isError envelopes);
-- `error` holds a JSON-RPC error object for infrastructure failures.
result JSONB,
error JSONB,
poll_interval_ms INTEGER NOT NULL DEFAULT 2000,
ttl_ms BIGINT NOT NULL DEFAULT 3600000,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL DEFAULT now() + interval '1 hour'
);
-- tasks/get looks up by id + creator; expiry cleanup scans expires_at.
CREATE INDEX idx_mcp_tasks_user ON public.mcp_tasks (user_id, status);
CREATE INDEX idx_mcp_tasks_company ON public.mcp_tasks (company_id);
CREATE INDEX idx_mcp_tasks_expires ON public.mcp_tasks (expires_at);
ALTER TABLE public.mcp_tasks ENABLE ROW LEVEL SECURITY;
-- Reads: the creating user only. tasks/get in the MCP handler scopes to the
-- creator, and task results carry whatever the underlying tool returned;
-- the DB grant must not be broader than that application contract
-- (data minimisation, GDPR Art. 5(1)(c)). Mirrors pending_operations.
CREATE POLICY "mcp_tasks_select_own" ON public.mcp_tasks
FOR SELECT USING (auth.uid() = user_id);
-- No INSERT/UPDATE/DELETE policies: all writes go through the service-role
-- MCP handler (mirrors pending_operations). Rows age out via expires_at.
CREATE TRIGGER mcp_tasks_updated_at
BEFORE UPDATE ON public.mcp_tasks
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- No audit trigger: operational task plumbing, not business records. The
-- underlying tool executions already emit mcp.tool_called telemetry, and any
-- committed bookkeeping effects carry their own audit trail.
NOTIFY pgrst, 'reload schema';