Files
gnoma/internal/mcp/client.go
vikingowl d2d79d65da feat(m8): MCP client, tool replaceability, and plugin system
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
2026-04-12 03:09:05 +02:00

129 lines
3.4 KiB
Go

package mcp
import (
"context"
"encoding/json"
"fmt"
"log/slog"
)
const protocolVersion = "2024-11-05"
// ServerInfo describes the MCP server identity.
type ServerInfo struct {
Name string `json:"name"`
Version string `json:"version"`
}
// MCPTool is a tool definition discovered from an MCP server.
type MCPTool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
InputSchema json.RawMessage `json:"inputSchema"`
}
// Client implements the MCP protocol lifecycle over a Transport.
type Client struct {
transport *Transport
serverInfo ServerInfo
logger *slog.Logger
}
// NewClient creates an MCP client. Call Initialize before using other methods.
func NewClient(transport *Transport, logger *slog.Logger) *Client {
return &Client{
transport: transport,
logger: logger,
}
}
// Initialize performs the MCP handshake: sends initialize request,
// receives server info, and sends initialized notification.
func (c *Client) Initialize(ctx context.Context) error {
params := struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities struct{} `json:"capabilities"`
ClientInfo struct {
Name string `json:"name"`
Version string `json:"version"`
} `json:"clientInfo"`
}{
ProtocolVersion: protocolVersion,
}
params.ClientInfo.Name = "gnoma"
params.ClientInfo.Version = "0.1.0"
result, err := c.transport.Call(ctx, "initialize", params)
if err != nil {
return fmt.Errorf("mcp initialize: %w", err)
}
var initResult struct {
ProtocolVersion string `json:"protocolVersion"`
ServerInfo ServerInfo `json:"serverInfo"`
}
if err := json.Unmarshal(result, &initResult); err != nil {
return fmt.Errorf("mcp initialize: decode result: %w", err)
}
c.serverInfo = initResult.ServerInfo
c.logger.Debug("mcp initialized",
"server", c.serverInfo.Name,
"version", c.serverInfo.Version,
"protocol", initResult.ProtocolVersion,
)
// Send initialized notification (no response expected).
if err := c.transport.Notify(ctx, "initialized", nil); err != nil {
return fmt.Errorf("mcp initialized notification: %w", err)
}
return nil
}
// ListTools calls tools/list and returns discovered tool definitions.
func (c *Client) ListTools(ctx context.Context) ([]MCPTool, error) {
result, err := c.transport.Call(ctx, "tools/list", nil)
if err != nil {
return nil, fmt.Errorf("mcp tools/list: %w", err)
}
var toolsResult struct {
Tools []MCPTool `json:"tools"`
}
if err := json.Unmarshal(result, &toolsResult); err != nil {
return nil, fmt.Errorf("mcp tools/list: decode: %w", err)
}
c.logger.Debug("mcp tools discovered", "count", len(toolsResult.Tools))
return toolsResult.Tools, nil
}
// CallTool invokes a tool on the MCP server and returns the raw result.
func (c *Client) CallTool(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error) {
params := struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments,omitempty"`
}{
Name: name,
Arguments: args,
}
result, err := c.transport.Call(ctx, "tools/call", params)
if err != nil {
return nil, fmt.Errorf("mcp tools/call %q: %w", name, err)
}
return result, nil
}
// ServerName returns the server's reported name.
func (c *Client) ServerName() string {
return c.serverInfo.Name
}
// Close shuts down the transport and server process.
func (c *Client) Close() error {
return c.transport.Close()
}