---
title: "ADR-116: Central tooling authority — nr_llm owns builtin + MCP tools"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-116@0.35"
source: "Adr/Adr116CentralToolingAuthority.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-116: Central tooling authority — nr_llm owns builtin + MCP tools

-   *Status:*

    Accepted (its transport section no longer refuses an event-stream framed
    answer — see [ADR-181](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-181@0.35))

-   *Date:* 2026-07-22
-   *Amended:* 2026-08-20 by [ADR-181](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-181@0.35)
-   *Authors:* Netresearch DTT GmbH

## Context

Two tooling systems exist in the stack today, and only one of them is nr_llm.

nr_llm is already the tooling authority for its own agent runs. It ships  45
builtin tools under `Classes/Service/Tool/Builtin/` (among them
`FetchLogsTool`, `GetLastExceptionTool`, `ListDeprecationsTool`,
`GetSystemStatusTool`, `SiteRagQueryTool` and `GetPageContentTool`),
collects them in `ToolRegistry` (`Classes/Service/Tool/ToolRegistry.php`),
and executes them through `ToolLoopService` ([ADR-038](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-038@0.35)) behind
the tool gate ([ADR-093](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-093@0.35)) and the global availability state
([ADR-039](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-039@0.35)). `AgentRuntime` ([ADR-101](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-101@0.35),
`Classes/Service/Agent/`) is the public application service that owns the
run lifecycle — approval ([ADR-084](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-084@0.35)), guardrails
([ADR-085](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-085@0.35)) and context-window bounding
([ADR-107](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-107@0.35)) — with the interface
`run` / `enqueue` / `runQueued` / `approve` / `submitInput` /
`cancel` / `events` / `status`.

The separate `nr_mcp_agent` extension is a **second, parallel** tool stack
that bypasses all of the above. It is itself an MCP client
(`Classes/Mcp/McpToolProvider`, `Classes/Mcp/McpConnection` — a stdio
JSON-RPC client over `proc_open` — and `McpServerRepository` over the
`tx_nrmcpagent_mcp_server` table, whose `transport` field offers `stdio`
and `sse`). It **re-implements its own agent loop** in
`ChatService::runAgentLoop` and sources tools *only* from external MCP
servers (e.g. the suggested `hn/typo3-mcp-server` dependency), mapping the
MCP wire shape onto nr_llm's `ToolSpec`. It uses nr_llm merely as a
completion provider (`ProviderInterface`), never its `ToolRegistry` or
`AgentRuntime`.

The result is two agent loops with two independently maintained sets of
fail-closed, approval, guardrail and context rules, and two disjoint tool
sources (builtin-only in nr_llm, MCP-only in nr_mcp_agent) that no consumer
can obtain together.

## Decision

nr_llm is **the single tooling authority** for the AI stack. All tooling —
builtin and MCP — is aggregated and executed there; consumers never reach an
MCP server directly.

-   **Add an MCP client to nr_llm.** nr_llm gains the ability to connect to
    external MCP servers over HTTP, list their tools, and register
    those tools into `ToolRegistry` **alongside** the builtin tools. MCP tools
    then flow through the exact same path as builtins: `ToolRegistry` →
    `ToolLoopService` / `AgentRuntime`, subject to the same tool gate
    ([ADR-093](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-093@0.35)), the same availability state
    ([ADR-039](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-039@0.35)), the same approval ([ADR-084](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-084@0.35)),
    guardrail ([ADR-085](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-085@0.35)) and context-window
    ([ADR-107](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-107@0.35)) enforcement.
-   **Consumers obtain all tooling exclusively via nr_llm.** A consumer (for
    example a backend AI-chat module) takes its tools from `ToolRegistry` and
    drives runs through `AgentRuntime`. It **never** opens an MCP connection
    itself. MCP servers are wired *only* through nr_llm.
-   **One loop, one trust boundary.** There is a single agent loop
    (`AgentRuntime` over `ToolLoopService`). Builtin and MCP tools share the
    one tool-data trust zone ([ADR-094](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-094@0.35)) and the one gate, so a
    tool's origin does not change how it is authorised, approved or audited.

## Consequences

-   The MCP-client capability moves **out of** nr_mcp_agent and **into** nr_llm:
    the connection, tool-listing and schema-normalisation logic that today lives
    in `McpToolProvider` / `McpConnection` becomes an nr_llm concern, and the
    server-configuration storage (transport, command/arguments, url/auth token)
    moves with it.
-   `ToolRegistry` becomes the aggregation point for builtin **and** MCP tools;
    the allow-list, availability toggle and gate apply uniformly regardless of
    where a tool came from.
-   `AgentRuntime` is the single agent loop. `nr_mcp_agent` **deletes**
    `ChatService::runAgentLoop` and stops assembling its own lifecycle; its
    divergent fail-closed / approval behaviour disappears with it.
-   `nr_mcp_agent` is reduced to a thin backend chat UI — module, toolbar and
    conversation store — driving `AgentRuntime`. With MCP gone it is arguably
    mis-named: a rename (candidate `nr_llm_chat`) or an outright fold-in to
    nr_llm are both on the table (see follow-up).
-   `hn/typo3-mcp-server` becomes *an* MCP server that nr_llm connects to, not
    a per-consumer composer dependency; any number of external MCP servers attach
    the same way.
-   New public surface lands in nr_llm (MCP client configuration + registration).
    It is a minor-release growth path and will carry its own ADR when the
    implementation is designed; the public-service count authority
    ([ADR-101](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-101@0.35)) is updated then, not here.

## Transports: HTTP only

This ADR first named "stdio / http / sse". That list was wrong in two ways and
is corrected here, because it would otherwise be read as a build order.

`sse` is not a peer of `http`. It is a response framing for an HTTP
connection, not a separate transport to select. Offering both as values of one
field invites a configuration that cannot be satisfied — which is what the
`transport` field in `nr_mcp_agent`'s own table already shows, where
`stdio` and `sse` are the only values and plain `http` does not exist.

`stdio` is out of scope, and not merely for performance. It means spawning a
process on the TYPO3 host from a request, with the command line taken from an
operator-editable record. Every control in the tool stack classifies a tool's
OUTPUT ([ADR-094](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-094@0.35)) or authorises its CALLER — nothing classifies
what a tool may do to the host it runs on. "One registry, one gate" is a true
statement about offering, approving and auditing a tool; it says nothing about
launching a process, and it must not be read as covering one. If a stdio
transport is ever wanted, it belongs on the CLI and queue paths behind an
explicit allow-list of executables, decided in its own ADR.

The client therefore speaks HTTP, and only HTTP.

## What an MCP tool resolves to, and why it needs a declaration

An MCP tool arrives without a group, and the two resolvers answer that
differently — one fail-closed, one fail-open. Both answers are wrong for MCP,
in opposite directions, and an implementation that does not address both is not
"the same gate as a builtin".

**Data class fails closed to unusable.** A tool whose group is unknown resolves
to `SECRET_ADJACENT`, which only a `LOCAL` trust zone permits. Against every
hosted provider such a tool is withheld. So MCP tools are not merely
"classified strictly" by default — they do not run at all outside a local
model.

**Effect fails open.** A registered tool that declares nothing resolves to
`READ_ONLY`, which is correct for the builtins (all of them read) and wrong
for an MCP server, where writes are ordinary. An undeclared remote write would
lose the write fence and the fail-closed audit of [ADR-111](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-111@0.35).

The implementation therefore requires a **per-server data-class declaration**
by the operator, and must treat an externally-sourced tool as a write unless
declared otherwise — the inverse of the builtin default. Neither is a
weakening: the first replaces "denied everywhere" with a stated ceiling, the
second replaces a guess with the strict answer.

**Why any of this is nr_llm's problem at all**, given that the MCP server
authorises its own resources: three things the server cannot see. Its output
travels onward to an LLM provider we chose, under a data-protection obligation
that is ours. Its authorisation is against one operator credential, so without
a gate here every backend user inherits the full rights of that credential. And
the write fence is our own retry bookkeeping, about our queue, which the server
knows nothing about. Securing the resource stays the server's job; where its
answers flow and in whose name we ask are ours.

## Migration and follow-up

Implementation is separate follow-up work; this ADR records the target only.

-   Build the MCP client in nr_llm: the HTTP transport, the `tools/list`
    handshake, inputSchema-to-provider-schema normalisation (the concern
    `McpToolProvider` already solves), and registration of the resulting tools
    into `ToolRegistry`.
-   Move the MCP server configuration model (`transport`, `command` /
    `arguments`, `url` / `auth_token`) into nr_llm.
-   Repoint `nr_mcp_agent`'s `ChatService` onto `AgentRuntime` and delete
    its `Classes/Mcp/` client and `runAgentLoop`.
-   Decide rename versus fold-in for `nr_mcp_agent` as a discrete step.
-   Reconcile streaming and context-window parity for MCP-sourced tools per the
    scope note in [ADR-107](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-107@0.35).
