From 069de590fcbf052255a34497a58d3d73a284176a Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Mon, 3 Aug 2026 13:17:34 +0530 Subject: [PATCH 1/4] feat(mcp): add 'invoke' tool and update documentation - Introduced the 'invoke' tool to send test requests to running Function instances. - Updated instructions to include usage details for the 'invoke' tool, emphasizing the importance of absolute paths and providing guidance on parameters. - Added help resources for the 'invoke' command in the MCP server setup. --- pkg/mcp/instructions.md | 11 +++ pkg/mcp/mcp.go | 2 + pkg/mcp/tools_invoke.go | 72 +++++++++++++++++++ pkg/mcp/tools_invoke_test.go | 131 +++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 pkg/mcp/tools_invoke.go create mode 100644 pkg/mcp/tools_invoke_test.go diff --git a/pkg/mcp/instructions.md b/pkg/mcp/instructions.md index 45af704c81..8ceaeae4bb 100644 --- a/pkg/mcp/instructions.md +++ b/pkg/mcp/instructions.md @@ -33,6 +33,7 @@ This is essential because: - `create` tool: path = absolute path to directory where Function will be created - `deploy` tool: path = absolute path to Function directory (where func.yaml exists) - `build` tool: path = absolute path to Function directory (where func.yaml exists) +- `invoke` 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) **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. @@ -56,6 +57,7 @@ This is essential because: - Before 'create' → Read `func://help/create` - Before 'deploy' → Read `func://help/deploy` - Before 'build' → Read `func://help/build` +- Before 'invoke' → Read `func://help/invoke` - Before 'list' → Read `func://help/list` - Before 'delete' → Read `func://help/delete` @@ -124,6 +126,15 @@ A first-time deploy can be detected by checking the func.yaml for a value in the - Uses same builder settings as deploy would use - The user should be notified this is an unnecessary step if they intend to deploy, as building is handled as part of deployment +### invoke + +- **FIRST:** Read `func://help/invoke` for authoritative usage information +- Sends a test request to a running Function instance, either local or remote +- **Optional** `path` parameter: directory containing the Function (defaults to the MCP server's current working directory) +- **Optional** `target` parameter: "local", "remote", or a URL. Defaults to preferring a locally running instance over remote +- **AGENT USE-CASE:** After deploying a Function, call `invoke` to verify it is live and responding before reporting success to the user (e.g. call with `target: "remote"` immediately after `deploy`) +- An error is returned if the invocation fails (e.g. non-2xx HTTP response), so a successful call without an error confirms the Function responded correctly + ### list - **FIRST:** Read `func://help/list` for authoritative usage information diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 02ea0f5d44..ba595c3f11 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -107,6 +107,7 @@ func New(options ...Option) *Server { mcp.AddTool(i, createTool, s.createHandler) mcp.AddTool(i, buildTool, s.buildHandler) mcp.AddTool(i, deployTool, s.deployHandler) + mcp.AddTool(i, invokeTool, s.invokeHandler) mcp.AddTool(i, listTool, s.listHandler) mcp.AddTool(i, deleteTool, s.deleteHandler) mcp.AddTool(i, configVolumesListTool, s.configVolumesListHandler) @@ -137,6 +138,7 @@ func New(options ...Option) *Server { 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")) + i.AddResource(newHelpResource(s, "Invoke Help", "help for 'invoke'", "invoke")) i.AddResource(newHelpResource(s, "List Help", "help for 'list'", "list")) i.AddResource(newHelpResource(s, "Delete Help", "help for delete", "delete")) diff --git a/pkg/mcp/tools_invoke.go b/pkg/mcp/tools_invoke.go new file mode 100644 index 0000000000..31eee74250 --- /dev/null +++ b/pkg/mcp/tools_invoke.go @@ -0,0 +1,72 @@ +package mcp + +import ( + "context" + "fmt" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +var invokeTool = &mcp.Tool{ + Name: "invoke", + Title: "Invoke Function", + Description: "Invoke a local or remote Function with a test request.", + Annotations: &mcp.ToolAnnotations{ + Title: "Invoke Function", + ReadOnlyHint: false, + IdempotentHint: false, // Invoking a Function may trigger arbitrary side effects in the Function's handler. + }, +} + +func (s *Server) invokeHandler(ctx context.Context, r *mcp.CallToolRequest, input InvokeInput) (result *mcp.CallToolResult, output InvokeOutput, err error) { + out, err := s.executor.Execute(ctx, "invoke", input.Args()...) + if err != nil { + err = fmt.Errorf("%w\n%s", err, string(out)) + return + } + output = InvokeOutput{ + Message: string(out), + } + return +} + +// InvokeInput defines the input parameters for the invoke tool. +type InvokeInput struct { + Path *string `json:"path,omitempty" jsonschema:"Path to the function project directory (default: current working directory)"` + Target *string `json:"target,omitempty" jsonschema:"Function instance to invoke: local, remote, or a URL (default: local)"` + Format *string `json:"format,omitempty" jsonschema:"Format of message to send: http or cloudevent (default: auto-detected)"` + ID *string `json:"id,omitempty" jsonschema:"CloudEvent id for the request data"` + Source *string `json:"source,omitempty" jsonschema:"CloudEvent source for the request data"` + Type *string `json:"type,omitempty" jsonschema:"CloudEvent type for the request data"` + Data *string `json:"data,omitempty" jsonschema:"Data (content) to send in the request"` + ContentType *string `json:"contentType,omitempty" jsonschema:"MIME type of the data"` + RequestType *string `json:"requestType,omitempty" jsonschema:"HTTP method override (e.g., GET, POST)"` + File *string `json:"file,omitempty" jsonschema:"Path to a file whose content is used as the request data (overrides data)"` + Insecure *bool `json:"insecure,omitempty" jsonschema:"Skip TLS verification when invoking over SSL"` + Verbose *bool `json:"verbose,omitempty" jsonschema:"Enable verbose logging output"` +} + +func (i InvokeInput) Args() []string { + args := []string{} + + args = appendStringFlag(args, "--path", i.Path) + args = appendStringFlag(args, "--target", i.Target) + args = appendStringFlag(args, "--format", i.Format) + args = appendStringFlag(args, "--id", i.ID) + args = appendStringFlag(args, "--source", i.Source) + args = appendStringFlag(args, "--type", i.Type) + args = appendStringFlag(args, "--data", i.Data) + args = appendStringFlag(args, "--content-type", i.ContentType) + args = appendStringFlag(args, "--request-type", i.RequestType) + args = appendStringFlag(args, "--file", i.File) + + args = appendBoolFlag(args, "--insecure", i.Insecure) + args = appendBoolFlag(args, "--verbose", i.Verbose) + + return args +} + +// InvokeOutput defines the structured output returned by the invoke tool. +type InvokeOutput struct { + Message string `json:"message" jsonschema:"Output message, including the response body from the invoked Function"` +} diff --git a/pkg/mcp/tools_invoke_test.go b/pkg/mcp/tools_invoke_test.go new file mode 100644 index 0000000000..3273fc6d8f --- /dev/null +++ b/pkg/mcp/tools_invoke_test.go @@ -0,0 +1,131 @@ +package mcp + +import ( + "context" + "errors" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "knative.dev/func/pkg/mcp/mock" +) + +// TestTool_Invoke_Args ensures the invoke tool executes with all arguments passed correctly. +func TestTool_Invoke_Args(t *testing.T) { + // Test data - defined once and used for both input and validation + stringFlags := map[string]struct { + jsonKey string + flag string + value string + }{ + "path": {"path", "--path", "."}, + "target": {"target", "--target", "remote"}, + "format": {"format", "--format", "http"}, + "id": {"id", "--id", "test-id"}, + "source": {"source", "--source", "test-source"}, + "type": {"type", "--type", "test-type"}, + "data": {"data", "--data", "hello world"}, + "contentType": {"contentType", "--content-type", "text/plain"}, + "requestType": {"requestType", "--request-type", "GET"}, + "file": {"file", "--file", "example.jpeg"}, + } + + boolFlags := map[string]string{ + "insecure": "--insecure", + "verbose": "--verbose", + } + + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + if subcommand != "invoke" { + t.Fatalf("expected subcommand 'invoke', got %q", subcommand) + } + + validateArgLength(t, args, len(stringFlags), len(boolFlags)) + validateStringFlags(t, args, stringFlags) + validateBoolFlags(t, args, boolFlags) + + return []byte("Received: 200 OK\n"), nil + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + // Build input arguments from test data + inputArgs := buildInputArgs(stringFlags, boolFlags) + + // Invoke tool with all optional arguments + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "invoke", + Arguments: inputArgs, + }) + if err != nil { + t.Fatal(err) + } + if result.IsError { + t.Fatalf("unexpected error result: %v", result) + } + if !executor.ExecuteInvoked { + t.Fatal("executor was not invoked") + } +} + +// TestTool_Invoke_NoArgs ensures the invoke tool can be called with no +// arguments, relying on defaults (path defaults to cwd, target auto-discovers). +func TestTool_Invoke_NoArgs(t *testing.T) { + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + if subcommand != "invoke" { + t.Fatalf("expected subcommand 'invoke', got %q", subcommand) + } + if len(args) != 0 { + t.Fatalf("expected no args, got %v", args) + } + return []byte("OK\n"), nil + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "invoke", + Arguments: map[string]any{}, + }) + if err != nil { + t.Fatal(err) + } + if result.IsError { + t.Fatalf("unexpected error result: %v", result) + } + if !executor.ExecuteInvoked { + t.Fatal("executor was not invoked") + } +} + +// TestTool_Invoke_Error ensures a failing invocation (e.g. non-2xx response) +// is surfaced as a tool error, allowing agents to detect invocation failures. +func TestTool_Invoke_Error(t *testing.T) { + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + return []byte(""), errors.New("failure invoking function (HTTP 500)") + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "invoke", + Arguments: map[string]any{"path": "."}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("expected invoke failure to be reported as a tool error") + } +} From b4e16c09539197b9796c34cf3a68019d86ab5f56 Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Mon, 3 Aug 2026 15:02:04 +0530 Subject: [PATCH 2/4] feat(mcp): enforce required 'path' parameter and enhance invoke tool safety - Updated the documentation to clarify that the 'path' parameter is now required for the invoke tool, ensuring users specify the function directory. - Added cautionary notes regarding potential side effects when invoking functions, especially in remote scenarios. - Enhanced test coverage for the invoke tool, including checks for missing 'path' arguments and readonly mode restrictions. --- pkg/mcp/instructions.md | 5 +-- pkg/mcp/tools_invoke.go | 17 +++++++---- pkg/mcp/tools_invoke_test.go | 59 ++++++++++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/pkg/mcp/instructions.md b/pkg/mcp/instructions.md index 8ceaeae4bb..9cb9a3e3f2 100644 --- a/pkg/mcp/instructions.md +++ b/pkg/mcp/instructions.md @@ -130,9 +130,10 @@ A first-time deploy can be detected by checking the func.yaml for a value in the - **FIRST:** Read `func://help/invoke` for authoritative usage information - Sends a test request to a running Function instance, either local or remote -- **Optional** `path` parameter: directory containing the Function (defaults to the MCP server's current working directory) +- **REQUIRED parameters:** + - `path` (directory containing the Function to invoke) - **Optional** `target` parameter: "local", "remote", or a URL. Defaults to preferring a locally running instance over remote -- **AGENT USE-CASE:** After deploying a Function, call `invoke` to verify it is live and responding before reporting success to the user (e.g. call with `target: "remote"` immediately after `deploy`) +- **CAUTION:** Invoking a Function executes its handler and may trigger arbitrary, real side effects (e.g. sending an email, charging a payment, writing to a database) — this is especially true with `target: "remote"`, which hits the live deployed instance. Do not invoke automatically without considering whether the Function's handler is safe to run; when in doubt, confirm with the user first, particularly before invoking a remote/production instance - An error is returned if the invocation fails (e.g. non-2xx HTTP response), so a successful call without an error confirms the Function responded correctly ### list diff --git a/pkg/mcp/tools_invoke.go b/pkg/mcp/tools_invoke.go index 31eee74250..9c99545dbe 100644 --- a/pkg/mcp/tools_invoke.go +++ b/pkg/mcp/tools_invoke.go @@ -12,13 +12,19 @@ var invokeTool = &mcp.Tool{ Title: "Invoke Function", Description: "Invoke a local or remote Function with a test request.", Annotations: &mcp.ToolAnnotations{ - Title: "Invoke Function", - ReadOnlyHint: false, - IdempotentHint: false, // Invoking a Function may trigger arbitrary side effects in the Function's handler. + Title: "Invoke Function", + ReadOnlyHint: false, + DestructiveHint: ptr(true), // Invoking a Function may trigger arbitrary, unrepeatable side effects in the Function's handler (e.g. sending an email, charging a payment). + IdempotentHint: false, // Invoking a Function may trigger arbitrary side effects in the Function's handler. }, } func (s *Server) invokeHandler(ctx context.Context, r *mcp.CallToolRequest, input InvokeInput) (result *mcp.CallToolResult, output InvokeOutput, err error) { + if s.readonly.Load() { + err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + return + } + out, err := s.executor.Execute(ctx, "invoke", input.Args()...) if err != nil { err = fmt.Errorf("%w\n%s", err, string(out)) @@ -32,7 +38,7 @@ func (s *Server) invokeHandler(ctx context.Context, r *mcp.CallToolRequest, inpu // InvokeInput defines the input parameters for the invoke tool. type InvokeInput struct { - Path *string `json:"path,omitempty" jsonschema:"Path to the function project directory (default: current working directory)"` + Path string `json:"path" jsonschema:"required,Path to the function project directory"` Target *string `json:"target,omitempty" jsonschema:"Function instance to invoke: local, remote, or a URL (default: local)"` Format *string `json:"format,omitempty" jsonschema:"Format of message to send: http or cloudevent (default: auto-detected)"` ID *string `json:"id,omitempty" jsonschema:"CloudEvent id for the request data"` @@ -47,9 +53,8 @@ type InvokeInput struct { } func (i InvokeInput) Args() []string { - args := []string{} + args := []string{"--path", i.Path} - args = appendStringFlag(args, "--path", i.Path) args = appendStringFlag(args, "--target", i.Target) args = appendStringFlag(args, "--format", i.Format) args = appendStringFlag(args, "--id", i.ID) diff --git a/pkg/mcp/tools_invoke_test.go b/pkg/mcp/tools_invoke_test.go index 3273fc6d8f..b32514ea16 100644 --- a/pkg/mcp/tools_invoke_test.go +++ b/pkg/mcp/tools_invoke_test.go @@ -71,16 +71,18 @@ func TestTool_Invoke_Args(t *testing.T) { } } -// TestTool_Invoke_NoArgs ensures the invoke tool can be called with no -// arguments, relying on defaults (path defaults to cwd, target auto-discovers). -func TestTool_Invoke_NoArgs(t *testing.T) { +// TestTool_Invoke_MinimalArgs ensures the invoke tool can be called with only +// the required 'path' argument, relying on defaults for everything else +// (target auto-discovers between local and remote). +func TestTool_Invoke_MinimalArgs(t *testing.T) { executor := mock.NewExecutor() executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { if subcommand != "invoke" { t.Fatalf("expected subcommand 'invoke', got %q", subcommand) } - if len(args) != 0 { - t.Fatalf("expected no args, got %v", args) + want := []string{"--path", "."} + if len(args) != len(want) || args[0] != want[0] || args[1] != want[1] { + t.Fatalf("expected args %v, got %v", want, args) } return []byte("OK\n"), nil } @@ -92,7 +94,7 @@ func TestTool_Invoke_NoArgs(t *testing.T) { result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ Name: "invoke", - Arguments: map[string]any{}, + Arguments: map[string]any{"path": "."}, }) if err != nil { t.Fatal(err) @@ -105,6 +107,51 @@ func TestTool_Invoke_NoArgs(t *testing.T) { } } +// TestTool_Invoke_MissingPath ensures the invoke tool rejects a call that +// omits the required 'path' argument, since the MCP server's own working +// directory is unrelated to the Function being tested. +func TestTool_Invoke_MissingPath(t *testing.T) { + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + t.Fatal("executor should not be invoked when 'path' is missing") + return nil, nil + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + _, err = client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "invoke", + Arguments: map[string]any{}, + }) + if err == nil { + t.Fatal("expected error when 'path' argument is missing") + } +} + +// TestTool_Invoke_Readonly ensures the invoke tool rejects requests in +// readonly mode, since invoking a Function may trigger arbitrary side +// effects in its handler. +func TestTool_Invoke_Readonly(t *testing.T) { + client, _, err := newTestPairWithReadonly(t, true) // readonly = true + if err != nil { + t.Fatal(err) + } + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "invoke", + Arguments: map[string]any{"path": "."}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("expected invoke to be rejected in readonly mode") + } +} + // TestTool_Invoke_Error ensures a failing invocation (e.g. non-2xx response) // is surfaced as a tool error, allowing agents to detect invocation failures. func TestTool_Invoke_Error(t *testing.T) { From d6b844de74fe662d907a71d50059f3954da51ade Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Mon, 3 Aug 2026 15:08:35 +0530 Subject: [PATCH 3/4] fix(mcp): update documentation for 'target' parameter in invoke tool - Clarified the 'target' parameter description in the InvokeInput struct to specify that it defaults to auto-discovery, preferring local when both local and remote instances are available. This enhances user understanding of the parameter's behavior. --- pkg/mcp/tools_invoke.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mcp/tools_invoke.go b/pkg/mcp/tools_invoke.go index 9c99545dbe..435284439b 100644 --- a/pkg/mcp/tools_invoke.go +++ b/pkg/mcp/tools_invoke.go @@ -39,7 +39,7 @@ func (s *Server) invokeHandler(ctx context.Context, r *mcp.CallToolRequest, inpu // InvokeInput defines the input parameters for the invoke tool. type InvokeInput struct { Path string `json:"path" jsonschema:"required,Path to the function project directory"` - Target *string `json:"target,omitempty" jsonschema:"Function instance to invoke: local, remote, or a URL (default: local)"` + Target *string `json:"target,omitempty" jsonschema:"Function instance to invoke: local, remote, or a URL (default: auto-discovery; prefers local when both local and remote are running)"` Format *string `json:"format,omitempty" jsonschema:"Format of message to send: http or cloudevent (default: auto-detected)"` ID *string `json:"id,omitempty" jsonschema:"CloudEvent id for the request data"` Source *string `json:"source,omitempty" jsonschema:"CloudEvent source for the request data"` From 0eb44089006b7abb14bdb86ec37a2dd27299a65e Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Mon, 3 Aug 2026 16:13:16 +0530 Subject: [PATCH 4/4] feat(mcp): enhance invoke tool functionality and documentation - Updated the invoke tool to support CloudEvent extension attributes, allowing users to pass key-value pairs as repeated '--extension key=value' flags. - Modified the documentation to reflect that the MCP server's read-only mode now also disables the invoke operation, alongside deploy and delete. - Enhanced test coverage for the invoke tool, ensuring proper handling of extensions and readonly mode restrictions. --- pkg/mcp/instructions_warning.md | 5 ++-- pkg/mcp/mcp.go | 2 +- pkg/mcp/tools_invoke.go | 37 ++++++++++++++++-------- pkg/mcp/tools_invoke_test.go | 50 +++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/pkg/mcp/instructions_warning.md b/pkg/mcp/instructions_warning.md index 98697ddeec..fefca96b0f 100644 --- a/pkg/mcp/instructions_warning.md +++ b/pkg/mcp/instructions_warning.md @@ -15,12 +15,13 @@ The Functions MCP server is currently running in **read-only mode**. **Disabled operations:** - Deploy to cluster - Delete from cluster +- Invoke a Function -These write operations are disabled to prevent unintended cluster modifications. +These operations are disabled to prevent unintended cluster modifications and side effects from invoking a Function's handler. ## Enabling Write Mode -If the user needs to deploy or delete Functions, you MUST inform them to enable write mode: +If the user needs to deploy, delete, or invoke Functions, you MUST inform them to enable write mode: 1. Close/exit this application completely 2. Set the environment variable: `FUNC_ENABLE_MCP_WRITE=true` diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index ba595c3f11..be377e46e6 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -24,7 +24,7 @@ const ( type Server struct { OnInit func(context.Context) // Invoked when the server is initialized prefix string // Command prefix ("func" or "kn func") - readonly atomic.Bool // disables deploy and delete when true + readonly atomic.Bool // disables deploy, delete, build, and invoke when true executor executor transport mcp.Transport // Transport to use (defaults to StdioTransport) impl *mcp.Server // implements the protocol diff --git a/pkg/mcp/tools_invoke.go b/pkg/mcp/tools_invoke.go index 435284439b..b39cb7cf6e 100644 --- a/pkg/mcp/tools_invoke.go +++ b/pkg/mcp/tools_invoke.go @@ -3,6 +3,7 @@ package mcp import ( "context" "fmt" + "sort" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -38,18 +39,19 @@ func (s *Server) invokeHandler(ctx context.Context, r *mcp.CallToolRequest, inpu // InvokeInput defines the input parameters for the invoke tool. type InvokeInput struct { - Path string `json:"path" jsonschema:"required,Path to the function project directory"` - Target *string `json:"target,omitempty" jsonschema:"Function instance to invoke: local, remote, or a URL (default: auto-discovery; prefers local when both local and remote are running)"` - Format *string `json:"format,omitempty" jsonschema:"Format of message to send: http or cloudevent (default: auto-detected)"` - ID *string `json:"id,omitempty" jsonschema:"CloudEvent id for the request data"` - Source *string `json:"source,omitempty" jsonschema:"CloudEvent source for the request data"` - Type *string `json:"type,omitempty" jsonschema:"CloudEvent type for the request data"` - Data *string `json:"data,omitempty" jsonschema:"Data (content) to send in the request"` - ContentType *string `json:"contentType,omitempty" jsonschema:"MIME type of the data"` - RequestType *string `json:"requestType,omitempty" jsonschema:"HTTP method override (e.g., GET, POST)"` - File *string `json:"file,omitempty" jsonschema:"Path to a file whose content is used as the request data (overrides data)"` - Insecure *bool `json:"insecure,omitempty" jsonschema:"Skip TLS verification when invoking over SSL"` - Verbose *bool `json:"verbose,omitempty" jsonschema:"Enable verbose logging output"` + Path string `json:"path" jsonschema:"required,Path to the function project directory"` + Target *string `json:"target,omitempty" jsonschema:"Function instance to invoke: local, remote, or a URL (default: auto-discovery; prefers local when both local and remote are running)"` + Format *string `json:"format,omitempty" jsonschema:"Format of message to send: http or cloudevent (default: auto-detected)"` + ID *string `json:"id,omitempty" jsonschema:"CloudEvent id for the request data"` + Source *string `json:"source,omitempty" jsonschema:"CloudEvent source for the request data"` + Type *string `json:"type,omitempty" jsonschema:"CloudEvent type for the request data"` + Data *string `json:"data,omitempty" jsonschema:"Data (content) to send in the request"` + ContentType *string `json:"contentType,omitempty" jsonschema:"MIME type of the data"` + RequestType *string `json:"requestType,omitempty" jsonschema:"HTTP method override (e.g., GET, POST)"` + File *string `json:"file,omitempty" jsonschema:"Path to a file whose content is used as the request data (overrides data)"` + Extensions map[string]string `json:"extensions,omitempty" jsonschema:"CloudEvent extension attributes as key-value pairs (cloudevent format only)"` + Insecure *bool `json:"insecure,omitempty" jsonschema:"Skip TLS verification when invoking over SSL"` + Verbose *bool `json:"verbose,omitempty" jsonschema:"Enable verbose logging output"` } func (i InvokeInput) Args() []string { @@ -65,6 +67,17 @@ func (i InvokeInput) Args() []string { args = appendStringFlag(args, "--request-type", i.RequestType) args = appendStringFlag(args, "--file", i.File) + if len(i.Extensions) > 0 { + keys := make([]string, 0, len(i.Extensions)) + for k := range i.Extensions { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + args = append(args, "--extension", fmt.Sprintf("%s=%s", k, i.Extensions[k])) + } + } + args = appendBoolFlag(args, "--insecure", i.Insecure) args = appendBoolFlag(args, "--verbose", i.Verbose) diff --git a/pkg/mcp/tools_invoke_test.go b/pkg/mcp/tools_invoke_test.go index b32514ea16..b3c4129130 100644 --- a/pkg/mcp/tools_invoke_test.go +++ b/pkg/mcp/tools_invoke_test.go @@ -71,6 +71,56 @@ func TestTool_Invoke_Args(t *testing.T) { } } +// TestTool_Invoke_Extensions ensures CloudEvent extension attributes are +// passed as repeated, deterministically-ordered '--extension key=value' flags. +func TestTool_Invoke_Extensions(t *testing.T) { + executor := mock.NewExecutor() + executor.ExecuteFn = func(ctx context.Context, subcommand string, args ...string) ([]byte, error) { + if subcommand != "invoke" { + t.Fatalf("expected subcommand 'invoke', got %q", subcommand) + } + want := []string{ + "--path", ".", + "--extension", "priority=high", + "--extension", "region=us-east", + } + if len(args) != len(want) { + t.Fatalf("expected args %v, got %v", want, args) + } + for i := range want { + if args[i] != want[i] { + t.Fatalf("expected args %v, got %v", want, args) + } + } + return []byte("OK\n"), nil + } + + client, _, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "invoke", + Arguments: map[string]any{ + "path": ".", + "extensions": map[string]any{ + "region": "us-east", + "priority": "high", + }, + }, + }) + if err != nil { + t.Fatal(err) + } + if result.IsError { + t.Fatalf("unexpected error result: %v", result) + } + if !executor.ExecuteInvoked { + t.Fatal("executor was not invoked") + } +} + // TestTool_Invoke_MinimalArgs ensures the invoke tool can be called with only // the required 'path' argument, relying on defaults for everything else // (target auto-discovers between local and remote).