Markdown for LLMs
Connect and discover
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# Connect and discover
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](/agent-engineering/integration-patterns/);
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](/operate/private-http-service/). The pattern on this page uses
MCP.
## Connect
### Public server and slices
Each MCP server has its own name, corpus slice and tool set, and every
answer names its jurisdiction.
```bash
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).
| Route | Status | Meaning |
|---|---|---|
| `https://mcp.arxo.io/mcp` | 200 | use this server |
| `https://mcp.arxo.io/mcp/islam` | 200 | use this server for the Islam slice |
| `https://mcp.arxo.io/mcp/kz` | 410 Gone | dead 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.
### Local server and typed client
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](/guide/mcp/); 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.
## The tool list is the authority
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](/guide/mcp-tools/).
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](/agent-engineering/publishing/)),
not when you connect.
## The discovery loop
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?
### 1. Start from the candidate list
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:
| # | Candidate | Kind | Score | Agent's reading |
|---|---|---|---|---|
| 1 | `feeding_break_too_short(employee, employer)` | predicate | 0.4027 | Matches the story: a break shorter than the minimum |
| 2 | `meal_break_too_short(employee, employer)` | predicate | 0.2475 | A different break: meal rest, not child feeding |
| 3 | `TK_ART82`, article 82 point 3 | source fragment | 0.2108 | The pinned source text behind candidate 1 |
| 4 | `TK_ART81`, article 81 point 1 | source fragment | 0.1883 | The text behind the meal-break question |
| 5 | `TK_ART82`, article 82 point 4 | source fragment | 0.1816 | Nearby text about joining breaks; not the question |
| 6 | `child_feeding_break_minutes(employee, employer, minutes)` | predicate | 0.1801 | Looks 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.
### 2. Compare before you commit
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](/agent-engineering/collect-facts/) 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.
### 3. Read the signature exactly
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
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:
| Field | What it tells the agent |
|---|---|
| `inputs` | The 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`, `gaps` | Whether the projection is complete; each gap names a node the projection cannot follow, with a reason |
| `derivedPredicates`, `directRules` | What the rules derive on the way, and the rules that produce the goal directly |
| `world` | The packages and their semantic hashes the contract was computed over |
| `inputTotal`, `truncated`, `next` | Paging: `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.
### 5. Pick the query kind
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 kind | What it returns | When the agent uses it |
|---|---|---|
| `truth` | Whether the statement is established on the facts | The user wants a yes / not-established finding |
| `collect` | The values a relation takes on the facts | The user wants a number, a date, or a list |
| `why_not` | A per-premise report on a goal | The user asks "why didn't this hold?" |
| `positions` | Normative positions arising from the facts, each with its modality (duty, prohibition, power, liberty, immunity …) and status | The user asks "who must, may, or may not do what?" |
| `deadline` | A computed time limit | The user asks "by when?" |
| `calc` | An arithmetic expression over values and normative constants from the canon | The 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](/agent-engineering/handle-answers/).
### The ask
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](/agent-engineering/context-and-time/).
```bash
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](/guide/mcp/) walks the same call in
full.
## Empty search results
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.
## Ambiguous terms and homonyms
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.
## Missing capabilities
Every absence has its own shape, and the agent must not collapse them:
| Absence | Looks like | Agent behavior |
|---|---|---|
| Tool not in `tools/list` | the name is simply not there | do not call it; do not emulate it; say which server would have it, if one is known |
| Slice without a contour | direct call refuses with a reason | relay the refusal; reroute to a server that offers it, or stop |
| Unknown predicate | refusal naming available addresses | search in your own words; never guess a name ([Empty search results](#empty-search-results)) |
| Unknown profile path | transport error, not an answer | check 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](/agent-engineering/agent-contract/) forbids.
## How to verify
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:
```sh
./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`.
## Validation record
| Check | Status | Date |
|---|---|---|
| Route probe: `/mcp` 200, `/mcp/islam` 200, `/mcp/kz` 410 Gone | ran (`curl`, below) | 2026-10-03 |
| Feeding-break truth ask; `law_ask` kind enum (six values) from `tools/list` | ran via the public MCP endpoint (root route) | 2026-10-03 |
| `law_search`, `law_packages`, `collect`; `law query producers` | ran via MCP (repo server); ran locally | earlier session |
| `law_rules` contract mode | schema and contract builder read; call NOT RUN | 2026-10-04 |
| Standalone `why_not`, `positions`, `deadline`, `calc`; local-versus-public byte comparison | NOT 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:
```sh
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](/agent-engineering/agent-contract/)
Next: [Pin context, editions and time](/agent-engineering/context-and-time/)
Related: [Choose an integration pattern](/agent-engineering/integration-patterns/) · [Read answers](/agent-engineering/handle-answers/)