Skip to content

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

@lkingland

Description

@lkingland

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 feat(mcp): add structured error categories for agentic error handling #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 feat(mcp): add check_prerequisites tool for environment validation #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 MCP: Add check_prerequisites tool for environment validation #3749 / feat(mcp): add check_prerequisites tool for environment validation #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

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions