func/pkg/mcp/mcp.go

484 lines
15 KiB
Go

package mcp
import (
"context"
"fmt"
"os/exec"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type MCPServer struct {
server *server.MCPServer
}
func NewServer() *MCPServer {
mcpServer := server.NewMCPServer(
"func-mcp",
"1.0.0",
server.WithToolCapabilities(true),
)
mcpServer.AddTool(
mcp.NewTool("healthcheck",
mcp.WithDescription("Checks if the server is running"),
),
handleHealthCheckTool,
)
mcpServer.AddTool(
mcp.NewTool("create",
mcp.WithDescription("Creates a Knative function project in the current or specified directory"),
mcp.WithString("cwd",
mcp.Required(),
mcp.Description("Current working directory of the MCP client"),
),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Name of the function to be created (used as subdirectory)"),
),
mcp.WithString("language",
mcp.Required(),
mcp.Description("Language runtime to use (e.g., node, go, python)"),
),
// Optional flags
mcp.WithString("template", mcp.Description("Function template (e.g., http, cloudevents)")),
mcp.WithString("repository", mcp.Description("URI to Git repo containing the template")),
mcp.WithBoolean("confirm", mcp.Description("Prompt to confirm options interactively")),
mcp.WithBoolean("verbose", mcp.Description("Print verbose logs")),
),
handleCreateTool,
)
mcpServer.AddTool(
mcp.NewTool("deploy",
mcp.WithDescription("Deploys the function to the cluster"),
mcp.WithString("registry",
mcp.Required(),
mcp.Description("Registry to be used to push the function image"),
),
mcp.WithString("cwd",
mcp.Required(),
mcp.Description("Full path of the function to be deployed"),
),
mcp.WithString("builder",
mcp.Required(),
mcp.Description("Builder to be used to build the function image"),
),
// Optional flags
mcp.WithString("image", mcp.Description("Full image name (overrides registry)")),
mcp.WithString("namespace", mcp.Description("Namespace to deploy the function into")),
mcp.WithString("git-url", mcp.Description("Git URL containing the function source")),
mcp.WithString("git-branch", mcp.Description("Git branch for remote deployment")),
mcp.WithString("git-dir", mcp.Description("Directory inside the Git repository")),
mcp.WithString("builder-image", mcp.Description("Custom builder image")),
mcp.WithString("domain", mcp.Description("Domain for the function route")),
mcp.WithString("platform", mcp.Description("Target platform to build for (e.g., linux/amd64)")),
mcp.WithString("path", mcp.Description("Path to the function directory")),
mcp.WithString("build", mcp.Description(`Build control: "true", "false", or "auto"`)),
mcp.WithString("pvc-size", mcp.Description("Custom volume size for remote builds")),
mcp.WithString("service-account", mcp.Description("Kubernetes ServiceAccount to use")),
mcp.WithString("remote-storage-class", mcp.Description("Storage class for remote volume")),
mcp.WithBoolean("confirm", mcp.Description("Prompt for confirmation before deploying")),
mcp.WithBoolean("push", mcp.Description("Push image to registry before deployment")),
mcp.WithBoolean("verbose", mcp.Description("Print verbose logs")),
mcp.WithBoolean("registry-insecure", mcp.Description("Skip TLS verification for registry")),
mcp.WithBoolean("build-timestamp", mcp.Description("Use actual time in image metadata")),
mcp.WithBoolean("remote", mcp.Description("Trigger remote deployment")),
),
handleDeployTool,
)
mcpServer.AddTool(
mcp.NewTool("list",
mcp.WithDescription("Lists all deployed functions in the current or specified namespace"),
// Optional flags
mcp.WithBoolean("all-namespaces", mcp.Description("List functions in all namespaces (overrides --namespace)")),
mcp.WithString("namespace", mcp.Description("The namespace to list functions in (default is current/active)")),
mcp.WithString("output", mcp.Description("Output format: human, plain, json, xml, yaml")),
mcp.WithBoolean("verbose", mcp.Description("Enable verbose output")),
),
handleListTool,
)
mcpServer.AddTool(
mcp.NewTool("build",
mcp.WithDescription("Builds the function image in the current directory"),
mcp.WithString("cwd",
mcp.Required(),
mcp.Description("Current working directory of the MCP client"),
),
mcp.WithString("builder",
mcp.Required(),
mcp.Description("Builder to be used to build the function image (pack, s2i, host)"),
),
mcp.WithString("registry",
mcp.Required(),
mcp.Description("Registry to be used to push the function image (e.g. ghcr.io/user)"),
),
// Optional flags
mcp.WithString("builder-image", mcp.Description("Custom builder image to use with buildpacks")),
mcp.WithString("image", mcp.Description("Full image name (overrides registry + function name)")),
mcp.WithString("path", mcp.Description("Path to the function directory (default is current dir)")),
mcp.WithString("platform", mcp.Description("Target platform, e.g. linux/amd64 (for s2i builds)")),
mcp.WithBoolean("confirm", mcp.Description("Prompt for confirmation before proceeding")),
mcp.WithBoolean("push", mcp.Description("Push image to registry after building")),
mcp.WithBoolean("verbose", mcp.Description("Enable verbose logging output")),
mcp.WithBoolean("registry-insecure", mcp.Description("Skip TLS verification for insecure registries")),
mcp.WithBoolean("build-timestamp", mcp.Description("Use actual time for image timestamp (buildpacks only)")),
),
handleBuildTool,
)
mcpServer.AddTool(
mcp.NewTool("delete",
mcp.WithDescription("Deletes a function from the cluster"),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Name of the function to be deleted"),
),
// Optional flags
mcp.WithString("namespace", mcp.Description("Namespace to delete from (default: current or active)")),
mcp.WithString("path", mcp.Description("Path to the function project (default is current directory)")),
mcp.WithString("all", mcp.Description(`Delete all related resources like Pipelines, Secrets ("true"/"false")`)),
mcp.WithBoolean("confirm", mcp.Description("Prompt to confirm before deletion")),
mcp.WithBoolean("verbose", mcp.Description("Enable verbose output")),
),
handleDeleteTool,
)
mcpServer.AddResource(mcp.NewResource(
"func://docs",
"Root Help Command",
mcp.WithResourceDescription("--help output of the func command"),
mcp.WithMIMEType("text/plain"),
), handleRootHelpResource)
mcpServer.AddPrompt(mcp.NewPrompt("help",
mcp.WithPromptDescription("help prompt for the root command"),
), handleHelpPrompt)
return &MCPServer{
server: mcpServer,
}
}
func (s *MCPServer) Start() error {
return server.ServeStdio(s.server)
}
func handleRootHelpResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
content, err := exec.Command("func", "--help").Output()
if err != nil {
return nil, err
}
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: "func://docs",
MIMEType: "text/plain",
Text: string(content),
},
}, nil
}
func handleHelpPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
return mcp.NewGetPromptResult(
"Help Prompt",
[]mcp.PromptMessage{
mcp.NewPromptMessage(
mcp.RoleUser,
mcp.NewTextContent("What can I do with the func command?"),
),
mcp.NewPromptMessage(
mcp.RoleAssistant,
mcp.NewEmbeddedResource(mcp.TextResourceContents{
URI: "func://docs",
MIMEType: "text/plain",
}),
),
},
), nil
}
func handleHealthCheckTool(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
body := []byte(fmt.Sprintf(`{"message": "%s"}`, "The MCP server is running!"))
return mcp.NewToolResultText(string(body)), nil
}
func handleCreateTool(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
cwd, err := request.RequireString("cwd")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
name, err := request.RequireString("name")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
language, err := request.RequireString("language")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
args := []string{"create", "-l", language}
// Optional flags
if v := request.GetString("template", ""); v != "" {
args = append(args, "--template", v)
}
if v := request.GetString("repository", ""); v != "" {
args = append(args, "--repository", v)
}
if request.GetBool("confirm", false) {
args = append(args, "--confirm")
}
if request.GetBool("verbose", false) {
args = append(args, "--verbose")
}
// `name` is passed as a positional argument (directory to create in)
args = append(args, name)
cmd := exec.Command("func", args...)
cmd.Dir = cwd
out, err := cmd.CombinedOutput()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("func create failed: %s", out)), nil
}
body := []byte(fmt.Sprintf(`{"result": "%s"}`, out))
return mcp.NewToolResultText(string(body)), nil
}
func handleDeployTool(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
cwd, err := request.RequireString("cwd")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
registry, err := request.RequireString("registry")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
builder, err := request.RequireString("builder")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
args := []string{"deploy", "--builder", builder, "--registry", registry}
// Optional flags
if v := request.GetString("image", ""); v != "" {
args = append(args, "--image", v)
}
if v := request.GetString("namespace", ""); v != "" {
args = append(args, "--namespace", v)
}
if v := request.GetString("git-url", ""); v != "" {
args = append(args, "--git-url", v)
}
if v := request.GetString("git-branch", ""); v != "" {
args = append(args, "--git-branch", v)
}
if v := request.GetString("git-dir", ""); v != "" {
args = append(args, "--git-dir", v)
}
if v := request.GetString("builder-image", ""); v != "" {
args = append(args, "--builder-image", v)
}
if v := request.GetString("domain", ""); v != "" {
args = append(args, "--domain", v)
}
if v := request.GetString("platform", ""); v != "" {
args = append(args, "--platform", v)
}
if v := request.GetString("path", ""); v != "" {
args = append(args, "--path", v)
}
if v := request.GetString("build", ""); v != "" {
args = append(args, "--build", v)
}
if v := request.GetString("pvc-size", ""); v != "" {
args = append(args, "--pvc-size", v)
}
if v := request.GetString("service-account", ""); v != "" {
args = append(args, "--service-account", v)
}
if v := request.GetString("remote-storage-class", ""); v != "" {
args = append(args, "--remote-storage-class", v)
}
if request.GetBool("confirm", false) {
args = append(args, "--confirm")
}
if request.GetBool("push", false) {
args = append(args, "--push")
}
if request.GetBool("verbose", false) {
args = append(args, "--verbose")
}
if request.GetBool("registry-insecure", false) {
args = append(args, "--registry-insecure")
}
if request.GetBool("build-timestamp", false) {
args = append(args, "--build-timestamp")
}
if request.GetBool("remote", false) {
args = append(args, "--remote")
}
cmd := exec.Command("func", args...)
cmd.Dir = cwd
out, err := cmd.CombinedOutput()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("func deploy failed: %s", out)), nil
}
body := []byte(fmt.Sprintf(`{"result": "%s"}`, out))
return mcp.NewToolResultText(string(body)), nil
}
func handleListTool(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
args := []string{"list"}
// Optional flags
if request.GetBool("all-namespaces", false) {
args = append(args, "--all-namespaces")
}
if v := request.GetString("namespace", ""); v != "" {
args = append(args, "--namespace", v)
}
if v := request.GetString("output", ""); v != "" {
args = append(args, "--output", v)
}
if request.GetBool("verbose", false) {
args = append(args, "--verbose")
}
cmd := exec.Command("func", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("func list failed: %s", out)), nil
}
body := []byte(fmt.Sprintf(`{"result": "%s"}`, out))
return mcp.NewToolResultText(string(body)), nil
}
func handleBuildTool(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
cwd, err := request.RequireString("cwd")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
builder, err := request.RequireString("builder")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
registry, err := request.RequireString("registry")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
args := []string{"build", "--builder", builder, "--registry", registry}
// Optional flags
if v := request.GetString("builder-image", ""); v != "" {
args = append(args, "--builder-image", v)
}
if v := request.GetString("image", ""); v != "" {
args = append(args, "--image", v)
}
if v := request.GetString("path", ""); v != "" {
args = append(args, "--path", v)
}
if v := request.GetString("platform", ""); v != "" {
args = append(args, "--platform", v)
}
if v := request.GetBool("confirm", false); v {
args = append(args, "--confirm")
}
if v := request.GetBool("push", false); v {
args = append(args, "--push")
}
if v := request.GetBool("verbose", false); v {
args = append(args, "--verbose")
}
if v := request.GetBool("registry-insecure", false); v {
args = append(args, "--registry-insecure")
}
if v := request.GetBool("build-timestamp", false); v {
args = append(args, "--build-timestamp")
}
cmd := exec.Command("func", args...)
cmd.Dir = cwd
out, err := cmd.CombinedOutput()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("func build failed: %s", out)), nil
}
body := []byte(fmt.Sprintf(`{"result": "%s"}`, out))
return mcp.NewToolResultText(string(body)), nil
}
func handleDeleteTool(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
name, err := request.RequireString("name")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
args := []string{"delete", name}
// Optional flags
if v := request.GetString("namespace", ""); v != "" {
args = append(args, "--namespace", v)
}
if v := request.GetString("path", ""); v != "" {
args = append(args, "--path", v)
}
if v := request.GetString("all", ""); v != "" {
args = append(args, "--all", v)
}
if request.GetBool("confirm", false) {
args = append(args, "--confirm")
}
if request.GetBool("verbose", false) {
args = append(args, "--verbose")
}
cmd := exec.Command("func", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("func delete failed: %s", out)), nil
}
body := []byte(fmt.Sprintf(`{"result": "%s"}`, out))
return mcp.NewToolResultText(string(body)), nil
}