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:
parent
c83a45f880
commit
abd0facf4e
6 changed files with 5727 additions and 471 deletions
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Reference in a new issue