ef25a87d75
* 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>
60 lines
2.9 KiB
SQL
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';
|