Repository navigation
feat(foreman): MCP client for agents (Phase 1: HTTP transport, context7) - #1014
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Code reviewFound 1 issue:
The first loop treats every trailing continuation byte as something to remove, but a continuation byte (
The existing test only exercises A minimal fix: after LLMKube/pkg/foreman/agent/mcp/tool.go Lines 199 to 218 in 697893f 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
truncateUTF8 walked back past a complete multibyte rune whenever the byte
cap landed on that rune's final byte: a continuation byte is also the last
byte of a fully-fitting rune, so the trailing-continuation-byte loop
stripped the whole rune and DecodeLastRuneInString then stripped one more.
It silently lost data on any MCP result that truncated on a rune boundary
(common: code comments, non-ASCII text, emoji), e.g. truncateUTF8("eee", 4)
returned "e" instead of "ee" for 2-byte runes.
Short-circuit when the cap already lands on a rune boundary
(utf8.ValidString) and keep the slice as-is; only walk back and drop the
trailing rune when the cap actually split one. Add a table-driven test
covering exact-boundary and split cases (reported by Jory on defilantech#1014).
Signed-off-by: Christopher Maher <chris@mahercode.io>
Signed-off-by: Christopher Maher <chris@mahercode.io>
Signed-off-by: Christopher Maher <chris@mahercode.io>
Signed-off-by: Christopher Maher <chris@mahercode.io>
Signed-off-by: Christopher Maher <chris@mahercode.io>
Signed-off-by: Christopher Maher <chris@mahercode.io>
…alidity Signed-off-by: Christopher Maher <chris@mahercode.io>
…n-failure) Wires ServerConfig -> dial -> listTools -> allowed-filter -> newTools into a single entry point the Foreman wiring layer can call with a list of MCP server configs. A server that fails to dial or list its tools is logged at warning and skipped rather than failing the whole call; the returned error is reserved for the one programmer error (nil ctx). The returned closer joins best-effort Close() errors across every successfully opened session. Signed-off-by: Christopher Maher <chris@mahercode.io>
Signed-off-by: Christopher Maher <chris@mahercode.io>
Signed-off-by: Christopher Maher <chris@mahercode.io>
…y factory Signed-off-by: Christopher Maher <chris@mahercode.io>
…hmark-gated) Signed-off-by: Christopher Maher <chris@mahercode.io>
mcpTool.Execute passed the raw run ctx straight to callTool, so the
CRD-configured Options.CallTimeout was parsed and defaulted but never
enforced: a stalled MCP server could block a single tool call for the
duration of the whole run. Wrap the call in context.WithTimeout using
the already-defaulted CallTimeout.
Also bound client.go's dial handshake (Connect) with a 15s timeout so
a server that accepts a TCP connection but never completes the MCP
handshake can't hang registry build. Confirmed safe by reading the
go-sdk source: ListTools/CallTool each take a fresh per-call ctx, and
the connect ctx handed to the connection's read loop is wrapped in the
SDK's internal notDone{} so its cancellation can't affect later calls.
Signed-off-by: Christopher Maher <chris@mahercode.io>
…-host header leak The Agent.spec.tools whitelist Filter is an allow-list intersection, and MCP tool names (mcp/<server>/<tool>) are dynamic, so no real agent's whitelist could ever name one -- Filter silently dropped every MCP tool for every agent that set spec.tools, making the feature inert. Native tools are now filtered first, then MCP tools are appended to the already-filtered registry via a new Registry.Add, so they bypass the whitelist by construction (they already carry their own per-server allowedTools gate). Also fixes three smaller issues an external review of the MCP client turned up: the dial http.Client now refuses cross-host redirects so a 3xx response can never replay the secret-sourced auth header to a different host; mcpTool.Execute now captures the tool-level isError flag instead of discarding it, so a failed MCP call is distinguishable from a successful one in both the record and the model-visible output; and Register no longer hands back a nil closer when Connect hits its nil-ctx programmer-error path. Signed-off-by: Christopher Maher <chris@mahercode.io>
…not foreman-system BuildServers resolved the MCP header secret from foremanNamespace (the operator's namespace, default foreman-system), but the Agent CR and its secrets live together in the agent's own namespace (e.g. default), exactly like provider auth (apiKeySecretRef) resolves. Off-cluster metal agents run with foreman-namespace=foreman-system, so an mcp-context7 secret in default was never found: BuildServers silently skipped the server and the coder got zero MCP tools. Resolve from ag.Namespace instead. Signed-off-by: Christopher Maher <chris@mahercode.io>
Emit an Info line for every run showing mcpConfigPresent / mcpEnabled / workloadMCPEnabled, plus a result line with serversConfigured / toolsAdded. Diagnostic for the freeform-registers-MCP / issue-fix-silent gap: one run of each kind will show which gate input differs. Signed-off-by: Christopher Maher <chris@mahercode.io>
truncateUTF8 walked back past a complete multibyte rune whenever the byte
cap landed on that rune's final byte: a continuation byte is also the last
byte of a fully-fitting rune, so the trailing-continuation-byte loop
stripped the whole rune and DecodeLastRuneInString then stripped one more.
It silently lost data on any MCP result that truncated on a rune boundary
(common: code comments, non-ASCII text, emoji), e.g. truncateUTF8("eee", 4)
returned "e" instead of "ee" for 2-byte runes.
Short-circuit when the cap already lands on a rune boundary
(utf8.ValidString) and keep the slice as-is; only walk back and drop the
trailing rune when the cap actually split one. Add a table-driven test
covering exact-boundary and split cases (reported by Jory on defilantech#1014).
Signed-off-by: Christopher Maher <chris@mahercode.io>
613ab33 to
60eab17
Compare
…1 ➔ 0.9.3) (#1489) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/defilantech/charts/llmkube](https://github.com/defilantech/LLMKube) | patch | `0.9.1` → `0.9.3` | --- ### Release Notes <details> <summary>defilantech/LLMKube (ghcr.io/defilantech/charts/llmkube)</summary> ### [`v0.9.3`](https://github.com/defilantech/LLMKube/blob/HEAD/CHANGELOG.md#093-2026-07-09) [Compare Source](defilantech/LLMKube@v0.9.2...v0.9.3) ##### Bug Fixes - **controller:** drop invalid --enable-metrics flag from vLLM runtime ([#​1031](defilantech/LLMKube#1031)) ([9b9450d](defilantech/LLMKube@9b9450d)) ### [`v0.9.2`](https://github.com/defilantech/LLMKube/blob/HEAD/CHANGELOG.md#092-2026-07-09) [Compare Source](defilantech/LLMKube@v0.9.1...v0.9.2) ##### Features - **foreman:** coder grounding rail (flag doc-contradicting metric writes) ([#​1017](defilantech/LLMKube#1017)) ([aa969ed](defilantech/LLMKube@aa969ed)) - **foreman:** distinct ALREADY-RESOLVED coder outcome ([#​970](defilantech/LLMKube#970)) ([#​1011](defilantech/LLMKube#1011)) ([8e041e0](defilantech/LLMKube@8e041e0)) - **foreman:** flag a GO that changes no functional production code ([#​1024](defilantech/LLMKube#1024)) ([3e1a6fb](defilantech/LLMKube@3e1a6fb)) - **foreman:** MCP client for agents (Phase 1: HTTP transport, context7) ([#​1014](defilantech/LLMKube#1014)) ([d34343f](defilantech/LLMKube@d34343f)) - **foreman:** semantic issueAsk verification instead of verbatim substring ([#​1020](defilantech/LLMKube#1020)) ([85d92ee](defilantech/LLMKube@85d92ee)) - **foreman:** str\_replace escalates to write\_file after repeated failures ([#​1026](defilantech/LLMKube#1026)) ([de1b8cb](defilantech/LLMKube@de1b8cb)) ##### Bug Fixes - **foreman:** exempt REJECT from grounded-finding demotion ([#​1008](defilantech/LLMKube#1008)) ([18098cb](defilantech/LLMKube@18098cb)) - **foreman:** gate Job downloads the go.mod toolchain (GOTOOLCHAIN=auto) ([#​1019](defilantech/LLMKube#1019)) ([5f4c51c](defilantech/LLMKube@5f4c51c)) - **foreman:** guard file-less findings + resolve bare/absolute paths in grounding rail ([#​1009](defilantech/LLMKube#1009)) ([304a2b8](defilantech/LLMKube@304a2b8)) - **foreman:** reap audit ConfigMaps older than retention ([#​990](defilantech/LLMKube#990)) ([#​1027](defilantech/LLMKube#1027)) ([08d06d6](defilantech/LLMKube@08d06d6)) - **foreman:** reviewer diffs against the upstream base, not stale local fork main ([#​1006](defilantech/LLMKube#1006)) ([6ef7278](defilantech/LLMKube@6ef7278)) - **metrics:** remove dead metrics + phantom PrometheusRule error-rate rule ([#​1010](defilantech/LLMKube#1010)) ([7c92f3f](defilantech/LLMKube@7c92f3f)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/1489
…mkube (0.9.1 ➔ 0.9.3) (#497) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/home-operations/charts-mirror/llmkube](https://github.com/defilantech/LLMKube) | patch | `0.9.1` → `0.9.3` | --- ### Release Notes <details> <summary>defilantech/LLMKube (ghcr.io/home-operations/charts-mirror/llmkube)</summary> ### [`v0.9.3`](https://github.com/defilantech/LLMKube/blob/HEAD/CHANGELOG.md#093-2026-07-09) [Compare Source](defilantech/LLMKube@v0.9.2...v0.9.3) ##### Bug Fixes - **controller:** drop invalid --enable-metrics flag from vLLM runtime ([#​1031](defilantech/LLMKube#1031)) ([9b9450d](defilantech/LLMKube@9b9450d)) ### [`v0.9.2`](https://github.com/defilantech/LLMKube/blob/HEAD/CHANGELOG.md#092-2026-07-09) [Compare Source](defilantech/LLMKube@v0.9.1...v0.9.2) ##### Features - **foreman:** coder grounding rail (flag doc-contradicting metric writes) ([#​1017](defilantech/LLMKube#1017)) ([aa969ed](defilantech/LLMKube@aa969ed)) - **foreman:** distinct ALREADY-RESOLVED coder outcome ([#​970](defilantech/LLMKube#970)) ([#​1011](defilantech/LLMKube#1011)) ([8e041e0](defilantech/LLMKube@8e041e0)) - **foreman:** flag a GO that changes no functional production code ([#​1024](defilantech/LLMKube#1024)) ([3e1a6fb](defilantech/LLMKube@3e1a6fb)) - **foreman:** MCP client for agents (Phase 1: HTTP transport, context7) ([#​1014](defilantech/LLMKube#1014)) ([d34343f](defilantech/LLMKube@d34343f)) - **foreman:** semantic issueAsk verification instead of verbatim substring ([#​1020](defilantech/LLMKube#1020)) ([85d92ee](defilantech/LLMKube@85d92ee)) - **foreman:** str\_replace escalates to write\_file after repeated failures ([#​1026](defilantech/LLMKube#1026)) ([de1b8cb](defilantech/LLMKube@de1b8cb)) ##### Bug Fixes - **foreman:** exempt REJECT from grounded-finding demotion ([#​1008](defilantech/LLMKube#1008)) ([18098cb](defilantech/LLMKube@18098cb)) - **foreman:** gate Job downloads the go.mod toolchain (GOTOOLCHAIN=auto) ([#​1019](defilantech/LLMKube#1019)) ([5f4c51c](defilantech/LLMKube@5f4c51c)) - **foreman:** guard file-less findings + resolve bare/absolute paths in grounding rail ([#​1009](defilantech/LLMKube#1009)) ([304a2b8](defilantech/LLMKube@304a2b8)) - **foreman:** reap audit ConfigMaps older than retention ([#​990](defilantech/LLMKube#990)) ([#​1027](defilantech/LLMKube#1027)) ([08d06d6](defilantech/LLMKube@08d06d6)) - **foreman:** reviewer diffs against the upstream base, not stale local fork main ([#​1006](defilantech/LLMKube#1006)) ([6ef7278](defilantech/LLMKube@6ef7278)) - **metrics:** remove dead metrics + phantom PrometheusRule error-rate rule ([#​1010](defilantech/LLMKube#1010)) ([7c92f3f](defilantech/LLMKube@7c92f3f)) </details> --- ### Configuration 📅 **Schedule**: (in timezone America/New_York) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI1Mi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.greyrock.io/greyrock-labs/home-ops/pulls/497
What
Phase 1 of the Foreman MCP (Model Context Protocol) client: coder/reviewer
agents can now call read-only MCP tools (e.g. context7 docs lookup) at run
time. MCP tools appear in the agent's tool loop as ordinary tools named
mcp/<server>/<tool>, flowing through the same dispatch loop and transcript asnative tools. They are gated by their own per-server
allowedToolslist ratherthan the
Agent.spec.toolswhitelist (which enumerates static native tools andcannot name dynamically-discovered MCP tools).
New CRD surface (all optional, back-compatible):
Plus
Workload.spec.mcpEnabled/AgenticTask.spec.mcpEnabled(a benchmarkopt-out: run the same coder with MCP off for an A/B control).
Why
Foreman agents hallucinate facts that live outside the model (metric names, API
groups, syntax): #409 invented a vLLM metric, #850 wrote the wrong API group,
#809 fabricated groundable behavior. An in-agent MCP client lets them consult
authoritative sources during a run and stop guessing.
Part of #1013 (this is Phase 1 of that epic; Brave search and native stdio
transport follow in later phases, so the epic stays open).
How
pkg/foreman/agent/mcp: a client wrapper over the officialgithubproxy.fjygbaifeng.eu.org/modelcontextprotocol/go-sdk(v1.6.1, HTTP/streamable transport) —the SDK is imported by exactly one file; a tool adapter turning discovered MCP
tools into
tools.Tools; aConnectentry point (dial → discover → filter byallowedTools→ adapt); and aRegister/BuildServerswiring helper.mcpimportsagent(foragent.ToolResult), soagentcannot importmcp. Registration thereforelives in the wiring layer
cmd/foreman-agent/main.go(makeRegistryFactory),and only a
workloadMCPEnabled bool+ctxare threaded through theRegistryFactoryseam. The registry is assembled by filtering the nativetools against
Agent.spec.toolsfirst, then adding the MCP tools onto thefiltered set — so MCP tools bypass the native whitelist (they are gated by
their own per-server
allowedTools) and reach the model even when an agent'sspec.toolslists only native tools.resolution, or an erroring/oversized tool result is logged and skipped or
returned as a soft tool error — it never fails an
AgenticTaskrun.maxResultBytes; each call isbounded by
callTimeout(and the dial handshake is bounded independently);auth headers come only from Secrets and are refused across cross-host
redirects (so a redirect can't leak the token); MCP results are treated as
untrusted model-facing data. Audit is the existing run transcript (MCP calls
appear as normal
mcp/<server>/<tool>tool calls).Built and reviewed task-by-task (11 commits) with a whole-branch review pass;
the one substantive finding (an inert
callTimeout) is fixed in the finalcommit. Fleet A/B validation on #409/#850 is tracked as the next step under
#1013.
Assisted-by: Claude Code (Foreman's authoring harness; generated the
implementation and tests task-by-task under my direction — I reviewed every
task, adjudicated the review findings, and ran
make test/make lintlocally before submitting).
Checklist
make testpasses locallymake lintpasses locallygit commit -s) per DCOmcpconfig block follow once the feature is validated on the fleet (tracked in [FEATURE] MCP client for Foreman agents (docs + search grounding) #1013)