Developer reference

Build with Laki.ai headless MCP

Connect a server-side agent or an internal Organization integration owned by a Laki.ai account to Finnish legal-source search and reading over MCP. EU legislation is onboarded but search is not yet enabled on this endpoint. Laki.ai supplies evidence and citations; your application controls orchestration and the final answer.

API keys are server-side credentials
Store LAKI_API_KEY only in a server-side secret manager. Never expose an API key or OAuth access token in browser JavaScript, mobile code, public repositories, analytics, logs, or end-user-visible traces.

Access

Authenticate a server-side agent or internal Organization integration with an API key carrying the legal_sources:search scope. The key belongs to the Laki.ai account that created it.

  • Personal and internal Organization keys require a PRO or Enterprise subscription in good standing. Integration keys require an enabled Developer subscription or an Enterprise contract with redistribution rights in good standing.
  • Account standing is checked on every call. New API keys require good standing. Existing API keys continue working during payment retries and stop at final lapse, except that existing Developer integration keys continue during a recorded, incomplete support-assisted switch with no termination requested, as provided by the Developer Addendum. This continuity grants no new keys or human access. Configured expiry, revocation, or account deletion still stops access.
  • A personal API key is for the account that owns it. An Organization API key is only for internal integrations serving members covered by the Organization subscription. Both may support ordinary professional client work permitted by the applicable terms.
  • A Laki.ai-based product or service offered to third parties requires a Developer subscription or an Enterprise contract with redistribution rights. End-customer attribution is optional for Developer subscriptions; negotiated contracts retain their own requirements.

Create and manage Organization API keys in Organization API key settings.

Configuring a CLI or coding agent? Use the personal-agent setup guide.

For Developer offers, switching assistance or negotiated Enterprise redistribution access, contact info@laki.ai.

The Developer Addendum sets out integration rights and responsibilities when expressly incorporated into your agreement.

Connection and contract

The headless endpoint is a widget-less JSON-RPC 2.0 server over stateless Streamable HTTP.

Endpoint

https://api.laki.ai/mcp/headless
  • Every modern POST sends Authorization: Bearer <token>, Content-Type: application/json, Accept: application/json, text/event-stream, MCP-Protocol-Version: 2026-07-28, and Mcp-Method matching the JSON-RPC method. tools/call and resources/read also send Mcp-Name matching the requested name or URI; encode non-ASCII and unsafe names with the MCP Base64 sentinel form.
  • Every modern request carries params._meta.io.modelcontextprotocol/protocolVersion and params._meta.io.modelcontextprotocol/clientCapabilities. clientInfo is optional after discovery. Call server/discover to inspect the advertised 2026-07-28, 2025-06-18, and 2025-03-26 revisions and the tools/resources capabilities.
  • Send exactly one JSON-RPC request per POST. Batches, notifications, client response messages, null request ids, sessions, resumability, and SSE responses are unsupported. GET and DELETE return 405 with Allow: POST.
  • Legacy-shaped requests keep the existing initialize flow. Legacy initialize accepts 2025-06-18, 2025-03-26, and the unadvertised 2024-11-05 compatibility revision; unsupported offers, including 2026-07-28 in a legacy initialize, negotiate to 2025-06-18.
  • The profile exposes no widgets or UI resources; resources/list returns an empty list.
  • By default, tools/list omits outputSchema and successful tool calls return model-readable content only.
  • For typed results, send params._meta["ai.laki/result-format"] = "structured" alongside the required modern metadata on every tools/list and tools/call request. The same six tools then advertise closed output schemas and return identical content plus conforming structuredContent.
  • Modern successes have resultType: complete and server identity in result._meta. Discovery and resource/list-style reads also return ttlMs: 0 and cacheScope: private; tool calls are not cacheable.

Use the authenticated tools/list response for the live contract; each structured-mode outputSchema validates that tool’s structuredContent. The downloadable snapshots document both request modes but are documentation artifacts, not a compatibility promise — new tools and optional inputs can appear additively, so do not generate client code against the snapshot files. A missing selector always chooses content mode, while malformed or unsupported selector values return JSON-RPC -32602.

Download the default tools snapshotDownload the structured tools snapshot

Research workflow and tools

The six tools are stateless. Preserve returned source identifiers and citations exactly in your own orchestration.

Workflow

  1. Connect with the MCP client, then call tools/list. Omit the result-format selector for content-only use, or select structured mode on this request and every tool call.
  2. Call search_legal_sources with distinct legal-concept queries to find candidate sources.
  3. Read every source used as evidence. Navigate large Finnish sources with get_table_of_contents or search_within_documents; use get_statute_section_history for historical Finnish statute sections, then read the returned ids. EU read and navigation are not enabled on this endpoint; call get_source_coverage for current availability.
  4. Have the answer generator return a cited draft and its exact cited source ids. Verify that each cited id was returned by read_document or get_statute_section_history.

Tools

The live schemas contain the complete field contract. These summaries retain the behavior that affects orchestration.

read_document

ids contains 1–10 source ids, each at most 256 characters. includeRelatedSourceStubs defaults to true. For every statute-section read with related stubs enabled, pass concise context (at most 2,000 characters)—either the user’s question or another short statement of the legal issue and desired related-source angle—on that same call because no context is retained between calls. It may be omitted for whole-document reads or when related stubs are disabled.

Returns Finnish documents and per-id errors independently. EU read is not enabled on this endpoint. Documents over 100,000 formatted characters return too_large and must be navigated in parts.

get_table_of_contents

ids contains 1–10 Finnish source ids. depth is 1–6 (default 2). EU navigation is not enabled on this endpoint.

Returns structural locators for navigating identified documents.

search_within_documents

ids contains 1–10 Finnish source ids. Use Finnish search terms. A semantic query is at most 200 characters; FTS and hybrid accept 500, with hybrid using the first 200 for its semantic leg. EU search-within is not enabled on this endpoint.

Returns up to 30 locators inside the supplied documents.

get_statute_section_history

id is one Finnish statute-section id of at most 256 characters. startFromIndex is an optional zero-based index into the genuine section-change history.

Returns up to five section changes per call, newest first, with amendment and preparatory-work information.

get_source_coverage

Accepts a closed empty object with no input fields.

Returns curated guidance about currently searchable Finnish and EU legal sources, important unavailable sources, and search tactics. It is not a live-health or corpus-count endpoint.

Evidence and citations

  • Search snippets, table-of-contents entries, within-document matches, related-source stubs, and history locators are leads. Read the exact source before relying on it.
  • Use a content block’s returned citation when present; otherwise use the document citation. Copy returned citations verbatim.
  • Never cite an unread or invented source. Keep any final cited-set bookkeeping inside your application; the headless endpoint does not persist it.

Examples and failures

Use the TypeScript MCP SDK for application integration. The curl sequence shows modern discovery and one stateless tools request.

The answer builder receives normalized read evidence and returns the answer plus the exact ids it cited. The integration requests structured mode on every list/call and verifies those ids before returning the answer.

Node.js 20+ setup
npm install @modelcontextprotocol/client@2.3.0
npm install --save-dev tsx
npx tsx caller.ts
research.ts
import type { AuthProvider } from '@modelcontextprotocol/client'
import {
  Client,
  StreamableHTTPClientTransport,
} from '@modelcontextprotocol/client'

interface SearchData {
  results: Array<{ items: Array<{ id: string }> }>
}

interface ReadError {
  id?: string
  reason: string
  message?: string
}

interface ReadData {
  documents: Array<{
    id: string
    citation: string
    contentBlocks: Array<{
      id: string
      text: string
      citation?: string
    }>
  }>
  errors: ReadError[]
}

interface EvidenceBlock {
  sourceId: string
  text: string
  citation: string
}

interface CitedDraft<T> {
  answer: T
  citedSourceIds: string[]
}

type AnswerBuilder<T> = (
  evidence: EvidenceBlock[],
  readErrors: ReadError[]
) => Promise<CitedDraft<T>>

const token = process.env.LAKI_API_KEY
if (!token) throw new Error('LAKI_API_KEY is required')

const authProvider: AuthProvider = {
  token: async () => token,
}

const endpoint = new URL('https://api.laki.ai/mcp/headless')
const resultMeta = { 'ai.laki/result-format': 'structured' } as const

export const runLegalResearch = async <T>(
  queries: string[],
  buildAnswer: AnswerBuilder<T>
): Promise<T> => {
  const client = new Client({
    name: 'my-backend',
    version: '1.0.0',
  }, {
    versionNegotiation: { mode: { pin: '2026-07-28' } },
  })
  const transport = new StreamableHTTPClientTransport(endpoint, {
    authProvider,
  })

  const callTool = async <TResult>(
    name: string,
    args: Record<string, unknown>
  ): Promise<TResult> => {
    const result = await client.callTool({
      name,
      arguments: args,
      _meta: resultMeta,
    })
    if (result.isError || !result.structuredContent) {
      const message = result.content?.find(
        (item): item is { type: 'text'; text: string } =>
          item.type === 'text'
      )?.text
      throw new Error(message ?? name + ' failed')
    }
    return result.structuredContent as TResult
  }

  try {
    await client.connect(transport)
    await client.listTools({ _meta: resultMeta })

    const search = await callTool<SearchData>('search_legal_sources', {
      queries,
      mode: 'hybrid',
      limit: 5,
    })
    const sourceIds = [
      ...new Set(
        search.results.flatMap(group =>
          group.items.map(item => item.id)
        )
      ),
    ].slice(0, 10)
    if (sourceIds.length === 0) throw new Error('No legal sources found')

    const read = await callTool<ReadData>('read_document', {
      ids: sourceIds,
      includeRelatedSourceStubs: false,
    })
    const evidence = read.documents.flatMap(document =>
      document.contentBlocks.map(block => ({
        sourceId: block.citation ? block.id : document.id,
        text: block.text,
        citation: block.citation ?? document.citation,
      }))
    )
    if (evidence.length === 0) throw new Error('No readable evidence found')

    const draft = await buildAnswer(evidence, read.errors)
    const readSourceIds = new Set(evidence.map(block => block.sourceId))
    const citedSourceIds = [...new Set(draft.citedSourceIds)]
    if (citedSourceIds.some(id => !readSourceIds.has(id))) {
      throw new Error('The answer cited a source that was not read')
    }

    return draft.answer
  } finally {
    await client.close()
  }
}
caller.ts
import { runLegalResearch } from './research.js'

const result = await runLegalResearch(
  ['perustuslaki'],
  async (evidence, readErrors) => {
    if (readErrors.length > 0) {
      console.warn('Some sources could not be read', readErrors)
    }

    const firstEvidenceBlock = evidence[0]
    if (!firstEvidenceBlock) throw new Error('No evidence returned')

    return {
      answer: {
        text: firstEvidenceBlock.text,
        citation: firstEvidenceBlock.citation,
        sourceId: firstEvidenceBlock.sourceId,
      },
      citedSourceIds: [firstEvidenceBlock.sourceId],
    }
  }
)

console.log(result)

Failure layers

Handle transport, request, and partial-result failures separately.

HTTP and authentication

Non-2xx response before a usable JSON-RPC result

401 includes a WWW-Authenticate challenge for missing, invalid, revoked, or expired credentials; 403 means insufficient scope. Modern parse, envelope, metadata, header, and version failures return 400; unsupported JSON-RPC methods return 404; unsupported HTTP methods return 405.

JSON-RPC and whole-tool failure

{ error: { code, message } } or { result: { isError: true } }

Handle JSON-RPC codes independently from a successful JSON-RPC response whose tool result has isError: true. Whole-tool failures remain content plus isError in either result mode.

Partial result and advisory

read_document content, or structuredContent.errors[] in structured mode

read_document can return useful documents alongside per-id failures. An errors[] entry without an id and reason missing_context is a non-fatal advisory that related sources were left unfiltered; the returned documents remain usable. Content mode renders failures and advisories inline, while structured mode also carries typed errors.

Coverage

Current production availability:

  • Direct search covers Finnish statutes, case law, government proposals, and Verohallinto / vero.fi tax guidance. The preparatory_works filter searches government proposals only.
  • Other parliamentary and preparatory documents are readable when reached through returned or related-source ids, but are not directly searchable. inForceOnly and asOfDate apply to statutes; section history applies to Finnish statutes.
  • EU primary-law and European Parliament/Council secondary-legislation search (treaties, the Charter, regulations, directives, and decisions) is onboarded but not yet enabled for this endpoint in production; call get_source_coverage to check current availability. EU case law and other EU source families are not yet searchable.
  • When EU search is enabled on this headless surface, use Finnish queries for Finnish law and separate English queries for EU law — do not combine both languages in one query. This surface uses lexical matching; Microsoft Copilot separately supports EU semantic matching. EU search is English-only, not cross-lingual.
  • Other national jurisdictions are not currently available through the headless endpoint.

Data handling

For new MCP calls, Laki.ai does not persist raw tool arguments, search queries, or read_document.context in its application database. Historical rows created before this policy may remain.

  • Operational records contain content-free metadata: account, tool name, profile, status, and duration. Token, provider, and cost records are created only for model-backed operations.
  • Headless calls do not create or update research-session rows, persist read sets, or persist final cited sets. read_document.context applies only to that call.
  • Finnish hybrid and semantic search send query text to Google’s embedding service. Finnish FTS starts without query embedding but can fall back to hybrid when it returns no results. When EU search is separately enabled for a profile, EU FTS and hybrid remain lexical and never trigger an embedding or semantic rescue.
  • When related-source filtering runs, the configured model provider can process read context, source text, and candidate snippets. Disable related stubs and omit context when that processing is not needed.

Sensitive data

Research inputs may contain confidential information and personal data when necessary for the research task and permitted by the terms governing the customer account. Never send passwords, API keys, OAuth tokens, other secret access or authentication credentials, or information the customer has no right to provide to the processing chain.

Developer metadata download and exit

Organization owners and administrators can download retained metadata from billing settings, including after Developer API access ends while retrieval remains open. Cancellation uses the existing billing controls; exceptional switching and retrieval extensions use switching and retrieval support.

The authenticated HTTP interface is GET /teams/:teamId/developer-subscription/export. It returns a no-store JSON attachment named developer-export-v1.json. GET /teams/:teamId/developer-subscription/exit supplies lifecycle dates and available actions. Settings submit separately authorized, revision-bound erasure instructions to POST /teams/:teamId/developer-subscription/erasure.

The format is UTF-8 JSON (RFC 8259), version developer-export-v1, with UTC ISO 8601/RFC 3339 timestamps, opaque stable IDs, integer durations and EUR minor-unit prices. Lifecycle calendar arithmetic uses Europe/Helsinki. No additional interchange standard or destination-provider connector is required for this metadata format.

FieldIncluded metadata
profileOrganization ID, name, slug, domain policy and legal profile. billingAddress is null; billingEmail mirrors the available billing identity.
billingIdentityMatching Stripe customer name, email and structured postal address; available/unavailable status and omission notice.
offerCustomer-visible offer for the selected Stripe mode, including monthly EUR minor-unit price and tax treatment.
membershipsCurrent member user IDs, roles and joining timestamps. Private personal user profiles are excluded.
agreementsAccepted document versions and URLs, Developer release hashes, representative/authority snapshots, acceptance timestamps and commercial terms.
subscriptionsRetained grant history, plan/source, status, start/end/cancellation dates and accepted Developer commercial terms.
keysOrganization-owned key IDs, labels, audience, scopes, creation/service provenance, expiry/revocation and last-used dates; includes retained expired/revoked keys.
toolCallsRetained headless event IDs, key/service IDs, timestamps, tool names, outcomes and durations in milliseconds.
customerAttributionSeparate retained event IDs, key/service IDs, timestamps and optional customer-supplied opaque subjects. These are unverified; no per-call join to toolCalls is available.

version, exportedAt, cutoff and snapshot describe the download. Rows are read in deterministic batches of at most 500 and include creation times up to the export-start cutoff. This is not a transactional snapshot: edits, revocations, membership changes and retention expiry can occur during download. A disconnected or failed stream stops; discard any incomplete JSON and retry. Dashboard windows do not restrict the retained events available for export.

If Stripe billing identity is unavailable or the customer mode does not match, the download continues with local records and an explicit omission notice. Separate Stripe invoices, tax records and payment instruments remain available through the billing portal where applicable.

Excluded: personal keys billed to the organization; credentials and hashes; other customers’ data; research requests/results, session histories and legacy raw log payloads; model/provider/task/token/cost diagnostics; private order-document/support/provider references; internal access/audit and retention controls. Headless research requests and results are not retained as research histories. Optional attribution is exported separately from model diagnostics.

Retrieval remains available for at least 30 Helsinki calendar days after confirmed termination or completed switching, longer if agreed. Delayed detection does not shorten that period. Paid API access and retrieval are separate. Explicit earlier erasure ends the bound Developer service and permits its scoped cleanup after provider confirmation. Shared organization/profile/membership records, reused keys, continuing services and legally retained agreements/accounting evidence are preserved. Whole-organization deletion waits for scoped erasure confirmation.

Account-specific terms govern customer use. Where those terms incorporate it, see Laki.ai Data Processing Addendum (DPA) and current subprocessor list.

Third-party-facing products require a Developer subscription or an Enterprise contract with redistribution rights. End-customer attribution is optional for Developer subscriptions; negotiated contracts retain their own requirements. The applicable agreement governs retention, residency, subprocessors, security roles, and data processing.