DEV Community

Solon Framework
Solon Framework

Posted on

Solon MCP: From Zero to AI Tool Service in Two Annotations

I've been writing a lot about Solon's AI stack over the past few weeks — ChatModel, RAG, Agents, Harness. But there's one piece I kept circling back to because it fundamentally changes how I think about AI tool integration: MCP (Model Context Protocol).

Not because it's new. Because Solon's implementation is so minimal that it feels like you're just writing a regular web endpoint. And then you realize it's being consumed by a language model, not a browser.

Here's the walkthrough.


One Dependency

<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-ai-mcp</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

That's it. Twenty-three kilobytes.


Server: A Two-Annotation Service

@McpServerEndpoint(channel = McpChannel.STREAMABLE, mcpEndpoint = "/mcp")
public class McpWeatherTool {

    @ToolMapping(description = "Get weather forecast for a location")
    public String getWeather(@Param(description = "City name or location") String location) {
        return "Sunny, 14°C";
    }
}

public class App {
    public static void main(String[] args) {
        Solon.start(App.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

Start it. You now have an MCP service listening on /mcp. A language model can discover it, discover the tool, and call it — all through the standard MCP protocol.

Compare this to what you'd need if you were writing the MCP lifecycle by hand. No handshake boilerplate. No capabilities negotiation. No initialization sequence. Just an endpoint annotation and a method annotation.

Enable the -parameters compiler flag so parameter names are preserved. Otherwise, you'll need to spell them out in @Param(name = "...").


Client: Consume Remote Tools Like Local Java

McpClientProvider client = McpClientProvider.builder()
        .channel(McpChannel.STREAMABLE)
        .url("http://localhost:8080/mcp")
        .build();

String result = client.callTool("getWeather", Map.of("location", "Hangzhou"))
        .getContent();
Enter fullscreen mode Exit fullscreen mode

McpClientProvider implements ToolProvider, ResourceProvider, and PromptProvider — so it integrates naturally wherever Solon AI expects a tool source.


Four Transport Channels, One Mental Model

Solon MCP supports all four MCP transport channels:

Channel Server Client Stateful Reverse Comms
STDIO Yes Yes
SSE Yes Yes (deprecated by MCP spec)
STREAMABLE Yes Yes
STREAMABLE_STATELESS ✅ (v3.8+) STREAMABLE No No

The interesting one is STREAMABLE_STATELESS. It drops the long-lived connection and reverse communication (no tool change notifications, no sampling callbacks from server to client). In exchange, you get load-balancer-friendly server clusters — no sticky sessions needed. For the ~80% of use cases that don't need reverse communication, it's the right default.

// Stateless server
@McpServerEndpoint(channel = McpChannel.STREAMABLE_STATELESS, mcpEndpoint = "/mcp")
public class McpWeatherTool { ... }

// Stateless-compatible client (always uses STREAMABLE)
McpClientProvider client = McpClientProvider.builder()
        .channel(McpChannel.STREAMABLE)
        .url("http://localhost:8080/mcp")
        .build();
Enter fullscreen mode Exit fullscreen mode

Wiring MCP Tools Into a ChatModel

This is the part I found most practical. You configure an MCP client and a ChatModel side by side in YAML, inject both, and wire them together in one bean method.

app.yml:

solon.ai:
  chat:
    assistant:
      apiUrl: "http://127.0.0.1:11434/api/chat"
      model: "qwen2.5:1.5b"
  mcp:
    client:
      weather:
        channel: "streamable"
        url: "http://localhost:8080/mcp"
Enter fullscreen mode Exit fullscreen mode

Config class:

@Configuration
public class AiConfig {

    @Bean
    public McpClientProvider mcpClient(
            @Inject("${solon.ai.mcp.client.weather}") McpClientProvider client) {
        return client;
    }

    @Bean
    public ChatModel chatModel(
            @Inject("${solon.ai.chat.assistant}") ChatConfig chatConfig,
            McpClientProvider toolProvider) {

        return ChatModel.of(chatConfig)
                .defaultToolAdd(toolProvider)
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now every prompt you send through chatModel automatically has access to the weather tool. The model decides when to call it. You don't touch the tool lifecycle at all.

This might sound obvious if you've used MCP before. But the key difference is the configuration-driven wiring: you can swap MCP providers by changing YAML values, and the rest of your application code doesn't move.


Client Configuration Surface

McpClientProvider has a few knobs worth knowing about:

Property Default Why You'd Change It
cacheSeconds 30 Tool/resource/prompt description caching
heartbeatInterval 15s null or 0s to disable
timeout 30s Catch-all for MCP operation timeout
requestTimeout MCP request wait timeout
initializationTimeout MCP handshake timeout

The provider is designed as a singleton — it maintains a long-lived connection internally. Call close() when you're done, or reopen() if you need to reconnect.


The McpProxy

There's a small but useful utility: McpProxy. It converts between transport channels — think of it as a protocol bridge for MCP.

// Expose a stdio-based MCP tool as an SSE/streamable endpoint
// (pseudocode — the proxy is built into the solon-ai-mcp module)
Enter fullscreen mode Exit fullscreen mode

This is handy when you have local CLI-based MCP tools (stdio) that you want to expose as network services for remote AI applications. I haven't needed it yet, but I can see it being the difference between "this tool works only on my machine" and "this tool works from anywhere."


Honest Limitations

A few things I hit during exploration:

  1. McpClientProvider is a singleton. Don't create one per request. Build it once, inject it, reuse it. The close()/reopen() methods exist for lifecycle management, not for per-call patterns.

  2. STREAMABLE_STATELESS means no server → client calls. If your use case requires tool change notifications or LLM sampling callbacks from the server side, stick with stateful STREAMABLE.

  3. The MCP spec is still evolving. Solon's implementation targets the 2025-03-26 spec revision. If the upstream spec changes significantly, check the v4.0.0 migration notes for compatibility guidance.


Bottom Line

Solon's MCP support is thin on ceremony and thick on integration. Two annotations to publish a tool service. One builder to consume it. A YAML key to wire it into a ChatModel. The McpProxy handles protocol bridging when you need it.

If you're already running Solon AI applications, adding MCP is a natural extension of the same patterns. If you're not — this is the kind of "it just works" that makes trying it worthwhile.


Tags: java, mcp, ai, backend

Top comments (0)