Skip to content

MCP Aggregator Component

Overview

The MCP Aggregator (internal/aggregator) is the core component responsible for unifying tools from multiple sources into a single, coherent interface for AI agents. It provides a unified MCP protocol interface that exposes:

  • 11 Meta-Tools as the primary interface (list_tools, call_tool, etc.)
  • 36 Core Built-in Tools across 5 functional categories (accessed via call_tool)
  • Dynamic Workflow Tools generated from workflow definitions (accessed via call_tool)
  • External Tools from configured MCP servers (accessed via call_tool)

The aggregator acts as an intelligent proxy that discovers, registers, filters, and routes tool calls to the appropriate underlying sources while providing consistent tool discovery and execution patterns.

Key Architectural Note: MCP clients see only the meta-tools. All actual tools (core_, workflow_, x_*) are accessed through the call_tool meta-tool. This design enables session-scoped tool visibility and unified authentication handling.

Meta-Tools Interface

The aggregator exposes only meta-tools as its MCP interface. This is the primary way clients interact with muster.

Tool Discovery Meta-Tools

Meta-Tool Description
list_tools List one bounded page of the current session's tools (limit/offset, total, truncated)
describe_tool Get detailed schema for a specific tool
filter_tools Search tools by pattern
list_core_tools List only muster core tools

Tool Execution Meta-Tool

Meta-Tool Description
call_tool Execute any tool by name with arguments

Resource Meta-Tools

Meta-Tool Description
list_resources List available MCP resources
describe_resource Get resource metadata
get_resource Read resource contents

Prompt Meta-Tools

Meta-Tool Description
list_prompts List available prompts
describe_prompt Get prompt details
get_prompt Execute a prompt

Tool Execution Flow

All tool calls go through the call_tool meta-tool:

MCP Client → call_tool(name="core_workflow_list", args={})
           → Aggregator.CallToolInternal()
           → callCoreToolDirectly() or forward to backend MCP server
           → Result wrapped in structured JSON response

Session-Scoped Visibility

The list_tools response includes: 1. Available tools: One page (50 by default) of the tools from connected/authenticated servers, summarised; total and truncated tell the caller whether more pages exist 2. Servers requiring auth: Information about OAuth-protected servers that need authentication

Architecture

Core Responsibilities

  1. Meta-Tools Interface: Expose meta-tools (list_tools, call_tool, etc.) as the only MCP interface
  2. Core Tool Management: Provide built-in tools across 4 categories (config, mcpserver, service, workflow)
  3. Dynamic Tool Generation: Generate workflow execution tools (workflow_*) from workflow definitions
  4. MCP Server Discovery: Automatically discover and connect to configured external MCP servers
  5. Tool Aggregation: Collect and register tools from all sources into a unified registry
  6. Session-Scoped Visibility: Manage tool visibility based on authentication state
  7. Request Routing: Route tool calls to appropriate handlers (core, workflow engine, or external servers)
  8. Response Wrapping: Wrap tool responses in structured JSON for consistent handling

Component Structure

internal/aggregator/
├── server.go          # Main aggregator MCP server implementation
├── registry.go        # Tool registration and management
├── tool_factory.go    # Dynamic tool creation and proxying
├── event_handler.go   # Server lifecycle event processing
├── manager.go         # High-level orchestration
└── types.go          # Core data structures

Key Components

Registry (registry.go)

Purpose: Central tool registry managing all aggregated tools

Key Functionality: - Tool registration from multiple sources - Conflict resolution for duplicate tool names - Tool metadata management - Dynamic tool discovery

Implementation Pattern:

type Registry struct {
    tools     map[string]*ToolEntry
    servers   map[string]*ServerEntry
    mutex     sync.RWMutex
}

type ToolEntry struct {
    Name        string
    Description string
    Schema      *ToolSchema
    ServerID    string
    LastSeen    time.Time
}

Key Methods: - RegisterTool(serverID, toolName string, schema *ToolSchema) error - UnregisterTool(serverID, toolName string) error - GetTool(name string) (*ToolEntry, error) - ListTools(filter *ToolFilter) ([]*ToolEntry, error)

Tool Factory (tool_factory.go)

Purpose: Dynamic creation of meta-tools for tool discovery and execution

Meta-Tools Provided: 1. list_tools - Enumerate available tools with filtering 2. filter_tools - Apply filters to tool lists 3. call_tool - Execute tools on underlying servers 4. get_tool_schema - Retrieve detailed tool schemas

Implementation Pattern:

type ToolFactory struct {
    registry  *Registry
    mcpClient MCPClientInterface
}

func (f *ToolFactory) CreateListToolsTool() *Tool {
    return &Tool{
        Name:        "list_tools",
        Description: "List available tools from all connected MCP servers",
        Schema:      listToolsSchema,
        Handler:     f.handleListTools,
    }
}

func (f *ToolFactory) handleListTools(ctx context.Context, args map[string]interface{}) (interface{}, error) {
    filter := parseToolFilter(args)
    tools, err := f.registry.ListTools(filter)
    if err != nil {
        return nil, fmt.Errorf("failed to list tools: %w", err)
    }
    return formatToolList(tools), nil
}

Event Handler (event_handler.go)

Purpose: Process server lifecycle events and maintain tool registry consistency

Event Types: - Server connected/disconnected - Tool added/removed/updated - Server health status changes

Implementation Pattern:

type EventHandler struct {
    registry   *Registry
    notifier   EventNotifier
}

func (h *EventHandler) HandleServerConnected(serverID string, tools []*ToolDefinition) error {
    h.logger.Info("Server connected", "server_id", serverID, "tool_count", len(tools))

    for _, tool := range tools {
        if err := h.registry.RegisterTool(serverID, tool.Name, tool.Schema); err != nil {
            h.logger.Error("Failed to register tool", "tool", tool.Name, "error", err)
        }
    }

    h.notifier.NotifyServerStateChange(serverID, "connected")
    return nil
}

Integration Patterns

API Integration

The aggregator integrates with the central API through the adapter pattern:

// internal/aggregator/api_adapter.go
type Adapter struct {
    server *Server
    logger *slog.Logger
}

func (a *Adapter) ListTools(ctx context.Context, filter *ToolFilter) ([]*Tool, error) {
    return a.server.listTools(ctx, filter)
}

func (a *Adapter) CallTool(ctx context.Context, name string, args map[string]interface{}) (interface{}, error) {
    return a.server.callTool(ctx, name, args)
}

func (a *Adapter) Register() {
    api.RegisterAggregatorHandler(a)
}

MCP Server Communication

Communication with underlying MCP servers follows the standard MCP protocol:

type MCPClient interface {
    Connect(ctx context.Context, serverConfig *ServerConfig) error
    ListTools(ctx context.Context) ([]*ToolDefinition, error)
    CallTool(ctx context.Context, name string, args map[string]interface{}) (interface{}, error)
    Subscribe(handler EventHandler) error
    Disconnect(ctx context.Context) error
}

Usage Patterns

Tool Discovery Flow

  1. Agent Request: AI agent calls list_tools meta-tool
  2. Registry Query: Query tool registry with filters
  3. Response Generation: Format and return tool list
func (s *Server) HandleListTools(ctx context.Context, request *ListToolsRequest) (*ListToolsResponse, error) {
    // Query registry
    tools, err := s.registry.ListTools(&ToolFilter{
        Patterns:    request.Patterns,
        ServerIDs:   request.ServerFilter,
        Categories:  request.Categories,
    })
    if err != nil {
        return nil, fmt.Errorf("failed to query tools: %w", err)
    }

    // Format response
    return &ListToolsResponse{
        Tools: formatToolDescriptions(tools),
        Total: len(tools),
    }, nil
}

Tool Execution Flow

  1. Agent Request: AI agent calls call_tool with tool name and arguments
  2. Tool Resolution: Resolve tool name to underlying server
  3. Request Forwarding: Forward request to appropriate MCP server
  4. Response Processing: Process and return response to agent
func (s *Server) HandleCallTool(ctx context.Context, request *CallToolRequest) (*CallToolResponse, error) {
    // Resolve tool to server
    tool, err := s.registry.GetTool(request.ToolName)
    if err != nil {
        return nil, fmt.Errorf("tool not found: %w", err)
    }

    // Get MCP client for server
    client, err := s.getServerClient(tool.ServerID)
    if err != nil {
        return nil, fmt.Errorf("server not available: %w", err)
    }

    // Forward request
    result, err := client.CallTool(ctx, request.ToolName, request.Arguments)
    if err != nil {
        return nil, fmt.Errorf("tool execution failed: %w", err)
    }

    return &CallToolResponse{
        Result: result,
        ToolName: request.ToolName,
        ServerID: tool.ServerID,
    }, nil
}

Configuration

Server Configuration

aggregator:
  # Server binding configuration
  bind:
    address: "127.0.0.1"
    port: 8080
    protocol: "stdio"  # or "http", "websocket"

  # Server discovery configuration
  discovery:
    auto_discovery: true
    discovery_interval: "30s"
    health_check_interval: "10s"

  # Performance tuning
  performance:
    max_concurrent_calls: 100
    tool_cache_ttl: "5m"
    connection_pool_size: 10

Error Handling

Error Categories

  1. Connection Errors: MCP server unavailable or disconnected
  2. Tool Errors: Tool not found or execution failure
  3. Configuration Errors: Invalid configuration or missing dependencies
  4. Authorization Errors: Caller not authorized for the requested tool

Error Response Format

type AggregatorError struct {
    Type      string `json:"type"`
    Message   string `json:"message"`
    ServerID  string `json:"server_id,omitempty"`
    ToolName  string `json:"tool_name,omitempty"`
    Timestamp string `json:"timestamp"`
}

func (e *AggregatorError) Error() string {
    return fmt.Sprintf("[%s] %s", e.Type, e.Message)
}

Performance Considerations

Caching Strategy

  • Tool Metadata: Cache tool schemas and descriptions
  • Server Status: Cache server health and availability
  • Response Caching: Cache responses for idempotent operations

Connection Management

  • Connection Pooling: Maintain persistent connections to MCP servers
  • Health Monitoring: Regular health checks for all connected servers
  • Graceful Degradation: Continue operation when servers are unavailable

Concurrency Handling

  • Request Concurrency: Handle multiple simultaneous tool calls
  • Registry Locking: Use read/write locks for registry access
  • Server Communication: Async communication with MCP servers

Monitoring and Observability

Metrics

  • Tool Call Rate: Number of tool calls per second
  • Server Health: Connected/disconnected server count
  • Error Rate: Failed tool calls and connection errors
  • Response Time: Tool call latency distribution

Logging

func (s *Server) logToolCall(ctx context.Context, toolName, serverID string, duration time.Duration, err error) {
    fields := []slog.Attr{
        slog.String("tool_name", toolName),
        slog.String("server_id", serverID),
        slog.Duration("duration", duration),
    }

    if err != nil {
        fields = append(fields, slog.String("error", err.Error()))
        s.logger.LogAttrs(ctx, slog.LevelError, "Tool call failed", fields...)
    } else {
        s.logger.LogAttrs(ctx, slog.LevelInfo, "Tool call completed", fields...)
    }
}

Testing Strategy

Unit Testing

func TestToolRegistration(t *testing.T) {
    registry := NewRegistry()

    tool := &ToolDefinition{
        Name:        "test_tool",
        Description: "Test tool",
        Schema:      &ToolSchema{},
    }

    err := registry.RegisterTool("server1", tool.Name, tool.Schema)
    assert.NoError(t, err)

    retrieved, err := registry.GetTool("test_tool")
    assert.NoError(t, err)
    assert.Equal(t, "test_tool", retrieved.Name)
    assert.Equal(t, "server1", retrieved.ServerID)
}

Integration Testing

func TestAggregatorIntegration(t *testing.T) {
    // Setup mock MCP servers
    mockServer1 := &MockMCPServer{
        Tools: []*ToolDefinition{
            {Name: "server1_tool", Description: "Tool from server 1"},
        },
    }

    // Create aggregator
    aggregator := NewAggregator()
    aggregator.AddServer("server1", mockServer1)

    // Test tool discovery
    tools, err := aggregator.ListTools(context.Background(), nil)
    assert.NoError(t, err)
    assert.Len(t, tools, 1)
    assert.Equal(t, "server1_tool", tools[0].Name)

    // Test tool execution
    result, err := aggregator.CallTool(context.Background(), "server1_tool", map[string]interface{}{})
    assert.NoError(t, err)
    assert.NotNil(t, result)
}

The MCP Aggregator serves as the intelligent hub that makes muster's unified tool interface possible, providing seamless integration between AI agents and multiple underlying MCP servers while maintaining security, performance, and reliability.