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
5 changes: 5 additions & 0 deletions pkg/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func New(options ...Option) *Server {
mcp.AddTool(i, buildTool, s.buildHandler)
mcp.AddTool(i, deployTool, s.deployHandler)
mcp.AddTool(i, listTool, s.listHandler)
mcp.AddTool(i, describeTool, s.describeHandler)
mcp.AddTool(i, deleteTool, s.deleteHandler)
mcp.AddTool(i, configVolumesListTool, s.configVolumesListHandler)
mcp.AddTool(i, configVolumesAddTool, s.configVolumesAddHandler)
Expand Down Expand Up @@ -138,6 +139,10 @@ func New(options ...Option) *Server {
i.AddResource(newHelpResource(s, "Envs Add Help", "help for 'config envs add'", "config", "envs", "add"))
i.AddResource(newHelpResource(s, "Envs Remove Help", "help for 'config envs remove'", "config", "envs", "remove"))

// Prompts
// -------
i.AddPrompt(funcWorkflowPrompt, funcWorkflowHandler)

s.impl = i

return s
Expand Down
53 changes: 53 additions & 0 deletions pkg/mcp/prompts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package mcp

import (
"context"
"fmt"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

var funcWorkflowPrompt = &mcp.Prompt{
Name: "func-workflow",
Title: "Function Lifecycle Workflow",
Description: "Guides through the full Function lifecycle: create, build, and deploy.",
Arguments: []*mcp.PromptArgument{
{
Name: "language",
Description: "Programming language for the Function (e.g. go, python, node).",
},
},
}

func funcWorkflowHandler(_ context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
lang := req.Params.Arguments["language"]
return &mcp.GetPromptResult{
Description: "Step-by-step guide for the Function lifecycle.",
Messages: []*mcp.PromptMessage{
{
Role: "user",
Content: &mcp.TextContent{Text: funcWorkflowText(lang)},
},
},
}, nil
}

func funcWorkflowText(language string) string {
langLine := ""
if language != "" {
langLine = fmt.Sprintf("\nUse language: %s\n", language)
}
return fmt.Sprintf(`Guide me through the full Function lifecycle.%s
Step 1 - Create:
Use the "create" tool to scaffold a new Function.
Check "func://languages" for available languages and "func://templates" for templates.

Step 2 - Build:
Use the "build" tool to compile the Function into a container image.

Step 3 - Deploy:
Use the "deploy" tool to deploy the Function to the cluster.
On first deploy, a container registry is required (e.g. docker.io/user).

Use "func://function" to check current Function state at any point.`, langLine)
}
92 changes: 92 additions & 0 deletions pkg/mcp/prompts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package mcp

import (
"strings"
"testing"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

func TestPrompt_Listed(t *testing.T) {
client, _, err := newTestPair(t)
if err != nil {
t.Fatal(err)
}

result, err := client.ListPrompts(t.Context(), nil)
if err != nil {
t.Fatal(err)
}

var found bool
for _, p := range result.Prompts {
if p.Name == "func-workflow" {
found = true
if p.Description == "" {
t.Error("expected non-empty description")
}
break
}
}
if !found {
t.Fatal("func-workflow prompt not found in listing")
}
}

func TestPrompt_Get(t *testing.T) {
client, _, err := newTestPair(t)
if err != nil {
t.Fatal(err)
}

result, err := client.GetPrompt(t.Context(), &mcp.GetPromptParams{
Name: "func-workflow",
})
if err != nil {
t.Fatal(err)
}

if len(result.Messages) != 1 {
t.Fatalf("expected 1 message, got %d", len(result.Messages))
}
msg := result.Messages[0]
if msg.Role != "user" {
t.Fatalf("expected role 'user', got %q", msg.Role)
}
tc, ok := msg.Content.(*mcp.TextContent)
if !ok {
t.Fatalf("expected TextContent, got %T", msg.Content)
}
if !strings.Contains(tc.Text, "create") {
t.Error("expected prompt text to mention create")
}
if !strings.Contains(tc.Text, "deploy") {
t.Error("expected prompt text to mention deploy")
}
}

func TestPrompt_GetWithLanguage(t *testing.T) {
client, _, err := newTestPair(t)
if err != nil {
t.Fatal(err)
}

result, err := client.GetPrompt(t.Context(), &mcp.GetPromptParams{
Name: "func-workflow",
Arguments: map[string]string{"language": "go"},
})
if err != nil {
t.Fatal(err)
}

if len(result.Messages) != 1 {
t.Fatalf("expected 1 message, got %d", len(result.Messages))
}
tc, ok := result.Messages[0].Content.(*mcp.TextContent)
if !ok {
t.Fatalf("expected TextContent, got %T", result.Messages[0].Content)
}
if !strings.Contains(tc.Text, "go") {
t.Error("expected prompt text to contain the language 'go'")
}
}
69 changes: 69 additions & 0 deletions pkg/mcp/tools_describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package mcp

import (
"context"
"fmt"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

var describeTool = &mcp.Tool{
Name: "describe",
Title: "Describe Function",
Description: "Show the name, route, subscriptions (extractors), and other details of a deployed function.",
Annotations: &mcp.ToolAnnotations{
Title: "Describe Function",
ReadOnlyHint: true,
IdempotentHint: true,
},
}

func (s *Server) describeHandler(ctx context.Context, r *mcp.CallToolRequest, input DescribeInput) (result *mcp.CallToolResult, output DescribeOutput, err error) {
// Validate: path and name are mutually exclusive
if input.Path != nil && input.Name != nil {
err = fmt.Errorf("'path' and 'name' are mutually exclusive")
return
}

out, err := s.executor.Execute(ctx, "describe", input.Args()...)
if err != nil {
err = fmt.Errorf("%w\n%s", err, string(out))
return
}
output = DescribeOutput{
Message: string(out),
}
return
}

// DescribeInput defines the input parameters for the describe tool.
// Path and Name are mutually exclusive. If neither is provided, the function
// in the current directory is described.
type DescribeInput struct {
Path *string `json:"path,omitempty" jsonschema:"Path to the function project directory (mutually exclusive with name)"`
Name *string `json:"name,omitempty" jsonschema:"Name of the deployed function to describe (mutually exclusive with path)"`
Namespace *string `json:"namespace,omitempty" jsonschema:"Kubernetes namespace of the function (default: current namespace)"`
Output *string `json:"output,omitempty" jsonschema:"Output format: human, plain, json, xml, or yaml"`
Verbose *bool `json:"verbose,omitempty" jsonschema:"Enable verbose logging output"`
}

func (i DescribeInput) Args() []string {
args := []string{}

// Either path flag or positional name argument
if i.Path != nil {
args = append(args, "--path", *i.Path)
} else if i.Name != nil {
args = append(args, *i.Name)
}

args = appendStringFlag(args, "--namespace", i.Namespace)
args = appendStringFlag(args, "--output", i.Output)
args = appendBoolFlag(args, "--verbose", i.Verbose)
return args
}

// DescribeOutput defines the structured output returned by the describe tool.
type DescribeOutput struct {
Message string `json:"message" jsonschema:"Output message"`
}
Loading