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>
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);
}
}
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
-parameterscompiler 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();
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();
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"
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();
}
}
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)
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:
McpClientProvideris a singleton. Don't create one per request. Build it once, inject it, reuse it. Theclose()/reopen()methods exist for lifecycle management, not for per-call patterns.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.The MCP spec is still evolving. Solon's implementation targets the
2025-03-26spec 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)