Liking cljdoc? Tell your friends :D

The MCP server

Covers: vaelii.agents.mcp.* — a Model Context Protocol server over a Vaelii KB: sessions, forks, the six verb tools, the two transports, and the audit journal. Not here: the model-provider clients and the generated KB read tools (llm.md), which a model-calling agent uses instead of, or beside, this server. Assumes: the engine's model (README).

What it is

An MCP server that gives a client — an editor, an agent runtime, anything that speaks MCP — write access to a Vaelii knowledge base through six tools, each a verb an authenticated principal calls against its own fork. Unlike vaelii.agents.tools (generated read-only tools dispatched through the engine's op table), this server's verbs write: assert and unassert change belief state, under a durable audit journal and a security-authority registry that can revoke a principal mid-session and have that revocation fence a write already in flight.

namespacewhat it holds
vaelii.agents.mcp.verbsthe six verb tools (assert, unassert, unassert_handle, query, argue, matching), their MCP tool schemas, and the fail-closed dispatch table
vaelii.agents.mcp.dispatchertransport-neutral JSON-RPC dispatch, the audit-sink protocol and its durable JournalAuditSink, and effect fencing
vaelii.agents.mcp.sessionthe session/fork/authority registry: one atom, commit-before-ack persistence, lease expiry
vaelii.agents.mcp.auditpure v1 audit-record construction, validation, normalization and hashing — no I/O (audit-journal-v1-design.md)
vaelii.agents.mcp.configthe EDN server-config reader: bearer-token fingerprints to principal/fork identity
vaelii.agents.mcp.transportthe stdio JSON-RPC 2.0 loop
vaelii.agents.mcp.http-transportthe Streamable HTTP transport (/mcp) and a raw-operation route (/op)
vaelii.agents.mcp.overlayan optional directory of extra .clj/excepts.edn knowledge loaded into the base KB at server start
vaelii.agents.mcp.serverthe production -main entry point: startup ordering, custody checks, and wiring
vaelii.agents.mcp.arguea standalone four-valued (:true/:false/:unknown/:contradiction) epistemic query over ground assertions, independent of the verb of the same name, which calls vaelii.core/argue directly
vaelii.agents.mcp.kb-utilssmall vaelii.core helpers the overlay loader needs

Running it

clojure -M -m vaelii.agents.mcp.server --config config.edn --port 3000

A minimal config.edn:

{:state-dir "state"
 :forks-dir "forks"
 :principals
 {"alice" {:token-sha256 "<sha-256 hex of alice's bearer token>"
           :fork-id      "alice"
           :lifecycle    :durable}}}

:state-dir is always required (durable authority — revocations and fork metadata — persists there even for an all-leased config); :forks-dir is required once any principal's :lifecycle is :durable. VAELII_AUDIT_DIR (an absolute, symlink-free path outside /tmp and disjoint from :forks-dir/:state-dir) must name the audit journal's directory; production refuses to start without it.

Two or more principals may name the same :fork-id: they then share one forked KB, each write server-recorded with its own acting principal (mcp-shared-fork-proof.md). The first principal to name a fork fixes its :lifecycle/:lease-ttl-ms; a later principal naming the same fork with a different shape is refused at config-read time.

The six verbs

toolreads or writeswhat it does
assertwritestore a sentence in a context, capped at :default strength
unassertwriteexcept every visible sentex matching a sentence from a context
unassert_handlewriteexcept one sentex by its handle
queryreadanswer a goal (a sentence, or a vector for a conjunctive query), optionally with its inference justification
arguereadthe four-valued epistemic verdict for a ground (closed) goal
matchingreadstored, believed sentexes matching a single literal pattern — no inference, no subtype expansion

Every verb's sentence/goal/context/pattern argument is a JSON string holding an EDN s-expression ("(dog ?x)"), read with clojure.edn/read-string — never read-string — so a client's output cannot execute code. A write verb additionally re-derives, ahead of the write, every predicate the sentence would actually store under (rule surfaces, ist nesting, exceptWhen peeling, forall desugaring) and refuses an undeclared one before anything is stored; see the "Reaching into the engine" note below.

A result's wire shape is a JSON object serialized from the verb's result map; an error is {"isError": true, "content": [{"type": "text", "text": "{:code ... :error ...}"}]}. Every stored-claim node in a justification tree carries a server-recorded :provenance ({:creator :asserted_at}), read from the fork, never trusted from the caller.

Transports

Stdio (vaelii.agents.mcp.transport/start-stdio!): newline-delimited JSON-RPC on stdin/stdout, authentication by VAELII_TOKEN or the initialize call's _meta.token.

Streamable HTTP (vaelii.agents.mcp.http-transport): POST/DELETE/GET on /mcp (MCP Streamable HTTP), bearer-token authentication against config.edn, an Origin allowlist, and a bounded request body. POST /op is a narrower raw-operation route for a client that only needs to add facts: {:op :edit :args [{:add [[sentence context] ...]}]} goes through the same assert verb, on an ephemeral session, so audit, write-fencing and provenance recording apply exactly as on /mcp.

Audit and authority

Every call — successful, refused, or denied before authentication — leaves a durable Attempt and Completion record in an append-only, fsync'd, process-locked JSONL journal (vaelii.agents.mcp.dispatcher/JournalAuditSink); see audit-journal-v1-design.md for the full schema. Audit unavailability fails closed: no verb runs without a durable Attempt first.

The session registry (vaelii.agents.mcp.session) holds one atom for sessions, revoked principals, fork handles and metadata, and a monotonic generation counter. A write acquires a generation permit at admission and checks it again at the mutation boundary, under the same authority lock a revocation takes — so a principal revoked mid-request either fences the write before it commits, or loses the race and its write commits with a receipt that states exactly what happened, never a response that claims both outcomes at once.

Engine reach

vaelii.agents.mcp.* calls 26 engine functions outside vaelii.core: 23 in vaelii.impl.* across seven namespaces, and three in vaelii.host.guard. None is required directly from vaelii.agents.mcp.*. Every call goes through one re-exporting var in vaelii.agents.engine, so an engine change is repaired in one namespace (README, "Reaching into the engine"). By owning namespace, with the caller:

engine namespacevarscalled from, for
vaelii.host.guardrequire-token!, api-token, public-bind?server: the browser-bind token check, run before any fork store opens
vaelii.impl.cachesregister-cacheserver: registers the MCP request body limit with the daemon's bounded-state census
vaelii.impl.io.textread-forms, load-entries!overlay: the text-KB reader and loader, reused for overlay files
vaelii.impl.namingcontext?, literalsoverlay and verbs: context-argument checks; the predicate walk
vaelii.impl.assert-entryist-parts, storage-contextverbs: the predicate walk
vaelii.impl.rulessoft-priority-surface?, cardinality-surface?, minimize-surface?, split-exceptWhen, inner-rule, expand-ruleverbs: the predicate walk
vaelii.impl.sentexconjunction?, peel-exception-strength, forall?, desugar-forall-literal, defn-sentence?, rule-functorverbs: the predicate walk
vaelii.impl.sentexfree-vars, sentex-handleverbs: ground-goal checks in query/argue; the except term unassert writes
vaelii.impl.resolutionsubstitute, unifyverbs: grounding a conjunctive answer's justification tree

The predicate walk. Before assert stores a sentence, verbs/asserted-predicate-uses lists every predicate the sentence would store under, and the context each one stores in. It handles rule surfaces, ist nesting, except/exceptWhen peeling, forall desugaring, and a decontextualized predicate's storage context. An undeclared predicate is refused before anything is stored. 15 of the 26 vars exist only for this walk, and the walk duplicates decisions the engine's own assert path already makes.

Proposed vaelii.core entry point.

(vaelii.core/predicate-uses kb sentence context)
;; => a distinct seq of [predicate storage-context] pairs: every predicate
;;    (assert kb sentence context) would store under, and where.
;;    Reads only; stores nothing.

The engine's assert path computes these pairs itself, so this entry point would answer from that code rather than from a copy. With it, asserted-predicate-uses becomes one call, and the 15 walk vars leave vaelii.agents.engine. The other 11 are narrower: sentex-handle already has a vaelii.core counterpart (kb-utils calls vaelii.core/sentex-handle) and can move to it, and free-vars, substitute and unify are general term operations a vaelii.core read could expose. The guard, caches and io.text reaches serve a server process, not a verb, and can stay component-local.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close