Pure policy for the Tier 2 execute tools (BOU-102): read-only SQL classification, row-limit clamping, and migration-direction validation.
No I/O and no throwing — the shell executors (boundary.mcp.shell.tools) call these to decide, then run the side effect and enforce the decision. These are defense-in-depth: the real guards are the security gate (only :full context reaches Tier 2) and, for query-db, a read-only database role. The SQL classifier is a coarse keyword check, not a parser; it can be fooled by a write keyword used as an identifier, which is why the read-only role is the primary control.
Pure policy for the Tier 2 execute tools (BOU-102): read-only SQL classification, row-limit clamping, and migration-direction validation. No I/O and no throwing — the shell executors (boundary.mcp.shell.tools) call these to decide, then run the side effect and enforce the decision. These are defense-in-depth: the real guards are the security gate (only :full context reaches Tier 2) and, for query-db, a read-only database role. The SQL classifier is a coarse keyword check, not a parser; it can be fooled by a write keyword used as an identifier, which is why the read-only role is the primary control.
Pure construction of guardrail error payloads — "guardrail, not straitjacket" (ADR-032).
Every enforcing tool returns the same shape: the BND code + rule that fired, the principle behind it, a suggested fix, and — when the rule is overridable — the audited bypass.
Two classes of denial:
--allow, which is
audited.This namespace is pure: it maps a denial to a BND code and assembles the payload from a catalog entry passed in. The catalog lookup (I/O) lives in boundary.mcp.shell.guardrail.
Pure construction of guardrail error payloads — "guardrail, not
straitjacket" (ADR-032).
Every enforcing tool returns the same shape: the BND code + rule that fired,
the principle behind it, a suggested fix, and — when the rule is overridable
— the audited bypass.
Two classes of denial:
* Security/capability gates (BOU-97): tier exceeded, read-only, allowlist,
disabled. HARD — not bypassable per call; the audited override is
changing the environment/context (e.g. MCP_CAPABILITY_MODE).
* Codegen guardrails (Tier 1, BOU-101): FC/IS and convention violations in
generated code. SOFT — bypassable per call with `--allow`, which is
audited.
This namespace is pure: it maps a denial to a BND code and assembles the
payload from a catalog entry passed in. The catalog lookup (I/O) lives in
boundary.mcp.shell.guardrail.Pure JSON-RPC dispatch. (handle registry msg) -> response map for a
request, or nil for a notification / handled-without-reply message. No I/O;
the shell transport feeds parsed messages in and writes responses out.
Pure JSON-RPC dispatch. `(handle registry msg)` -> response map for a request, or nil for a notification / handled-without-reply message. No I/O; the shell transport feeds parsed messages in and writes responses out.
Pure JSON-RPC 2.0 + MCP message construction. No I/O — the shell codec (boundary.mcp.shell.codec) owns JSON serialization.
Maps here use the exact JSON-RPC / MCP wire keys, including camelCase where the spec dictates (e.g. :protocolVersion, :serverInfo). This namespace IS the protocol boundary, so the wire schema is the representation; the usual kebab-case-internal rule does not apply to these protocol field names.
Pure JSON-RPC 2.0 + MCP message construction. No I/O — the shell codec (boundary.mcp.shell.codec) owns JSON serialization. Maps here use the exact JSON-RPC / MCP wire keys, including camelCase where the spec dictates (e.g. :protocolVersion, :serverInfo). This namespace IS the protocol boundary, so the wire schema is the representation; the usual kebab-case-internal rule does not apply to these protocol field names.
Tool and resource registry as plain data. Empty in the skeleton (BOU-96). Tier 0 read/analyze tools (BOU-100) and reflective resources (BOU-99) register their definitions here; the stdio transport stays unchanged.
Tool and resource registry as plain data. Empty in the skeleton (BOU-96). Tier 0 read/analyze tools (BOU-100) and reflective resources (BOU-99) register their definitions here; the stdio transport stays unchanged.
Pure producers for the reflective MCP resources (BOU-99).
A resource's content is derived from a snapshot of the running project — never hardcoded, so it cannot drift from reality (the answer to version skew). Producers are pure functions of the snapshot; the shell (boundary.mcp.shell.system-source) builds the snapshot by reflecting the live system / project (in-process now; nREPL bridge later, ADR-033).
Resources that reflect live runtime state (schema-registry, routes, workflows, lib/{name}) return an :unavailable placeholder until the snapshot carries that data, so the wiring is complete and adding reflection later is purely a shell concern.
Pure producers for the reflective MCP resources (BOU-99).
A resource's content is derived from a *snapshot* of the running project —
never hardcoded, so it cannot drift from reality (the answer to version
skew). Producers are pure functions of the snapshot; the shell
(boundary.mcp.shell.system-source) builds the snapshot by reflecting the
live system / project (in-process now; nREPL bridge later, ADR-033).
Resources that reflect live runtime state (schema-registry, routes,
workflows, lib/{name}) return an :unavailable placeholder until the snapshot
carries that data, so the wiring is complete and adding reflection later is
purely a shell concern.Pure capability/context gating for the MCP server (BOU-97 threat model).
The question this answers: an agent may be pointed at a live REPL with a production database. What is it allowed to do? Every tool declares a capability tier; the active context grants a ceiling derived from the environment. Authorization is a pure decision here; the shell enforces it and audits the outcome.
See ADR-031 for the threat model and rationale.
Pure capability/context gating for the MCP server (BOU-97 threat model). The question this answers: an agent may be pointed at a live REPL with a production database. What is it allowed to do? Every tool declares a capability tier; the active context grants a ceiling derived from the environment. Authorization is a pure decision here; the shell enforces it and audits the outcome. See ADR-031 for the threat model and rationale.
Catalog of the MCP tools. Pure data: each tool declares its MCP wire fields (name, description, JSON-Schema inputSchema) and its capability tier. Execution lives in boundary.mcp.shell.tools; the gate + audit live in the dispatch.
Tier 0 (BOU-100): :read — zero mutation (explain-error, lint, validate-schema, describe-module, sql-preview).
Tier 1 (BOU-101): :generate — write to disk, reversible via git
(scaffold-module, add-field, gen-tests, gen-migration). Each runs the closed
verify loop (generate → write → kondo → FC/IS → run affected tests →
structured report) so the agent self-corrects. The allow flag requests an
audited override of the soft guardrails (FC/IS BND-806, convention
BND-807); kondo errors and test failures are never overridable.
Tier 2 (BOU-102): :execute — the RCE surface, off by default (run-tests, eval, run-migration, query-db). The security gate denies :execute in every context except :full (local dev), so these refuse in prod/CI; the dispatch audits every call, and each executor additionally audits its payload. The real work is injected (test-runner / evaluator / migrator / db-query) so it targets the project, not the server, and is stubbable in tests.
Catalog of the MCP tools. Pure data: each tool declares its MCP wire fields (name, description, JSON-Schema inputSchema) and its capability tier. Execution lives in boundary.mcp.shell.tools; the gate + audit live in the dispatch. Tier 0 (BOU-100): :read — zero mutation (explain-error, lint, validate-schema, describe-module, sql-preview). Tier 1 (BOU-101): :generate — write to disk, reversible via git (scaffold-module, add-field, gen-tests, gen-migration). Each runs the closed verify loop (generate → write → kondo → FC/IS → run affected tests → structured report) so the agent self-corrects. The `allow` flag requests an audited override of the *soft* guardrails (FC/IS BND-806, convention BND-807); kondo errors and test failures are never overridable. Tier 2 (BOU-102): :execute — the RCE surface, off by default (run-tests, eval, run-migration, query-db). The security gate denies :execute in every context except :full (local dev), so these refuse in prod/CI; the dispatch audits every call, and each executor additionally audits its payload. The real work is injected (test-runner / evaluator / migrator / db-query) so it targets the project, not the server, and is stubbable in tests.
Pure aggregation of the Tier 1 closed verify loop (BOU-101).
The shell runs the steps that need I/O — generate/write (scaffolder), kondo, FC/IS, run the affected tests — and hands the raw per-step results here. This namespace turns them into the single structured report the agent reads to self-correct: a flat list of issues (file, line, BND code, kind, expected/actual) plus an overall verdict. No I/O, no side effects.
The loop the report describes: generate → write → kondo → Malli/FC-IS → run affected core tests → report
Issue :severity drives the verdict:
:error — kondo error, FC/IS (BND-806) or convention (BND-807) violation,
or a failing test. Blocks (:status :fail) unless soft and
overridden.
:warning — kondo warning; never blocks.
FC/IS (BND-806) and convention (BND-807) issues are soft guardrails: an
audited {:allow true} override turns a would-be :fail into :overridden
("guardrail, not straitjacket", ADR-032). kondo errors and test failures
are hard — they are never overridable, because they mean the generated code
does not compile or does not pass.
Pure aggregation of the Tier 1 closed verify loop (BOU-101).
The shell runs the steps that need I/O — generate/write (scaffolder), kondo,
FC/IS, run the affected tests — and hands the raw per-step results here.
This namespace turns them into the single structured report the agent reads
to self-correct: a flat list of issues (file, line, BND code, kind,
expected/actual) plus an overall verdict. No I/O, no side effects.
The loop the report describes:
generate → write → kondo → Malli/FC-IS → run affected core tests → report
Issue `:severity` drives the verdict:
:error — kondo error, FC/IS (BND-806) or convention (BND-807) violation,
or a failing test. Blocks (`:status :fail`) unless soft and
overridden.
:warning — kondo warning; never blocks.
FC/IS (BND-806) and convention (BND-807) issues are *soft* guardrails: an
audited `{:allow true}` override turns a would-be `:fail` into `:overridden`
("guardrail, not straitjacket", ADR-032). kondo errors and test failures
are *hard* — they are never overridable, because they mean the generated code
does not compile or does not pass.Transport seam for the MCP server. The stdio transport (shell) implements this; future HTTP/SSE transports implement the same protocol, so the core dispatch loop never depends on a concrete transport.
Transport seam for the MCP server. The stdio transport (shell) implements this; future HTTP/SSE transports implement the same protocol, so the core dispatch loop never depends on a concrete transport.
Audit sinks for security events. The logging sink writes structured JSON events to stderr via tools.logging (never stdout — that stream is reserved for the protocol; see ADR-031). The in-memory sink supports tests.
Audit sinks for security events. The logging sink writes structured JSON events to stderr via tools.logging (never stdout — that stream is reserved for the protocol; see ADR-031). The in-memory sink supports tests.
JSON <-> Clojure data codec for the JSON-RPC boundary. Lives in shell so cheshire (an I/O-adjacent dependency) stays out of the functional core.
JSON <-> Clojure data codec for the JSON-RPC boundary. Lives in shell so cheshire (an I/O-adjacent dependency) stays out of the functional core.
Reads the process environment and resolves the active MCP security context. The pure decision lives in boundary.mcp.core.security; this shell wrapper only supplies the environment (the I/O).
Reads the process environment and resolves the active MCP security context. The pure decision lives in boundary.mcp.core.security; this shell wrapper only supplies the environment (the I/O).
Shell-level request dispatch. Pure protocol methods delegate to boundary.mcp.core.handlers; resources/read is handled here because it needs I/O (snapshot reflection, JSON serialization), the security gate, and the guardrail payload on denial.
deps is {:registry :security :audit :system-source :ai-provider}.
Shell-level request dispatch. Pure protocol methods delegate to
boundary.mcp.core.handlers; resources/read is handled here because it needs
I/O (snapshot reflection, JSON serialization), the security gate, and the
guardrail payload on denial.
`deps` is {:registry :security :audit :system-source :ai-provider}.Default evaluator for the Tier 2 eval tool. Reads and evaluates Clojure
forms in the server's own JVM, capturing the value and stdout.
This is RCE by design: the security gate restricts :execute to the :full
context (local dev), so this is never reachable in prod/CI. The evaluator is
injected as the :evaluator dep so it is swappable — an nREPL-bridge
evaluator can target the project's live REPL instead of the server JVM
(mirroring the SystemSource nREPL plan).
Default evaluator for the Tier 2 `eval` tool. Reads and evaluates Clojure forms in the server's own JVM, capturing the value and stdout. This is RCE by design: the security gate restricts :execute to the :full context (local dev), so this is never reachable in prod/CI. The evaluator is injected as the `:evaluator` dep so it is swappable — an nREPL-bridge evaluator can target the *project's* live REPL instead of the server JVM (mirroring the SystemSource nREPL plan).
Resolves guardrail payloads against the shared BND error catalog (boundary.devtools.error-codes — the single source of truth). The catalog lookup loads a resource (I/O), so it lives in the shell; payload assembly stays pure in boundary.mcp.core.guardrail.
Resolves guardrail payloads against the shared BND error catalog (boundary.devtools.error-codes — the single source of truth). The catalog lookup loads a resource (I/O), so it lives in the shell; payload assembly stays pure in boundary.mcp.core.guardrail.
Default migrator for the Tier 2 run-migration tool. Shells out to the
project's own :migrate alias in the current working directory.
Why shell out (like the test-runner): migrations need the project's full
classpath and config (datasource, env), not the MCP server's. Injected as the
:migrator dep, so it is swappable and stubbable in tests.
Default migrator for the Tier 2 `run-migration` tool. Shells out to the project's own `:migrate` alias in the current working directory. Why shell out (like the test-runner): migrations need the *project's* full classpath and config (datasource, env), not the MCP server's. Injected as the `:migrator` dep, so it is swappable and stubbable in tests.
Entry point. Resolves the security context from the environment, seeds the registry with the reflective resources, boots the stdio MCP server, and audits startup. Run with: clojure -M:run
Tool handlers added in BOU-100/101 must call
boundary.mcp.core.security/authorize against the context and record the
decision via the audit log before performing any mutation.
Entry point. Resolves the security context from the environment, seeds the registry with the reflective resources, boots the stdio MCP server, and audits startup. Run with: clojure -M:run Tool handlers added in BOU-100/101 must call `boundary.mcp.core.security/authorize` against the context and record the decision via the audit log before performing any mutation.
Newline-delimited JSON-RPC over stdin/stdout (MCP stdio transport). stdout carries protocol messages only; all logging goes to stderr so it never corrupts the message stream.
Newline-delimited JSON-RPC over stdin/stdout (MCP stdio transport). stdout carries protocol messages only; all logging goes to stderr so it never corrupts the message stream.
SystemSource adapters (BOU-99 / ADR-033). The in-process adapter reflects the project by reading its files and (later) the live Integrant system; the static adapter serves a fixed snapshot for tests. All I/O lives here so the core resource producers stay pure.
SystemSource adapters (BOU-99 / ADR-033). The in-process adapter reflects the project by reading its files and (later) the live Integrant system; the static adapter serves a fixed snapshot for tests. All I/O lives here so the core resource producers stay pure.
Default test-runner for the Tier 1 verify loop. Shells out to the project's own Kaocha runner in the current working directory and parses the result.
Why shell out rather than run in-process: Kaocha exposes no stable
programmatic API, and the affected tests must run in the project's full
classpath (H2, the module under test, fixtures) — not the MCP server's. The
runner is injected into the tool deps as :test-runner, so this is swappable
(and stubbed in unit tests).
The structured parse is best-effort over Kaocha's textual output; the raw output tail is always included so the agent has the real failure text even when a failure shape isn't recognized.
Default test-runner for the Tier 1 verify loop. Shells out to the project's own Kaocha runner in the current working directory and parses the result. Why shell out rather than run in-process: Kaocha exposes no stable programmatic API, and the affected tests must run in the *project's* full classpath (H2, the module under test, fixtures) — not the MCP server's. The runner is injected into the tool deps as `:test-runner`, so this is swappable (and stubbed in unit tests). The structured parse is best-effort over Kaocha's textual output; the raw output tail is always included so the agent has the real failure text even when a failure shape isn't recognized.
Executors for the MCP tools. All real work (running clj-kondo, Malli validation, project reflection, AI calls, scaffolding + the verify loop) lives here; the core stays a pure catalog. Each executor takes (args deps) and returns result data; throwing is fine — the dispatch maps it to an isError result.
Tier 0 (BOU-100): read/analyze. Tier 1 (BOU-101): generate — scaffold/write to disk (reversible via git) then run the closed verify loop (boundary.mcp.shell.verify).
Executors for the MCP tools. All real work (running clj-kondo, Malli validation, project reflection, AI calls, scaffolding + the verify loop) lives here; the core stays a pure catalog. Each executor takes (args deps) and returns result data; throwing is fine — the dispatch maps it to an isError result. Tier 0 (BOU-100): read/analyze. Tier 1 (BOU-101): generate — scaffold/write to disk (reversible via git) then run the closed verify loop (boundary.mcp.shell.verify).
Shell side of the Tier 1 closed verify loop (BOU-101). Runs the steps that touch the filesystem / subprocesses against the files a generate tool just wrote, and hands the raw results to boundary.mcp.core.verify for the pure report assembly.
Steps:
core-source-paths discovery.:test-runner
(a fn of the module name). The runner is injected, not hardcoded,
because there is no stable programmatic Kaocha API and the tests
must run in the project's classpath, not the MCP server's; the
default shell-out runner lives in boundary.mcp.shell.test-runner.
Absent runner → an honest :unavailable step (never a silent pass).Malli validation of the generate request happens in the scaffolder service
before any file is written; its failures arrive as :errors on the generate
result and are surfaced by the core report, so there is no separate Malli
step here.
Shell side of the Tier 1 closed verify loop (BOU-101). Runs the steps that
touch the filesystem / subprocesses against the files a generate tool just
wrote, and hands the raw results to boundary.mcp.core.verify for the pure
report assembly.
Steps:
* kondo — clj-kondo over the written .clj files (static; in-process).
* fcis — boundary.tools.check-fcis/check-file over written core/ files,
so FC/IS (BND-806) violations are caught per-file regardless of
the monorepo's `core-source-paths` discovery.
* tests — the project's affected tests, via an injected `:test-runner`
(a fn of the module name). The runner is injected, not hardcoded,
because there is no stable programmatic Kaocha API and the tests
must run in the *project's* classpath, not the MCP server's; the
default shell-out runner lives in boundary.mcp.shell.test-runner.
Absent runner → an honest :unavailable step (never a silent pass).
Malli validation of the generate *request* happens in the scaffolder service
before any file is written; its failures arrive as `:errors` on the generate
result and are surfaced by the core report, so there is no separate Malli
step here.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |