Integrations changelog
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.
- New scope
gap:issue, new MCP toolissueGapReport(tools/callwithanalysisId), and new REST routePOST /api/v1/gaps/{analysisId}/issue. Response:issuance_id,price_usd,is_free,reused,content_sha256. - Issuance is idempotent on content. Issuing the same gap items and resolved citations again returns the existing issuance with
reused: true, writes no usage row and bills nothing. - The first issuance in each workspace is free. Later issuances are priced at the server price, fixed on the issuance row. Downloads of an issued report are never billed.
- DataFed units:
gap_reportwithcount: 1for a new issuance andcount: 0for a reused one. - Downloads:
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 with409andcodecontent_changed. - Issuing requires an active owner, admin or member; analyses with no resolved citation return
422no_resolved_citations. The free report is per workspace (at most 3 per user under the MAN-222 cap). - Deploy order: the code tolerates migration 00064 not being applied yet. Until it is,
POST /api/v1/gaps/{analysisId}/issueand the MCPissueGapReporttool return503gap_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. - Soft-deleted analyses cannot be issued. Issuance rows are protected by
ON DELETE RESTRICTforeign keys. - Tool count: 16 (2 statute, 10 Slice-4, 4 Matters).
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 when it is valid (1 to 128 characters from A-Z a-z 0-9 . _ -), otherwise a minted UUID, and it is read once per request. Successful dispatcher responses also echo the header. The usage row for POST /api/v1/citations/verify now records this same id, so an error body, its header and its usage row correlate.
Changed: error envelope in the OpenAPI document. ErrorResponse now documents the optional request_id. The document previously omitted it while additionalProperties rejected it.
2026-10-22 (MAN-163) #
Changed (MAN-163) #
- Title matching uses a citation-shaped title, once migration 00067 is applied. This ships with the code. It is active only after
statutes.citable_titleexists (migration 00067). Until then the resolver answers exactly as before, and citable-title matching returns no candidates. The new column is generated fromtitlefor Barbados rows: the act-number suffix is dropped and the year kept (Data Protection Act, 2019-29becomesData Protection Act 2019), andActis appended when no legislative noun ends the title (Accidental FiresbecomesAccidental Fires Act). An exact match on that title is tried before the existing title path. Fuzzy matching is unchanged. - A chapter is never affirmed on a title alone. When a citation names an Act and a chapter, and the corpus holds no chapter for that statute, the result is
not_foundwith reasonact_title_cap_unverified. It is notresolved. A stored chapter that differs iscap_title_mismatch. Chapters compare after normalisation (015matches15,309amatches309A). - A year the corpus does not carry is unverified. A citation with a year the stored title lacks resolves as
title-fuzzywith reasonact_title_year_unverified, at the fuzzy confidence band. - Cap resolution is unchanged until the Barbados chapter mapping is generated. Most Barbados statutes have no stored chapter until then. The three Acts the proposal names (Companies Act Cap. 308, Financial Management and Audit Act Cap. 5, Public Procurement Act 2021) are not yet in the corpus.
- No response shape changes. Existing
reasonvalues are unchanged;act_title_cap_unverifiedandact_title_year_unverifiedare new.
2026-10-22 #
Added (MAN-232) #
GET /api/v1/evidence/export?from=&to=(scopeusage:read, owner or admin only) returns a zip evidence bundle for one workspace and period:manifest.json,usage.csv,gap_reports.json,citations.jsonandaudit.json. Schema version 1. Seedocs/integrations/evidence-bundle.md. The field list is a draft pending the Lab's review.- A bundle over 4 MB is refused with
413and codeEVIDENCE_TOO_LARGE. Each file is capped at 50,000 rows; a truncated file is marked in the manifest. - The DataFed upstream contract is pinned by a test: an MCP client on protocol 2025-06-18 initialises, lists only scope-granted tools, and receives
_meta.datafed.unitsfromsearchStatutesandverifyCitation. The live variant runs whenMANDATUM_E2E_URLandMANDATUM_E2E_KEYare set.
Contract change (MAN-222). POST /api/v1/skills/run can now refuse a run before it is enqueued.
402withcode: "BUDGET_EXCEEDED"when the workspace has reached its monthly agent budget. The body'serrornames the ceiling, the month-to-date spend, the estimate and what is left. No run row is created and no event is sent.429withcode: "too_many_requests"and aRetry-Afterheader (seconds) when the caller or the workspace has started too many runs in the last minute.- Runs started from the app (analysis, deep chat, document review, workflows) are gated by the same budget. The budget is set per workspace by the platform team; a new workspace defaults to a placeholder ceiling.
Breaking (MAN-229). Removed generateClause, generateRedline, formatAkomaNtoso, searchGazettes (scaffolds, not real); compareLaws description corrected.
generateClause,generateRedline,formatAkomaNtosoreturned deterministic string scaffolds with no model call.formatAkomaNtosoalso interpolated the clause into XML unescaped. They are removed fromtools/list,tools/calland the exposable catalogue.searchGazetteswas astatutes.titleILIKE query. No gazette tables exist. It is removed from MCP and from the REST route.POST /api/v1/drafts/generateandGET/POST /api/v1/gazette/searchnow return410 Gonewith{ "error": "removed until real", "code": "gone", "see": "<this changelog>" }for authenticated callers. Unauthenticated callers still receive401.- The scopes
draft:generateandgazette:readare deprecated. They are still accepted when keys are issued, shared or validated, so stored keys keep working, but they unlock no tool. compareLawskeeps its name and scope. Its description now reads: "Title match of statutes across the requested jurisdictions; returns matching statute titles, not a semantic comparison".verifyCitationdescription now states what it does today: it resolves Act/Cap and section against the corpus and returnstitle_not_found,cap_not_foundorsection_not_foundwhen it cannot. Currency (current,repealed,amended_by,currency) is tri-state and is read from the derived currency model (MAN-62):nullmeans unknown, andunknown_reasonssays why.docs/integrations/tool-catalog.mdnow documents all 15 exposed tools, including the fourmatters:readtools.
Tools exposed after this change: 15 (2 statute, 9 Slice-4, 4 Matters).
Changed (MAN-236) #
- Per-request MCP server.
POSTandDELETE /api/mcpbuild a freshMcpServerafterenforce(), connect it, handle the request and close it infinally. 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.tsruns 50 concurrenttools/callrequests and checks each response has its own id. GET /api/mcpanswers405withAllow: POST, DELETE, afterenforce(). 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.- Tool annotations. Every tool in
tools/listcarriesannotations(MCPToolAnnotations). Reads, searches, verifications andparseDocument(which parses without persisting) arereadOnlyHint: true,idempotentHint: true.triggerWorkflowRunis the one write:readOnlyHint: false,idempotentHint: false,destructiveHint: true, so clients should confirm before calling it. All tools areopenWorldHint: false. Annotations are hints, not an access control. Seetool-catalog.md. - Registry listing.
public/.well-known/mcp/server.json(nameai.intergentech.mandatum/mandatum, streamable-http remotehttps://mandatum.intergentech.ai/api/mcp, OAuth discovery at/.well-known/oauth-protected-resource). It validates against the registry schema stored atopenapi/mcp-registry-server.schema.json. Publishing is a Q1 item:registry.md. - Protocol revisions.
2026-07-28is newly accepted in theMCP-Protocol-Versionheader, and it works only oninitialize. 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 toinitializeand negotiates2025-11-25. Of the other revisions in the list,2025-11-25,2025-06-18,2025-03-26,2024-11-05and2024-10-07were already accepted before this change. The endpoint is still legacy-era, does not serveserver/discover, and does not serve modern_metarequests. The accepted list is inmcp-endpoint-contract.md. - Sources read 2026-10-22: https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning, https://modelcontextprotocol.io/specification/2026-07-28/basic/transports, https://github.com/modelcontextprotocol/registry/tree/main/docs/modelcontextprotocol-io (quickstart, authentication, remote-servers), https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json.
Changed (MAN-30) #
Audit log is tenant-scoped and tamper-evident. Migration 00066_audit_log_tenant_chain.sql. Full design: docs/integrations/security-model.md, "Audit log".
- Audit rows carry
organization_id. Members read only their organisation's rows. Anonymous callers read and write nothing. Theaudit_insertpolicy foranonandauthenticatedis dropped; no application code wrote audit rows through a user session, and every writer uses the service role. - Rows are append-only for every role.
UPDATE,DELETEandTRUNCATEare refused. Retention by DELETE is no longer possible. - Each organisation has its own sha256 hash chain, and rows with no tenant form a system chain. The database computes every hash; a caller-supplied hash is ignored. Rows before the
audit.chain_genesisrow are a legacy md5 segment and are not verified. verify_my_audit_chain(organization_id)(owners and admins of that organisation) andverify_audit_chain()(service role) recompute a chain in SQL. The GraphQLverifyAuditChainquery takes an optionalorganizationId; the compliance audit check uses the same verifier.- Key issue, rotate, revoke and policy-change rows keep their
actionanddetail(previously dropped). Analysis and invite audit rows are written as objects with the tenant set; invite rows no longer write the email intoresource_id. - Index:
idx_audit_organizationis created by the migration only for tables up to 100,000 rows. Larger tables need theCREATE INDEX CONCURRENTLYstep in the runbook. - The MCP route's
tools/callaudit row names the tool (external_tool:<tool>), covered by a test.
Changed (MAN-174) #
Additive, plus one meaning made precise. No served field is renamed or removed.
-
POST /api/v1/citations/verifyand theverifyCitationMCP tool return two new nullable fields on each citation:section_designationandsection_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 arenullwhen no section resolved. -
Pinpoints match the printed designation, not a positional counter. A citation to
s. 41no longer resolves to whichever row is 41st in the chunker's heading sequence. It resolves to the section whose designation is41, or it refuses. -
New
reasonvalues for a pinpoint the corpus cannot answer:section_designation_unknown(the Act resolved and we hold no designations for its sections) andambiguous_section_in_corpus:N(N rows claim the designation in one container). Neither asserts that the section does not exist. -
section_not_foundnow 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 answerssection_partially_designatedinstead (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 issection_designation_unknownwith reasonsection_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. -
Until the designation backfill has run for an Act, a section-level citation to it answers
section_designation_unknown. This is fewer resolved pinpoints, deliberately. Anchor to the statute and say the pinpoint is unverified. -
The API tolerates the migration not being applied yet.
section_designationandsection_designation_sourceare in theCitationVerificationschema of the published OpenAPI document. Until migration00068_section_truth_columns.sqlis applied, the section reads hit an undefined-column error. The service logs one warning and treats every Act as undesignated: pinpoints are refused assection_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.
Changed (MAN-220) #
matters:readnow 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.- Migration 00059 backfills an owner row for matters with no members, so legacy matters stay visible to their creator's keys.
- API key creation, revocation and sharing edits are owner or admin only, enforced in RLS as well as the server action.
- Removing a workspace member, or deactivating a user, revokes that user's OAuth tokens and API keys for the affected workspace.
Changed (MAN-200) #
- Statute search response shape.
searchStatutesover 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. - MCP
jurisdictioninput isstring[]. A comma-separated string is still accepted. - Empty jurisdiction means "not provided". An empty
jurisdiction([], or?jurisdiction=) applies the workspace home jurisdiction. Useallto search every jurisdiction. - Home jurisdiction default. When a caller sends no jurisdiction filter, the workspace home jurisdiction (Settings, General) applies, for REST, MCP and agent searches.
Added (MAN-225) #
- Every successful MCP tool result carries
_meta.datafed.units: onecallunit for every tool exceptverifyCitation, which reportsverified_citationunits, one per resolved citation. Failed or rejected calls report no units. POST /api/v1/citations/verifyresponses carry the sameunitsarray, and each result carriesverdict(resolved,unresolved) andcurrency_statebeside the tri-state currency fields.