# DKE MCP — Tutorial **Version:** 1.0.4, changed in rev 163 · alpha · API 1.4 · rev 170\ **Status:** Published tutorial for the DKE service wire.\ **Audience:** Anyone connecting a client to the DKE service for the first time.\ **This document as Markdown:** [dke-mcp-tutorial.md](/tutorial/dke/mcp/dke-mcp-tutorial.md) — the same text this page is rendered from, for readers and tools that would rather have the source than the page. This tutorial walks the connection itself: getting a key, speaking the handshake, submitting your first module, running your first program, and reading what comes back. The [MCP Reference](/ref/dke/mcp/) is the exhaustive companion — every tool, every parameter, every field. The language you send is [DKE Python](/tutorial/dke/python/). This tutorial assumes you have used an HTTP API before. It does not assume you have used MCP. --- ## Chapter 1 — What you are talking to DKE is a **deterministic reasoning engine addressed over MCP**. That sentence carries three claims worth unpacking before you send anything, because each one changes how you should use the service. **Deterministic.** The same program against the same store returns the same answer, byte for byte. There is no sampling, no temperature, no model. If a call surprised you, the store changed or the program did — those are the only two possibilities, which makes debugging a matter of looking rather than guessing. **A reasoning engine.** You do not send questions; you send *programs*. The unit of work is a DKE Python source, and what comes back is a transcript of what that source did. **Addressed over MCP.** The transport is the Model Context Protocol: JSON-RPC 2.0 over HTTPS, protocol version `2024-11-05`. If your client already speaks MCP, DKE is an ordinary MCP server and you can skip to Chapter 3. If it does not, MCP is three request shapes, and Chapter 2 shows all three. One consequence of "engine, not model" is worth stating early: **you are billed per operation, not per token.** A program that reads ten thousand claims costs what those reads cost, whether you wrote it in five lines or fifty. --- ## Chapter 2 — Connecting ### The endpoint and the key | | | |---|---| | **Endpoint** | `https://dke.langsyn.net` | | **Protocol** | MCP over HTTPS, JSON-RPC 2.0, `2024-11-05` | | **Auth** | `Authorization: Bearer dke_s_…` | | **Server identity** | `serverInfo = { name: "dke", version: "1.3" }`, and the release — stage, API version, revision — on `_meta` | You get a **store key** — the `dke_s_` kind — from your [account dashboard](/dashboard/). It is the only kind this endpoint takes. Any other kind of key is refused here — in particular one beginning `dke_c_`: that is your customer key, which no endpoint takes, so keep it private. If a key is refused and you are confident it is valid, check the prefix first; it is the most common mistake. If no key arrives at all — a header left out of the configuration, or one a client dropped — the service answers `401` with `AUTH_MISSING`, but your client may not show you that. An MCP client that meets a `401` can take it as a request to sign in with OAuth, look for OAuth metadata this service does not publish, and report the failure of *that* instead: Claude Code, for one, says `Dynamic Client Registration rejected (HTTP 404)`. That message means the `Authorization` header never reached the service. Add it, and the same configuration connects. Your key addresses **exactly one store**. You do not choose a store per call and you cannot reach another account's data with it. Creating, deleting and snapshotting stores happens in the dashboard, not here — this wire governs the data *inside* your store, never the store itself. ### One key, one client **Give every client its own key.** A key is not just a password for your account — it is the name your work is recorded under. Every claim written through it is stamped with that key, and `written_by` reports it. Two clients sharing one key produce a history in which their work cannot be told apart, and nothing can separate it afterwards. This is easy to get wrong because of where the key lives. An MCP client usually reads it from a config file belonging to your user account, so *every* program you run as that user inherits the same key. A second assistant started in a fresh window is a fresh conversation, not a fresh credential: it will pick up the key already there and connect as you, without being asked for one. So when you add a client, issue it a key from the dashboard rather than pasting the one you already have. Issuing is the same two clicks either way, and a key you can name is a key you can revoke on its own when that client is finished. A client with no browser — a CI job, an agent, a server — can have its key issued on its own machine instead. `dkekey`, a program for Linux, macOS and Windows, and `dke.keys`, a module of the `dkepy` package, each issue a store key there and send the account service only its hash, never the key ([MCP Reference §3.1](/ref/dke/mcp/#issuing-a-store-key-on-your-own-machine)). **Give it only the access it needs.** A key is issued at one of five access levels — read; read-add, which records new facts and cannot change or delete anything already recorded; read-write; read-write-delete; or none, which keeps the key but lets it do nothing until it is given a level again. An additional key defaults to **read** — the right level for anything exploring your data rather than building it. Raise it when the client actually needs to write. A client that only reports what it sees, such as a sensor or an import job, needs no more than **read-add**. The dashboard shows, per key, when it was last used and which clients have connected with it, so one key in two places is visible without watching anything run. That last part rests on `clientInfo` in the handshake below, which a client sends about itself: it is **self-declared**, so read it as a label, not as proof. It is enough to notice a key spread by accident — the case that actually happens — and it is not authentication. ### The handshake MCP opens with `initialize`, then you ask what tools exist, then you call one. ```json {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "my-client", "version": "0.1"}}} ``` The answer carries the server's identity and every number you were built against: ``` result = { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "dke", version: "1.3" }, instructions: "DKE alpha, API 1.4, rev …. One revision for the whole published surface: …", _meta: { stage: "alpha", api_version: "1.3", revision: … } } ``` `serverInfo.version` is the API version, `MAJOR.MINOR`: MAJOR moves only on a change you would notice, and MINOR when something is added. `_meta` carries the release — `stage`, `api_version` and `revision` — the three facts on the first lines of every published document, the [MCP Reference](/ref/dke/mcp/) and the [Python Specification](/spec/dke/python/) included. The revision is one number for the whole surface: it moves whenever any published document changes, so compare it with the revision you implemented against, here, before anything else. When it moved, each document's Revision history says whether that document changed and how. `instructions` says the same in words, for clients that show it to a model and never surface `_meta`. ```json {"jsonrpc": "2.0", "id": 2, "method": "tools/list"} ``` `tools/list` is worth calling once and reading, rather than trusting a list you copied from somewhere. It is the authority on what this deployment serves, and a tool name it does not return is not served — calling one gets you `-32601`. Everything after that is `tools/call`: ```json {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "stores", "arguments": {}}} ``` `stores` is a good first call: it touches nothing, and a successful response proves your key, your transport and your store all work before you have written a line of DKE Python. --- ## Chapter 3 — The two verbs that compute Nineteen tools are served, but only two of them *compute*. The rest inspect and manage. The two are `compile` and `run`, and they differ in exactly one respect: > **`compile` keeps the compilation. `run` does not.** "Keep it" versus "try it." Almost every question about which verb to use answers itself once you know that. ### `compile` — store a module ```json {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "compile", "arguments": {"name": "inventory", "source": "class Item:\n on_hand: int\n\ndef restock(id: string, n: int):\n remember(Item(id).on_hand, n, \"restock-job\")\n"}}} ``` This stores a module called `inventory` and **runs its body**. That second half surprises people: compiling is not a silent validation step. A module body's data declarations execute, its rules install, its classes install, and its `def`s become callable. Compiling is how a module puts itself into effect. If the source does not compile, **nothing is stored** — you get `isError: true`, `_meta.status = "compile_error"`, and `_meta.diagnostics` with a line, a column and a message per problem. A module that fails to compile leaves the store exactly as it was; there is no partial install to clean up. ### `run` — execute instructions ```json {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "run", "arguments": {"source": "import inventory\ninventory.restock(\"widget-1\", 40)"}}} ``` A `run` compiles and executes once and stores nothing. Anything it `remember`s persists — the *data* is durable — but nothing is left callable and nothing shows up in `list`. Two limits follow from "instructions, not definitions", and both are refusals rather than silent surprises: - **A program cannot define.** A `source` containing a `def`, a `class`, a `@rule` or a computed field is refused, with a message pointing you at `compile`. Definitions are durable things; `run` is the verb that keeps nothing. - **A program has no parameters and cannot `return`.** Write the values you want into the instructions, and report results by printing or binding them. ### Which one do I want? Ask what you want to survive the call. | You want | Verb | |---|---| | a procedure you will call repeatedly | `compile` | | a class, a standing rule, a computed field | `compile` | | facts loaded once, as part of a module | `compile` | | a one-off query or a script call | `run` | | to try something before committing to it | `run` | --- ## Chapter 4 — Reading the response Every `tools/call` returns a JSON-RPC success envelope. Whether the *call* succeeded is a separate question from whether the *transport* did, and that distinction is the thing to get right early. ``` result = { content: [ { type: "text", text: … }, ], isError: , _meta: { status, tenant_id, key_id, answered_at, ops, … } } ``` ### Route on status, never on prose `_meta.status` is one of `success`, `refuse`, `engine_error`, `compile_error`, and `isError` is exactly `status != "success"` — the two can never disagree. So `isError` alone is enough to tell a completed call from a failed one, and you never have to read the human-readable text to find out which happened. When it did fail, the status tells you **whose problem it is**, which is what you actually route on: | Status | Whose problem | What to do | |---|---|---| | `compile_error` | your source | read `diagnostics`, fix the source | | `refuse` | the call was declined | read `refuse_reason` | | `engine_error` | the service, or something your program assumed | read the message: it says what happened | An `engine_error` promises nothing about the next call, so read the message. Three cases are worth memorising. **Naming something that does not exist — a script, a module — is an `engine_error` that calling again will not change.** The call is well-formed and there is simply nothing at that address. Call `list` or `list_modules` and find the right name. **So is a program that uses a value nothing holds.** Reading `.value` off an empty read stops the run, and the message names the line and the guard it needs, a `match` with `case empty:`. The fix is in your program, not in the service. **A store busy with another operation is an `engine_error` too.** The statement that met it kept nothing, and the message says the store was busy and that nothing in the call needs changing. ### Two channels, saying different things The structured block appears twice, and this is deliberate rather than redundant. - **`_meta` is canonical, and carries the whole run** — every step, including the working of any module your program imported. - **The mirror** (a trailing `content[]` text item, JSON-parseable) carries the same *fields* but not the whole run: it is your **results** — what you printed and the final value of each statement you wrote. A module's internal working is not in it. It exists because some MCP clients never surface `_meta` to the model. Prefer `_meta`; treat the mirror as a compatibility affordance. The useful way to hold the difference: **read the mirror to see what a program answered, and `_meta` to see how it got there.** ### Whose line number is that? Each transcript entry carries a `line` — and it is a line of the source *the entry came from*, which is not always the source you submitted. When a module's lines run as part of your run, its entries join your transcript carrying **`from_module`**. Your own entries have no such field. So: read `line` against `from_module` when it is present, and against your own source when it is not. Your line 12 and a module's line 12 are never confusable. A call adds nothing under your line on the script's behalf. Its parameters take your arguments without an entry, whether you call it as a statement or bind what it returns, so an entry with no `from_module` is always a statement you wrote. ### Why a loop's transcript is shorter than its loop A line that runs many times is **reported, not repeated** — otherwise a response would be as long as the work rather than as long as the answer. For each line, the first few entries and **always the last** are kept; the repetitions between are dropped. The last is kept because it holds the value that line finally produced. Your own `print` output is never dropped. And when anything was dropped, `transcript_omitted` gives the count — so a shortened transcript can never be mistaken for a short one. ### Why a long value is shortened A value that is very long would make the response as long as the data rather than as long as the answer: a program that saved a 16-qubit state would echo two megabytes of hex to say it had saved one thing. So a value in the transcript whose text is longer than 65,536 characters is shortened, and the entry says so. A list of 30,000 zeros reads `30000 entries [0, 0, 0, 0, 0, 0, 0, 0, +29992 more]`, and a blob reads ``. Nothing you compute is shortened. `print` writes the whole value, and so do `str()` and `len()`: when you want a long value back, print it. --- ## Chapter 5 — When things go wrong **Check the HTTP status first.** A request that never gets as far as the tool surface is answered with a status other than `200` and a small body `{"code": "…"}` — *not* a JSON-RPC envelope. Parsing every response as JSON-RPC is the first thing that breaks, usually on a mistyped key: | Status | `code` | Meaning | |---|---|---| | `401` | `AUTH_MISSING` / `AUTH_MALFORMED` / `AUTH_EMPTY_TOKEN` | no key, or not a well-formed one | | `401` | `AUTH_CUSTOMER_KEY_NOT_ALLOWED` | your customer key (`dke_c_`) — keep it private, and send a store key (`dke_s_`) | | `429` | `RATE_LIMITED` | too many requests — wait and retry | | `503` | `UPSTREAM_UNAVAILABLE` | not reachable right now — retry | Everything below answers `200`. So a non-`200` means you never reached the tools, and that is the cheapest thing to test for. Transport-level failures that *did* reach them arrive as JSON-RPC error codes: | Code | Meaning | |---|---| | `-32700` | malformed JSON | | `-32600` | not valid JSON-RPC 2.0 | | `-32601` | tool not found — any name `tools/list` does not return | | `-32602` | invalid params — missing `source`, unsupported language, an unqualified script name | | `-32000` | server error | | `-32001` | payment required — clears by **paying** | | `-32002` | rate limited — clears by **waiting**; a `Retry-After` header says how long | Three of these are worth a word. `-32602` on an **unqualified script name** catches people early. Scripts are addressed `module.script`, always — `inventory.restock`, never `restock`. The qualification is what lets two modules each define a `restock` without collision, so the wire insists on it rather than guessing. `-32001` covers every reason the call cannot be paid for: the balance will not cover it, the account is suspended, or the account does not include API access. A run is refused *before it executes* — the service never runs on credit. `-32002` is a separate code from `-32001` on purpose, and the distinction is the whole point of having two: **one clears by paying, the other by waiting.** Retrying a `-32001` will never work, and topping up after a `-32002` buys you nothing. Route on the code, never on the message. One thing you will not get is a diagnosis of a refusal's internal cause. Refusals name a category, not a reason: a scope refusal does not tell you which permission was missing, and a capacity refusal is a generic "service at capacity." ### The failure that is not an error The hardest case is the one that never errors. A query that returns nothing is either a correct negative — the reasoning ran and the answer really is no — or a rule that never fired, because the facts it reads are not there. Both come back as an empty result, and when the reasoning came from a module you did not write, nothing in the response distinguishes them. `checkup()` is the tool for exactly that moment. It takes no arguments and changes nothing: ```json {"name": "checkup", "arguments": {}} ``` ``` OK checkup: counts rules=4 unfirable=1 unreadable=0 declined=0 kinds=3 unread=1 constraints=0 unevaluable=0 violated=0 unmet audit.Dependency.version no-such-kind unread Dependancy near Dependancy ~ audit.Dependency ``` Read the `near` line first. Here the store holds facts under `Dependancy` and the reasoning reads `audit.Dependency` — one letter apart, which is why the query came back empty and why nothing complained. `unmet` names a cell some rule needs a value at that holds nothing; `unread` names a kind of yours that no rule reads. A mis-spelling produces one of each, and the pair names the mistake. Findings name cells rather than rules, because a cell is the part you can act on: what it needs is your data. --- ## Chapter 6 — What a call costs `_meta.ops` is the number of operations the call consumed. Three rules govern it, and together they make cost predictable: 1. **It counts what the program asked for.** The same question costs the same however you spell it, so you can write the clearer form without paying for it. 2. **An operation is counted each time it runs**, not once for each time you wrote it. An operation inside a loop over a hundred subjects is a hundred operations — cost follows the *data*, not the source length. 3. **1 LCN (LangCoin) buys 100,000 operations**, debited from the account's balance after the response is served. --- ## Chapter 7 — The other tools Everything that is not `compile` or `run` inspects or manages. Address scripts and rules by their qualified `module.script` / `module.rule` name, and modules by their bare name. | Tool | What it answers | |---|---| | `spec(edition, topic?)` | the DKE Python language contract, whole or by topic | | `list(target)` | stored script signatures, standing-rule names, the kinds your data is stored under, or the marks standing in your store | | `info(name)` | a script's compile provenance | | `list_modules()` | module handles — name, script count, created-at | | `info_module(name)` | a module's record — init language, scripts, classes, version, updated-at | | `forget(name)` | remove one script (idempotent) | | `forget_rule(name)` | remove one standing rule, leaving its module in place | | `forget_constraint(name)` | remove one standing constraint, leaving its module in place | | `forget_module(name, confirm)` | remove a module as a unit — recoverable only while a checkpoint stands | | `checkup()` | whether your store meets the reasoning standing over it | | `checkpoint(name)` | mark where the store is now, under a name you choose | | `diff(since)` | what changed since the mark of that name | | `restore(since)` | bring back the values removed since that mark | | `forget_checkpoint(name)` | drop a mark, letting go of the removals it alone was keeping | | `stores()` | your store's id, size, created-at | | `stop()` | stop the call running on your store, and say whether it stopped | | `report(kind, text, …)` | tell us that something went wrong, or what you would like | **`list(target = kinds)` answers a question the others cannot**, and it is worth knowing which question that is. `checkup` reports what your store is *missing* for the reasoning standing over it, so it names a kind only while no rule reads it — write a rule over a kind and it stops being mentioned. `subjects(...)` and `current(...)` report what is *in force*, so a value whose validity window has closed reads exactly like a cell nobody ever wrote. This target reports what is *there*. Reach for it before you write facts into a store you did not start from empty. A kind you did not expect means data you did not put there — and a read that comes back empty may be reading past it rather than past nothing. `get(K.s.a)` then shows every claim recorded at that cell, in force or not. Six behaviours here are easy to trip over: - **`forget_module` requires `confirm=true`** and cascade-removes the module's scripts, rules, kinds *and their data*. With a checkpoint standing, `restore` brings it back as a unit — rules included, reasoning again — all of it or none of it; with no mark standing, it is gone. It also **refuses**, naming the referrer, if another stored module still references it — so you cannot leave a dangling reference behind. - **`forget` is idempotent, but `forget_module` on a missing module is a clean not-found.** Neither is an error you need to guard against. - **`info` describes the compilation, not any run.** Invocations are not tracked. If you want to know how a *value* came to be, that is the in-language `why(K.s.a)`, which returns a proof tree — a different question with a different answer shape. - **`restore` only reaches removals that happened while a mark was standing.** Taking a `checkpoint` is what makes the removals *after* it recoverable, so it is worth doing before anything uncertain rather than after: nothing warns you at the moment of a removal that no mark was held. It brings back values and whole modules; a rule removed on its own with `forget_rule` comes back by compiling it again. A mark keeps what is removed after it until you drop it with `forget_checkpoint`, and that keeping takes storage, so drop a mark once you no longer need to restore from it — and not before. - **`stop` waits, and `stopping` is not a failure.** It stops whatever call is running on your store — a long `run` another connection is still waiting on, say — at that call's next statement, then waits for it to let the store go. A statement already running finishes first, so if one is still running when the wait ends, the answer is `stopping`: call `stop` again in a moment, and `idle` then means it has stopped. A `run` it stops keeps what it wrote before then, as any failed run does. - **`report` sends only what you write in it, and we can read it.** Your store is sealed to your key and we cannot look inside it, so a report is the one way to show us a program that misbehaves: put it in `source`, since nothing is taken from your store for you. A refusal that names its reason is not a bug — its `fix` is the next step (Chapter 5). The answer is an id, `rpt_…`, and no reply is sent; an account may send 20 a day. --- ## Chapter 8 — Where to go next - The [MCP Reference](/ref/dke/mcp/) is this tutorial's exhaustive companion: every parameter, every field of the envelope, the full error model. - The [Python Tutorial](/tutorial/dke/python/) teaches the language you send over this wire. - The [Python Reference](/ref/dke/python/) is the entry-per-construct lookup, including modules (§9). - The [Library Tutorial](/tutorial/dke/library/) covers the modules we publish, which you `compile` into your store instead of writing the same declarations yourself. The shape to carry away: two verbs compute and the rest manage; `compile` keeps and `run` does not; route on `_meta.status`, never on prose; and read `_meta` for how, the mirror for what. ## Revision history Newest first. The revision is the surface's — one number stated by every published document, the web pages and the service wire, moving whenever any document changes. This list records the revisions at which this document changed and what changed in it; a revision absent here left this document as it was. The Version line at the top states this document's version, the revision it last changed in and the current revision; from rev 148 each entry opens with the version that revision gave this document. The history starts at rev 1. What a revision promises is stated in the [Python Specification](/spec/dke/python/), its section 11.1. - **1.0.4** · **rev 163** (2026-11-14) — The Version line states API 1.4: `range` is a value of its own, walked without being built, so a `for` over a million numbers runs, and a range keeps its three bounds as `.start`, `.stop` and `.step` ([Specification §8.6](/spec/dke/python/#indexing-slicing-and-string-methods)), and an addition moves the API's MINOR. - **1.0.3** · **rev 151** (2026-11-13) — `serverInfo.version` and `_meta.api_version` are `"1.3"`: a rule premise may test part of a text, as `r.path.startswith("/srv/")`, `t.name.endswith(".test")` and `"admin" in a.role` ([Specification §14.2](/spec/dke/python/#premises)), and an addition moves the API's MINOR. - **1.0.2** · **rev 150** (2026-11-13) — `serverInfo.version` and `_meta.api_version` are `"1.2"`: calls made with keys that may only read run on one store together, where every call on a store ran alone ([MCP §9.8](/ref/dke/mcp/#stop-end-the-call-running-on-your-store)), and an addition moves the API's MINOR. The Version line states API 1.2. Nothing else changed. - **1.0.1** · **rev 149** (2026-11-13) — `serverInfo.version` and `_meta.api_version` are `"1.1"`: the API gained a form a rule's conclusion may take, a text written from values the rule read (Python Specification §14.3), and an addition moves MINOR. The Version line states API 1.1. Nothing else changed. - **1.0.0** · **rev 148** (2026-11-12) — This document states a version of its own, 1.0.0, first on the Version line and followed by the revision it last changed in, and from this revision each entry here opens with the version it made (Python Specification §11.1). Nothing else changed. - **rev 147** (2026-11-12) — `serverInfo.version` and `_meta.api_version` are `"1.0"`, the API version as `MAJOR.MINOR`: MAJOR moves only on a change you would notice, MINOR when something is added. The Version line states API 1.0. - **rev 115** (2026-11-07) — Chapter 4: an `engine_error` is the service's or something the program assumed, and a program that uses a value nothing holds is the third case worth memorising. - **rev 113** (2026-11-06) — Chapter 4: why a long value in the transcript is shortened, and that `print` writes it whole. - **rev 80** (2026-11-03) — Chapters 3 and 7: `report`, the nineteenth tool, and what it sends. - **rev 33** (2026-10-23) — The `-32601` row covers any tool name `tools/list` does not return. - **rev 31** (2026-10-19) — Chapter 8 names the Library Tutorial at its new address. - **rev 26** (2026-10-17) — The header states Version, Status, Audience and the Markdown copy, one field to a line, and the introduction follows it. Chapter 2: the account dashboard is linked, and a client with no browser can be issued its key on its own machine, by `dkekey` or by `dke.keys` in `dkepy` (MCP Reference §3.1). Chapter 8 names the Modules Tutorial. The rev 5 entry below sits under this section's introduction, with the others. - **rev 5** (2026-10-11) — Chapter 2: a key is issued at one of five access levels; read-add records new facts and cannot change or delete anything already recorded. - **rev 4** (2026-10-11) — Chapter 1: DKE is a deterministic reasoning engine, the words the landing uses. Chapter 2: the `initialize` answer carries the release as `_meta.stage`, `_meta.api_version` and `_meta.revision` — one revision for the whole published surface — and `instructions` states the same; the two per-document revisions it carried at rev 2 are gone. A key is issued, not minted, at one of four access levels, none included; the kinds of key this endpoint refuses are no longer named by prefix beyond the customer key. LCN is spelled out as LangCoin at its first use. Cross-references take the published documents' short names, the ones the footer uses — MCP Reference, Python Tutorial, Python Reference, Python Specification. - **rev 3** (2026-10-11) — The revision is the surface's: one number shared by every published document, the web pages and the service wire. This section says so, and the Version line moves from rev 2 to it. Nothing else changed. - **rev 2** (2026-10-11) — Chapter 2: the `initialize` answer is shown, and how to compare its `_meta.contract_revision` and `_meta.spec_revision` with the revisions you implemented against. - **rev 1** (2026-10-11) — The Version line states the document's revision and this section is added. Nothing else changed.