knative / knative/func

MCP server should call `pkg/functions` directly, not shell out to the `func` binary

Open
#3,771 6 comments 0 reactions 1 assignee Claimed by @vishal-iiserb View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.