feat: Add MCP server with Server stdio/sse transport improve MCP client

- Implement MCP server with async/sync support and builder pattern configuration
 - Add Server stdio/SSE transport layers with robust error handling
 - Convert McpClient and McpServer to interfaces for better extensibility
 - Add sampling capabilities for LLM interactions (text/image)
 - Implement dynamic root management and runtime tool management
 - Add comprehensive prompt and resource capabilities
 - Add support for dynamic root management (add/remove roots at runtime)

 Architecture Changes

 - Introduce registration records for tools, resources, and prompts
 - Improve thread safety with concurrent collections
 - Enhance resource template handling
 - Implement builder patterns for Server/Client capabilities
 - Add support for server capabilities and implementation info

Transport Layer:
 - Implement SseServerTransport with SSE endpoints
 - Add StdioServerTransport for process-based communication
 - Reorganize transport packages for better organization
 - Improve transport error handling and shutdown processes

Documentation:

 - Add detailed MCP reference documentation
 - Include architecture diagrams and API documentation
 - Add comprehensive test coverage for all components
 - Include sample projects demonstrating stdio/SSE transports

BREAKING CHANGES:

 Transport Package Reorganization

 - Move StdioClientTransport to o.s.ai.mcp.client.transport
 - Move SseClientTransport to o.s.ai.mcp.client.transport
 - Move ServerParameters to o.s.ai.mcp.client.transport
 - StdioClientTransport.errorHandler changed to private with
   setter method Migration: Use setErrorHandler() method
   instead of direct field access

 API Changes

 - Replace static factory methods with builder pattern
 - Update ServerCapabilities to use builder pattern
 - Make StdioClientTransport.errorHandler private with setter
 - Replace rootsListProvider with direct root management
 - Remove deprecated static factory methods

Resolves #27,#25,#24,#11
Related to #12
This commit is contained in:
Christian Tzolov 2024-12-19 19:38:38 +01:00
commit abd0facf4e
6 changed files with 5727 additions and 471 deletions

View file

@ -0,0 +1,33 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.sample.client;
import org.springframework.ai.mcp.client.transport.SseClientTransport;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Christian Tzolov
*/
public class ClientSse {
public static void main(String[] args) {
var transport = new SseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
new SampleClient(transport).run();
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.sample.client;
import org.springframework.ai.mcp.client.transport.ServerParameters;
import org.springframework.ai.mcp.client.transport.StdioClientTransport;
/**
* @author Christian Tzolov
*/
public class ClientStdio {
public static void main(String[] args) {
var stdioParams = ServerParameters.builder("java")
.args("-Dtransport.mode=stdio", "-jar",
"spring-ai-mcp-sample/target/spring-ai-mcp-sample-0.3.0-SNAPSHOT.jar")
.build();
var transport = new StdioClientTransport(stdioParams);
new SampleClient(transport).run();
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.sample.client;
import java.util.Map;
import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest;
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest;
import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult;
import org.springframework.ai.mcp.spec.McpSchema.ListToolsResult;
import org.springframework.ai.mcp.spec.McpSchema.ReadResourceRequest;
import org.springframework.ai.mcp.spec.McpTransport;
/**
* @author Christian Tzolov
*/
public class SampleClient {
private final McpTransport transport;
public SampleClient(McpTransport transport) {
this.transport = transport;
}
public void run() {
var client = McpClient.using(this.transport).sync();
client.initialize();
client.ping();
// List and demonstrate tools
ListToolsResult toolsList = client.listTools();
System.out.println("Available Tools = " + toolsList);
CallToolResult weatherResponse = client.callTool(new CallToolRequest("weather", Map.of("city", "Sofia")));
System.out.println("Weather Response = " + weatherResponse);
CallToolResult calcResponse = client
.callTool(new CallToolRequest("calculator", Map.of("operation", "multiply", "a", 2.0, "b", 3.0)));
System.out.println("Calculator Response = " + calcResponse);
// List and demonstrate resources
var resourcesList = client.listResources();
System.out.println("\nAvailable Resources = " + resourcesList);
// Read the system info resource
var systemInfo = client.readResource(new ReadResourceRequest("system://info"));
System.out.println("System Info = " + systemInfo);
// List and demonstrate prompts
ListPromptsResult promptsList = client.listPrompts();
System.out.println("\nAvailable Prompts = " + promptsList);
// Try the greeting prompt
var greetingResponse = client.getPrompt(new GetPromptRequest("greeting", Map.of("name", "Spring")));
System.out.println("Greeting Response = " + greetingResponse);
client.closeGracefully();
}
}

View file

@ -0,0 +1,13 @@
package org.springframework.ai.mcp.sample.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}

View file

@ -0,0 +1,187 @@
package org.springframework.ai.mcp.sample.server;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.server.McpAsyncServer;
import org.springframework.ai.mcp.server.McpServer;
import org.springframework.ai.mcp.server.McpServer.PromptRegistration;
import org.springframework.ai.mcp.server.McpServer.ResourceRegistration;
import org.springframework.ai.mcp.server.McpServer.ToolRegistration;
import org.springframework.ai.mcp.server.transport.SseServerTransport;
import org.springframework.ai.mcp.server.transport.StdioServerTransport;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult;
import org.springframework.ai.mcp.spec.McpSchema.PromptMessage;
import org.springframework.ai.mcp.spec.McpSchema.Role;
import org.springframework.ai.mcp.spec.McpSchema.TextContent;
import org.springframework.ai.mcp.spec.McpTransport;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
@Configuration
public class McpServerConfig {
@Bean
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio")
public StdioServerTransport stdioServerTransport() {
return new StdioServerTransport();
}
@Bean
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
public SseServerTransport sseServerTransport() {
return new SseServerTransport(new ObjectMapper(), "/mcp/message");
}
@Bean
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
public RouterFunction<?> mcpRouterFunction(SseServerTransport transport) {
return transport.getRouterFunction();
}
@Bean
public McpAsyncServer mcpServer(McpTransport transport) { // @formatter:off
// Configure server capabilities with resource support
var capabilities = McpSchema.ServerCapabilities.builder()
.resources(false, true) // No subscribe support, but list changes notifications
.tools(true) // Tool support with list changes notifications
.prompts(true) // Prompt support with list changes notifications
.build();
// Create the server with both tool and resource capabilities
return McpServer.using(transport)
.info("MCP Demo Server", "1.0.0")
.capabilities(capabilities)
.tools(List.of(weatherTool(), calculatorTool()))
.resources(Map.of("system://info", systemInfoResource()))
.prompts(List.of(greetingPrompt()))
.async();
} // @formatter:on
private static ResourceRegistration systemInfoResource() {
// Create a resource registration for system information
var systemInfoResource = new McpSchema.Resource( // @formatter:off
"system://info",
"System Information",
"Provides basic system information including Java version, OS, etc.",
"application/json", null
);
var resourceRegistration = new ResourceRegistration(systemInfoResource, (request) -> {
try {
var systemInfo = Map.of(
"javaVersion", System.getProperty("java.version"),
"osName", System.getProperty("os.name"),
"osVersion", System.getProperty("os.version"),
"osArch", System.getProperty("os.arch"),
"processors", Runtime.getRuntime().availableProcessors(),
"timestamp", System.currentTimeMillis());
String jsonContent = new ObjectMapper().writeValueAsString(systemInfo);
return new McpSchema.ReadResourceResult(
List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent)));
}
catch (Exception e) {
throw new RuntimeException("Failed to generate system info", e);
}
}); // @formatter:on
return resourceRegistration;
}
private static ToolRegistration weatherTool() {
return new ToolRegistration(
new McpSchema.Tool("weather", "Weather forecast tool by location", Map.of("city", "String")),
(arguments) -> {
String city = (String) arguments.get("city");
TextContent content = new TextContent("Weather forecast for " + city + " is sunny");
return new CallToolResult(List.of(content), false);
});
}
private static ToolRegistration calculatorTool() {
return new ToolRegistration(new McpSchema.Tool("calculator",
"Performs basic arithmetic operations (add, subtract, multiply, divide)", """
{
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The arithmetic operation to perform"
},
"a": {
"type": "number",
"description": "First operand"
},
"b": {
"type": "number",
"description": "Second operand"
}
},
"required": ["operation", "a", "b"]
}
"""), arguments -> {
String operation = (String) arguments.get("operation");
double a = (Double) arguments.get("a");
double b = (Double) arguments.get("b");
double result;
switch (operation) {
case "add":
result = a + b;
break;
case "subtract":
result = a - b;
break;
case "multiply":
result = a * b;
break;
case "divide":
if (b == 0) {
return new McpSchema.CallToolResult(
java.util.List.of(new McpSchema.TextContent("Division by zero")), true);
}
result = a / b;
break;
default:
return new McpSchema.CallToolResult(
java.util.List.of(new McpSchema.TextContent("Unknown operation: " + operation)),
true);
}
return new McpSchema.CallToolResult(
java.util.List.of(new McpSchema.TextContent(String.valueOf(result))), false);
});
}
private static PromptRegistration greetingPrompt() {
var nameArg = new McpSchema.PromptArgument("name", "The name to greet", true // required
// parameter
);
var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt", List.of(nameArg));
return new PromptRegistration(prompt, request -> {
String name = (String) request.arguments().get("name");
if (name == null) {
name = "friend";
}
var assistantMessage = new PromptMessage(Role.ASSISTANT,
new TextContent("Hello " + name + "! How can I assist you today?"));
return new GetPromptResult("A personalized greeting message", List.of(assistantMessage));
});
}
}

View file

@ -0,0 +1,16 @@
# Server port
# server.port=8080
# Logging
# logging.level.org.springframework.ai.mcp=DEBUG
# logging.level.org.springframework.web=INFO
spring.main.banner-mode=off
# logging.level.root=OFF
# spring.main.web-application-type=none
# logging.pattern.console=
logging.level.org.springframework.ai.mcp.*=OFF
logging.level.org.springframework.ai.mcp.spec.*=OFF
logging.level.org.springframework.**=OFF
transport.mode=sse