Skip to content

Support MCP (Model Context Protocol) tool calling #159

@xerial

Description

@xerial

Summary

Add support for Model Context Protocol (MCP) to enable standardized tool calling across AI agents. MCP provides a universal protocol for AI applications to discover and use tools from external systems, making tool integration more consistent and interoperable.

Motivation

The Model Context Protocol (MCP) is becoming a standard for AI tool integration, supported by major AI platforms. Integrating MCP would:

  • Enable scala-ai agents to use any MCP-compatible tool without custom integration
  • Allow scala-ai tools to be exposed via MCP for use by other AI systems
  • Provide a standardized way to handle tool discovery, invocation, and results
  • Support both local and remote tool execution via stdio/WebSocket connections

Current State Analysis

The current scala-ai architecture has:

  • ✅ Clean ToolSpec abstraction for defining tools
  • ✅ Tool registration at the agent level via withTools()
  • ✅ Tool call request/response flow with ToolCallRequest and ToolResultMessage
  • ✅ Provider-agnostic tool handling
  • ❌ Missing: Actual tool execution layer (tools are defined but not executed)
  • ❌ Missing: Tool discovery mechanism
  • ❌ Missing: Tool registry/executor pattern

Proposed Design

1. Core Components

import wvlet.airframe.rx.Rx

// Tool executor trait (currently missing in scala-ai)
trait ToolExecutor:
  def executeToolCall(toolCall: ToolCallRequest): Rx[ToolResultMessage]
  def availableTools: Seq[ToolSpec]

// MCP client implementation
class MCPToolExecutor(servers: Seq[MCPServer]) extends ToolExecutor:
  def executeToolCall(toolCall: ToolCallRequest): Rx[ToolResultMessage]
  def availableTools: Seq[ToolSpec] // Discovered from MCP servers
  def discoverTools(): Rx[Seq[ToolSpec]] // Dynamic tool discovery

// MCP server abstraction
trait MCPServer:
  def connect(): Rx[MCPConnection]
  def listTools(): Rx[Seq[MCPTool]]
  def invokeTool(name: String, args: Map[String, Any]): Rx[MCPToolResult]

2. Integration Points

  1. Extend ChatSession to support tool execution:
trait ToolEnabledChatSession extends ChatSession:
  def withToolExecutor(executor: ToolExecutor): ToolEnabledChatSession
  def executeToolCalls(toolCalls: Seq[ToolCallRequest]): Rx[Seq[ToolResultMessage]]
  1. MCP Tool Adapter to convert between formats:
object MCPToolAdapter:
  def fromMCPTool(mcpTool: MCPTool): ToolSpec
  def toMCPArguments(args: Map[String, Any]): Json
  def fromMCPResult(result: MCPToolResult): ToolResultMessage
  1. Configuration for MCP servers with Airframe retry:
import wvlet.airframe.control.Retry.withJitter

case class MCPConfig(
  servers: Seq[MCPServerConfig],
  timeout: Duration = 30.seconds,
  retryPolicy: RetryContext = withJitter() // Use Airframe's jittering retry
)

case class MCPServerConfig(
  name: String,
  transport: MCPTransport, // stdio, websocket, http
  command: Option[String], // For stdio transport
  url: Option[String]      // For websocket/http transport
)

Implementation Plan

Phase 1: Tool Execution Layer (Foundation)

  • Create ToolExecutor trait using Rx[T] for async operations
  • Implement basic LocalToolExecutor for testing
  • Extend ChatSession to support tool execution
  • Add tool execution to chat flow

Phase 2: MCP Client Implementation

  • Implement MCP JSON-RPC protocol handling
  • Create MCPToolExecutor with stdio transport support
  • Implement tool discovery from MCP servers
  • Add WebSocket transport support
  • Use airframe-control's retry with jitter for resilience

Phase 3: Tool Adaptation

  • Create adapters between MCP tools and ToolSpec
  • Handle parameter type conversions
  • Support structured tool results
  • Add output schema validation

Phase 4: Integration & Testing

  • Create MCP-enabled agent builder
  • Add integration tests with sample MCP servers
  • Document MCP usage and configuration
  • Create example MCP tool implementations

Example Usage

import wvlet.airframe.control.Retry.withJitter

// Configure MCP servers
val mcpConfig = MCPConfig(
  servers = Seq(
    MCPServerConfig("filesystem", MCPTransport.Stdio, Some("mcp-server-filesystem")),
    MCPServerConfig("slack", MCPTransport.WebSocket, Some("ws://localhost:8080/mcp"))
  ),
  retryPolicy = withJitter(maxRetry = 3)
)

// Create MCP-enabled agent
val agent = LLMAgent("assistant")
  .withModel(LLM.Bedrock.Claude3Sonnet)
  .withMCPTools(mcpConfig) // Discovers and registers MCP tools

// Tools are automatically executed during chat
val session = agent.newChatSession
val response = session.chat("List all files in the current directory")
// Agent discovers filesystem tool, calls it via MCP, and returns results

Benefits

  1. Interoperability: Use any MCP-compatible tool without custom integration
  2. Standardization: Consistent tool calling across different AI systems
  3. Extensibility: Easy to add new tools via MCP servers
  4. Security: Built-in human-in-the-loop approval for tool invocations
  5. Discovery: Dynamic tool discovery at runtime
  6. Reactive: Uses Airframe's Rx for composable async operations
  7. Resilient: Built-in retry with jitter for fault tolerance

Technical Notes

  • Use wvlet.airframe.rx.Rx instead of Scala Future for consistency with Airframe ecosystem
  • Use wvlet.airframe.control.Retry.withJitter for retry logic instead of custom exponential backoff
  • Leverage existing Airframe components for HTTP, JSON handling, and control flow

References

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions