MCP server should call `pkg/functions` directly, not shell out to the `func` binary
- Dominant language
- Go
- Stars
- 365
- Forks
- 223
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 25
Description
## Summary
The MCP server (`pkg/mcp`) currently exposes Functions to AI agents by shelling out to the `func` binary as a subprocess. This was an expedient — it let the MCP surface land quickly by piggy-backing on the existing CLI. It should be replaced with direct use of the core library at `pkg/functions`, in the same way the CLI itself does.
## Architectural principle
The real logic of Functions lives in `pkg/functions` (and friends — `pkg/k8s`, `pkg/docker`, `pkg/knative`, etc.). The `cmd/` package wraps that library as a CLI. The `pkg/mcp` package should wrap the same library as an MCP server.
```
┌──────────────┐
│ pkg/functions│ ← core: where the logic lives
└──────┬───────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌─────┐ ┌─────┐ ┌──────┐
│ cli │ │ mcp │ │ ... │ (future surfaces)
└─────┘ └─────┘ └──────┘
```
Today's reality is the bottom-left arrow points sideways — `pkg/mcp` calls into `cli`, which calls into `pkg/functions`. That extra hop is a leaky abstraction.
## Why this matters now
- Every MCP tool handler runs `exec.Command("func", ...)`, captures stdout/stderr blobs, and either passes the text to the agent verbatim or tries to parse it. The parsing is brittle (see #3753 for a worked example), the structured-output story is thin, and adding any tool requires routing through an `executor` interface that has been growing leaky over time (e.g., the recently-proposed `ExecuteRaw` in #3752).
- Subprocess overhead is real (~100ms+ per MCP call just to fork `func`).
- Error propagation is constrained to exit codes + text scraping rather than the rich typed errors that already exist in the Go API.
- The in-process API gets exercised consistently by both consumers, surfacing bugs and API gaps faster.
The TODO comment already in the tree at `pkg/mcp/mcp.go:154-155` flags exactly this direction.
## Scope
- Replace every MCP tool handler in `pkg/mcp/tools_*.go` with direct calls into `pkg/functions` (and `pkg/k8s`, `pkg/docker`, etc. where appropriate).
- Drop the `executor` interface and its mocks once all handlers are migrated.
- Replace text-blob results with typed values flowing into `CallToolResult.StructuredContent`.
- Error mapping uses Go's typed errors (`errors.As`) rather than substring matching.
## Phased migration suggested
1. Add an interim adapter that lets shellout-backed and library-backed handlers coexist (so we can land one tool at a time without breaking the others).
2. Migrate read-only tools first (`list`, `describe`, `healthcheck`, `check_prerequisites` if that lands first via #3749 / #3752) — lowest risk, easiest to validate.
3. Migrate mutating tools (`build`, `deploy`, `delete`, `config_*`).
4. Remove the `executor` interface, its mocks, and the `func` binary as a runtime dependency of the MCP server.
## Open design questions
- **Long-running operations** (`func deploy`, `func logs`): how do we surface progress events to the agent? MCP supports progress notifications via the protocol; the in-process API would need to emit these.
- **Interactive prompts**: `func` has prompt flows; MCP context never has a TTY. Handlers must either resolve prompts via the request payload or fail cleanly.
- **Testing surface**: the current `executor` mock made it easy to test handlers in isolation. The replacement is to inject the same `pkg/functions` client or its interfaces — most of which already exist as injectable points.
- **Concurrency**: the CLI assumes one operation per process; direct library use means handlers may run concurrently. Review `pkg/functions` for thread safety where MCP would call into it from multiple goroutines.
## Relationship to other work
- **#3769** (`--json` output for every `func` subcommand) remains valuable — for scripts, CI, third-party tools, and IDEs that drive `func` as a subprocess. It is not blocked by this issue and does not block this issue.
- **#3770** (MCP consume `func --json` and forward via `StructuredContent`) **is superseded by this issue**. If `pkg/mcp` calls `pkg/functions` directly, there is no `func --json` subprocess output to consume.
- **#3752** (`check_prerequisites` tool): a current-day example of the friction this issue resolves — needed to add `ExecuteRaw` to the executor to shell out to `docker`/`kubectl`. Once the MCP server uses `pkg/functions` and `pkg/k8s` directly, the same checks become idiomatic Go.
- **#3750 / #3753** (structured error categories): becomes a non-issue. Typed errors from `pkg/functions` reach MCP handlers directly; categorization is a pure mapping, not a parsing problem.
## Non-goals
- Removing the `func` CLI. The CLI is the primary human-facing surface and stays as-is.
- Removing the MCP server's *behavior*. Tools keep the same names, inputs, and observable outputs. This is internals only.
Contributor guide
Assessment
This issue has not been assessed yet.