Expose discovered MCP tools through the act allowlist (#251)
Second half of the MCP client: the tools the manager discovers become rows in the existing act allowlist instead of a parallel capability system. An MCP tool is encoded in the columns that already exist — cmd ["mcp",<server>,<tool>], scope mcp:<server> — so no migration, and ProposeTool/EnableTool/DisableTool, tool.Matcher and the confirm turn need no changes. One branch in Executor.Exec routes such a row to the manager instead of exec, and "mcp" is never run as a binary. Discovery only ever PROPOSES. destructive comes from the inverse of the MCP readOnlyHint, so a tool that does not promise to be read-only inherits the confirm turn, and enabling stays on /tools behind step-up. Voice args are positional and MCP args are named, so CallPositional binds only what it can defend: no required properties runs bare, and a read-only tool with exactly one required string or number gets the tail. Everything else refuses with ErrNeedsArgs rather than guessing. The read-only condition was learned against the live Vikunja server: update_task requires only task_id and takes the rest as optional, so one guessed argument blanked the fields it did not mention. A partially-filled write destroys what it omits, so a mutating tool never receives a guessed argument. Also: a read-only mcp_servers IPC method and an "MCP servers" card on /tools showing transport, target and state, with the trust level of a local target spelled out. There is deliberately no call-a-tool IPC method and no run button, so mutation keeps exactly one path. Vikunja #251
This commit is contained in:
@@ -56,6 +56,45 @@ func (s *Store) ProposeTool(ctx context.Context, name, utterance, scope string,
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// ProposeMCPTool is ProposeTool for a tool discovered on an MCP server
|
||||
// (Vikunja #251): the proposal already knows what it would run, so cmd and
|
||||
// destructive are written with it and Kami only has to press enable.
|
||||
//
|
||||
// It is still a PROPOSAL. Discovery cannot grant a capability — that is the
|
||||
// whole reason a server can be configured without its tools becoming live.
|
||||
// Like ProposeTool it never touches an existing row, so re-discovery on every
|
||||
// restart is idempotent and cannot silently re-arm a tool that was disabled or
|
||||
// change the cmd of one already enabled.
|
||||
func (s *Store) ProposeMCPTool(ctx context.Context, name, scope string, cmd []string, destructive bool, utterance string, ts time.Time) (bool, error) {
|
||||
if len(cmd) == 0 {
|
||||
return false, ErrToolCmd
|
||||
}
|
||||
if scope == "" {
|
||||
scope = "homelab"
|
||||
}
|
||||
raw, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("propose mcp tool: %w", err)
|
||||
}
|
||||
d := 0
|
||||
if destructive {
|
||||
d = 1
|
||||
}
|
||||
res, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO tools (name, scope, cmd, destructive, status, utterance, created_ts, updated_ts)
|
||||
VALUES (?, ?, ?, ?, 'proposed', ?, ?, ?)
|
||||
ON CONFLICT(name) DO NOTHING`,
|
||||
name, scope, string(raw), d, utterance, ts.UnixMilli(), ts.UnixMilli())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("propose mcp tool: %w", err)
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("propose mcp tool: rows affected: %w", err)
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// EnableTool fills cmd + destructive and flips status to 'enabled'. This is the
|
||||
// human "enable" act (the authed surface calls it); it upserts so enabling a
|
||||
// name that was never proposed still works. An empty cmd is refused — an
|
||||
|
||||
Reference in New Issue
Block a user