c4fde583f5
Apply gofmt -w across the codebase (struct field comment realignment only — no semantic changes) and silence two errcheck warnings on fmt.Sscanf / fmt.Fprintf return values in internal/router/discovery with explicit `_, _ =` discards. Required so `make check` is green before tagging v0.1.0.
133 lines
3.4 KiB
Go
133 lines
3.4 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
|
|
policy ToolPolicy
|
|
}
|
|
|
|
// Compile-time interface checks.
|
|
var (
|
|
_ tool.Tool = (*Adapter)(nil)
|
|
_ tool.DeferrableTool = (*Adapter)(nil)
|
|
_ tool.PathSensitiveTool = (*Adapter)(nil)
|
|
)
|
|
|
|
// NewAdapter creates a tool adapter for the given MCP tool.
|
|
func NewAdapter(serverName string, mcpTool MCPTool, client *Client, policy ToolPolicy) *Adapter {
|
|
return &Adapter{
|
|
serverName: serverName,
|
|
mcpTool: mcpTool,
|
|
client: client,
|
|
policy: policy,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (a *Adapter) ExtractPaths(args json.RawMessage) []string {
|
|
var m map[string]any
|
|
if err := json.Unmarshal(args, &m); err != nil {
|
|
return nil
|
|
}
|
|
var paths []string
|
|
for _, argName := range a.policy.PathArgs {
|
|
if v, ok := m[argName]; ok {
|
|
if s, ok := v.(string); ok {
|
|
paths = append(paths, s)
|
|
}
|
|
}
|
|
}
|
|
return paths
|
|
}
|
|
|
|
// 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
|
|
}
|