Changes to the MCP tool catalogue and REST surfaces that affect external clients.
2026-10-22 (MAN-224)
Added (MAN-224). The gap report is issued as a billable, append-only record.
gap:issue, new MCP tool issueGapReport (tools/call with analysisId), and new REST route POST /api/v1/gaps/{analysisId}/issue. Response: issuance_id, price_usd, is_free, reused, content_sha256.reused: true, writes no usage row and bills nothing.gap_report with count: 1 for a new issuance and count: 0 for a reused one.GET /api/export/gap-report/{issuanceId}/{docx|pdf} (app session only). The report is regenerated from the stored analysis; if the analysis has changed since issue, the download is refused with 409 and code content_changed.422 no_resolved_citations. The free report is per workspace (at most 3 per user under the MAN-222 cap).POST /api/v1/gaps/{analysisId}/issue and the MCP issueGapReport tool return 503 gap_issuance_unavailable ("not provisioned; migration 00064 has not been applied"), the workbench panel shows the same sentence, and the usage page shows one line in place of the issued-reports table. Nothing else on the usage or workbench pages fails.ON DELETE RESTRICT foreign keys.2026-10-22 (MAN-234)
Added: SDKs. @mandatum/sdk (TypeScript, 0.1.0) and mandatum (Python, 0.1.0) are generated from openapi/mandatum-v1.json. Each has one method per operation, retries a 429 up to three times honouring Retry-After (each wait capped at 60 seconds), enforces https base URLs, applies a per-attempt timeout, and raises a typed error carrying status, code and request_id. See docs/integrations/rest-api.md, section SDKs.
Changed: every REST error carries request_id. Before, most REST error bodies omitted request_id. Every /api/v1 error response now carries it in the body and in an x-request-id response header. The id is the caller's x-request-id
2026-10-22 (MAN-163)
statutes.citable_title exists (migration 00067). Until then the resolver answers exactly as before, and citable-title matching returns no candidates. The new column is generated from title for Barbados rows: the act-number suffix is dropped and the year kept (Data Protection Act, 2019-29 becomes Data Protection Act 2019), and Act is appended when no legislative noun ends the title (Accidental Fires becomes Accidental Fires Act). An exact match on that title is tried before the existing title path. Fuzzy matching is unchanged.not_found with reason act_title_cap_unverified. It is not resolvedGET /api/v1/evidence/export?from=&to= (scope usage:read, owner or admin only) returns a zip evidence bundle for one workspace and period: manifest.json, usage.csv, gap_reports.json, citations.json and audit.json. Schema version 1. See docs/integrations/evidence-bundle.md. The field list is a draft pending the Lab's review.413 and code EVIDENCE_TOO_LARGE. Each file is capped at 50,000 rows; a truncated file is marked in the manifest._meta.datafed.units from searchStatutes and verifyCitationA-Z a-z 0-9 . _ -POST /api/v1/citations/verifyChanged: error envelope in the OpenAPI document. ErrorResponse now documents the optional request_id. The document previously omitted it while additionalProperties rejected it.
cap_title_mismatch01515309a309Atitle-fuzzy with reason act_title_year_unverified, at the fuzzy confidence band.reason values are unchanged; act_title_cap_unverified and act_title_year_unverified are new.MANDATUM_E2E_URLMANDATUM_E2E_KEYContract change (MAN-222). POST /api/v1/skills/run can now refuse a run before it is enqueued.
402 with code: "BUDGET_EXCEEDED" when the workspace has reached its monthly agent budget. The body's error names the ceiling, the month-to-date spend, the estimate and what is left. No run row is created and no event is sent.429 with code: "too_many_requests" and a Retry-After header (seconds) when the caller or the workspace has started too many runs in the last minute.Breaking (MAN-229). Removed generateClause, generateRedline, formatAkomaNtoso, searchGazettes (scaffolds, not real); compareLaws description corrected.
generateClause, generateRedline, formatAkomaNtoso returned deterministic string scaffolds with no model call. formatAkomaNtoso also interpolated the clause into XML unescaped. They are removed from tools/list, tools/call and the exposable catalogue.searchGazettes was a statutes.title ILIKE query. No gazette tables exist. It is removed from MCP and from the REST route.POST /api/v1/drafts/generate and GET/POST /api/v1/gazette/search now return 410 Gone with { "error": "removed until real", "code": "gone", "see": "<this changelog>" } for authenticated callers. Unauthenticated callers still receive 401.draft:generate and gazette:read are deprecated. They are still accepted when keys are issued, shared or validated, so stored keys keep working, but they unlock no tool.compareLaws keeps its name and scope. Its description now reads: "Title match of statutes across the requested jurisdictions; returns matching statute titles, not a semantic comparison".verifyCitation description now states what it does today: it resolves Act/Cap and section against the corpus and returns title_not_found, cap_not_found or section_not_found when it cannot. Currency (current, repealed, amended_by, currency) is tri-state and is read from the derived currency model (MAN-62): null means unknown, and unknown_reasons says why.docs/integrations/tool-catalog.md now documents all 15 exposed tools, including the four matters:read tools.Tools exposed after this change: 15 (2 statute, 9 Slice-4, 4 Matters).
POST and DELETE /api/mcp build a fresh McpServer after enforce(), connect it, handle the request and close it in finally. The module-level server that concurrent requests used to close under each other is gone. Every security step (enforce once, origin, request id, audit, usage scope) is unchanged. app/api/mcp/__tests__/concurrency.integration.test.ts runs 50 concurrent tools/call requests and checks each response has its own id.GET /api/mcp answers 405 with Allow: POST, DELETE, after enforce(). The endpoint is sessionless and never serves a standalone SSE stream. Before this change, a GET answered 200 with an empty stream. An unauthenticated GET still gets the 401 discovery challenge. A refused GET writes no audit or usage row.tools/list carries annotations (MCP ToolAnnotations). Reads, searches, verifications and parseDocument (which parses without persisting) are readOnlyHint: true, idempotentHint: true. triggerWorkflowRun is the one write: readOnlyHint: false, idempotentHint: false, destructiveHint: true, so clients should confirm before calling it. All tools are openWorldHint: false. Annotations are hints, not an access control. See tool-catalog.md.public/.well-known/mcp/server.json (name ai.intergentech.mandatum/mandatum, streamable-http remote https://mandatum.intergentech.ai/api/mcp, OAuth discovery at /.well-known/oauth-protected-resource). It validates against the registry schema stored at openapi/mcp-registry-server.schema.json. Publishing is a Q1 item: registry.md.2026-07-28 is newly accepted in the MCP-Protocol-Version header, and it works only on initialize. The SDK transport's own version check does not list 2026-07-28, so it answers every non-initialize request that carries that header with 400. A dual-era client falls back to initialize and negotiates 2025-11-25. Of the other revisions in the list, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 and 2024-10-07 were already accepted before this change. The endpoint is still legacy-era, does not serve server/discover, and does not serve modern _meta requests. The accepted list is in mcp-endpoint-contract.md.Audit log is tenant-scoped and tamper-evident. Migration 00066_audit_log_tenant_chain.sql. Full design: docs/integrations/security-model.md, "Audit log".
organization_id. Members read only their organisation's rows. Anonymous callers read and write nothing. The audit_insert policy for anon and authenticated is dropped; no application code wrote audit rows through a user session, and every writer uses the service role.UPDATE, DELETE and TRUNCATE are refused. Retention by DELETE is no longer possible.audit.chain_genesis row are a legacy md5 segment and are not verified.verify_my_audit_chain(organization_id) (owners and admins of that organisation) and verify_audit_chain() (service role) recompute a chain in SQL. The GraphQL verifyAuditChain query takes an optional organizationId; the compliance audit check uses the same verifier.action and detail (previously dropped). Analysis and invite audit rows are written as objects with the tenant set; invite rows no longer write the email into resource_id.idx_audit_organization is created by the migration only for tables up to 100,000 rows. Larger tables need the CREATE INDEX CONCURRENTLY step in the runbook.tools/call audit row names the tool (external_tool:<tool>), covered by a test.Additive, plus one meaning made precise. No served field is renamed or removed.
POST /api/v1/citations/verify and the verifyCitation MCP tool return two new nullable fields on each citation: section_designation and section_designation_source. They carry the designation the corpus holds for the resolved section, as the publisher prints it, and how it is attested. Render these, not the number the request asked with. Both are null when no section resolved.s. 41 no longer resolves to whichever row is 41st in the chunker's heading sequence. It resolves to the section whose designation is 41, or it refuses.reason values for a pinpoint the corpus cannot answer: section_designation_unknown (the Act resolved and we hold no designations for its sections) and ambiguous_section_in_corpus:N (N rows claim the designation in one container). Neither asserts that the section does not exist.section_not_found now means only that every row of the Act carries a designation and the cited one is not among them. An Act with any undesignated row answers section_partially_designated instead (see below).section_partially_designated. When an Act has some designated rows, the cited section is not among them, and some rows have none, the answer is section_designation_unknown with reason section_partially_designated. This is what an interrupted backfill leaves, and the missing section may be one of the undesignated rows, so its absence is not asserted. Re-running the backfill for the Act completes it.section_designation_unknown. This is fewer resolved pinpoints, deliberately. Anchor to the statute and say the pinpoint is unverified.section_designation and section_designation_source are in the CitationVerification schema of the published OpenAPI document. Until migration 00068_section_truth_columns.sql is applied, the section reads hit an undefined-column error. The service logs one warning and treats every Act as undesignated: pinpoints are refused as section_designation_unknown, and statute-level citations still resolve. This is intended. It never falls back to the positional value. The API re-probes the columns every ten minutes, so it picks up the migration without a redeploy. Each probe is one query; a failing probe restarts the ten-minute interval.Requires migration 00068_section_truth_columns.sql. Runbook: mandatum/docs/runbooks/section-truth-backfill.md.
matters:read now returns only matters the key's owner is a member of. Add the owner to a matter to expose it. Organisation-wide reads are not available through keys.searchStatutes over REST (GET/POST /api/v1/statutes/search) and MCP now returns { "results": [...], "applied_filter": { "jurisdictions": [...] | null, "source": "param" | "home" | "none" } } instead of a bare array of rows. Deliberate, documented break; no external consumer was live.jurisdiction input is string[]. A comma-separated string is still accepted.jurisdiction ([], or ?jurisdiction=) applies the workspace home jurisdiction. Use all to search every jurisdiction._meta.datafed.units: one call unit for every tool except verifyCitation, which reports verified_citation units, one per resolved citation. Failed or rejected calls report no units.POST /api/v1/citations/verify responses carry the same units array, and each result carries verdict (resolved, unresolved) and currency_state beside the tri-state currency fields.© 2026 Mandatum · Intergen Technology Inc.