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

Open
#3,771 6 comments 0 reactions 1 assignee View on GitHub

@vishal-iiserb is already working on this.

Since May 17, 2026.

Assessment

This issue has not been assessed yet.

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.
Dominant language
Go
Stars
365
Forks
223
Avg merge
2d 3h
Merged PRs (30d)
25

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from knative/func

All issues in knative/func

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.