From 2e49dd0de8ad8d97498bf97bade5949cf41b2d3f Mon Sep 17 00:00:00 2001 From: Kapil Sareen Date: Tue, 5 Aug 2025 12:43:45 +0530 Subject: [PATCH] mcp: adds remote template support (#2951) Signed-off-by: kapil --- pkg/mcp/help.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ pkg/mcp/mcp.go | 18 ++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/pkg/mcp/help.go b/pkg/mcp/help.go index 68d2b5fe6..2e575482d 100644 --- a/pkg/mcp/help.go +++ b/pkg/mcp/help.go @@ -2,13 +2,75 @@ package mcp import ( "context" + "encoding/json" "fmt" + "io" + "net/http" "os/exec" "strings" "github.com/mark3labs/mcp-go/mcp" ) +type template struct { + Repository string `json:"repository"` + Language string `json:"language"` + TemplateName string `json:"template"` +} + +func fetchTemplates() ([]template, error) { + var out []template + seen := make(map[string]bool) + + for _, repoURL := range TEMPLATE_RESOURCE_URIS { + owner, repo := parseGitHubURL(repoURL) + api := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees/main?recursive=1", owner, repo) + + resp, err := http.Get(api) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var tree struct { + Tree []struct { + Path string `json:"path"` + } `json:"tree"` + } + if err := json.Unmarshal(body, &tree); err != nil { + return nil, err + } + + for _, item := range tree.Tree { + parts := strings.Split(item.Path, "/") + if len(parts) >= 2 && !strings.HasPrefix(parts[0], ".") { + lang, name := parts[0], parts[1] + key := lang + "/" + name + if !seen[key] { + out = append(out, template{ + Language: lang, + TemplateName: name, + Repository: repoURL, + }) + seen[key] = true + } + } + } + } + return out, nil +} + +func parseGitHubURL(url string) (owner, repo string) { + trim := strings.TrimPrefix(url, "https://github.com/") + parts := strings.Split(trim, "/") + return parts[0], parts[1] +} + func handleRootHelpResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { content, err := exec.Command("func", "--help").Output() if err != nil { @@ -39,6 +101,25 @@ func runHelpCommand(args []string, uri string) ([]mcp.ResourceContents, error) { }, nil } +func handleListTemplatesResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + templates, err := fetchTemplates() + if err != nil { + return nil, err + } + content, err := json.MarshalIndent(templates, "", " ") + if err != nil { + return nil, err + } + + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: "func://templates", + MIMEType: "text/plain", + Text: string(content), + }, + }, nil +} + func handleCmdHelpPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { cmd := request.Params.Arguments["cmd"] if cmd == "" { @@ -86,3 +167,22 @@ func handleRootHelpPrompt(ctx context.Context, request mcp.GetPromptRequest) (*m }, ), nil } + +func handleListTemplatesPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + return mcp.NewGetPromptResult( + "List Templates Prompt", + []mcp.PromptMessage{ + mcp.NewPromptMessage( + mcp.RoleUser, + mcp.NewTextContent("List available function templates"), + ), + mcp.NewPromptMessage( + mcp.RoleAssistant, + mcp.NewEmbeddedResource(mcp.TextResourceContents{ + URI: "func://templates", + MIMEType: "text/plain", + }), + ), + }, + ), nil +} diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 37b83197d..45e0da113 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -7,6 +7,10 @@ import ( "github.com/mark3labs/mcp-go/server" ) +var TEMPLATE_RESOURCE_URIS = []string{ + "https://github.com/gauron99/func-templates", +} + type MCPServer struct { server *server.MCPServer } @@ -43,7 +47,7 @@ func NewServer() *MCPServer { // 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.WithString("repository", mcp.Description("URI to Git repo containing the template. Overrides default template selection when provided.")), mcp.WithBoolean("confirm", mcp.Description("Prompt to confirm options interactively")), mcp.WithBoolean("verbose", mcp.Description("Print verbose logs")), ), @@ -325,6 +329,14 @@ func NewServer() *MCPServer { return runHelpCommand([]string{"config", "envs", "remove"}, "func://config/envs/remove/docs") }) + // Static resource for listing available templates + mcpServer.AddResource(mcp.NewResource( + "func://templates", + "Available Templates", + mcp.WithResourceDescription("List of available function templates"), + mcp.WithMIMEType("plain/text"), + ), handleListTemplatesResource) + mcpServer.AddPrompt(mcp.NewPrompt("help", mcp.WithPromptDescription("help prompt for the root command"), ), handleRootHelpPrompt) @@ -337,6 +349,10 @@ func NewServer() *MCPServer { ), ), handleCmdHelpPrompt) + mcpServer.AddPrompt(mcp.NewPrompt("list_templates", + mcp.WithPromptDescription("prompt to list available function templates"), + ), handleListTemplatesPrompt) + return &MCPServer{ server: mcpServer, }