Complete the remaining M8 extensibility deliverables:
- MCP client with JSON-RPC 2.0 over stdio transport, protocol
lifecycle (initialize/tools-list/tools-call), and process group
management for clean shutdown
- MCP tool adapter implementing tool.Tool with mcp__{server}__{tool}
naming convention and replace_default for swapping built-in tools
- MCP manager for multi-server orchestration with parallel startup,
tool discovery, and registry integration
- Plugin system with plugin.json manifest (name/version/capabilities),
directory-based discovery (global + project scopes with precedence),
loader that merges skills/hooks/MCP configs into existing registries,
and install/uninstall/list lifecycle manager
- Config additions: MCPServerConfig, PluginsSection with opt-in/opt-out
enabled/disabled resolution
- TUI /plugins command for listing installed plugins
- 54 tests across internal/mcp and internal/plugin packages
114 lines
3.0 KiB
Go
114 lines
3.0 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"somegit.dev/Owlibou/gnoma/internal/tool"
|
|
)
|
|
|
|
// Adapter wraps an MCPTool as a gnoma tool.Tool.
|
|
type Adapter struct {
|
|
serverName string
|
|
mcpTool MCPTool
|
|
client *Client
|
|
overrideName string // non-empty when replacing a built-in
|
|
}
|
|
|
|
// Compile-time interface checks.
|
|
var (
|
|
_ tool.Tool = (*Adapter)(nil)
|
|
_ tool.DeferrableTool = (*Adapter)(nil)
|
|
)
|
|
|
|
// NewAdapter creates a tool adapter for the given MCP tool.
|
|
func NewAdapter(serverName string, mcpTool MCPTool, client *Client) *Adapter {
|
|
return &Adapter{
|
|
serverName: serverName,
|
|
mcpTool: mcpTool,
|
|
client: client,
|
|
}
|
|
}
|
|
|
|
// SetOverrideName sets a replacement name (used for replace_default).
|
|
func (a *Adapter) SetOverrideName(name string) {
|
|
a.overrideName = name
|
|
}
|
|
|
|
// Name returns the tool name. Uses mcp__{server}__{tool} convention,
|
|
// or the override name when replacing a built-in.
|
|
func (a *Adapter) Name() string {
|
|
if a.overrideName != "" {
|
|
return a.overrideName
|
|
}
|
|
return fmt.Sprintf("mcp__%s__%s", a.serverName, a.mcpTool.Name)
|
|
}
|
|
|
|
// Description returns the MCP tool's description.
|
|
func (a *Adapter) Description() string {
|
|
return a.mcpTool.Description
|
|
}
|
|
|
|
// Parameters returns the MCP tool's input schema (zero-copy passthrough).
|
|
func (a *Adapter) Parameters() json.RawMessage {
|
|
return a.mcpTool.InputSchema
|
|
}
|
|
|
|
// Execute calls the MCP server's tools/call method.
|
|
func (a *Adapter) Execute(ctx context.Context, args json.RawMessage) (tool.Result, error) {
|
|
result, err := a.client.CallTool(ctx, a.mcpTool.Name, args)
|
|
if err != nil {
|
|
// RPC errors are surfaced as tool output so the LLM can see them.
|
|
var rpcErr *RPCError
|
|
if errors.As(err, &rpcErr) {
|
|
return tool.Result{
|
|
Output: fmt.Sprintf("MCP error: %s", rpcErr.Message),
|
|
}, nil
|
|
}
|
|
// Transport-level errors are Go errors (broken pipe, timeout).
|
|
return tool.Result{}, err
|
|
}
|
|
|
|
output, err := extractTextContent(result)
|
|
if err != nil {
|
|
return tool.Result{
|
|
Output: fmt.Sprintf("MCP response parse error: %v\nRaw: %s", err, result),
|
|
}, nil
|
|
}
|
|
|
|
return tool.Result{Output: output}, nil
|
|
}
|
|
|
|
// IsReadOnly returns false conservatively — MCP tools may have side effects.
|
|
func (a *Adapter) IsReadOnly() bool { return false }
|
|
|
|
// IsDestructive returns false — use permission rules for granular control.
|
|
func (a *Adapter) IsDestructive() bool { return false }
|
|
|
|
// ShouldDefer returns true — MCP tools start deferred to reduce token overhead.
|
|
func (a *Adapter) ShouldDefer() bool { return true }
|
|
|
|
// extractTextContent concatenates text blocks from an MCP tools/call result.
|
|
func extractTextContent(raw json.RawMessage) (string, error) {
|
|
var result struct {
|
|
Content []struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
} `json:"content"`
|
|
}
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var parts []string
|
|
for _, c := range result.Content {
|
|
if c.Type == "text" {
|
|
parts = append(parts, c.Text)
|
|
}
|
|
}
|
|
return strings.Join(parts, "\n"), nil
|
|
}
|