From 4019180e93981141c858861ac0a22343f0357bdf Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Fri, 7 Aug 2026 19:37:16 +0530 Subject: [PATCH 1/2] feat: add version MCP tool The MCP server's healthcheck tool only reports its own hardcoded protocol version, not the version of the func binary it drives via the executor. Agents had no way to query that, so they could not gate feature usage on server version support. Add a version tool that shells out to "func version --output json", following the same executor pattern as the other tools (list, create, etc.), and returns the version and git commit hash. --- pkg/mcp/mcp.go | 1 + pkg/mcp/tools_version.go | 56 +++++++++++++ pkg/mcp/tools_version_test.go | 147 ++++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 pkg/mcp/tools_version.go create mode 100644 pkg/mcp/tools_version_test.go diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 02ea0f5d44..a7f1125abf 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -104,6 +104,7 @@ func New(options ...Option) *Server { // ----- // One for each command or command group mcp.AddTool(i, healthCheckTool, s.healthcheckHandler) + mcp.AddTool(i, versionTool, s.versionHandler) mcp.AddTool(i, createTool, s.createHandler) mcp.AddTool(i, buildTool, s.buildHandler) mcp.AddTool(i, deployTool, s.deployHandler) diff --git a/pkg/mcp/tools_version.go b/pkg/mcp/tools_version.go new file mode 100644 index 0000000000..3370869278 --- /dev/null +++ b/pkg/mcp/tools_version.go @@ -0,0 +1,56 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +var versionTool = &mcp.Tool{ + Name: "version", + Title: "Version", + Description: "Reports the version of the func client binary, so agents can gate feature usage on version support.", + Annotations: &mcp.ToolAnnotations{ + Title: "Version", + ReadOnlyHint: true, + IdempotentHint: true, + }, +} + +func (s *Server) versionHandler(ctx context.Context, r *mcp.CallToolRequest, input VersionInput) (result *mcp.CallToolResult, output VersionOutput, err error) { + out, err := s.executor.Execute(ctx, "version", "--output", "json") + if err != nil { + err = fmt.Errorf("%w\n%s", err, string(out)) + return + } + + // raw mirrors only the fields of cmd.Version's JSON output that we need + // (see cmd/version.go); importing cmd directly would create an import + // cycle since cmd/mcp.go imports this package. + var raw struct { + Vers string `json:"version,omitempty"` + Hash string `json:"commit,omitempty"` + } + if err = json.Unmarshal(out, &raw); err != nil { + err = fmt.Errorf("error parsing version output: %w\n%s", err, string(out)) + return + } + + output = VersionOutput{ + Version: raw.Vers, + GitRevision: raw.Hash, + } + return +} + +// VersionInput defines the input parameters for the version tool. +// No parameters are required for version. +type VersionInput struct{} + +// VersionOutput defines the structured output returned by the version tool. +type VersionOutput struct { + Version string `json:"version" jsonschema:"Version of the func client binary"` + GitRevision string `json:"gitRevision,omitempty" jsonschema:"Git commit hash the binary was built from, if available"` +} diff --git a/pkg/mcp/tools_version_test.go b/pkg/mcp/tools_version_test.go new file mode 100644 index 0000000000..571d1ca260 --- /dev/null +++ b/pkg/mcp/tools_version_test.go @@ -0,0 +1,147 @@ +package mcp + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "knative.dev/func/pkg/mcp/mock" +) + +// TestTool_Version verifies the version tool executes "version --output json" +// and maps the result into VersionOutput. +func TestTool_Version(t *testing.T) { + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + if subcommand != "version" { + t.Fatalf("expected subcommand 'version', got %q", subcommand) + } + validateArgLength(t, args, 1, 0) + validateStringFlags(t, args, map[string]struct { + jsonKey string + flag string + value string + }{ + "output": {"output", "--output", "json"}, + }) + return []byte(`{"version":"v1.16.0","commit":"abc123"}`), nil + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "version", + Arguments: map[string]any{}, + }) + if err != nil { + t.Fatalf("version tool call failed: %v", err) + } + if result.IsError { + t.Fatalf("version returned an error result: %v", resultToString(result)) + } + if !executor.ExecuteInvoked { + t.Fatal("executor was not invoked") + } + + var output VersionOutput + if err := json.Unmarshal([]byte(resultToString(result)), &output); err != nil { + t.Fatalf("failed to parse version output as JSON: %v", err) + } + if output.Version != "v1.16.0" { + t.Errorf("expected version 'v1.16.0', got %q", output.Version) + } + if output.GitRevision != "abc123" { + t.Errorf("expected gitRevision 'abc123', got %q", output.GitRevision) + } +} + +// TestTool_Version_NoCommit verifies a missing "commit" field (e.g. a build +// from source without ldflags) results in an empty GitRevision, not an error. +func TestTool_Version_NoCommit(t *testing.T) { + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + return []byte(`{"version":"v0.0.0+source"}`), nil + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "version", + Arguments: map[string]any{}, + }) + if err != nil { + t.Fatalf("version tool call failed: %v", err) + } + if result.IsError { + t.Fatalf("version returned an error result: %v", resultToString(result)) + } + + var output VersionOutput + if err := json.Unmarshal([]byte(resultToString(result)), &output); err != nil { + t.Fatalf("failed to parse version output as JSON: %v", err) + } + if output.Version != "v0.0.0+source" { + t.Errorf("expected version 'v0.0.0+source', got %q", output.Version) + } + if output.GitRevision != "" { + t.Errorf("expected empty gitRevision, got %q", output.GitRevision) + } +} + +// TestTool_Version_ExecutorError verifies an executor failure surfaces as an +// error result rather than a panic or malformed output. +func TestTool_Version_ExecutorError(t *testing.T) { + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + return []byte("boom"), errors.New("executor error") + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "version", + Arguments: map[string]any{}, + }) + if err != nil { + t.Fatalf("unexpected transport-level error: %v", err) + } + if !result.IsError { + t.Fatal("expected an error result when the executor fails") + } +} + +// TestTool_Version_MalformedJSON verifies malformed JSON from the executor +// surfaces as an error result rather than a panic. +func TestTool_Version_MalformedJSON(t *testing.T) { + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + return []byte("not json"), nil + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "version", + Arguments: map[string]any{}, + }) + if err != nil { + t.Fatalf("unexpected transport-level error: %v", err) + } + if !result.IsError { + t.Fatal("expected an error result when the executor returns malformed JSON") + } +} From c48ac5ccd1baba8e1d91f473aac729ff464decff Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Mon, 10 Aug 2026 11:05:31 +0530 Subject: [PATCH 2/2] feat: enhance version help in MCP documentation and resources Added detailed instructions for the 'version' command in the MCP documentation, emphasizing the importance of reading the help resource before use. Updated the MCP server to include a new help resource for 'version', and added corresponding tests to ensure proper functionality of the version help command. --- pkg/mcp/instructions.md | 8 ++++++++ pkg/mcp/mcp.go | 1 + pkg/mcp/resources_test.go | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/pkg/mcp/instructions.md b/pkg/mcp/instructions.md index 45af704c81..79739491dc 100644 --- a/pkg/mcp/instructions.md +++ b/pkg/mcp/instructions.md @@ -53,6 +53,7 @@ This is essential because: ### General Rules **CRITICAL:** Before invoking ANY tool, ALWAYS read its help resource first to understand parameters and usage: +- Before 'version' → Read `func://help/version` - Before 'create' → Read `func://help/create` - Before 'deploy' → Read `func://help/deploy` - Before 'build' → Read `func://help/build` @@ -61,6 +62,13 @@ This is essential because: The help text provides authoritative parameter information and usage context. +### version + +- **FIRST:** Read `func://help/version` for authoritative usage information +- Takes no parameters +- Reports the version (and git commit hash, when available) of the func client binary being driven +- Use this to gate usage of newer tools/flags on the version of func actually installed, before assuming they are supported + ### create - **FIRST:** Read `func://help/create` for authoritative usage information diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index a7f1125abf..b447baf02f 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -135,6 +135,7 @@ func New(options ...Option) *Server { // A resource for each command which returns its help // eg. "config volumes add" -> "func://help/config/volumes/add") i.AddResource(newHelpResource(s, "Help", "help for the command root")) + i.AddResource(newHelpResource(s, "Version Help", "help for 'version'", "version")) i.AddResource(newHelpResource(s, "Create Help", "help for 'create'", "create")) i.AddResource(newHelpResource(s, "Build Help", "help for 'build'", "build")) i.AddResource(newHelpResource(s, "Deploy Help", "help for 'deploy'", "deploy")) diff --git a/pkg/mcp/resources_test.go b/pkg/mcp/resources_test.go index b4deee1645..4c71d31472 100644 --- a/pkg/mcp/resources_test.go +++ b/pkg/mcp/resources_test.go @@ -100,6 +100,11 @@ func TestResource_Help(t *testing.T) { uri: "func://help", wantArgs: []string{"--help"}, }, + { + name: "version help", + uri: "func://help/version", + wantArgs: []string{"version", "--help"}, + }, { name: "create help", uri: "func://help/create",