Skip to content
docs
Arxo ↗

Connect and discover

For LLMs8 sections

This page covers one integration pattern: the agent explores a canon itself. It connects to an MCP server (directly or through a typed client), searches in its own words, picks one question, learns which inputs that question needs, and asks it. If your application instead exposes its own domain tools and the model never sees law_* calls, or prints a canon into code, or runs a pinned service, the choice is made on Choose an integration pattern; this page does not repeat that comparison.

MCP is not the only network interface: law serve is a long-running HTTP service over one pinned world with an audit journal and /v1/... routes (/v1/ask, /v1/world, plus health and metrics), run as a private deployment; see Private HTTP service with law serve. The pattern on this page uses MCP.

Each MCP server has its own name, corpus slice and tool set, and every answer names its jurisdiction.

Terminal
claude mcp add --transport http law-dsl https://mcp.arxo.io/mcp
claude mcp add --transport http law-dsl-islam https://mcp.arxo.io/mcp/islam

The root server carries the whole corpus with Kazakhstan as its main jurisdiction, under the law of the Republic of Kazakhstan with its official calendar. The Islam slice sees Islamic doctrine (fiqh, faraid, the Quran), which is not the law of a state and has no calendar. Never present a doctrinal or scientific slice as state law; the answers say what they are, and the agent repeats that.

Route check, dated: every path below was probed with tools/list on 2026-10-03 (probe command in the Validation record).

RouteStatusMeaning
https://mcp.arxo.io/mcp200use this server
https://mcp.arxo.io/mcp/islam200use this server for the Islam slice
https://mcp.arxo.io/mcp/kz410 Gonedead address; see the migration note

Treat every slice path as a claim to re-check with tools/list, not as a constant.

Migration note (diagnostics only). The old Kazakhstan slice address https://mcp.arxo.io/mcp/kz answers 410 Gone: that slice no longer exists as a separate server. If a stored configuration still points at it, move it to the root route; the root server already carries Kazakhstan as its main jurisdiction. Never put the dead address in a fresh example.

The repository carries an MCP configuration that starts the same server over stdio with the Kazakhstan profile selected. The network variant serves HTTP on loopback by default; binding off loopback requires an explicit authentication decision. As published in the MCP guide; not re-run for this page.

From TypeScript, @arxo/law-client offers a method per tool over Streamable HTTP, a generic call(name, args), and registry access. It is transport and shape validation only: no legal semantics, no engine. Its listTools() asks the live server, and the server’s list, not the client’s built-in table, is the truth about what is available.

Call tools/list before planning anything. The set differs per server and slice: slices do not carry the author workbench or adversarial analytics, and the server instructions name what is missing so an agent does not plan those calls. This section keeps no tool list and no tool count, because both go stale and the server’s answer does not. A tool-by-tool reading lives in the MCP tool reference.

Two related lists deserve the same treatment:

  • Prompts. Servers may offer prompts (task routes, review checklists). List them; a prompt that calls tools your profile denies is not offered. Do not reconstruct it by hand.
  • Answer capabilities. The answer-capabilities matrix describes one thing only: which answer preparations a server supports. It is not a catalogue of the platform. Read it when you prepare or publish an answer (Prepare and publish an answer), not when you connect.

Five steps: search in your own words, compare the candidates, read the winner’s signature, ask the rules for its inputs, pick the query kind. A wrong pick gives a correct answer to the wrong question.

The through-example comes from the Labour Code of the Republic of Kazakhstan (kz-labour-code): a worker with one child under eighteen months is given a twenty-minute feeding break. Is that shorter than the minimum?

Status: ran via MCP law_search against the repo server (jurisdiction Republic of Kazakhstan as reported by the server; no profile name is exposed).

JSON
// law_search request
{ "query": "перерыв для кормления ребёнка короче минимума", "limit": 6 }

The captured answer listed six candidates. The agent’s job is to compare, not to grab the top hit:

#CandidateKindScoreAgent’s reading
1feeding_break_too_short(employee, employer)predicate0.4027Matches the story: a break shorter than the minimum
2meal_break_too_short(employee, employer)predicate0.2475A different break: meal rest, not child feeding
3TK_ART82, article 82 point 3source fragment0.2108The pinned source text behind candidate 1
4TK_ART81, article 81 point 1source fragment0.1883The text behind the meal-break question
5TK_ART82, article 82 point 4source fragment0.1816Nearby text about joining breaks; not the question
6child_feeding_break_minutes(employee, employer, minutes)predicate0.1801Looks like an input fact, not the verdict

Similarity is not applicability: the score measures text overlap, not whether the norm governs this case. The list is discovery only and establishes no fact and no norm; only an ask can. Each candidate also carries a next field, a ready template of the follow-up call; use it instead of assembling arguments from memory.

Candidate 1 wins and candidate 2 is the trap: both are “break too short” questions in the same act, and their labels differ by one concept. The agent reads both signatures and source fragments and records why the loser lost: the meal-break predicate is about daily rest-and-meal breaks, the story is about feeding a child.

Candidate 6 is a different case. Its three-place signature (employee, employer, minutes) marks it as data the case supplies; the two-place candidate 1 is the verdict the rules derive. Asking candidate 6 would only echo the input, so the agent asks candidate 1 and feeds candidate 6 in as a fact. Collect facts and evidence develops this input-versus-verdict split.

When two candidates survive comparison, the honest move is to ask the user a clarifying question, or to run both asks and report both. Never silently merge two questions into one.

The agent reads the winner’s full name, argument order, argument types and namespace from the package catalog.

Status: ran via MCP law_packages (package kz-labour-code, Labour Code of the Republic of Kazakhstan), same session as the search above. The catalog confirms three-place and two-place signatures with typed parameters, for example feeding_break_too_short(employee: Employee, employer: Employer) and child_feeding_break_minutes(employee: Employee, employer: Employer, minutes: Integer).

Short-name matching is not enough. On the local engine, a structural query for producers of the bare name feeding_break_too_short returned No matches. Rows: 0, while the same query with the namespaced name urn:kz:corpus:clir:labour-code#feeding_break_too_short returned both strict rules (commands under How to verify). Keep the full namespace on every name you store; two acts can define similar names, and the namespace is what keeps them apart.

4. Ask the rules which inputs the question needs

Section titled “4. Ask the rules which inputs the question needs”

Do not guess facts, and do not rely only on the model reading rule text. law_rules has two modes:

  • Readable mode (predicate set): the rules that derive the predicate, verbalized, with their premises and source articles. For a large act, package alone returns a paged table of contents (limit, offset, and a next call for the remainder).
  • Machine contract mode (predicate set, contract: true): an input contract computed over the same import and dependency world that law_ask would use. No case is evaluated; the server states that the result describes the program, not a legal answer.

Status: labeled pseudocode. The request shape and field names below are read from the server’s tool schema and contract builder; this call was NOT RUN for this page. Reproduce by sending it to the root server and comparing the field names.

JSON
// law_rules request, machine contract mode
{ "package": "kz-labour-code",
"predicate": "feeding_break_too_short",
"contract": true,
"inputLimit": 50 }

The answer’s inputContract lists what the agent must collect:

FieldWhat it tells the agent
inputsThe leaf declarations the question depends on: canonical id, owning package, typed parameters (enum variants listed), argsTemplate, and inputKind: case (the case supplies it), table (a table input), or unresolved
complete, gapsWhether the projection is complete; each gap names a node the projection cannot follow, with a reason
derivedPredicates, directRulesWhat the rules derive on the way, and the rules that produce the goal directly
worldThe packages and their semantic hashes the contract was computed over
inputTotal, truncated, nextPaging: next is a ready law_rules call with the following inputOffset

The top-level next is a law_ask template for the goal. Two rules follow. If complete is false, the input list is not exhaustive: say so rather than presenting it as everything the norm needs. And contract requires predicate; a package-only contract call is refused.

The same predicate can be asked in several query kinds; the authority for the set is the kind enum in the server’s law_ask schema, which on the check date held six values. The agent picks the kind that matches what the user actually needs:

Query kindWhat it returnsWhen the agent uses it
truthWhether the statement is established on the factsThe user wants a yes / not-established finding
collectThe values a relation takes on the factsThe user wants a number, a date, or a list
why_notA per-premise report on a goalThe user asks “why didn’t this hold?”
positionsNormative positions arising from the facts, each with its modality (duty, prohibition, power, liberty, immunity …) and statusThe user asks “who must, may, or may not do what?”
deadlineA computed time limitThe user asks “by when?”
calcAn arithmetic expression over values and normative constants from the canonThe user wants a computed amount or comparison

Status for truth: ran via the public MCP endpoint (root route); the full call is below. Status for collect: ran via MCP law_ask with kind collect, asking child_feeding_break_minutes with the minutes argument left as the sample variable ?minutes and one case fact (25 minutes) supplied; the captured answer returned the single value 25, grounded on that fact.

Status for why_not: observed as the per-premise report embedded in the captured truth answers (satisfied / not-satisfied / undefined premises); a standalone call was NOT RUN (reproduce: the minutes-only truth call with kind why_not). Status for positions, deadline and calc: NOT RUN; the through-example needed none. A deadline ask names its deadline policy explicitly: without one, a norm with a term in days answers MISSING_POLICY, and the server does not assume a policy.

Two cautions on reading the result. An empty positions list does not by itself establish a cause; do not explain it as “missing facts” without the diagnostics. And an empty collection, an unknown value, a zero and a proven absence are four different outcomes; only the question’s own contract says which one an empty result means. Never translate “no rows” into “no obligation”. Every status and the next step it allows are in the single table on Read answers.

The whole loop ends in one call. Field names below are the contract; values illustrate one run (check date in the Validation record). The date of law is named by the human, never defaulted to today; see Pin context, editions and time.

Terminal
curl -s https://mcp.arxo.io/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"law_ask",
"arguments":{
"kind":"truth",
"package":"kz-labour-code",
"predicate":"feeding_break_too_short",
"args":["Айгуль","Работодатель"],
"facts":[
{"predicate":"child_feeding_break_minutes","args":["Айгуль","Работодатель",20]},
{"predicate":"children_under_eighteen_months","args":["Айгуль",1]}
],
"legalTime":"2026-09-01"}}}'

Status: ran via the public MCP endpoint (root route) on 2026-10-03. The reply carries a human line plus the same answer as a document: truthStatus TRUE_ONLY, the rule for one child (“not less than thirty minutes”, Article 82), the Labour Code as provenance, and a resultHash. The MCP guide walks the same call in full.

Sometimes the search returns nothing useful. The session’s garbage query is instructive.

Status: ran via MCP law_search with a nonsense query string. The captured answer returned a single low-score candidate (score 0.158) from an unrelated act about construction in seismic zones.

Three lessons for the agent:

  1. A near-empty result says nothing about whether a norm exists in the system. The catalog may hold the norm under words the query did not use; the search tool itself states that an empty result does not mean the norm is absent.
  2. A lone low-score candidate is usually noise. The agent must not inflate it into a “closest match” answer.
  3. The recovery is to rephrase, not to conclude. Try the act’s own vocabulary (found via the package catalog), narrow the search with package or kinds, split the story into smaller concepts, or ask the user for the document or article they have in mind.

If rephrasing still yields nothing, the agent reports “no suitable question found in the executable model” and names the queries it tried. That report is itself useful output. Keep it distinct from two stronger findings: a matching question is absent from the checked package version; or a discovered question has no executable coverage in the current model. Those findings require inspection of the package or the question’s coverage, not an empty search response. Never conclude from a search alone that the norm was never formalized, and never conclude that the law is silent: “the model does not contain this norm” and “the law says nothing” are different answers.

Legal language reuses words: “break” in the Labour Code means at least meal rest and child feeding, and “minimum” appears across dozens of thresholds. Compare full labels rather than single words, open the source fragments (article 82 on nursing breaks, article 81 on meal rest), check which facts the rules read (child counts and minutes versus shift schedules), and keep namespaces. When the story genuinely fits two questions, run both and label both. A candidate list is a starting point for checks, never a completed check.

Every absence has its own shape, and the agent must not collapse them:

AbsenceLooks likeAgent behavior
Tool not in tools/listthe name is simply not theredo not call it; do not emulate it; say which server would have it, if one is known
Slice without a contourdirect call refuses with a reasonrelay the refusal; reroute to a server that offers it, or stop
Unknown predicaterefusal naming available addressessearch in your own words; never guess a name (Empty search results)
Unknown profile pathtransport error, not an answercheck the URL; a transport failure is never an answer about the model

The failure case to memorize: an agent plans a workbench call on a slice, gets a refusal, and “helpfully” performs the step itself in prose. The refusal named a boundary; crossing it in prose is exactly what the agent contract forbids.

  1. Re-run the curl above; expect TRUE_ONLY with the Article 82 rule and a resultHash (values may move with the corpus; the field names are the stable part).
  2. Call tools/list on each server you use; diff the sets and confirm your agent plans only listed tools.
  3. Repeat the law_search, law_packages and collect calls and compare with the six candidates, the two signatures and the value 25.
  4. Send the law_rules contract request above and compare the inputs inputContract names, and its complete flag, with the facts the law_ask call supplies.
  5. Reproduce the namespace lesson on the local engine:
Terminal
./law version
./law query producers \
--param predicate='urn:kz:corpus:clir:labour-code#feeding_break_too_short' \
--param pkg=kz.corpus.labour_code --format human
./law query producers \
--param predicate='feeding_break_too_short' \
--param pkg=kz.corpus.labour_code --format human

Status: ran locally (the three commands above). The namespaced query returned the two strict producers; the short-name query returned No matches. Rows: 0.

Show commands, versions and results
CheckStatusDate
Route probe: /mcp 200, /mcp/islam 200, /mcp/kz 410 Goneran (curl, below)2026-10-03
Feeding-break truth ask; law_ask kind enum (six values) from tools/listran via the public MCP endpoint (root route)2026-10-03
law_search, law_packages, collect; law query producersran via MCP (repo server); ran locallyearlier session
law_rules contract modeschema and contract builder read; call NOT RUN2026-10-04
Standalone why_not, positions, deadline, calc; local-versus-public byte comparisonNOT RUN (no local server started; reproduce from the checkout’s MCP configuration)—

The 2026-10-03 checks ran at commit 2b894bea80bae78d081202f93f0911615eeeec04 (uncommitted changes in the tree not covered). Route probe, once per route:

Terminal
curl -s -o /dev/null -w '%{http_code}\n' https://mcp.arxo.io/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Previous: The agent contract Next: Pin context, editions and time Related: Choose an integration pattern · Read answers

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.