package mcp import ( "regexp" "strings" ) // CmdPrefix is the reserved first argv element that marks an allowlist row as // an MCP call rather than a process. An MCP tool row looks like // // name: "vikunja_list_tasks" cmd: ["mcp", "vikunja", "list_tasks"] // // which is why there is no new column and no migration: the store, the /tools // page, ProposeTool, EnableTool, DisableTool, the act matcher and the confirm // turn all keep working unchanged. The executor is the only place that has to // know the difference, and it is one branch on Cmd[0]. // // The rest of the allowlist discipline is inherited whole: a row that is not // status='enabled' does not run, and a row marked destructive does not run on // first hearing. Nothing here can enable itself — discovery only proposes. const CmdPrefix = "mcp" // Cmd builds the argv encoding for a discovered tool. func Cmd(server, tool string) []string { return []string{CmdPrefix, server, tool} } // ParseCmd recognises an MCP allowlist row. ok=false for an ordinary process // tool, which is what almost every row is. func ParseCmd(cmd []string) (server, tool string, ok bool) { if len(cmd) != 3 || cmd[0] != CmdPrefix { return "", "", false } if cmd[1] == "" || cmd[2] == "" { return "", "", false } return cmd[1], cmd[2], true } var notName = regexp.MustCompile(`[^a-z0-9_]+`) // LocalName is the allowlist name for a discovered tool: the server handle, an // underscore, the remote name, lowercased and stripped of anything that is not // a word character. Namespacing by server is what keeps two servers that both // offer "search" from colliding, and what makes the provenance of a row on the // /tools page obvious without opening the diff. func LocalName(server, tool string) string { clean := func(s string) string { return strings.Trim(notName.ReplaceAllString(strings.ToLower(strings.TrimSpace(s)), "_"), "_") } s, t := clean(server), clean(tool) switch { case s == "": return t case t == "": return s } return s + "_" + t } // Scope is the store scope for a server's rows, so the /tools page can group // them and a human can tell at a glance where a capability came from. func Scope(server string) string { return "mcp:" + server }