Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions pkg/mcp/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ This is essential because:
- `deploy` tool: path = absolute path to Function directory (where func.yaml exists)
- `build` tool: path = absolute path to Function directory (where func.yaml exists)
- `config_*_list`, `config_*_add`, `config_*_remove` tools: path = absolute path to Function directory (where func.yaml exists)
- `run` tool: path = absolute path to Function directory (where func.yaml exists)
- `run_stop` tool: path = the SAME absolute path passed to `run` when starting it

**IMPORTANT:** You must use absolute paths (e.g., `/Users/name/myproject/myfunc`), NOT relative paths (e.g., `.` or `myfunc`). The MCP server process runs in a different directory than your current working directory, so relative paths will not resolve correctly.

Expand All @@ -58,6 +60,7 @@ This is essential because:
- Before 'build' → Read `func://help/build`
- Before 'list' → Read `func://help/list`
- Before 'delete' → Read `func://help/delete`
- Before 'run' → Read `func://help/run`

The help text provides authoritative parameter information and usage context.

Expand Down Expand Up @@ -140,6 +143,28 @@ A first-time deploy can be detected by checking the func.yaml for a value in the
- Exactly ONE of 'path' or 'name' must be provided, not both
- Deleting does not affect local files (source). Only cluster resources.

### run

- **FIRST:** Read `func://help/run` for authoritative usage information
- **REQUIRED parameters:**
- `path` (absolute path to the directory containing the Function to run; there is no CWD-based default — the MCP server's own working directory is unrelated to yours)
- **OPTIONAL parameters:**
- `registry` (container registry; only needed if the Function's image must be named/built)
- `build` (force a rebuild before running; omit to let func build automatically only when out of date)
- `port` (host port to bind; omit to use 8080 or the first available port)
- Builds the Function locally if needed, starts it, and returns once it is up: `pid` (process ID) and `url`
- The Function keeps running in the background after the tool call returns
- Only ONE run may be active per Function path at a time — calling `run` again for a path that is already running fails with a clear error; call `run_stop` first
- ALWAYS call `run_stop` with the matching `path` when finished, to free the port and clean up
- Not a cluster operation, so unaffected by read-only mode

### run_stop

- Stops a Function previously started with `run`
- **REQUIRED:** `path` (absolute) must match the path used to start it with `run`
- Idempotent: calling `run_stop` for a path with no active run (already stopped, or never started) succeeds with an informational message rather than erroring
- Not a cluster operation, so unaffected by read-only mode

### config_envs_list, config_envs_add, config_envs_remove

- **BEFORE calling add/remove:** Consider reading `func://help/config/envs` for authoritative usage
Expand Down
1 change: 1 addition & 0 deletions pkg/mcp/instructions_warning.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The Functions MCP server is currently running in **read-only mode**.
- Build Functions
- Configure Functions (envs, labels, volumes)
- Inspect Functions
- Run/stop Functions locally (not cluster operations, so unaffected by read-only mode)

**Disabled operations:**
- Deploy to cluster
Expand Down
28 changes: 25 additions & 3 deletions pkg/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ type Server struct {
prefix string // Command prefix ("func" or "kn func")
readonly atomic.Bool // disables deploy and delete when true
executor executor
transport mcp.Transport // Transport to use (defaults to StdioTransport)
impl *mcp.Server // implements the protocol
transport mcp.Transport // Transport to use (defaults to StdioTransport)
impl *mcp.Server // implements the protocol
starter processStarter // starts long-lived "func run" subprocesses
runs *runRegistry // tracks active local runs, keyed by function path
}

type executor interface {
Expand Down Expand Up @@ -61,6 +63,14 @@ func WithExecutor(executor executor) Option {
}
}

// WithProcessStarter sets a custom process starter for the "run" tool; used
// in tests.
func WithProcessStarter(starter processStarter) Option {
return func(s *Server) {
s.starter = starter
}
}

// WithTransport sets a custom transport for the server; used in tests.
func WithTransport(transport mcp.Transport) Option {
return func(s *Server) {
Expand All @@ -83,6 +93,8 @@ func New(options ...Option) *Server {
OnInit: func(_ context.Context) {},
}
s.executor = defaultExecutor{s}
s.starter = defaultProcessStarter{s}
s.runs = newRunRegistry()
for _, o := range options {
o(s)
}
Expand All @@ -109,6 +121,8 @@ func New(options ...Option) *Server {
mcp.AddTool(i, deployTool, s.deployHandler)
mcp.AddTool(i, listTool, s.listHandler)
mcp.AddTool(i, deleteTool, s.deleteHandler)
mcp.AddTool(i, runTool, s.runHandler)
mcp.AddTool(i, runStopTool, s.runStopHandler)
mcp.AddTool(i, configVolumesListTool, s.configVolumesListHandler)
mcp.AddTool(i, configVolumesAddTool, s.configVolumesAddHandler)
mcp.AddTool(i, configVolumesRemoveTool, s.configVolumesRemoveHandler)
Expand Down Expand Up @@ -160,8 +174,16 @@ func New(options ...Option) *Server {
// Start the MCP server using the configured transport.
// The server's readonly mode is determined at construction time via
// WithReadonly; it cannot be changed after the server is created.
//
// When Run returns, on normal shutdown (client disconnect, or the caller
// canceling ctx in response to SIGINT/SIGTERM), any Function runs left
// active by the "run" tool are stopped so no subprocess (and the port it
// holds) is left behind. A hard kill of the server process itself (e.g. a
// second SIGKILL) bypasses this; the OS reaps the orphaned children instead.
func (s *Server) Start(ctx context.Context) error {
return s.impl.Run(ctx, s.transport)
err := s.impl.Run(ctx, s.transport)
s.runs.stopAll()
return err
}

// For now the executor is a simple run of the command "func" or "kn func"
Expand Down
31 changes: 31 additions & 0 deletions pkg/mcp/mock/process_starter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package mock

import (
"context"
)

// ProcessStarter is a mock implementation of the process-starter interface
// used by the "run" tool. It avoids spawning real subprocesses in tests.
// It implements the same interface as mcp.processStarter through structural
// typing.
type ProcessStarter struct {
StartInvoked bool
StartFn func(ctx context.Context, subcommand string, args ...string) (pid int, host, port string, stop func() error, err error)
}

// NewProcessStarter creates a new mock process starter.
func NewProcessStarter() *ProcessStarter {
return &ProcessStarter{}
}

// Start implements the processStarter interface, recording invocation
// details and delegating to StartFn if provided.
func (m *ProcessStarter) Start(ctx context.Context, subcommand string, args ...string) (pid int, host, port string, stop func() error, err error) {
m.StartInvoked = true

if m.StartFn != nil {
return m.StartFn(ctx, subcommand, args...)
}

return 1234, "127.0.0.1", "8080", func() error { return nil }, nil
}
Loading
Loading