Spring Boot starter providing auto-configuration for Model Context Protocol (MCP) client with Spring WebFlux, enabling reactive AI applications to connect to MCP servers via SSE and Streamable HTTP transports
Auto-configuration for standard input/output (stdio) based MCP client transport in the Model Context Protocol (MCP).
Note: Stdio transport is part of the common MCP client autoconfiguration, not WebFlux-specific. This functionality is available in all MCP client starters (WebFlux, WebMVC, etc.) through the shared common autoconfiguration dependency.
The stdio transport auto-configuration provides communication with local MCP servers through standard input/output streams. This is ideal for running MCP servers as local processes that communicate via stdin/stdout, such as command-line tools, scripts, or local executables.
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Auto-configuration for stdio-based client transport.
* Part of common MCP client autoconfiguration - available in all starters.
* Creates transports that communicate with local processes via stdin/stdout.
* Thread-safe - process spawning is synchronized.
*/
@org.springframework.boot.autoconfigure.AutoConfiguration
@org.springframework.boot.autoconfigure.condition.ConditionalOnClass(io.modelcontextprotocol.spec.ServerParameters.class)
@org.springframework.boot.context.properties.EnableConfigurationProperties({
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties.class,
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties.class
})
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
public class StdioTransportAutoConfiguration {
}This auto-configuration is conditionally enabled when:
io.modelcontextprotocol.spec.ServerParameters is on the classpathspring.ai.mcp.client.enabled is true (default) or not specifiedIf these conditions are not met, the auto-configuration is skipped.
Maven:
<dependency>
<groupId>io.modelcontextprotocol</groupId>
<artifactId>mcp-core</artifactId>
</dependency>Gradle:
implementation 'io.modelcontextprotocol:mcp-core'package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Creates a list of stdio-based transports for MCP communication.
*
* Each transport is configured with:
* - Command to execute (e.g., "node", "python", "/path/to/executable")
* - Command arguments (list of strings)
* - Environment variables (optional)
*
* Process lifecycle:
* 1. Process spawned when transport is created
* 2. Communication via process stdin/stdout
* 3. Process terminated when Spring context closes
*
* Thread-safe - process spawning is synchronized.
* Blocking - process I/O is blocking.
*
* @param stdioProperties Stdio client properties containing server configurations
* @return List of named MCP transports, one for each configured stdio connection (never null, may be empty)
* @throws IllegalArgumentException if command configuration is invalid
* @throws IOException if process fails to start
*/
@org.springframework.context.annotation.Bean
public java.util.List<NamedClientMcpTransport> stdioTransports(
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties stdioProperties
);Return Type: List<NamedClientMcpTransport>
Configure stdio connections in application.yml:
spring.ai.mcp.client.stdio:
connections:
local-server:
command: node
args:
- /path/to/mcp-server.js
python-server:
command: python
args:
- /path/to/mcp_server.py
- --debug
env:
PYTHONPATH: /custom/path
DEBUG: "true"Properties format:
spring.ai.mcp.client.stdio.connections.local-server.command=node
spring.ai.mcp.client.stdio.connections.local-server.args[0]=/path/to/mcp-server.js
spring.ai.mcp.client.stdio.connections.python-server.command=python
spring.ai.mcp.client.stdio.connections.python-server.args[0]=/path/to/mcp_server.py
spring.ai.mcp.client.stdio.connections.python-server.args[1]=--debug
spring.ai.mcp.client.stdio.connections.python-server.env.PYTHONPATH=/custom/path
spring.ai.mcp.client.stdio.connections.python-server.env.DEBUG=trueYou can also configure stdio servers from an external JSON file:
spring.ai.mcp.client.stdio:
servers-configuration: classpath:mcp-servers.jsonThe JSON file format (Claude Desktop format compatible):
{
"mcpServers": {
"local-server": {
"command": "node",
"args": ["/path/to/mcp-server.js"]
},
"python-server": {
"command": "python",
"args": ["/path/to/mcp_server.py", "--debug"],
"env": {
"PYTHONPATH": "/custom/path",
"DEBUG": "true"
}
}
}
}See Configuration Properties for complete stdio configuration options.
import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Service demonstrating stdio transport usage.
* Typically you don't use transports directly - inject McpSyncClient or McpAsyncClient instead.
* This example shows how to access transports for advanced scenarios.
*/
@Service
public class McpStdioService {
private final List<NamedClientMcpTransport> stdioTransports;
/**
* Constructor injection of stdio transports.
* If no stdio connections configured, list will be empty.
* Processes are already spawned and ready for communication.
*
* @param stdioTransports Auto-configured stdio transports
*/
public McpStdioService(List<NamedClientMcpTransport> stdioTransports) {
this.stdioTransports = stdioTransports;
}
/**
* Find specific stdio transport by name.
* Throws if not found - check existence first in production.
*
* @throws IllegalStateException if transport not found
*/
public void connectToLocalServer() {
// Find the local-server transport
NamedClientMcpTransport localTransport = stdioTransports.stream()
.filter(t -> "local-server".equals(t.name()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("local-server transport not found"));
// Use the transport (typically via an MCP client)
io.modelcontextprotocol.spec.McpClientTransport transport = localTransport.transport();
// ... connect and use transport
}
/**
* List all configured stdio connections.
* Useful for debugging and monitoring.
*/
public void listAllStdioConnections() {
stdioTransports.forEach(t ->
System.out.println("Stdio Transport: " + t.name())
);
}
}If your application uses multiple transport types, you can filter for stdio transports:
import io.modelcontextprotocol.client.transport.StdioClientTransport;
/**
* Filter transports by type.
* Useful when multiple transport types are configured.
*
* @param allTransports All configured transports (stdio, SSE, streamable HTTP)
*/
public void findStdioTransports(List<NamedClientMcpTransport> allTransports) {
List<NamedClientMcpTransport> stdioTransports = allTransports.stream()
.filter(t -> t.transport() instanceof StdioClientTransport)
.toList();
// Use only stdio transports
stdioTransports.forEach(t -> {
System.out.println("Stdio: " + t.name());
});
}You can configure stdio, SSE, and Streamable HTTP transports in the same application:
spring.ai.mcp.client:
# Stdio connections for local servers
stdio:
connections:
local-node-server:
command: node
args:
- ./servers/mcp-server.js
local-python-server:
command: python
args:
- ./servers/mcp_server.py
# SSE connections for remote servers
sse:
connections:
remote-notifications:
url: http://localhost:8080
# Streamable HTTP connections
streamable-http:
connections:
remote-api:
url: http://localhost:9000Stdio transport is ideal for running local tool servers that provide specialized capabilities:
spring.ai.mcp.client.stdio:
connections:
filesystem-tools:
command: npx
args:
- -y
- "@modelcontextprotocol/server-filesystem"
- /home/user/documents
env:
NODE_ENV: production
database-tools:
command: python
args:
- ./tools/database-mcp-server.py
env:
DATABASE_URL: postgresql://localhost:5432/mydb
LOG_LEVEL: INFOUse stdio for local development servers:
spring.ai.mcp.client.stdio:
connections:
dev-server:
command: npm
args:
- run
- mcp-server:dev
env:
NODE_ENV: development
DEBUG: "mcp:*"
PORT: "3000"Configure different commands based on the operating system:
# application-linux.yml
spring.ai.mcp.client.stdio:
connections:
shell-tools:
command: /bin/bash
args:
- ./scripts/mcp-server.sh
# application-windows.yml
spring.ai.mcp.client.stdio:
connections:
shell-tools:
command: cmd.exe
args:
- /c
- .\\scripts\\mcp-server.bat
# application-mac.yml
spring.ai.mcp.client.stdio:
connections:
shell-tools:
command: /bin/zsh
args:
- ./scripts/mcp-server.shInternally, the auto-configuration creates stdio transports using the MCP SDK:
// Conceptual example (not for direct use) - shows internal process
package org.springframework.ai.mcp.client.common.autoconfigure;
// For each configured stdio connection:
for (Map.Entry<String, Parameters> entry : connections.entrySet()) {
String connectionName = entry.getKey();
Parameters params = entry.getValue();
// Convert to ServerParameters
io.modelcontextprotocol.spec.ServerParameters serverParams =
new io.modelcontextprotocol.spec.ServerParameters(
params.command(), // Command to execute
params.args(), // Command arguments (may be null)
params.env() // Environment variables (may be null)
);
// Create stdio transport
var transport = io.modelcontextprotocol.client.transport.StdioClientTransport.builder()
.serverParameters(serverParams)
.build();
// Wrap in named transport
transports.add(new NamedClientMcpTransport(connectionName, transport));
}Each configured connection gets its own transport instance with an independent process.
While not directly supported in properties, you can customize the working directory through environment variables or shell wrappers:
spring.ai.mcp.client.stdio:
connections:
custom-dir-server:
command: sh
args:
- -c
- cd /custom/dir && node mcp-server.jsStdio transports automatically manage the lifecycle of spawned processes:
Set resource limits through environment variables or command arguments:
spring.ai.mcp.client.stdio:
connections:
limited-server:
command: node
args:
- --max-old-space-size=512 # Limit Node.js heap to 512MB
- mcp-server.js
env:
NODE_OPTIONS: --max-old-space-size=512For Python:
spring.ai.mcp.client.stdio:
connections:
python-limited:
command: python
args:
- -X
- dev # Enable Python development mode
- mcp_server.py
env:
PYTHONMALLOC: debug # Enable memory debuggingIf no stdio transports are created, check:
spring.ai.mcp.client.stdio.connections is properly configuredspring.ai.mcp.client.enabled=true (or not set, as true is default)logging.level.org.springframework.ai.mcp=DEBUGIf the stdio process fails to start:
/usr/bin/node instead of nodechmod +x /path/to/scriptnode /path/to/mcp-server.jsIf communication with the stdio server fails:
process.stdout.write() auto-flushessys.stdout.flush() after each message\n (LF) for line endings, not \r\n (CRLF)If processes don't terminate cleanly:
ApplicationContext.close()If you see permission errors:
| Feature | Stdio Transport | HTTP Transports (SSE/Streamable) |
|---|---|---|
| Location | Local process | Remote server |
| Communication | stdin/stdout | HTTP |
| Setup | No server setup needed | Requires running server |
| Security | Process isolation | Network security (TLS, auth) |
| Deployment | Command execution | URL configuration |
| Use Case | Local tools, dev environments | Production, distributed systems |
| Scalability | One process per connection | Shared server, multiple clients |
| Firewall | No network concerns | May require firewall rules |
| Latency | Very low (~1-5ms) | Higher (network latency) |
| Resource Usage | One process per connection | Shared server resources |
| Debugging | Process logs to stderr | Network traffic inspection |
Choose stdio for:
Choose HTTP transports for:
#!/usr/bin/env node
// Simple Node.js MCP server for stdio transport
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
// Read JSON-RPC messages from stdin
rl.on('line', (line) => {
try {
const message = JSON.parse(line);
// Handle MCP initialize request
if (message.method === 'initialize') {
const response = {
jsonrpc: '2.0',
id: message.id,
result: {
protocolVersion: '2024-11-05',
capabilities: {
tools: {},
resources: {},
prompts: {}
},
serverInfo: {
name: 'example-server',
version: '1.0.0'
}
}
};
// Write response to stdout (auto-flushes)
console.log(JSON.stringify(response));
}
// Handle other MCP methods...
} catch (error) {
// Log errors to stderr (not stdout!)
console.error('Error:', error);
}
});
// Log startup to stderr
console.error('MCP Server started');#!/usr/bin/env python3
import sys
import json
def handle_message(message):
"""Handle incoming MCP message."""
if message.get('method') == 'initialize':
response = {
'jsonrpc': '2.0',
'id': message['id'],
'result': {
'protocolVersion': '2024-11-05',
'capabilities': {
'tools': {},
'resources': {},
'prompts': {}
},
'serverInfo': {
'name': 'example-python-server',
'version': '1.0.0'
}
}
}
# Write response to stdout
print(json.dumps(response), flush=True) # IMPORTANT: flush=True
def main():
# Log startup to stderr (not stdout!)
print('MCP Server started', file=sys.stderr)
# Read JSON-RPC messages from stdin
for line in sys.stdin:
try:
message = json.loads(line)
handle_message(message)
except Exception as error:
# Log errors to stderr
print(f'Error: {error}', file=sys.stderr)
if __name__ == '__main__':
main()tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux@1.1.0