First Task: Integrating MCP into LLM4S       PR#70PR#102

My first task in GSoC has been to integrate MCP (Model Context Protocol) into LLM4S. It's funny how this task helped me understand how the project is wired, but at the same time, the project's structure helped me understand MCP's design and purpose.
I had used agents before, and it was as easy as giving a prompt. But actually diving into the agentic loop itself? That was pretty cool.

Understanding the Agent

The way I see it, it all comes down to a loop that contains some states and passes messages and tools to an LLM.
The list of messages contains the system prompt, the user prompt and the LLM's response.
This back-and-forth loop is exactly what enables the model's reflection: each new LLM call sees its own previous outputs in context.

Along with that list of messages that gets populated during agent execution, we also give a list of available tools to the LLM. This is what lets the LLM access tools and evaluate whether it needs to call them, based on the user prompt. Just like for the messages, the tool's output is given back to the LLM.

This basic Agent implementation fits in about 300 lines of Scala, and Scala's powerful pattern matching makes it look pretty elegant.

Understanding Where MCP Fits

The Model Context Protocol (MCP) is an open, language-agnostic standard by Anthropic that lets LLM-based applications dynamically discover, invoke, and integrate external tools and data sources through a unified JSON-RPC interface.

Can be an obscure definition when you're a junior dev and it did take LLM4S's specific usecase for me to get it.

It's only after getting familiar with the agentic loop and the tool API that I understood how powerful MCP was. The MCP connector allows our agent to dynamically discover and connect to any external MCP server, which means our LLM can basically call any tool.
BASICALLY:
Instead of getting familiar with every single external tool's API and hard-coding each tool's interface locally, MCP's standardized protocol handles schema discovery, versioning and invocation.
Pretty freaking cool.

Check out Hugging Face's MCP course to learn more.

My Implementation Approach

Challenge: Integration Without Breaking Existing Architecture

MCP had to be integrated on top of the existing ToolAPI, so that both local and external tools are given to the agent in the same way.

Solution: I created an MCPToolRegistry extending the existing ToolRegistry. This approach allowed me to make no changes to existing agent architecture AND ensure MCP tools were indistinguishable from local tools from the LLM's perspective.

Priority System

The registry implements a priority system where local tools override MCP tools with identical names, preventing conflicts while maintaining predictable behavior.

Keeping Up with the Spec

The second PR of this task was about updating MCP to the latest spec. (Yes - it seems like there's a new spec each month… 😅)

File Breakdown

Core MCP Implementation

1. MCPToolRegistry.scala

  • Purpose: The cornerstone integration that extends ToolRegistry to seamlessly blend local and MCP tools
  • Impact: Enables transparent MCP tool access without any changes to existing agent code - local tools take precedence over MCP tools with the same name

2. MCPClientImpl.scala

  • Purpose: Core MCP client implementing the full JSON-RPC 2.0 protocol with automatic transport detection
  • Impact: Handles protocol handshake, tool discovery, execution delegation, and transport fallback from 2025-06-18 Streamable HTTP to legacy 2024-11-05 SSE

3. MCPTransport.scala

  • Purpose: Transport abstraction supporting stdio, SSE, and Streamable HTTP with session management
  • Impact: Provides protocol flexibility and future-proofing, with automatic detection and graceful fallback between transport versions

Protocol and Configuration

4. MCPTypes.scala

  • Purpose: Complete type-safe modeling of JSON-RPC 2.0 and MCP protocol structures with upickle serialization
  • Impact: Ensures protocol compliance and provides structured error handling with proper error codes

5. MCPServerConfig.scala

  • Purpose: Fluent configuration API for different MCP server types and transports
  • Impact: Simplifies server setup with factory methods for stdio, SSE, and Streamable HTTP configurations

Demonstration and Examples

6. DemonstrationMCPServer.scala

  • Purpose: Production-ready MCP server implementing the 2025-06-18 Streamable HTTP specification
  • Impact: Provides a working reference implementation with session management, structured tool output, and protocol version negotiation

7. MCPAgentExample.scala

  • Purpose: Complete agent example demonstrating local + MCP tool integration with automatic fallback
  • Impact: Shows real-world usage patterns and demonstrates the seamless integration achieved

Some thoughts

In the first version of my proposal back in March, I had not even mentioned MCP. It was pretty new and I did not understand what it could mean for LLM4S. It's only a few days before the deadline that my mentor advised me to mention it. - It turned out to be my very first task.

Between my first and my second MCP PR, a new MCP specification came out.

The speed at which AI development evolves is pretty astonishing, and keeping up is challenging. 🐸