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
24 changes: 24 additions & 0 deletions pkg/mcp/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,30 @@ This is essential because:
- **OVERRIDE specific settings**: Call "deploy" tool with specific flags (e.g., --builder pack, --registry docker.io/user)
- Example: "deploy with pack builder" → call deploy tool with --builder pack only

## Prompts

This server also exposes prompts: named, parameterized workflows the client
invokes on the user's behalf.

### onboard

- Drives full end-to-end onboarding: prerequisites → language → scaffold →
registry → local run and invoke → deploy → remote invoke → summary
- The registry is gathered before the local run, because a containerized build
has to name an image and so fails without one
- All four arguments (`language`, `template`, `registry`, `cluster`) are
optional. Supplied values are treated as decided; omitted ones are gathered
from the user as the relevant step is reached
- `template` defaults to `http` and `cluster` defaults to `local`. Only
`cluster` is validated against a fixed set; `language` and `template` are
passed through, since the valid values depend on the installed binary and on
any added template repositories
- In read-only mode the deploy and remote-invoke steps are omitted from the
returned prompt, since `deploy` would be refused
- If a user asks to "get started", "set up a Function from scratch", or
similar, suggest they invoke this prompt rather than improvising the
sequence yourself

## Tool Usage Guide

### General Rules
Expand Down
5 changes: 5 additions & 0 deletions pkg/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ func New(options ...Option) *Server {
mcp.AddTool(i, configGitSetTool, s.configGitSetHandler)
mcp.AddTool(i, configGitRemoveTool, s.configGitRemoveHandler)

// Prompts
// -------
// Multi-step, parameterized workflows the client can invoke by name
i.AddPrompt(onboardPrompt, s.onboardHandler)

// Resources
// ---------
// Current Function state
Expand Down
63 changes: 63 additions & 0 deletions pkg/mcp/prompts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package mcp

import (
"fmt"
"strings"

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

// prompt helpers:

// newUserPromptResult returns a GetPromptResult carrying a single user-role
// text message. Every func prompt is a single, self-contained instruction
// block for the agent, so this is the only shape currently needed.
func newUserPromptResult(description, text string) *mcp.GetPromptResult {
return &mcp.GetPromptResult{
Description: description,
Messages: []*mcp.PromptMessage{
{
Role: "user",
Content: &mcp.TextContent{Text: text},
},
},
}
}

// rawPromptArg returns the named argument from the request verbatim, for
// values which are case-sensitive (such as a container registry). Returns ""
// when the argument was not provided.
func rawPromptArg(r *mcp.GetPromptRequest, name string) string {
if r == nil || r.Params == nil {
return ""
}
return r.Params.Arguments[name]
}

// promptArg returns the named argument from the request, normalized by
// trimming surrounding whitespace and lowercasing. Prompt arguments arrive as
// free-form strings typed by a human (or filled in by an agent), so " Local "
// and "local" must be treated as the same value. Use this only for arguments
// whose valid values are enumerated by the prompt itself; values passed
// through to func (a runtime, a template, a registry) must keep their case
// and so use rawPromptArg. Returns "" when the argument was not provided.
func promptArg(r *mcp.GetPromptRequest, name string) string {
return strings.ToLower(strings.TrimSpace(rawPromptArg(r, name)))
}

// validateChoice ensures value is one of allowed, returning an error naming
// the offending argument and the valid set. An empty value is always
// accepted: prompt arguments are optional by design, and an omitted one means
// "the agent should ask the user", not "invalid".
func validateChoice(name, value string, allowed []string) error {
if value == "" {
return nil
}
for _, a := range allowed {
if value == a {
return nil
}
}
return fmt.Errorf("invalid %q argument %q: must be one of %s",
name, value, strings.Join(allowed, ", "))
}
142 changes: 142 additions & 0 deletions pkg/mcp/prompts_onboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package mcp

import (
"context"
_ "embed"
"fmt"
"strings"
"text/template"

"github.com/modelcontextprotocol/go-sdk/mcp"
"knative.dev/func/pkg/oci"
)

//go:embed prompts_onboard.md
var onboardPromptBody string

// onboardPromptTemplate is parsed once at init; a parse failure is a
// programming error in the embedded markdown, so panicking is correct.
var onboardPromptTemplate = template.Must(
template.New("onboard").Parse(onboardPromptBody))

// Defaults applied when the caller omits the argument. Language and registry
// have no default on purpose: there is no sane guess, so the agent is
// instructed to ask the user.
const (
defaultOnboardTemplate = "http"
defaultOnboardCluster = "local"
)

// onboardClusters are the accepted values for the cluster argument. Unlike
// language, template and registry, cluster is not passed through to func: it
// only selects which guidance the prompt renders, so the set of valid values
// is fully known here and can be validated.
var onboardClusters = []string{"local", "remote"}

var onboardPrompt = &mcp.Prompt{
Name: "onboard",
Title: "Onboard a Function",
Description: "Multi-step onboarding: check prerequisites, choose a language, " +
"scaffold, configure a registry, run and invoke locally, deploy, invoke " +
"the live instance, and summarize. All arguments are optional; omitted " +
"ones are gathered from the user as the steps run.",
Arguments: []*mcp.PromptArgument{
{
Name: "language",
Title: "Language",
Description: "Target runtime, as reported by the func://languages resource, " +
"e.g. go, node, python (asked for if omitted)",
},
{
Name: "template",
Title: "Template",
Description: "Function template available for the chosen runtime, as reported " +
"by the func://templates resource (default: " + defaultOnboardTemplate + ")",
},
{
Name: "registry",
Title: "Registry",
Description: "Container registry prefix, e.g. docker.io/alice (asked for if omitted)",
},
{
Name: "cluster",
Title: "Cluster",
Description: "Deployment target: local (e.g. kind) or remote (default: " + defaultOnboardCluster + ")",
},
},
}

// onboardParams are the values rendered into the onboarding prompt.
// Language and Registry are empty when the caller did not supply them, which
// the template renders as an explicit "ask the user" instruction.
type onboardParams struct {
Language string
Template string
Registry string
Cluster string
Readonly bool
// HostBuilder reports whether the host builder supports Language, and is
// what the deploy step branches on rather than naming runtimes itself.
// False when no language was supplied.
HostBuilder bool
}

func (s *Server) onboardHandler(_ context.Context, r *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
p, err := newOnboardParams(r, s.readonly.Load())
if err != nil {
return nil, err
}

var buf strings.Builder
if err := onboardPromptTemplate.Execute(&buf, p); err != nil {
return nil, fmt.Errorf("error rendering onboarding prompt: %w", err)
}

return newUserPromptResult(onboardPrompt.Description, buf.String()), nil
}

// newOnboardParams validates the request's arguments and applies defaults.
//
// Only cluster is validated against a fixed set. Language, template and
// registry are passed through: the runtimes and templates which actually
// exist depend on the installed binary and on any template repositories the
// user has added (see the repository_add tool), so a hardcoded list here
// would reject legitimate values. The prompt instructs the agent to verify
// them against the func://languages and func://templates resources instead,
// and create reports plainly when either is wrong.
func newOnboardParams(r *mcp.GetPromptRequest, readonly bool) (p onboardParams, err error) {
p = onboardParams{
// Values handed to func are only trimmed, never case-folded: a
// registry is a case-sensitive image reference prefix, and a runtime
// or template name is matched against a directory name in a template
// repository.
Language: strings.TrimSpace(rawPromptArg(r, "language")),
Template: strings.TrimSpace(rawPromptArg(r, "template")),
Registry: strings.TrimSpace(rawPromptArg(r, "registry")),
Cluster: promptArg(r, "cluster"),
Readonly: readonly,
}

if err = validateChoice("cluster", p.Cluster, onboardClusters); err != nil {
return
}

switch {
case p.Template == "":
p.Template = defaultOnboardTemplate
case strings.EqualFold(p.Template, "cloudevent"):
// "cloudevent" is how the format is spelled elsewhere (e.g. invoke's
// --format), but no runtime ships a template by that name: the one on
// disk is "cloudevents". Correcting it is safe precisely because the
// value given cannot be valid as-is.
p.Template = "cloudevents"
}

if p.Cluster == "" {
p.Cluster = defaultOnboardCluster
}

// Single source of truth for which runtimes the host builder supports.
p.HostBuilder = oci.IsSupported(strings.ToLower(p.Language))
return
}
Loading
Loading