Skip to content

Commit e9003f0

Browse files
authored
Merge pull request #54 from ScriptKittyOS/slice/060-mcp-client
feat(s060): complete slice 060 (MCP client) Signed-off-by: Ayla Croft <aylacroft@proton.me>
2 parents c96adbe + 0d2ec55 commit e9003f0

61 files changed

Lines changed: 4140 additions & 47 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ Pre-alpha, and usable from source. Milestones M0 to M4 are approved, and the fir
3535
credentials and instructions to ignore safety, and promoted through the permission gate with a
3636
receipt.
3737

38-
Not there yet: scheduled tasks and MCP (M5a), messaging gateways and subagents (M5b),
38+
Not there yet: scheduled tasks and the rest of MCP (M5a: the client is built at 060, the server
39+
with approvals and the authorization roles follow), messaging gateways and subagents (M5b),
3940
the native desktop shell and signed releases (M6), executable skills in a sandbox (M7). `ROADMAP.md` carries the live status of every
4041
slice, and the [Milestones](#milestones) section below explains how to read it.
4142

@@ -77,8 +78,9 @@ model registry is `config/llm.exs`; keys come from the environment (`.env` is gi
7778
pages: `/` conversations, `/s/:id` a conversation and `/s/:id/receipts` its receipts, `/search`
7879
full-text search, `/personas` and `/memory` the persona and its memory (with the semantic tab and
7980
the embedding model's download), `/skills` the skills, the changes waiting for your decision and
80-
the learn form, `/permissions` the rules and pending approvals, `/settings` the
81-
export. `mix trinity.export` and `mix trinity.import` do what `/settings` does from a terminal;
81+
the learn form, `/mcp` the MCP servers you connect to (their health and the tools they
82+
contribute), `/permissions` the rules and pending approvals (a server's question to you, when
83+
one of its tools asks for input mid-call, is answered there too), `/settings` the export. `mix trinity.export` and `mix trinity.import` do what `/settings` does from a terminal;
8284
`docs/backup.md` explains the archive.
8385

8486
Semantic recall needs the local embedding model (91 MB, downloaded from the memory page on your
@@ -161,7 +163,7 @@ catalogued effect to it: Trinity proposes, Requisition decides, and Trinity keep
161163
Trinity refuses to start if the module is absent or does not implement `Trinity.Authority`. See
162164
`docs/adr/0008-authority-is-an-adapter.md` and `docs/adr/0010-authority-selection-at-boot.md`.
163165

164-
**Ultraviolet** is the purple-team tool. Add it to `config :trinity, :mcp_servers` like any other MCP server. Its
166+
**Ultraviolet** is the purple-team tool. Add it on the `/mcp` page like any other MCP server (since slice 060 a server is a row, not a config entry). Its
165167
read tools return query-receipted results, its proposal tools return proposal ids rather than effects, and content
166168
coming back from it is tagged untrusted like any other external content.
167169

@@ -179,8 +181,10 @@ every project space.
179181
the BEAM from the same organisation, on Hex as `beam_mcp`. Trinity depends on it since slice 059,
180182
pinned at 0.8.0 and reached only through the `Trinity.MCP` boundary; slice 059's `FINDINGS.md`
181183
measures what it ships against the 2026-07-28 checklist, and the MCP phase (milestone M5a) builds
182-
Trinity's client driver, the approval loop and authorization above it. The client side and the
183-
authorization server are Trinity's own work. `docs/adr/0007-mcp-2026-07-28-target-and-library.md`
184+
Trinity's client driver (slice 060: a thin driver over the core's decoder and validator, stdio
185+
and Streamable HTTP, 2026-07-28 preferred and 2025-11-25 as the fallback, a server's mid-call
186+
question answered as an approval), the approval loop and authorization above it. The client side
187+
and the authorization server are Trinity's own work. `docs/adr/0007-mcp-2026-07-28-target-and-library.md`
184188
records the protocol target and the layering.
185189

186190
## License

‎ROADMAP.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ standards register names the rows that ask for them.
5353
| 041 | Skill self-management with staged approval + scanner | 4 Skills | M | 040, 021 | approved |
5454
| 050 | Scheduler: Oban cron agent tasks with delivery targets | 5 Automation | M | 012 | planned |
5555
| 059 | MCP capability gap against beam_mcp, and the server seam probe | 6 MCP | S/M | 020 | approved |
56-
| 060 | MCP client: Trinity's thin driver (2026-07-28 preferred, 2025-11-25 compat, MRTR, Tasks) | 6 MCP | L | 059, 021 | planned |
56+
| 060 | MCP client: Trinity's thin driver (2026-07-28 preferred, 2025-11-25 compat, MRTR, Tasks) | 6 MCP | L | 059, 021 | done |
5757
| 061 | MCP server (stateless 2026-07-28 + compat, MRTR approvals, headless profile) | 6 MCP | M | 060, 024 | planned |
5858
| 062 | MCP authorization: OAuth client role, RS, embedded AS, Enterprise Managed Authorization (ID-JAG) | 6 MCP | L | 061 | planned |
5959
| 070 | Gateway core: adapter behaviour, routing, PubSub fan-out | 7 Gateways | M | 012 | planned |

‎config/test.exs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,8 @@ config :trinity, :skills,
191191
pending_dir: Path.join(System.tmp_dir!(), "trinity-test-pending-skills"),
192192
watch: true,
193193
index_tokens: 338
194+
195+
# Slice 060: no MCP client boots from rows in the suite (each test starts the clients it
196+
# needs); a short backoff so the reconnect test runs in seconds.
197+
config :trinity, :mcp_boot, false
198+
config :trinity, :mcp_client, backoff_ms: 50, max_backoff_ms: 400, connect_timeout: 5_000

‎coverage.tsv‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ slice_id percent sha date
1919
040 79.86 dc9dc6b 2026-09-21
2020
041 80.55 ae200d7 2026-09-21
2121
059 80.54 53c9091 2026-09-21
22+
060 80.32 49f40a4 2026-09-21

‎docs/01-architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ without anything failing.
9090
| `Trinity.Memory` | Always-on tiers with their budget and consolidator (030), search (031), semantic store and retrieval (032), compaction (023) | LLM (summaries/embeddings), Repo |
9191
| `Trinity.Skills` | SKILL.md parsing, registry, loader, manager, scanner (as built at 040: parser, sources, registry, index, the three tools; at 041: staging, promotion, manager, scanner, diff, learn, `skill_manage` and `learn`) | Repo, Permissions, **Effects**, **Receipts**, Sandbox (as built at 041: Tools, Memory, Permissions, Receipts and LLM; Effects is not a dependency: the promotion is not a tool call, it writes its own effect receipt; Tools never depends on Skills) |
9292
| `Trinity.Scheduler` | Oban workers for agent tasks, delivery | Sessions, Gateways, **Repo** |
93-
| `Trinity.MCP` | Client manager, tool bridge, server (as built at 059: the boundary alone, holding the core's version, its JSON depth and its telemetry event names) | Tools, **Effects**, **Permissions**, Memory (as built at 059: a top-level boundary, like `Trinity.Smoke`, with `deps: [Trinity, BeamMCP.JSON]`; the boundary compiler checks every call into the `beam_mcp` application and this boundary alone lists its modules) |
93+
| `Trinity.MCP` | Client manager, tool bridge, server (as built at 059: the boundary alone, holding the core's version, its JSON depth and its telemetry event names; at 060: `Servers` and `ServerConfig` (the rows), `Supervisor` and `Boot` (one `Client` per enabled row), `Client` with its `Wire` (the outbound request, the headers, the core's decoder and validator) and two transports (`Transport.Stdio`, a child on a Port; `Transport.HTTP`, one POST per request), `Bridge` (the one tool module every MCP tool runs through) and `Client.Auth` (062's seam, a static token at 060)) | Tools, **Effects**, **Permissions**, Memory (as built at 059: a top-level boundary, like `Trinity.Smoke`, with `deps: [Trinity, BeamMCP.JSON]`; the boundary compiler checks every call into the `beam_mcp` application and this boundary alone lists its modules; at 060 the deps are `[Trinity, BeamMCP.JSON, BeamMCP.Schema]`, Trinity's tools, permissions and receipts reached through `Trinity`'s exports, and `Trinity.Application` and `TrinityWeb` list `Trinity.MCP`) |
9494
| `Trinity.Gateways` | Adapter behaviour, router, allowlists, pairing | Sessions, **Permissions**, PubSub |
9595
| `Trinity.Subagents` | Delegation, result collection | Sessions, Tools |
9696
| `Trinity.Sandbox` | Luerl runners, resource limits | none |

‎docs/05-data-model.md‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ One row per request, the audit trail this slice owns; slice 024 reads it for dec
161161
| decided_at, decided_by | timestamp, string | every decided row has both ("expiry" is a decider) |
162162
| consumed_at | nullable | a "once" allowance or a denial is spent by the execution that reads it |
163163
| expires_at | timestamp | pending past it becomes "expired" with decision "deny" |
164+
| request | map, nullable | slice 060: a server's input request as sent (`kind`, `server`, `inputRequests`), when the approval is a multi-round-trip question and not a yes or no; the server's `requestState` is never here (it lives in the client process) |
165+
| answer | map, nullable | slice 060: the answer the decision carried, the revision's `inputResponses` map keyed as `inputRequests` were |
164166

165167
### tasks (Slice 050)
166168
| column | type | notes |
@@ -230,9 +232,19 @@ and `receipt_hash`. Chain scopes: `session:<id>` and `boot`.
230232
| reason | string | "count" \| "time" \| "shutdown" \| "rehydrate" \| "manual" |
231233
A row, never a write onto a receipt: `receipts` stays append-only and a checkpoint states its own coverage.
232234

233-
### mcp_servers (Slice 060)
234-
`name`, `transport`, `command_or_url`, `env_refs`, `enabled`, `effect_default ∈ {none, artifact}`, per-tool
235-
effect and risk overrides. A server config claiming `:catalog` is refused at load and receipted.
235+
### mcp_servers (Slice 060, as built)
236+
One row per server the client connects to; the row's `name` is the namespace segment of every tool it contributes.
237+
| column | type | notes |
238+
|---|---|---|
239+
| name | string, unique | `[a-z0-9][a-z0-9_-]{0,31}`; the tools are `mcp:<name>:<tool>` |
240+
| transport | string | "stdio" \| "http" |
241+
| command, args | string, {array, string} | stdio: the child and its arguments |
242+
| url | string | http: one POST endpoint |
243+
| env_refs | {array, string} | names of environment variables passed to a stdio child; never a value (the child sees the process basics and these, nothing else of Trinity's environment) |
244+
| enabled | boolean | a client runs for an enabled row |
245+
| effect_default | string | "none" \| "artifact"; the effect of every tool the row does not override |
246+
| tool_overrides | map | tool name to `{"effect": …}`; `catalog` is refused by the changeset and, on a row that carries it anyway, at load with a decision receipt on the chain scope `mcp:<name>`. No override lowers a tool's tier: it is `:ask` for every namespaced name, and a rule on the permissions page is what allows one |
247+
| last_error | text, nullable | |
236248

237249
### task_runs (Slice 050)
238250
`task_id`, `scheduled_at`, `session_id`, `status`, `summary`, `error`. Unique on `(task_id, scheduled_at)` so a

‎docs/07-security-model.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,35 @@ root loads as any other and is not scanned (a follow-up in the slice's NOTES).
137137
- Luerl with reduction limits, no `os`/`io`/`require`, no filesystem; explicit host functions only.
138138
- Native/shell code is never "sandboxed" by the BEAM: the UI says so plainly when approving `:exec`.
139139

140+
## MCP client (Slice 060, as built)
141+
142+
- **Every tool a server lists is a dynamic tool under `mcp:<server>:<tool>`**, registered through one bridge
143+
module with the server's own description and `inputSchema` as its definition (digested like any other). The
144+
tier is `:ask`, because the gate maps core names alone and answers `:ask` for the rest; a row cannot lower it,
145+
and the owner allows a tool the way every tool is allowed, with a rule on the permissions page. The effect is
146+
`:none` unless the row says `:artifact` for that tool; `:catalog` is refused at write by the changeset and at
147+
load by the registry, with a decision receipt of outcome `deny` on the server's chain scope `mcp:<server>`.
148+
- **Every result is untrusted.** The server's content parts become one `Trinity.Content.Part` tainted
149+
`:untrusted` with origin `tool:mcp:<server>:<tool>` and source `mcp://<server>/<tool>`; an image or audio
150+
part is a line naming its type and size (the bytes never reach the model); `isError` is a marked line the
151+
model reads as the server's error, not a crash.
152+
- **A stdio child sees an allow-listed environment.** A Port's `env:` adds to the inherited environment, so the
153+
driver unsets by name every variable this VM holds outside PATH, HOME, the locale and temp variables and the
154+
row's `env_refs`; provider keys are the first thing that must not cross. `env_refs` are names, never values.
155+
The child's stderr is the VM's; its stdout is the wire, and a line that is not JSON-RPC is dropped with a
156+
warning.
157+
- **A server's question is an approval, never an automatic answer.** A `tools/call` result with `resultType`
158+
`input_required` (the revision's multi-round-trip pattern) holds the call: the server's `inputRequests` go
159+
into an approval row's `request` column, the card renders the server's message and a form from its
160+
`requestedSchema`, and the owner's answer travels with the decision (`answer`) to the retry. The client
161+
declares form elicitation alone in its capabilities, so a server may not ask Trinity to sample a model or
162+
list roots. The server's `requestState` is held in the client process, keyed by session and call, and
163+
echoed byte-for-byte on the retry; it is never in a row, never shown, never parsed (the revision's MUST
164+
NOT). A server that answers `input_required` forever is a server whose every round asks the owner again.
165+
- **The driver is thin.** It builds the outbound request and nothing else of the protocol; decoding and
166+
argument validation are the core's public functions (`BeamMCP.JSON.decode/1`, `BeamMCP.Schema.validate/2`),
167+
and a census test holds the population to that (060 AC7).
168+
140169
## Secrets
141170

142171
- Env vars in dev; OS keychain via `Trinity.Secrets` from Slice 100 (Tauri stronghold/store or a keychain NIF).

‎docs/08-standards.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,18 @@ Microsoft, OpenAI. Trinity targets AAIF-governed standards first; vendor-specifi
5151
`requestState` echoed back unmodified**. That echo is what makes MRTR work on a stateless server: all the
5252
state rides in the payload, so any instance can resume the work. Consequence: our permission gate over MCP maps naturally to MRTR
5353
(approval = input_required → user decides → retry), and our client must implement the retry loop.
54+
As built at 060 (client side): `inputRequests` and `inputResponses` are maps keyed alike, each request a
55+
`{method, params}` object and each response an `ElicitResult` (`action`, `content`); the retry is a new
56+
JSON-RPC id; only requests the client declared in its capabilities may be sent, and Trinity's client
57+
declares form elicitation alone, so the owner answers from the permissions page and no sampling or roots
58+
request reaches it. The core (beam_mcp) refuses MRTR on the server side by design; Trinity's server (061)
59+
speaks it above the core.
5460
3. **Deprecated:** Roots, Sampling, Logging (use tool params/resource URIs, provider APIs directly, stderr/OTel),
5561
HTTP+SSE transport, DCR. Do not build new code on these.
5662
4. **Extensions framework** with `extensions` in capabilities. Tasks (`io.modelcontextprotocol/tasks`,
57-
poll-based `tasks/get`, `tasks/update`) is the one we use.
63+
poll-based `tasks/get`, `tasks/update`) is the one we use. At 060 the client builds nothing for it: the
64+
core neither builds nor refuses it (059 finding 4), and a client for an extension the server core does
65+
not speak would be tested against a double alone; the question goes to beam_mcp's board first.
5866
5. **Subscriptions** via a single `subscriptions/listen` stream per opted-in notification type
5967
(`toolsListChanged`, …) instead of the GET endpoint.
6068
6. **Cacheable lists**: `tools/list` etc. MUST return `ttlMs` + `cacheScope`; return tools in deterministic

‎lib/trinity.ex‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ defmodule Trinity do
4747
Memory.Retriever,
4848
Memory.Embedders.Bumblebee,
4949
Tools,
50+
# Slice 060: what the MCP bridge (a top-level boundary) implements and returns.
51+
Tools.Tool,
52+
Tools.Context,
53+
Tools.Result,
54+
Tools.Untrusted,
5055
Permissions,
5156
Permissions.Approval,
5257
Permissions.Rule,

‎lib/trinity/application.ex‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ defmodule Trinity.Application do
77
# Trinity.Smoke is its own top-level boundary: it is the `--smoke` boot path and has to ask
88
# TrinityWeb.Endpoint what port it bound, which Trinity (deps: []) may not do. Adding it here
99
# is what lets the child list mention it.
10-
use Boundary, top_level?: true, deps: [Trinity, TrinityWeb, Trinity.Smoke], exports: []
10+
use Boundary,
11+
top_level?: true,
12+
deps: [Trinity, TrinityWeb, Trinity.Smoke, Trinity.MCP],
13+
exports: []
1114

1215
# See https://elixir.hexdocs.pm/Application.html
1316
# for more information on OTP Applications
@@ -57,6 +60,10 @@ defmodule Trinity.Application do
5760
# Slice 040: the skills index, scanned from its roots and watched; after the tools
5861
# (conditional activation asks which exist) and before the sessions that read it.
5962
Trinity.Skills.Registry,
63+
# Slice 060: the MCP clients, one per enabled server row, after the tools they
64+
# register into and the gate their approvals go through.
65+
Trinity.MCP.Supervisor,
66+
Trinity.MCP.Boot,
6067
Trinity.Sessions.Supervisor
6168
] ++
6269
Trinity.Smoke.probe(Trinity.Smoke.argv()) ++

0 commit comments

Comments
 (0)