refactor: update MCP API usage to version 0.8.0
- Remove all book-library MCP examples (servlet, webflux, and webmvc implementations) - Update weather example to use MCP version 0.8.0-SNAPSHOT - Refactor transport handling to use transport providers instead of direct transport objects - Update method calls from toSyncToolRegistration to toSyncToolSpecifications - Add central-portal-snapshots repository to pom.xml for dependency resolution - Align with the new MCP client class names Add MCP Sampling capability with weather example Adds MCP Sampling implementation that demonstrates how to delegate LLM requests to multiple providers. - add a weather server that retrieves data and uses MCP Sampling to generate creative content - add a client that routes requests to different LLM providers (OpenAI and Anthropic) based on model hints - add README documentation explaining the MCP Sampling workflow and implementation details The MCP Sampling capability enables applications to leverage multiple LLM providers within a single workflow, allowing for creative content generation, model comparison, and specialized task delegation. refactor: migrate to spring-ai-mcp-client-spring-boot-starter - Replace spring-ai-mcp dependency with spring-ai-mcp-client-spring-boot-starter - Update import statements from org.springframework.ai.mcp.* to io.modelcontextprotocol.client.* - Replace McpFunctionCallback with SyncMcpToolCallbackProvider - Update Spring AI version from 1.0.0-M5 to 1.0.0-SNAPSHOT in multiple projects - Enable tool callback auto-configuration with spring.ai.mcp.client.toolcallback.enabled refactor: update Spring AI artifact IDs to new naming convention - Update all Spring AI dependencies to use the new naming convention: spring-ai-*-spring-boot-starter → spring-ai-starter-* spring-ai-openai-spring-boot-starter → spring-ai-starter-model-openai spring-ai-mcp-client-spring-boot-starter → spring-ai-starter-mcp-client - And similar patterns for other artifacts - Enable debug mode in brave module's application.properties Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
committed by
Mark Pollack
parent
1ca981eba5
commit
41c921bc33
4
mcp.weather.log
Normal file
4
mcp.weather.log
Normal file
@@ -0,0 +1,4 @@
|
||||
2025-03-17T21:20:53.178+01:00 INFO 52952 --- [main] o.s.a.m.s.server.McpServerApplication : Starting McpServerApplication using Java 17.0.12 with PID 52952 (/Users/christiantzolov/Dev/projects/spring-ai-examples/model-context-protocol/weather/manual-webflux-server/target/classes started by christiantzolov in /Users/christiantzolov/Dev/projects/spring-ai-examples)
|
||||
2025-03-17T21:20:53.179+01:00 INFO 52952 --- [main] o.s.a.m.s.server.McpServerApplication : No active profile set, falling back to 1 default profile: "default"
|
||||
2025-03-17T21:20:53.788+01:00 INFO 52952 --- [main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080 (http)
|
||||
2025-03-17T21:20:53.797+01:00 INFO 52952 --- [main] o.s.a.m.s.server.McpServerApplication : Started McpServerApplication in 0.782 seconds (process running for 0.909)
|
||||
@@ -1,222 +0,0 @@
|
||||
# Spring AI MCP Servlet SSE Server Sample Project
|
||||
|
||||
This sample project demonstrates the usage of the Spring AI Model Context Protocol (MCP) implementation with HttpServletSseServerTransport. It showcases how to create and use MCP servers with Servlet-based SSE transport and various capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample provides:
|
||||
- Two transport mode implementations:
|
||||
- HttpServletSseServerTransport for SSE (Server-Sent Events)
|
||||
- StdioServerTransport for standard I/O
|
||||
- Server capabilities:
|
||||
- Tools support with list changes notifications
|
||||
- Resources support with list changes notifications (no subscribe support)
|
||||
- Prompts support with list changes notifications
|
||||
- Sample implementations:
|
||||
- Two MCP tools: Weather and Calculator
|
||||
- One MCP resource: System Information
|
||||
- One MCP prompt: Greeting
|
||||
|
||||
## Building the Project
|
||||
|
||||
```bash
|
||||
./mvnw clean install -DskipTests
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
The server can be started in two transport modes, controlled by the `transport.mode` property:
|
||||
|
||||
### Stdio Mode (Default)
|
||||
|
||||
```bash
|
||||
java -Dtransport.mode=stdio -jar target/mcp-servlet-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
The Stdio mode server is automatically started by the client - no explicit server startup is needed.
|
||||
But you have to build the server jar first: `./mvnw clean install -DskipTests`.
|
||||
|
||||
In Stdio mode the server must not emit any messages/logs to the console (e.g. standard out) but the JSON messages produced by the server.
|
||||
|
||||
### SSE Mode with HttpServletSseServerTransport
|
||||
```bash
|
||||
java -Dtransport.mode=sse -jar target/mcp-servlet-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
The HttpServletSseServerTransport provides two endpoints:
|
||||
- SSE endpoint at `/sse` - For server-to-client events
|
||||
- Message endpoint at `/mcp/message` - For client-to-server messages
|
||||
|
||||
## Sample Clients
|
||||
|
||||
The project includes example clients for both transport modes:
|
||||
|
||||
### Stdio Client (ClientStdio.java)
|
||||
```java
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dtransport.mode=stdio", "-jar",
|
||||
"target/mcp-servlet-server-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
|
||||
var transport = new StdioClientTransport(stdioParams);
|
||||
var client = McpClient.using(transport).sync();
|
||||
```
|
||||
|
||||
### SSE Client (HttpClientSse.java)
|
||||
```java
|
||||
var transport = new HttpClientSseClientTransport("http://localhost:8080");
|
||||
var client = McpClient.using(transport).sync();
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Weather Tool
|
||||
- Name: `weather`
|
||||
- Description: Weather forecast tool by location
|
||||
- Parameters:
|
||||
- `city`: String - The city to get weather for
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("weather", Map.of("city", "Sofia"))
|
||||
);
|
||||
```
|
||||
|
||||
### Calculator Tool
|
||||
- Name: `calculator`
|
||||
- Description: Performs basic arithmetic operations
|
||||
- Parameters (JSON Schema):
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("calculator",
|
||||
Map.of("operation", "multiply", "a", 2.0, "b", 3.0))
|
||||
);
|
||||
```
|
||||
|
||||
## Available Resources
|
||||
|
||||
### System Information Resource
|
||||
- URI: `system://info`
|
||||
- Description: Provides basic system information including Java version, OS, etc.
|
||||
- MIME Type: application/json
|
||||
- Returns: JSON object containing: javaVersion, osName, osVersion, osArch, processors, timestamp
|
||||
|
||||
## Available Prompts
|
||||
|
||||
### Greeting Prompt
|
||||
- Name: `greeting`
|
||||
- Description: A friendly greeting prompt
|
||||
- Parameters:
|
||||
- `name`: String (required) - The name to greet
|
||||
- Returns: A personalized greeting message from an assistant
|
||||
|
||||
## Client Usage Example
|
||||
|
||||
```java
|
||||
// Initialize client
|
||||
client.initialize();
|
||||
|
||||
// Test connection
|
||||
client.ping();
|
||||
|
||||
// List available tools
|
||||
ListToolsResult tools = client.listTools();
|
||||
System.out.println("Available tools: " + tools);
|
||||
|
||||
// Call weather tool
|
||||
CallToolResult weather = client.callTool(
|
||||
new CallToolRequest("weather", Map.of("city", "Sofia"))
|
||||
);
|
||||
System.out.println("Weather: " + weather);
|
||||
|
||||
// Access system info resource
|
||||
ReadResourceResult sysInfo = client.readResource(
|
||||
new ReadResourceRequest("system://info")
|
||||
);
|
||||
System.out.println("System Info: " + sysInfo);
|
||||
|
||||
// Use greeting prompt
|
||||
GetPromptResult greeting = client.getPrompt(
|
||||
new GetPromptRequest("greeting", Map.of("name", "John"))
|
||||
);
|
||||
System.out.println("Greeting: " + greeting);
|
||||
|
||||
// Close client
|
||||
client.closeGracefully();
|
||||
```
|
||||
|
||||
## Server Configuration Example
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class McpServerConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public HttpServletSseServerTransport webSseServerTransport() {
|
||||
return new HttpServletSseServerTransport(new ObjectMapper(), "/mcp/message");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public ServletRegistrationBean customServletBean(HttpServletSseServerTransport servlet) {
|
||||
return new ServletRegistrationBean(servlet, "/sse", "/mcp/message");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public McpAsyncServer mcpServer(ServerMcpTransport transport) {
|
||||
// Configure server capabilities
|
||||
var capabilities = McpSchema.ServerCapabilities.builder()
|
||||
.resources(false, true) // Resource support with list changes notifications
|
||||
.tools(true) // Tool support with list changes notifications
|
||||
.prompts(true) // Prompt support with list changes notifications
|
||||
.build();
|
||||
|
||||
// Create and configure the server
|
||||
return McpServer.using(transport)
|
||||
.serverInfo("MCP Demo Server", "1.0.0")
|
||||
.capabilities(capabilities)
|
||||
// Add your tools, resources, and prompts here
|
||||
.async();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The application can be configured through `application.properties`:
|
||||
|
||||
- `transport.mode`: Transport mode to use (stdio/sse)
|
||||
- `server.port`: Server port for SSE mode (default: 8080)
|
||||
- Various logging configurations are available for debugging
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The sample demonstrates:
|
||||
- Using HttpServletSseServerTransport for SSE-based communication
|
||||
- Creating an MCP server with custom tools, resources, and prompts
|
||||
- Configuring different transport modes
|
||||
- Implementing tool handlers with JSON schema validation
|
||||
- Resource implementations with dynamic content generation
|
||||
- Prompt implementations with parameter handling
|
||||
- Error handling and response formatting
|
||||
- Synchronous client usage patterns
|
||||
@@ -1,76 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.6</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>mcp-servlet-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<name>Spring AI MCP Servlet Server Sample</name>
|
||||
<description>Sample application demonstrating MCP Servlet server usage</description>
|
||||
|
||||
<properties>
|
||||
<sprign-ai.version>1.0.0-SNAPSHOT</sprign-ai.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${sprign-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
@@ -1,147 +0,0 @@
|
||||
package org.springframework.ai.mcp.sample.servlet.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.server.McpServer;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransport;
|
||||
import io.modelcontextprotocol.server.transport.StdioServerTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Role;
|
||||
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
||||
import io.modelcontextprotocol.spec.ServerMcpTransport;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.mcp.McpToolUtils;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbacks;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class McpServerConfig implements WebMvcConfigurer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(McpServerConfig.class);
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public HttpServletSseServerTransport webMvcSseServerTransport() {
|
||||
return new HttpServletSseServerTransport(new ObjectMapper(), "/mcp/message");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public ServletRegistrationBean customServletBean(HttpServletSseServerTransport servlet) {
|
||||
return new ServletRegistrationBean(servlet);
|
||||
}
|
||||
|
||||
// STDIO transport
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio")
|
||||
public StdioServerTransport stdioServerTransport() {
|
||||
return new StdioServerTransport();
|
||||
}
|
||||
|
||||
public static record ToUpperCaseInput(String input) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public McpSyncServer mcpServer(ServerMcpTransport transport, OpenLibrary openLibrary) { // @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
|
||||
.logging() // Logging support
|
||||
.build();
|
||||
|
||||
// Create the server with both tool and resource capabilities
|
||||
var server = McpServer.sync(transport)
|
||||
.serverInfo("MCP Demo Servlet Server", "1.0.0")
|
||||
.capabilities(capabilities)
|
||||
.resources(systemInfoResourceRegistration())
|
||||
.prompts(greetingPromptRegistration())
|
||||
.tools(
|
||||
McpToolUtils.toSyncToolRegistration(
|
||||
FunctionToolCallback.builder("toUpperCase", (Function<ToUpperCaseInput, String>) s -> s.input().toUpperCase())
|
||||
.description("To upper case")
|
||||
.inputType(ToUpperCaseInput.class)
|
||||
.build()))
|
||||
.tools(openLibraryToolRegistrations(openLibrary))
|
||||
.build();
|
||||
return server; // @formatter:on
|
||||
} // @formatter:on
|
||||
|
||||
public static List<SyncToolRegistration> openLibraryToolRegistrations(OpenLibrary openLibrary) {
|
||||
ToolCallback[] tools = ToolCallbacks.from(openLibrary);
|
||||
return McpToolUtils.toSyncToolRegistration(tools);
|
||||
}
|
||||
|
||||
private static SyncResourceRegistration systemInfoResourceRegistration() {
|
||||
|
||||
// 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 SyncResourceRegistration(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 SyncPromptRegistration greetingPromptRegistration() {
|
||||
|
||||
var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt",
|
||||
List.of(new McpSchema.PromptArgument("name", "The name to greet", true)));
|
||||
|
||||
return new SyncPromptRegistration(prompt, getPromptRequest -> {
|
||||
|
||||
String nameArgument = (String) getPromptRequest.arguments().get("name");
|
||||
if (nameArgument == null) {
|
||||
nameArgument = "friend";
|
||||
}
|
||||
|
||||
var userMessage = new PromptMessage(Role.USER,
|
||||
new TextContent("Hello " + nameArgument + "! How can I assist you today?"));
|
||||
|
||||
return new GetPromptResult("A personalized greeting message", List.of(userMessage));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.springframework.ai.mcp.sample.servlet.server;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class McpServletServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(McpServletServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* 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.servlet.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@Service
|
||||
public class OpenLibrary {
|
||||
|
||||
private RestClient restClient;
|
||||
|
||||
public OpenLibrary(RestClient.Builder restClientBuilder) {
|
||||
this.restClient = restClientBuilder.baseUrl("https://openlibrary.org").build();
|
||||
}
|
||||
|
||||
public record Books(Integer numFound, Integer start, Boolean numFoundExact, List<Map<String, Object>> docs) {
|
||||
}
|
||||
|
||||
public record Book(List<String> isbn, String title, List<String> authorName) {
|
||||
}
|
||||
|
||||
@Tool(description = "Search for books by title")
|
||||
public List<Book> getBooks(String title) {
|
||||
Books books = restClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/search.json").queryParam("q", title).build())
|
||||
.retrieve()
|
||||
.body(Books.class);
|
||||
if (books == null) {
|
||||
return List.of();
|
||||
}
|
||||
return books.docs.stream()
|
||||
.map(doc -> new Book((List<String>) doc.get("isbn"), (String) doc.get("title"),
|
||||
(List<String>) doc.get("author_name")))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Tool(description = "Search for books by author")
|
||||
public List<String> getBookTitlesByAuthor(String authorName) {
|
||||
var books = restClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/search/authors.json").queryParam("q", authorName).build())
|
||||
.retrieve()
|
||||
.body(Books.class);
|
||||
if (books == null) {
|
||||
return List.of();
|
||||
}
|
||||
return books.docs.stream().map(doc -> (String) doc.get("top_work")).toList();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
OpenLibrary openLibrary = new OpenLibrary(RestClient.builder());
|
||||
List<Book> books = openLibrary.getBooks("Spring Framework");
|
||||
System.out.println(books);
|
||||
List<String> booksByAuthor = openLibrary.getBookTitlesByAuthor("Craig Walls");
|
||||
System.out.println(booksByAuthor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
spring.main.web-application-type=SERVLET
|
||||
|
||||
# NOTE: You must disable the banner and the console logging
|
||||
# to allow the STDIO transport to work !!!
|
||||
spring.main.banner-mode=off
|
||||
logging.pattern.console=
|
||||
|
||||
transport.mode=sse
|
||||
|
||||
logging.file.name=mcp.servlet-server.log
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* 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.servlet.client;
|
||||
|
||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
|
||||
/**
|
||||
* With stdio transport, the MCP server is automatically started by the client. But you
|
||||
* have to build the server jar first: <pre>./mvnw clean install -DskipTests</pre>
|
||||
*/
|
||||
public class ClientStdio {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dtransport.mode=stdio", "-Dspring.main.web-application-type=none", "-jar",
|
||||
"model-context-protocol/book-library/manual-servlet-server/target/mcp-servlet-server-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
|
||||
var transport = new StdioClientTransport(stdioParams);
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* 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.servlet.client;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
|
||||
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
|
||||
public class SampleClient {
|
||||
|
||||
private final ClientMcpTransport transport;
|
||||
|
||||
public SampleClient(ClientMcpTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
||||
var client = McpClient.sync(this.transport).build();
|
||||
|
||||
client.initialize();
|
||||
|
||||
client.ping();
|
||||
|
||||
// List and demonstrate tools
|
||||
ListToolsResult toolsList = client.listTools();
|
||||
System.out.println("Available Tools = " + toolsList);
|
||||
|
||||
CallToolResult upperCaseResult = client
|
||||
.callTool(new CallToolRequest("toUpperCase", Map.of("input", "accountName")));
|
||||
System.out.println("Upper case Response = " + upperCaseResult);
|
||||
|
||||
CallToolResult books = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework")));
|
||||
System.out.println("Books Response = " + books);
|
||||
|
||||
// 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp-sample</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
<name>Spring AI MCP Sample</name>
|
||||
<description>Sample Spring Boot application demonstrating MCP server usage</description>
|
||||
<organization>
|
||||
<name>Spring Framework</name>
|
||||
<url>https://spring.io</url>
|
||||
</organization>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache 2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Christian Tzolov</name>
|
||||
</developer>
|
||||
<developer>
|
||||
<name>Dariusz Jędrzejczyk</name>
|
||||
</developer>
|
||||
</developers>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webflux-sse-transport</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,19 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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
|
||||
#
|
||||
# http://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.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
@@ -1,257 +0,0 @@
|
||||
# Spring AI MCP Sample Project
|
||||
|
||||
This sample project demonstrates the usage of the Spring AI Model Context Protocol (MCP) implementation. It showcases how to create and use MCP servers and clients with different transport modes and capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample provides:
|
||||
- Two transport mode implementations: Stdio and SSE (Server-Sent Events)
|
||||
- Server capabilities:
|
||||
- Tools support with list changes notifications
|
||||
- Resources support with list changes notifications (no subscribe support)
|
||||
- Prompts support with list changes notifications
|
||||
- Sample implementations:
|
||||
- Two MCP tools: Weather and Calculator
|
||||
- One MCP resource: System Information
|
||||
- One MCP prompt: Greeting
|
||||
|
||||
## Building the Project
|
||||
|
||||
```bash
|
||||
./mvnw clean package
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
The server can be started in two transport modes, controlled by the `transport.mode` property:
|
||||
|
||||
### Stdio Mode (Default)
|
||||
|
||||
```bash
|
||||
java -Dtransport.mode=stdio -jar model-context-protocol/mcp-webflux-server/target/mcp-webflux-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
The Stdio mode server is automatically started by the client - no explicit server startup is needed.
|
||||
But you have to build the server jar first: `./mvnw clean install -DskipTests`.
|
||||
|
||||
In Stdio mode the server must not emit any messages/logs to the console (e.g. standard out) but the JSON messages produced by the server.
|
||||
|
||||
### SSE Mode
|
||||
```bash
|
||||
java -Dtransport.mode=sse -jar model-context-protocol/mcp-webflux-server/target/mcp-webflux-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
## Sample Clients
|
||||
|
||||
The project includes example clients for both transport modes:
|
||||
|
||||
### Stdio Client (ClientStdio.java)
|
||||
```java
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dtransport.mode=stdio", "-jar",
|
||||
"model-context-protocol/mcp-webflux-server/target/mcp-webflux-server-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
|
||||
var transport = new StdioClientTransport(stdioParams);
|
||||
var client = McpClient.using(transport).sync();
|
||||
```
|
||||
|
||||
### SSE Client (ClientWebFluxSse.java)
|
||||
```java
|
||||
var transport = new SseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
|
||||
var client = McpClient.using(transport).sync();
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Weather Tool
|
||||
- Name: `weather`
|
||||
- Description: Weather forecast tool by location
|
||||
- Parameters:
|
||||
- `city`: String - The city to get weather for
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("weather", Map.of("city", "Sofia"))
|
||||
);
|
||||
```
|
||||
|
||||
### Calculator Tool
|
||||
- Name: `calculator`
|
||||
- Description: Performs basic arithmetic operations
|
||||
- Parameters (JSON Schema):
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("calculator",
|
||||
Map.of("operation", "multiply", "a", 2.0, "b", 3.0))
|
||||
);
|
||||
```
|
||||
|
||||
## Available Resources
|
||||
|
||||
### System Information Resource
|
||||
- URI: `system://info`
|
||||
- Description: Provides basic system information including Java version, OS, etc.
|
||||
- MIME Type: application/json
|
||||
- Returns: JSON object containing: javaVersion, osName, osVersion, osArch, processors, timestamp
|
||||
|
||||
## Available Prompts
|
||||
|
||||
### Greeting Prompt
|
||||
- Name: `greeting`
|
||||
- Description: A friendly greeting prompt
|
||||
- Parameters:
|
||||
- `name`: String (required) - The name to greet
|
||||
- Returns: A personalized greeting message from an assistant
|
||||
|
||||
## Client Usage Example
|
||||
|
||||
```java
|
||||
// Initialize client
|
||||
client.initialize();
|
||||
|
||||
// Test connection
|
||||
client.ping();
|
||||
|
||||
// List available tools
|
||||
ListToolsResult tools = client.listTools();
|
||||
System.out.println("Available tools: " + tools);
|
||||
|
||||
// Call weather tool
|
||||
CallToolResult weather = client.callTool(
|
||||
new CallToolRequest("weather", Map.of("city", "Sofia"))
|
||||
);
|
||||
System.out.println("Weather: " + weather);
|
||||
|
||||
// Call calculator tool
|
||||
CallToolResult calc = client.callTool(
|
||||
new CallToolRequest("calculator",
|
||||
Map.of("operation", "multiply", "a", 2.0, "b", 3.0))
|
||||
);
|
||||
System.out.println("Calculation: " + calc);
|
||||
|
||||
// Access system info resource
|
||||
ReadResourceResult sysInfo = client.readResource(
|
||||
new ReadResourceRequest("system://info")
|
||||
);
|
||||
System.out.println("System Info: " + sysInfo);
|
||||
|
||||
// Use greeting prompt
|
||||
GetPromptResult greeting = client.getPrompt(
|
||||
new GetPromptRequest("greeting", Map.of("name", "John"))
|
||||
);
|
||||
System.out.println("Greeting: " + greeting);
|
||||
|
||||
// Close client
|
||||
client.closeGracefully();
|
||||
```
|
||||
|
||||
## Server Usage Example
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class CustomMcpServerConfig {
|
||||
@Bean
|
||||
public McpAsyncServer mcpServer(McpTransport transport) {
|
||||
// Configure server capabilities
|
||||
var capabilities = McpSchema.ServerCapabilities.builder()
|
||||
.resources(false, true) // Resource support with list changes notifications
|
||||
.tools(true) // Tool support with list changes notifications
|
||||
.prompts(true) // Prompt support with list changes notifications
|
||||
.build();
|
||||
|
||||
// Create custom tool
|
||||
var customTool = new ToolRegistration(
|
||||
new McpSchema.Tool("custom-tool", "Description", Map.of("param", "String")),
|
||||
(arguments) -> {
|
||||
String param = (String) arguments.get("param");
|
||||
return new CallToolResult(
|
||||
List.of(new TextContent("Result: " + param)),
|
||||
false
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Create custom resource
|
||||
var customResource = new ResourceRegistration(
|
||||
new McpSchema.Resource(
|
||||
"custom://resource",
|
||||
"Custom Resource",
|
||||
"Description",
|
||||
"application/json",
|
||||
null
|
||||
),
|
||||
(request) -> new ReadResourceResult(
|
||||
List.of(new TextResourceContents(
|
||||
request.uri(),
|
||||
"application/json",
|
||||
"{\"data\": \"example\"}"
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
// Create custom prompt
|
||||
var customPrompt = new PromptRegistration(
|
||||
new McpSchema.Prompt(
|
||||
"custom-prompt",
|
||||
"Description",
|
||||
List.of(new McpSchema.PromptArgument("input", "Description", true))
|
||||
),
|
||||
request -> {
|
||||
String input = (String) request.arguments().get("input");
|
||||
var message = new PromptMessage(
|
||||
Role.ASSISTANT,
|
||||
new TextContent("Response to: " + input)
|
||||
);
|
||||
return new GetPromptResult("Result", List.of(message));
|
||||
}
|
||||
);
|
||||
|
||||
// Create and configure the server
|
||||
return McpServer.using(transport)
|
||||
.info("Custom MCP Server", "1.0.0")
|
||||
.capabilities(capabilities)
|
||||
.tools(List.of(customTool))
|
||||
.resources(Map.of("custom://resource", customResource))
|
||||
.prompts(List.of(customPrompt))
|
||||
.async();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The application can be configured through `application.properties`:
|
||||
|
||||
- `transport.mode`: Transport mode to use (stdio/sse)
|
||||
- `server.port`: Server port for SSE mode (default: 8080)
|
||||
- Various logging configurations are available for debugging
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The sample demonstrates:
|
||||
- Creating an MCP server with custom tools, resources, and prompts
|
||||
- Configuring different transport modes
|
||||
- Implementing tool handlers with JSON schema validation
|
||||
- Resource implementations with dynamic content generation
|
||||
- Prompt implementations with parameter handling
|
||||
- Error handling and response formatting
|
||||
- Synchronous client usage patterns
|
||||
@@ -1,259 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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
|
||||
#
|
||||
# http://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.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
@@ -1,149 +0,0 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
@@ -1,109 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.6</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
|
||||
<artifactId>mcp-webflux-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<name>Spring AI MCP StdIO and WebFluxSSE Sample</name>
|
||||
<description>Sample Spring Boot application demonstrating MCP server usage</description>
|
||||
|
||||
<properties>
|
||||
<spring-ai-mcp.version>0.6.0</spring-ai-mcp.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-bom</artifactId>
|
||||
<version>${spring-ai-mcp.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webflux-sse-transport</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.spring.javaformat</groupId>
|
||||
<artifactId>spring-javaformat-maven-plugin</artifactId>
|
||||
<version>0.0.43</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>validate</phase>
|
||||
<inherited>true</inherited>
|
||||
<goals>
|
||||
<goal>validate</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
@@ -1,13 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package org.springframework.ai.mcp.sample.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.mcp.server.McpServer;
|
||||
import org.springframework.ai.mcp.server.McpServerFeatures;
|
||||
import org.springframework.ai.mcp.server.McpSyncServer;
|
||||
import org.springframework.ai.mcp.server.transport.StdioServerTransport;
|
||||
import org.springframework.ai.mcp.server.transport.WebFluxSseServerTransport;
|
||||
import org.springframework.ai.mcp.spec.McpSchema;
|
||||
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.ServerMcpTransport;
|
||||
import org.springframework.ai.mcp.spring.ToolHelper;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
|
||||
@Configuration
|
||||
public class McpServerConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(McpServerConfig.class);
|
||||
|
||||
// STDIO transport
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio")
|
||||
public StdioServerTransport stdioServerTransport() {
|
||||
return new StdioServerTransport();
|
||||
}
|
||||
|
||||
// SSE transport
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public WebFluxSseServerTransport sseServerTransport() {
|
||||
return new WebFluxSseServerTransport(new ObjectMapper(), "/mcp/message");
|
||||
}
|
||||
|
||||
// Router function for SSE transport used by Spring WebFlux to start an HTTP
|
||||
// server.
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransport transport) {
|
||||
return transport.getRouterFunction();
|
||||
}
|
||||
|
||||
public static record ToUpperCaseInput(String input) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public McpSyncServer mcpServer(ServerMcpTransport transport, OpenLibrary openLibrary) { // @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
|
||||
.logging() // Logging support
|
||||
.build();
|
||||
|
||||
// Create the server with both tool and resource capabilities
|
||||
McpSyncServer server = McpServer.sync(transport)
|
||||
.serverInfo("MCP Demo WebFlux Server", "1.0.0")
|
||||
.capabilities(capabilities)
|
||||
.resources(systemInfoResourceRegistration()) // Resources
|
||||
.prompts(greetingPromptRegistration()) // Prompts
|
||||
.tools(openLibraryToolRegistrations(openLibrary)) // Method based tools
|
||||
.tools(ToolHelper.toSyncToolRegistration( // java.util.Function based tools
|
||||
FunctionCallback.builder()
|
||||
.function("toUpperCase", (Function<ToUpperCaseInput, String>) s -> s.input().toUpperCase())
|
||||
.description("To upper case")
|
||||
.inputType(ToUpperCaseInput.class)
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
return server; // @formatter:on
|
||||
} // @formatter:on
|
||||
|
||||
public static List<McpServerFeatures.SyncToolRegistration> openLibraryToolRegistrations(OpenLibrary openLibrary) {
|
||||
|
||||
var books = FunctionCallback.builder()
|
||||
.method("getBooks", String.class)
|
||||
.description("Get list of Books by title")
|
||||
.targetObject(openLibrary)
|
||||
.build();
|
||||
|
||||
var bookTitlesByAuthor = FunctionCallback.builder()
|
||||
.method("getBookTitlesByAuthor", String.class)
|
||||
.description("Get book titles by author")
|
||||
.targetObject(openLibrary)
|
||||
.build();
|
||||
|
||||
return ToolHelper.toSyncToolRegistration(books, bookTitlesByAuthor);
|
||||
}
|
||||
|
||||
private static McpServerFeatures.SyncResourceRegistration systemInfoResourceRegistration() {
|
||||
|
||||
// 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 McpServerFeatures.SyncResourceRegistration(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 McpServerFeatures.SyncPromptRegistration greetingPromptRegistration() {
|
||||
|
||||
var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt",
|
||||
List.of(new McpSchema.PromptArgument("name", "The name to greet", true)));
|
||||
|
||||
return new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> {
|
||||
|
||||
String nameArgument = (String) getPromptRequest.arguments().get("name");
|
||||
if (nameArgument == null) {
|
||||
nameArgument = "friend";
|
||||
}
|
||||
|
||||
var userMessage = new PromptMessage(Role.USER,
|
||||
new TextContent("Hello " + nameArgument + "! How can I assist you today?"));
|
||||
|
||||
return new GetPromptResult("A personalized greeting message", List.of(userMessage));
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenLibrary openLibrary() {
|
||||
return new OpenLibrary(RestClient.builder());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* 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.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
|
||||
public class OpenLibrary {
|
||||
|
||||
private RestClient restClient;
|
||||
|
||||
public OpenLibrary(RestClient.Builder restClientBuilder) {
|
||||
this.restClient = restClientBuilder.baseUrl("https://openlibrary.org").build();
|
||||
}
|
||||
|
||||
public record Books(Integer numFound, Integer start, Boolean numFoundExact, List<Map<String, Object>> docs) {
|
||||
}
|
||||
|
||||
public record Book(List<String> isbn, String title, List<String> authorName) {
|
||||
}
|
||||
|
||||
public List<Book> getBooks(String title) {
|
||||
Books books = restClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/search.json").queryParam("q", title).build())
|
||||
.retrieve()
|
||||
.body(Books.class);
|
||||
if (books == null) {
|
||||
return List.of();
|
||||
}
|
||||
return books.docs.stream()
|
||||
.map(doc -> new Book((List<String>) doc.get("isbn"), (String) doc.get("title"),
|
||||
(List<String>) doc.get("author_name")))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<String> getBookTitlesByAuthor(String authorName) {
|
||||
var books = restClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/search/authors.json").queryParam("q", authorName).build())
|
||||
.retrieve()
|
||||
.body(Books.class);
|
||||
if (books == null) {
|
||||
return List.of();
|
||||
}
|
||||
return books.docs.stream().map(doc -> (String) doc.get("top_work")).toList();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
OpenLibrary openLibrary = new OpenLibrary(RestClient.builder());
|
||||
List<Book> books = openLibrary.getBooks("Spring Framework");
|
||||
System.out.println(books);
|
||||
List<String> booksByAuthor = openLibrary.getBookTitlesByAuthor("Craig Walls");
|
||||
System.out.println(booksByAuthor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# spring.main.web-application-type=none
|
||||
|
||||
# NOTE: You must disable the banner and the console logging
|
||||
# to allow the STDIO transport to work !!!
|
||||
spring.main.banner-mode=off
|
||||
logging.pattern.console=
|
||||
|
||||
# transport.mode=stdio
|
||||
transport.mode=sse
|
||||
|
||||
logging.file.name=mcp.webflux.log
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* 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.io.File;
|
||||
|
||||
import org.springframework.ai.mcp.client.transport.ServerParameters;
|
||||
import org.springframework.ai.mcp.client.transport.StdioClientTransport;
|
||||
|
||||
/**
|
||||
* With stdio transport, the MCP server is automatically started by the client. But you
|
||||
* have to build the server jar first: <pre>./mvnw clean install -DskipTests</pre>
|
||||
*/
|
||||
public class ClientStdio {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
System.out.println(new File(".").getAbsolutePath());
|
||||
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dtransport.mode=stdio", "-Dspring.main.web-application-type=none", "-jar",
|
||||
"model-context-protocol/mcp-webflux-server/target/mcp-webflux-server-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
|
||||
var transport = new StdioClientTransport(stdioParams);
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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.WebFluxSseClientTransport;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class ClientWebFluxSse {
|
||||
|
||||
public static void main(String[] args) {
|
||||
var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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.ClientMcpTransport;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
|
||||
public class SampleClient {
|
||||
|
||||
private final ClientMcpTransport transport;
|
||||
|
||||
public SampleClient(ClientMcpTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
||||
var client = McpClient.sync(this.transport).build();
|
||||
|
||||
client.initialize();
|
||||
|
||||
client.ping();
|
||||
|
||||
// List and demonstrate tools
|
||||
ListToolsResult toolsList = client.listTools();
|
||||
System.out.println("Available Tools = " + toolsList);
|
||||
|
||||
CallToolResult upperCaseResult = client
|
||||
.callTool(new CallToolRequest("toUpperCase", Map.of("input", "accountName")));
|
||||
System.out.println("Upper case Response = " + upperCaseResult);
|
||||
|
||||
CallToolResult books = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework")));
|
||||
System.out.println("Books Response = " + books);
|
||||
|
||||
// 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp-webmvc-sample</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
<name>Spring AI MCP Sample</name>
|
||||
<description>Sample Spring Boot application demonstrating MCP server usage</description>
|
||||
<organization>
|
||||
<name>Spring Framework</name>
|
||||
<url>https://spring.io</url>
|
||||
</organization>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache 2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Christian Tzolov</name>
|
||||
</developer>
|
||||
<developer>
|
||||
<name>Dariusz Jędrzejczyk</name>
|
||||
</developer>
|
||||
</developers>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webmvc-sse-transport</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,19 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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
|
||||
#
|
||||
# http://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.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
@@ -1,257 +0,0 @@
|
||||
# Spring AI MCP WebMVC Server Sample Project
|
||||
|
||||
This sample project demonstrates the usage of the Spring AI Model Context Protocol (MCP) implementation. It showcases how to create and use MCP WebMvc servers and Http Sse clients with different capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample provides:
|
||||
- Two transport mode implementations: Stdio and SSE (Server-Sent Events)
|
||||
- Server capabilities:
|
||||
- Tools support with list changes notifications
|
||||
- Resources support with list changes notifications (no subscribe support)
|
||||
- Prompts support with list changes notifications
|
||||
- Sample implementations:
|
||||
- Two MCP tools: Weather and Calculator
|
||||
- One MCP resource: System Information
|
||||
- One MCP prompt: Greeting
|
||||
|
||||
## Building the Project
|
||||
|
||||
```bash
|
||||
./mvnw clean package
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
The server can be started in two transport modes, controlled by the `transport.mode` property:
|
||||
|
||||
### Stdio Mode (Default)
|
||||
|
||||
```bash
|
||||
java -Dtransport.mode=stdio -jar target/mcp-webmvc-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
The Stdio mode server is automatically started by the client - no explicit server startup is needed.
|
||||
But you have to build the server jar first: `./mvnw clean install -DskipTests`.
|
||||
|
||||
In Stdio mode the server must not emit any messages/logs to the console (e.g. standard out) but the JSON messages produced by the server.
|
||||
|
||||
### SSE Mode
|
||||
```bash
|
||||
java -Dtransport.mode=sse -jar target/smcp-webmvc-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
## Sample Clients
|
||||
|
||||
The project includes example clients for both transport modes:
|
||||
|
||||
### Stdio Client (ClientStdio.java)
|
||||
```java
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dtransport.mode=stdio", "-jar",
|
||||
"target/mcp-webmvc-server-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
|
||||
var transport = new StdioClientTransport(stdioParams);
|
||||
var client = McpClient.using(transport).sync();
|
||||
```
|
||||
|
||||
### SSE Client (ClientSse.java)
|
||||
```java
|
||||
var transport = new HttpClientSseClientTransport("http://localhost:8080");
|
||||
var client = McpClient.using(transport).sync();
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Weather Tool
|
||||
- Name: `weather`
|
||||
- Description: Weather forecast tool by location
|
||||
- Parameters:
|
||||
- `city`: String - The city to get weather for
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("weather", Map.of("city", "Sofia"))
|
||||
);
|
||||
```
|
||||
|
||||
### Calculator Tool
|
||||
- Name: `calculator`
|
||||
- Description: Performs basic arithmetic operations
|
||||
- Parameters (JSON Schema):
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("calculator",
|
||||
Map.of("operation", "multiply", "a", 2.0, "b", 3.0))
|
||||
);
|
||||
```
|
||||
|
||||
## Available Resources
|
||||
|
||||
### System Information Resource
|
||||
- URI: `system://info`
|
||||
- Description: Provides basic system information including Java version, OS, etc.
|
||||
- MIME Type: application/json
|
||||
- Returns: JSON object containing: javaVersion, osName, osVersion, osArch, processors, timestamp
|
||||
|
||||
## Available Prompts
|
||||
|
||||
### Greeting Prompt
|
||||
- Name: `greeting`
|
||||
- Description: A friendly greeting prompt
|
||||
- Parameters:
|
||||
- `name`: String (required) - The name to greet
|
||||
- Returns: A personalized greeting message from an assistant
|
||||
|
||||
## Client Usage Example
|
||||
|
||||
```java
|
||||
// Initialize client
|
||||
client.initialize();
|
||||
|
||||
// Test connection
|
||||
client.ping();
|
||||
|
||||
// List available tools
|
||||
ListToolsResult tools = client.listTools();
|
||||
System.out.println("Available tools: " + tools);
|
||||
|
||||
// Call weather tool
|
||||
CallToolResult weather = client.callTool(
|
||||
new CallToolRequest("weather", Map.of("city", "Sofia"))
|
||||
);
|
||||
System.out.println("Weather: " + weather);
|
||||
|
||||
// Call calculator tool
|
||||
CallToolResult calc = client.callTool(
|
||||
new CallToolRequest("calculator",
|
||||
Map.of("operation", "multiply", "a", 2.0, "b", 3.0))
|
||||
);
|
||||
System.out.println("Calculation: " + calc);
|
||||
|
||||
// Access system info resource
|
||||
ReadResourceResult sysInfo = client.readResource(
|
||||
new ReadResourceRequest("system://info")
|
||||
);
|
||||
System.out.println("System Info: " + sysInfo);
|
||||
|
||||
// Use greeting prompt
|
||||
GetPromptResult greeting = client.getPrompt(
|
||||
new GetPromptRequest("greeting", Map.of("name", "John"))
|
||||
);
|
||||
System.out.println("Greeting: " + greeting);
|
||||
|
||||
// Close client
|
||||
client.closeGracefully();
|
||||
```
|
||||
|
||||
## Server Usage Example
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class CustomMcpServerConfig {
|
||||
@Bean
|
||||
public McpAsyncServer mcpServer(McpTransport transport) {
|
||||
// Configure server capabilities
|
||||
var capabilities = McpSchema.ServerCapabilities.builder()
|
||||
.resources(false, true) // Resource support with list changes notifications
|
||||
.tools(true) // Tool support with list changes notifications
|
||||
.prompts(true) // Prompt support with list changes notifications
|
||||
.build();
|
||||
|
||||
// Create custom tool
|
||||
var customTool = new ToolRegistration(
|
||||
new McpSchema.Tool("custom-tool", "Description", Map.of("param", "String")),
|
||||
(arguments) -> {
|
||||
String param = (String) arguments.get("param");
|
||||
return new CallToolResult(
|
||||
List.of(new TextContent("Result: " + param)),
|
||||
false
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Create custom resource
|
||||
var customResource = new ResourceRegistration(
|
||||
new McpSchema.Resource(
|
||||
"custom://resource",
|
||||
"Custom Resource",
|
||||
"Description",
|
||||
"application/json",
|
||||
null
|
||||
),
|
||||
(request) -> new ReadResourceResult(
|
||||
List.of(new TextResourceContents(
|
||||
request.uri(),
|
||||
"application/json",
|
||||
"{\"data\": \"example\"}"
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
// Create custom prompt
|
||||
var customPrompt = new PromptRegistration(
|
||||
new McpSchema.Prompt(
|
||||
"custom-prompt",
|
||||
"Description",
|
||||
List.of(new McpSchema.PromptArgument("input", "Description", true))
|
||||
),
|
||||
request -> {
|
||||
String input = (String) request.arguments().get("input");
|
||||
var message = new PromptMessage(
|
||||
Role.ASSISTANT,
|
||||
new TextContent("Response to: " + input)
|
||||
);
|
||||
return new GetPromptResult("Result", List.of(message));
|
||||
}
|
||||
);
|
||||
|
||||
// Create and configure the server
|
||||
return McpServer.using(transport)
|
||||
.info("Custom MCP Server", "1.0.0")
|
||||
.capabilities(capabilities)
|
||||
.tools(List.of(customTool))
|
||||
.resources(Map.of("custom://resource", customResource))
|
||||
.prompts(List.of(customPrompt))
|
||||
.async();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The application can be configured through `application.properties`:
|
||||
|
||||
- `transport.mode`: Transport mode to use (stdio/sse)
|
||||
- `server.port`: Server port for SSE mode (default: 8080)
|
||||
- Various logging configurations are available for debugging
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The sample demonstrates:
|
||||
- Creating an MCP server with custom tools, resources, and prompts
|
||||
- Configuring different transport modes
|
||||
- Implementing tool handlers with JSON schema validation
|
||||
- Resource implementations with dynamic content generation
|
||||
- Prompt implementations with parameter handling
|
||||
- Error handling and response formatting
|
||||
- Synchronous client usage patterns
|
||||
@@ -1,259 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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
|
||||
#
|
||||
# http://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.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
@@ -1,149 +0,0 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
@@ -1,114 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.6</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>mcp-webmvc-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<name>Spring AI MCP WebMvc Server Sample</name>
|
||||
<description>Sample Spring Boot application demonstrating MCP WebMvc server usage</description>
|
||||
|
||||
<properties>
|
||||
<spring-ai-mcp.version>0.6.0</spring-ai-mcp.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-bom</artifactId>
|
||||
<version>${spring-ai-mcp.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webmvc-sse-transport</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Used only for the client webflux transport -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webflux-sse-transport</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.spring.javaformat</groupId>
|
||||
<artifactId>spring-javaformat-maven-plugin</artifactId>
|
||||
<version>0.0.43</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>validate</phase>
|
||||
<inherited>true</inherited>
|
||||
<goals>
|
||||
<goal>validate</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.springframework.ai.mcp.sample.webmvc.server;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class McpMvcServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(McpMvcServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package org.springframework.ai.mcp.sample.webmvc.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.mcp.server.McpServer;
|
||||
import org.springframework.ai.mcp.server.McpServerFeatures.SyncPromptRegistration;
|
||||
import org.springframework.ai.mcp.server.McpServerFeatures.SyncResourceRegistration;
|
||||
import org.springframework.ai.mcp.server.McpServerFeatures.SyncToolRegistration;
|
||||
import org.springframework.ai.mcp.server.McpSyncServer;
|
||||
import org.springframework.ai.mcp.server.transport.StdioServerTransport;
|
||||
import org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport;
|
||||
import org.springframework.ai.mcp.spec.McpSchema;
|
||||
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.ServerMcpTransport;
|
||||
import org.springframework.ai.mcp.spring.ToolHelper;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class McpServerConfig implements WebMvcConfigurer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(McpServerConfig.class);
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public WebMvcSseServerTransport webMvcSseServerTransport() {
|
||||
return new WebMvcSseServerTransport(new ObjectMapper(), "/mcp/message");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public RouterFunction<ServerResponse> routerFunction(WebMvcSseServerTransport transport) {
|
||||
return transport.getRouterFunction();
|
||||
}
|
||||
|
||||
// STDIO transport
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio")
|
||||
public StdioServerTransport stdioServerTransport() {
|
||||
return new StdioServerTransport();
|
||||
}
|
||||
|
||||
public static record ToUpperCaseInput(String input) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public McpSyncServer mcpServer(ServerMcpTransport transport, OpenLibrary openLibrary) { // @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
|
||||
.logging() // Logging support
|
||||
.build();
|
||||
|
||||
// Create the server with both tool and resource capabilities
|
||||
var server = McpServer.sync(transport)
|
||||
.serverInfo("MCP Demo WebMVC Server", "1.0.0")
|
||||
.capabilities(capabilities)
|
||||
.resources(systemInfoResourceRegistration())
|
||||
.prompts(greetingPromptRegistration())
|
||||
.tools(
|
||||
ToolHelper.toSyncToolRegistration(
|
||||
FunctionCallback.builder()
|
||||
.function("toUpperCase", (Function<ToUpperCaseInput, String>) s -> s.input().toUpperCase())
|
||||
.description("To upper case")
|
||||
.inputType(ToUpperCaseInput.class)
|
||||
.build()))
|
||||
.tools(openLibraryToolRegistrations(openLibrary))
|
||||
.build();
|
||||
return server; // @formatter:on
|
||||
} // @formatter:on
|
||||
|
||||
public static List<SyncToolRegistration> openLibraryToolRegistrations(OpenLibrary openLibrary) {
|
||||
|
||||
var books = FunctionCallback.builder()
|
||||
.method("getBooks", String.class)
|
||||
.description("Get list of Books by title")
|
||||
.targetObject(openLibrary)
|
||||
.build();
|
||||
|
||||
var bookTitlesByAuthor = FunctionCallback.builder()
|
||||
.method("getBookTitlesByAuthor", String.class)
|
||||
.description("Get book titles by author")
|
||||
.targetObject(openLibrary)
|
||||
.build();
|
||||
|
||||
return ToolHelper.toSyncToolRegistration(books, bookTitlesByAuthor);
|
||||
}
|
||||
|
||||
private static SyncResourceRegistration systemInfoResourceRegistration() {
|
||||
|
||||
// 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 SyncResourceRegistration(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 SyncPromptRegistration greetingPromptRegistration() {
|
||||
|
||||
var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt",
|
||||
List.of(new McpSchema.PromptArgument("name", "The name to greet", true)));
|
||||
|
||||
return new SyncPromptRegistration(prompt, getPromptRequest -> {
|
||||
|
||||
String nameArgument = (String) getPromptRequest.arguments().get("name");
|
||||
if (nameArgument == null) {
|
||||
nameArgument = "friend";
|
||||
}
|
||||
|
||||
var userMessage = new PromptMessage(Role.USER,
|
||||
new TextContent("Hello " + nameArgument + "! How can I assist you today?"));
|
||||
|
||||
return new GetPromptResult("A personalized greeting message", List.of(userMessage));
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenLibrary openLibrary() {
|
||||
return new OpenLibrary(RestClient.builder());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* 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.webmvc.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
|
||||
public class OpenLibrary {
|
||||
|
||||
private RestClient restClient;
|
||||
|
||||
public OpenLibrary(RestClient.Builder restClientBuilder) {
|
||||
this.restClient = restClientBuilder.baseUrl("https://openlibrary.org").build();
|
||||
}
|
||||
|
||||
public record Books(Integer numFound, Integer start, Boolean numFoundExact, List<Map<String, Object>> docs) {
|
||||
}
|
||||
|
||||
public record Book(List<String> isbn, String title, List<String> authorName) {
|
||||
}
|
||||
|
||||
public List<Book> getBooks(String title) {
|
||||
Books books = restClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/search.json").queryParam("q", title).build())
|
||||
.retrieve()
|
||||
.body(Books.class);
|
||||
if (books == null) {
|
||||
return List.of();
|
||||
}
|
||||
return books.docs.stream()
|
||||
.map(doc -> new Book((List<String>) doc.get("isbn"), (String) doc.get("title"),
|
||||
(List<String>) doc.get("author_name")))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<String> getBookTitlesByAuthor(String authorName) {
|
||||
var books = restClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/search/authors.json").queryParam("q", authorName).build())
|
||||
.retrieve()
|
||||
.body(Books.class);
|
||||
if (books == null) {
|
||||
return List.of();
|
||||
}
|
||||
return books.docs.stream().map(doc -> (String) doc.get("top_work")).toList();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
OpenLibrary openLibrary = new OpenLibrary(RestClient.builder());
|
||||
List<Book> books = openLibrary.getBooks("Spring Framework");
|
||||
System.out.println(books);
|
||||
List<String> booksByAuthor = openLibrary.getBookTitlesByAuthor("Craig Walls");
|
||||
System.out.println(booksByAuthor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
spring.main.web-application-type=SERVLET
|
||||
|
||||
# NOTE: You must disable the banner and the console logging
|
||||
# to allow the STDIO transport to work !!!
|
||||
spring.main.banner-mode=off
|
||||
logging.pattern.console=
|
||||
|
||||
transport.mode=sse
|
||||
|
||||
logging.file.name=mcp.webmvc.log
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* 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.webmvc.client;
|
||||
|
||||
import org.springframework.ai.mcp.client.transport.ServerParameters;
|
||||
import org.springframework.ai.mcp.client.transport.StdioClientTransport;
|
||||
|
||||
/**
|
||||
* With stdio transport, the MCP server is automatically started by the client. But you
|
||||
* have to build the server jar first: <pre>./mvnw clean install -DskipTests</pre>
|
||||
*/
|
||||
public class ClientStdio {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dtransport.mode=stdio", "-Dspring.main.web-application-type=none", "-jar",
|
||||
"model-context-protocol/mcp-webmvc-server/target/mcp-webmvc-server-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
|
||||
var transport = new StdioClientTransport(stdioParams);
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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.webmvc.client;
|
||||
|
||||
import org.springframework.ai.mcp.client.transport.WebFluxSseClientTransport;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class ClientWebFluxSse {
|
||||
|
||||
public static void main(String[] args) {
|
||||
var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* 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.webmvc.client;
|
||||
|
||||
import org.springframework.ai.mcp.client.transport.HttpClientSseClientTransport;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class HttpClientSse {
|
||||
|
||||
public static void main(String[] args) {
|
||||
var transport = new HttpClientSseClientTransport("http://localhost:8080");
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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.webmvc.client;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ai.mcp.client.McpClient;
|
||||
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
|
||||
public class SampleClient {
|
||||
|
||||
private final ClientMcpTransport transport;
|
||||
|
||||
public SampleClient(ClientMcpTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
||||
var client = McpClient.sync(this.transport).build();
|
||||
|
||||
client.initialize();
|
||||
|
||||
client.ping();
|
||||
|
||||
// List and demonstrate tools
|
||||
ListToolsResult toolsList = client.listTools();
|
||||
System.out.println("Available Tools = " + toolsList);
|
||||
|
||||
CallToolResult upperCaseResult = client
|
||||
.callTool(new CallToolRequest("toUpperCase", Map.of("input", "accountName")));
|
||||
System.out.println("Upper case Response = " + upperCaseResult);
|
||||
|
||||
CallToolResult books = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework")));
|
||||
System.out.println("Books Response = " + books);
|
||||
|
||||
// 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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
|
||||
#
|
||||
# http://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.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
@@ -1,200 +0,0 @@
|
||||
# Spring AI MCP WebFlux Server Starter
|
||||
|
||||
This project demonstrates how to create a Spring AI Model Context Protocol (MCP) server using Spring WebFlux. It showcases how to implement MCP tools, resources, and prompts with Spring Boot's auto-configuration capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
The project provides:
|
||||
- Spring Boot starter for MCP server implementation
|
||||
- WebFlux-based transport mode
|
||||
- Server capabilities:
|
||||
- Tools support with OpenLibrary integration
|
||||
- Resources support with system information
|
||||
- Prompts support with greeting functionality
|
||||
|
||||
## Building the Project
|
||||
|
||||
```bash
|
||||
./mvnw clean package
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
The server uses WebFlux transport mode by default:
|
||||
|
||||
```bash
|
||||
./mvnw spring-boot:run
|
||||
```
|
||||
|
||||
## Running the Client Test
|
||||
|
||||
After starting the server, you can run the WebFlux SSE client test located in `src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java`. This test demonstrates how to connect to the server using Spring WebFlux's SSE transport.
|
||||
|
||||
To run the test:
|
||||
1. Ensure the server is running (previous step)
|
||||
2. Open `ClientWebFluxSse.java` in your IDE
|
||||
3. Run the main method in `ClientWebFluxSse` class
|
||||
|
||||
The test creates a WebFlux SSE client that connects to `http://localhost:8080` and demonstrates the client-server interaction using the Model Context Protocol.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### OpenLibrary Tools
|
||||
The server integrates with the OpenLibrary API to provide book-related functionality:
|
||||
|
||||
#### Get Books by Title
|
||||
- Description: Get list of Books by title
|
||||
- Parameters:
|
||||
- `title`: String - The book title to search for
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("getBooks", Map.of("title", "Spring Framework"))
|
||||
);
|
||||
```
|
||||
|
||||
#### Get Book Titles by Author
|
||||
- Description: Get book titles by author
|
||||
- Parameters:
|
||||
- `authorName`: String - The author name to search for
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("getBookTitlesByAuthor", Map.of("authorName", "Craig Walls"))
|
||||
);
|
||||
```
|
||||
|
||||
### Text Processing Tool
|
||||
#### toUpperCase
|
||||
- Description: Converts input text to uppercase
|
||||
- Parameters:
|
||||
- `input`: String - The text to convert
|
||||
- Example:
|
||||
```java
|
||||
CallToolResult response = client.callTool(
|
||||
new CallToolRequest("toUpperCase", Map.of("input", "hello world"))
|
||||
);
|
||||
```
|
||||
|
||||
## Available Resources
|
||||
|
||||
### System Information Resource
|
||||
- URI: `system://info`
|
||||
- Description: Provides basic system information including Java version, OS, etc.
|
||||
- MIME Type: application/json
|
||||
- Returns: JSON object containing:
|
||||
- javaVersion
|
||||
- osName
|
||||
- osVersion
|
||||
- osArch
|
||||
- processors
|
||||
- timestamp
|
||||
|
||||
## Available Prompts
|
||||
|
||||
### Greeting Prompt
|
||||
- Name: `greeting`
|
||||
- Description: A friendly greeting prompt
|
||||
- Parameters:
|
||||
- `name`: String (required) - The name to greet
|
||||
- Returns: A personalized greeting message
|
||||
|
||||
## Configuration
|
||||
|
||||
The application can be configured through `application.properties`:
|
||||
|
||||
```properties
|
||||
# Server Configuration
|
||||
spring.ai.mcp.server.enabled=true
|
||||
spring.ai.mcp.server.name=my-webflux-server-starter
|
||||
spring.ai.mcp.server.version=0.0.1
|
||||
spring.ai.mcp.server.transport=WEBFLUX
|
||||
|
||||
# Logging Configuration (Required for proper operation)
|
||||
spring.main.banner-mode=off
|
||||
logging.pattern.console=
|
||||
logging.file.name=./target/mcp.webflux-server-starter.log
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Server Configuration
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class McpServerApplication {
|
||||
@Bean
|
||||
public ToolCallbackProvider openLibraryTools(OpenLibrary openLibrary) {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(openLibrary).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallback tools(OpenLibrary openLibrary) {
|
||||
return FunctionToolCallback
|
||||
.builder("toUpperCase", (Function<ToUpperCaseInput, String>) s -> s.input().toUpperCase())
|
||||
.description("To upper case")
|
||||
.inputType(ToUpperCaseInput.class)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OpenLibrary Integration
|
||||
```java
|
||||
@Service
|
||||
public class OpenLibrary {
|
||||
@Tool(description = "Get list of Books by title")
|
||||
public List<Book> getBooks(String title) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
@Tool(description = "Get book titles by author")
|
||||
public List<String> getBookTitlesByAuthor(String authorName) {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Implementation
|
||||
```java
|
||||
@Bean
|
||||
public List<McpServerFeatures.SyncResourceRegistration> resourceRegistrations() {
|
||||
var systemInfoResource = new McpSchema.Resource(
|
||||
"system://info",
|
||||
"System Information",
|
||||
"Provides basic system information including Java version, OS, etc.",
|
||||
"application/json",
|
||||
null
|
||||
);
|
||||
|
||||
// Resource registration implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Prompt Implementation
|
||||
```java
|
||||
@Bean
|
||||
public List<McpServerFeatures.SyncPromptRegistration> promptRegistrations() {
|
||||
var prompt = new McpSchema.Prompt(
|
||||
"greeting",
|
||||
"A friendly greeting prompt",
|
||||
List.of(new McpSchema.PromptArgument("name", "The name to greet", true))
|
||||
);
|
||||
|
||||
// Prompt registration implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Spring Boot Integration**: Leverages Spring Boot's auto-configuration for easy setup
|
||||
2. **WebFlux Transport**: Uses reactive programming model for efficient request handling
|
||||
3. **OpenLibrary Integration**: Demonstrates external API integration
|
||||
4. **System Resource**: Shows how to expose system information as an MCP resource
|
||||
5. **Function Tools**: Examples of both service-based and function-based tool implementations
|
||||
6. **Prompt Support**: Implements basic prompt functionality with parameter handling
|
||||
|
||||
## Notes
|
||||
|
||||
- The server is configured to use file-based logging to ensure proper operation of the transport layer
|
||||
- Banner mode is disabled to prevent interference with the transport protocol
|
||||
- The OpenLibrary integration demonstrates how to integrate external REST APIs with MCP tools
|
||||
@@ -1,259 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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
|
||||
#
|
||||
# http://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.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
@@ -1,149 +0,0 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
@@ -1,117 +0,0 @@
|
||||
package org.springframework.ai.mcp.sample.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Role;
|
||||
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallbacks;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@SpringBootApplication
|
||||
public class McpServerApplication {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(McpServerApplication.class);
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(McpServerApplication.class, args);
|
||||
}
|
||||
|
||||
public static record ToUpperCaseInput(String input) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider openLibraryTools(OpenLibrary openLibrary) {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(openLibrary).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallback tools(OpenLibrary openLibrary) {
|
||||
return FunctionToolCallback
|
||||
.builder("toUpperCase", (Function<ToUpperCaseInput, String>) s -> s.input().toUpperCase())
|
||||
.description("To upper case")
|
||||
.inputType(ToUpperCaseInput.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<McpServerFeatures.SyncResourceRegistration> resourceRegistrations() {
|
||||
|
||||
// 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 McpServerFeatures.SyncResourceRegistration(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 List.of(resourceRegistration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<McpServerFeatures.SyncPromptRegistration> promptRegistrations() {
|
||||
|
||||
var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt",
|
||||
List.of(new McpSchema.PromptArgument("name", "The name to greet", true)));
|
||||
|
||||
var promptRegistration = new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> {
|
||||
|
||||
String nameArgument = (String) getPromptRequest.arguments().get("name");
|
||||
if (nameArgument == null) {
|
||||
nameArgument = "friend";
|
||||
}
|
||||
|
||||
var userMessage = new PromptMessage(Role.USER,
|
||||
new TextContent("Hello " + nameArgument + "! How can I assist you today?"));
|
||||
|
||||
return new GetPromptResult("A personalized greeting message", List.of(userMessage));
|
||||
});
|
||||
|
||||
return List.of(promptRegistration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Consumer<List<McpSchema.Root>> rootsChangeConsumer() {
|
||||
return roots -> {
|
||||
logger.info("Registering root resources: {}", roots);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* 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.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@Service
|
||||
public class OpenLibrary {
|
||||
|
||||
private RestClient restClient;
|
||||
|
||||
public OpenLibrary() {
|
||||
this(RestClient.builder());
|
||||
}
|
||||
|
||||
public OpenLibrary(RestClient.Builder restClientBuilder) {
|
||||
this.restClient = restClientBuilder.baseUrl("https://openlibrary.org").build();
|
||||
}
|
||||
|
||||
public record Books(Integer numFound, Integer start, Boolean numFoundExact, List<Map<String, Object>> docs) {
|
||||
}
|
||||
|
||||
public record Book(List<String> isbn, String title, List<String> authorName) {
|
||||
}
|
||||
|
||||
@Tool(description = "Get list of Books by title")
|
||||
public List<Book> getBooks(String title) {
|
||||
Books books = restClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/search.json").queryParam("q", title).build())
|
||||
.retrieve()
|
||||
.body(Books.class);
|
||||
if (books == null) {
|
||||
return List.of();
|
||||
}
|
||||
return books.docs.stream()
|
||||
.map(doc -> new Book((List<String>) doc.get("isbn"), (String) doc.get("title"),
|
||||
(List<String>) doc.get("author_name")))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Tool(description = "Get book titles by author")
|
||||
public List<String> getBookTitlesByAuthor(String authorName) {
|
||||
var books = restClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/search/authors.json").queryParam("q", authorName).build())
|
||||
.retrieve()
|
||||
.body(Books.class);
|
||||
if (books == null) {
|
||||
return List.of();
|
||||
}
|
||||
return books.docs.stream().map(doc -> (String) doc.get("top_work")).toList();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
OpenLibrary openLibrary = new OpenLibrary(RestClient.builder());
|
||||
List<Book> books = openLibrary.getBooks("Spring Framework");
|
||||
System.out.println(books);
|
||||
List<String> booksByAuthor = openLibrary.getBookTitlesByAuthor("Craig Walls");
|
||||
System.out.println(booksByAuthor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# NOTE: You must disable the banner and the console logging
|
||||
# to allow the STDIO transport to work !!!
|
||||
spring.main.banner-mode=off
|
||||
# logging.pattern.console=
|
||||
logging.file.name=./model-context-protocol/mcp-webflux-server-starter/target/mcp.webflux-server-starter.log
|
||||
|
||||
spring.ai.mcp.server.name=my-webflux-server-starter
|
||||
spring.ai.mcp.server.version=0.0.1
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* 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 io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
|
||||
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class ClientWebFluxSse {
|
||||
|
||||
public static void main(String[] args) {
|
||||
var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080"));
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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 io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
|
||||
public class SampleClient {
|
||||
|
||||
private final ClientMcpTransport transport;
|
||||
|
||||
public SampleClient(ClientMcpTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
||||
var client = McpClient.sync(this.transport).build();
|
||||
|
||||
client.initialize();
|
||||
|
||||
client.ping();
|
||||
|
||||
// List and demonstrate tools
|
||||
ListToolsResult toolsList = client.listTools();
|
||||
System.out.println("Available Tools = " + toolsList);
|
||||
|
||||
CallToolResult upperCaseResult = client
|
||||
.callTool(new CallToolRequest("toUpperCase", Map.of("input", "accountName")));
|
||||
System.out.println("Upper case Response = " + upperCaseResult);
|
||||
|
||||
CallToolResult books = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework")));
|
||||
System.out.println("Books Response = " + books);
|
||||
|
||||
// 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
BIN
model-context-protocol/brave/.DS_Store
vendored
Normal file
BIN
model-context-protocol/brave/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1,20 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.6</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>ai-mcp-brave</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>Spring AI - Model Context Protocol - Brave</name>
|
||||
<description>Simple AI Application using MCP client to use Brave for Internet search</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<spring-ai.version>1.0.0-SNAPSHOT</spring-ai.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -22,7 +25,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -37,16 +40,14 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -78,4 +79,4 @@
|
||||
</repositories>
|
||||
|
||||
|
||||
</project>
|
||||
</project>
|
||||
@@ -1,14 +1,13 @@
|
||||
package org.springframework.ai.mcp.samples.brave;
|
||||
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import java.util.List;
|
||||
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallback;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -26,16 +25,12 @@ public class Application {
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder,
|
||||
McpSyncClient mcpSyncClient, ConfigurableApplicationContext context) {
|
||||
List<McpSyncClient> mcpSyncClients, ConfigurableApplicationContext context) {
|
||||
|
||||
return args -> {
|
||||
|
||||
var chatClient = chatClientBuilder
|
||||
.defaultTools(mcpSyncClient.listTools(null)
|
||||
.tools()
|
||||
.stream()
|
||||
.map(tool -> new SyncMcpToolCallback(mcpSyncClient, tool))
|
||||
.toArray(SyncMcpToolCallback[]::new))
|
||||
.defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients))
|
||||
.build();
|
||||
|
||||
String question = "Does Spring AI supports the Model Context Protocol? Please provide some references.";
|
||||
@@ -45,20 +40,4 @@ public class Application {
|
||||
context.close();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public McpSyncClient mcpClient() {
|
||||
|
||||
// https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search
|
||||
var stdioParams = ServerParameters.builder("npx")
|
||||
.args("-y", "@modelcontextprotocol/server-brave-search")
|
||||
.addEnvVar("BRAVE_API_KEY", System.getenv("BRAVE_API_KEY"))
|
||||
.build();
|
||||
|
||||
var mcpClient = McpClient.sync(new StdioClientTransport(stdioParams)).build();
|
||||
var init = mcpClient.initialize();
|
||||
logger.info("MCP Initialized: {}", init);
|
||||
return mcpClient;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
debug=true
|
||||
spring.application.name=mcp
|
||||
spring.main.web-application-type=none
|
||||
|
||||
spring.ai.openai.api-key=${OPENAI_API_KEY}
|
||||
|
||||
spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
|
||||
|
||||
# Logging configuration
|
||||
logging.level.root=INFO
|
||||
logging.level.org.springframework.ai.mcp=WARN
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"brave-search": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-brave-search"
|
||||
],
|
||||
"env": {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,17 +36,17 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency> -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-anthropic</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -14,4 +14,6 @@ spring.ai.mcp.client.stdio.connections.brave-search.args=-y,@modelcontextprotoco
|
||||
logging.level.io.modelcontextprotocol.client=WARN
|
||||
logging.level.io.modelcontextprotocol.spec=WARN
|
||||
|
||||
ai.user.input=What tools are available?
|
||||
ai.user.input=What tools are available?
|
||||
|
||||
spring.ai.mcp.client.toolcallback.enabled=true
|
||||
@@ -36,17 +36,17 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency> -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-anthropic</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -13,4 +13,7 @@ spring.ai.mcp.client.stdio.connections.brave-search.args=-y,@modelcontextprotoco
|
||||
logging.level.io.modelcontextprotocol.client=WARN
|
||||
logging.level.io.modelcontextprotocol.spec=WARN
|
||||
|
||||
ai.user.input=What tools are available?
|
||||
ai.user.input=What tools are available?
|
||||
|
||||
# Enable the mcp client tool-callback auto-configuration
|
||||
spring.ai.mcp.client.toolcallback.enabled=true
|
||||
@@ -22,7 +22,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>1.0.0-M5</version>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -37,13 +37,12 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,14 +2,14 @@ package org.springframework.ai.mcp.samples.filesystem;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.mcp.client.McpClient;
|
||||
import org.springframework.ai.mcp.client.McpSyncClient;
|
||||
import org.springframework.ai.mcp.client.transport.ServerParameters;
|
||||
import org.springframework.ai.mcp.client.transport.StdioClientTransport;
|
||||
import org.springframework.ai.mcp.spring.McpFunctionCallback;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -25,22 +25,22 @@ public class Application {
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder,
|
||||
List<McpFunctionCallback> functionCallbacks, ConfigurableApplicationContext context) {
|
||||
McpSyncClient mcpClient, ConfigurableApplicationContext context) {
|
||||
|
||||
return args -> {
|
||||
var chatClient = chatClientBuilder
|
||||
.defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0]))
|
||||
.defaultTools(new SyncMcpToolCallbackProvider(mcpClient))
|
||||
.build();
|
||||
|
||||
System.out.println("Running predefined questions with AI model responses:\n");
|
||||
|
||||
// Question 1
|
||||
String question1 = "Can you explain the content of the spring-ai-mcp-overview.txt file?";
|
||||
String question1 = "Can you explain the content of the target/spring-ai-mcp-overview.txt file?";
|
||||
System.out.println("QUESTION: " + question1);
|
||||
System.out.println("ASSISTANT: " + chatClient.prompt(question1).call().content());
|
||||
|
||||
// Question 2
|
||||
String question2 = "Pleses summarize the content of the spring-ai-mcp-overview.txt file and store it a new summary.md as Markdown format?";
|
||||
String question2 = "Pleses summarize the content of the target/spring-ai-mcp-overview.txt file and store it a new target/summary.md as Markdown format?";
|
||||
System.out.println("\nQUESTION: " + question2);
|
||||
System.out.println("ASSISTANT: " +
|
||||
chatClient.prompt(question2).call().content());
|
||||
@@ -50,17 +50,6 @@ public class Application {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<McpFunctionCallback> functionCallbacks(McpSyncClient mcpClient) {
|
||||
|
||||
var callbacks = mcpClient.listTools(null)
|
||||
.tools()
|
||||
.stream()
|
||||
.map(tool -> new McpFunctionCallback(mcpClient, tool))
|
||||
.toList();
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public McpSyncClient mcpClient() {
|
||||
|
||||
|
||||
190
model-context-protocol/sampling/README.md
Normal file
190
model-context-protocol/sampling/README.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Spring AI MCP Sampling Examples
|
||||
|
||||
This directory contains examples demonstrating the Model Context Protocol (MCP) Sampling capability in Spring AI. MCP Sampling allows an MCP server to delegate certain requests to LLM providers, enabling multi-model interactions and creative content generation.
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP Sampling examples showcase:
|
||||
|
||||
- How an MCP server can delegate LLM requests to a client
|
||||
- How a client can route requests to different LLM providers based on model hints
|
||||
- Integration with multiple LLM providers (OpenAI and Anthropic)
|
||||
- Creative content generation using multiple models
|
||||
- Combining responses from different LLMs into a unified result
|
||||
|
||||
## Projects
|
||||
|
||||
This directory contains two main projects:
|
||||
|
||||
1. **[mcp-weather-webmvc-server](./mcp-weather-webmvc-server)**: An MCP server that provides weather information and uses MCP Sampling to generate creative content
|
||||
2. **[mcp-sampling-client](./mcp-sampling-client)**: An MCP client that handles sampling requests and routes them to different LLM providers
|
||||
|
||||
## What is MCP Sampling?
|
||||
|
||||
MCP Sampling is a powerful capability of the Model Context Protocol that allows:
|
||||
|
||||
- An MCP server to delegate certain requests to LLM providers
|
||||
- Specifying model preferences to target specific LLM providers
|
||||
- Routing requests based on model hints
|
||||
- Combining responses from multiple LLMs
|
||||
|
||||
This approach enables applications to leverage multiple LLM providers within a single workflow, allowing for creative content generation, model comparison, and specialized task delegation.
|
||||
|
||||
## How It Works
|
||||
|
||||
The MCP Sampling workflow in these examples follows these steps:
|
||||
|
||||
1. **Client Initialization**:
|
||||
- The client connects to the MCP Weather Server
|
||||
- It registers a sampling handler that can route requests to different LLM providers
|
||||
|
||||
2. **User Query**:
|
||||
- The user sends a weather-related query to the client
|
||||
- The client forwards the query to the MCP Weather Server
|
||||
|
||||
3. **Server Processing**:
|
||||
- The server retrieves weather data from the Open-Meteo API
|
||||
- It extracts the `McpSyncServerExchange` from the tool context
|
||||
- It creates sampling requests with different model preferences:
|
||||
- One targeting OpenAI with `ModelPreferences.builder().addHint("openai").build()`
|
||||
- One targeting Anthropic with `ModelPreferences.builder().addHint("anthropic").build()`
|
||||
|
||||
4. **Sampling Delegation**:
|
||||
- The server sends the sampling requests back to the client
|
||||
- The client's sampling handler receives the requests
|
||||
|
||||
5. **Model Routing**:
|
||||
- The client extracts the model hint from each request
|
||||
- It selects the appropriate LLM provider based on the hint
|
||||
- It forwards the prompt to the selected LLM
|
||||
|
||||
6. **Response Generation**:
|
||||
- Each LLM generates a creative response (in this case, a poem about the weather)
|
||||
- The client returns the responses to the server
|
||||
|
||||
7. **Result Combination**:
|
||||
- The server combines the responses from different LLMs
|
||||
- It returns the combined result to the user
|
||||
|
||||
## Server Implementation
|
||||
|
||||
The MCP Weather Server implements the server-side of MCP Sampling:
|
||||
|
||||
```java
|
||||
public String callMcpSampling(ToolContext toolContext, WeatherResponse weatherResponse) {
|
||||
String openAiWeatherPoem = "<no OpenAI poem>";
|
||||
String anthropicWeatherPoem = "<no Anthropic poem>";
|
||||
|
||||
if (toolContext != null && toolContext.getContext().containsKey("exchange")) {
|
||||
// Spring AI MCP Auto-configuration injects the McpSyncServerExchange into the ToolContext
|
||||
McpSyncServerExchange exchange = (McpSyncServerExchange) toolContext.getContext().get("exchange");
|
||||
if (exchange.getClientCapabilities().sampling() != null) {
|
||||
var messageRequestBuilder = McpSchema.CreateMessageRequest.builder()
|
||||
.systemPrompt("You are a poet!")
|
||||
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
|
||||
new McpSchema.TextContent(
|
||||
"Please write a poem about this weather forecast (temperature is in Celsius)..."))));
|
||||
|
||||
// Request poem from OpenAI
|
||||
var openAiLlmMessageRequest = messageRequestBuilder
|
||||
.modelPreferences(ModelPreferences.builder().addHint("openai").build())
|
||||
.build();
|
||||
CreateMessageResult openAiLlmResponse = exchange.createMessage(openAiLlmMessageRequest);
|
||||
openAiWeatherPoem = ((McpSchema.TextContent) openAiLlmResponse.content()).text();
|
||||
|
||||
// Request poem from Anthropic
|
||||
var anthropicLlmMessageRequest = messageRequestBuilder
|
||||
.modelPreferences(ModelPreferences.builder().addHint("anthropic").build())
|
||||
.build();
|
||||
CreateMessageResult anthropicAiLlmResponse = exchange.createMessage(anthropicLlmMessageRequest);
|
||||
anthropicWeatherPoem = ((McpSchema.TextContent) anthropicAiLlmResponse.content()).text();
|
||||
}
|
||||
}
|
||||
|
||||
// Combine responses
|
||||
return "OpenAI poem about the weather: " + openAiWeatherPoem + "\n\n" +
|
||||
"Anthropic poem about the weather: " + anthropicWeatherPoem + "\n" +
|
||||
ModelOptionsUtils.toJsonStringPrettyPrinter(weatherResponse);
|
||||
}
|
||||
```
|
||||
|
||||
## Client Implementation
|
||||
|
||||
The MCP Sampling Client implements the client-side handling of sampling requests:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
McpSyncClientCustomizer samplingCustomizer(Map<String, ChatClient> chatClients) {
|
||||
return (name, spec) -> {
|
||||
spec.sampling(llmRequest -> {
|
||||
var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
|
||||
String modelHint = llmRequest.modelPreferences().hints().get(0).name();
|
||||
|
||||
// Find the appropriate chat client based on the model hint
|
||||
ChatClient hintedChatClient = chatClients.entrySet().stream()
|
||||
.filter(e -> e.getKey().contains(modelHint)).findFirst()
|
||||
.orElseThrow().getValue();
|
||||
|
||||
// Generate response using the selected model
|
||||
String response = hintedChatClient.prompt()
|
||||
.system(llmRequest.systemPrompt())
|
||||
.user(userPrompt)
|
||||
.call()
|
||||
.content();
|
||||
|
||||
return CreateMessageResult.builder().content(new McpSchema.TextContent(response)).build();
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Running the Examples
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Java 17 or later
|
||||
- Maven 3.6+
|
||||
- OpenAI API key
|
||||
- Anthropic API key
|
||||
|
||||
### Step 1: Start the MCP Weather Server
|
||||
|
||||
```bash
|
||||
cd mcp-weather-webmvc-server
|
||||
./mvnw clean install -DskipTests
|
||||
java -jar target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
### Step 2: Set Environment Variables
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-openai-key
|
||||
export ANTHROPIC_API_KEY=your-anthropic-key
|
||||
```
|
||||
|
||||
### Step 3: Run the MCP Sampling Client
|
||||
|
||||
```bash
|
||||
cd mcp-sampling-client
|
||||
./mvnw clean install
|
||||
java -Dai.user.input='What is the weather in Amsterdam right now?' -jar target/mcp-sampling-client-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
## Sample Output
|
||||
|
||||
When you run the application, you'll see creative responses from both OpenAI and Anthropic models, each generating a poem about the current weather in Amsterdam.
|
||||
|
||||
## Related Projects
|
||||
|
||||
Spring AI provides several MCP client and server implementations:
|
||||
|
||||
- **[starter-default-client](../client-starter/starter-default-client)**: A default MCP client implementation using Spring Boot
|
||||
- **[starter-webflux-client](../client-starter/starter-webflux-client)**: An MCP client implementation using Spring WebFlux
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/)
|
||||
* [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html)
|
||||
* [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html)
|
||||
* [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/)
|
||||
* [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/)
|
||||
BIN
model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
2
model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
2
model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
|
||||
221
model-context-protocol/sampling/mcp-sampling-client/README.md
Normal file
221
model-context-protocol/sampling/mcp-sampling-client/README.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Spring AI MCP Sampling Client
|
||||
|
||||
This project demonstrates how to implement a client for the MCP (Model Context Protocol) Sampling capability using Spring AI. It showcases how to route LLM requests to different providers (OpenAI and Anthropic) based on model preferences.
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP Sampling Client:
|
||||
- Connects to an MCP server using SSE (Server-Sent Events) transport
|
||||
- Implements a sampling handler that routes requests to different LLM providers
|
||||
- Integrates with both OpenAI and Anthropic models
|
||||
- Demonstrates how to use model hints to select the appropriate LLM
|
||||
- Combines creative responses from multiple LLMs into a single result
|
||||
|
||||
## MCP Sampling Implementation
|
||||
|
||||
MCP Sampling is a powerful capability that allows an MCP server to delegate certain requests to LLM providers. This client implements the client-side handling of sampling requests:
|
||||
|
||||
1. **Sampling Handler Registration**: The client registers a sampling handler using the `McpSyncClientCustomizer`:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
McpSyncClientCustomizer samplingCustomizer(Map<String, ChatClient> chatClients) {
|
||||
return (name, spec) -> {
|
||||
spec.sampling(llmRequest -> {
|
||||
var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
|
||||
String modelHint = llmRequest.modelPreferences().hints().get(0).name();
|
||||
|
||||
// Find the appropriate chat client based on the model hint
|
||||
ChatClient hintedChatClient = chatClients.entrySet().stream()
|
||||
.filter(e -> e.getKey().contains(modelHint)).findFirst()
|
||||
.orElseThrow().getValue();
|
||||
|
||||
// Generate response using the selected model
|
||||
String response = hintedChatClient.prompt()
|
||||
.system(llmRequest.systemPrompt())
|
||||
.user(userPrompt)
|
||||
.call()
|
||||
.content();
|
||||
|
||||
return CreateMessageResult.builder().content(new McpSchema.TextContent(response)).build();
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. **Model Routing**: The client uses the model hint from the request to select the appropriate LLM provider:
|
||||
- If the hint is "openai", it routes to the OpenAI model
|
||||
- If the hint is "anthropic", it routes to the Anthropic model
|
||||
|
||||
3. **Chat Client Management**: The client creates and manages multiple chat clients, one for each LLM provider:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public Map<String, ChatClient> chatClients(List<ChatModel> chatModels) {
|
||||
return chatModels.stream().collect(Collectors.toMap(
|
||||
model -> model.getClass().getSimpleName().toLowerCase(),
|
||||
model -> ChatClient.builder(model).build()));
|
||||
}
|
||||
```
|
||||
|
||||
4. **Integration with Spring AI**: The client leverages Spring AI's auto-configuration to set up the necessary components:
|
||||
|
||||
```java
|
||||
var mcpToolProvider = new SyncMcpToolCallbackProvider(
|
||||
mcpClientsProvider.stream().flatMap(List::stream).toList());
|
||||
|
||||
ChatClient chatClient = ChatClient.builder(openAiChatModel)
|
||||
.defaultTools(mcpToolProvider)
|
||||
.build();
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
The project requires the following Spring AI dependencies:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Application Properties
|
||||
|
||||
The application is configured through `application.properties`:
|
||||
|
||||
```properties
|
||||
spring.application.name=mcp
|
||||
spring.main.web-application-type=none
|
||||
|
||||
# Disable the chat client auto-configuration because we are using multiple chat models
|
||||
spring.ai.chat.client.enabled=false
|
||||
|
||||
# API keys for LLM providers
|
||||
spring.ai.openai.api-key=${OPENAI_API_KEY}
|
||||
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
|
||||
|
||||
# MCP server connection
|
||||
spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080
|
||||
|
||||
# Logging configuration
|
||||
logging.level.io.modelcontextprotocol.client=WARN
|
||||
logging.level.io.modelcontextprotocol.spec=WARN
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The application demonstrates MCP Sampling with a weather-related query:
|
||||
|
||||
1. The client connects to the MCP Weather Server
|
||||
2. It sends a weather-related question: "What is the weather in Amsterdam right now?"
|
||||
3. The server retrieves the weather data and sends sampling requests to the client
|
||||
4. The client routes each request to the appropriate LLM based on model hints
|
||||
5. Each LLM generates a creative poem about the weather
|
||||
6. The server combines the responses and returns them to the user
|
||||
|
||||
## Running the Application
|
||||
|
||||
1. First, start the MCP Weather Server:
|
||||
```bash
|
||||
cd ../mcp-weather-webmvc-server
|
||||
./mvnw clean install -DskipTests
|
||||
java -jar target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
2. Set the required environment variables:
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-openai-key
|
||||
export ANTHROPIC_API_KEY=your-anthropic-key
|
||||
```
|
||||
|
||||
3. Build and run the MCP Sampling Client:
|
||||
```bash
|
||||
cd ../mcp-sampling-client
|
||||
./mvnw clean install
|
||||
java -Dai.user.input='What is the weather in Amsterdam right now?' -jar target/mcp-sampling-client-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
The application will:
|
||||
1. Connect to the MCP Weather Server
|
||||
2. Process the weather query
|
||||
3. Handle sampling requests from the server
|
||||
4. Route each request to the appropriate LLM
|
||||
5. Display the combined creative responses
|
||||
|
||||
## Sample Output
|
||||
|
||||
When you run the application, you'll see output similar to:
|
||||
|
||||
```
|
||||
> USER: What is the weather in Amsterdam right now?
|
||||
Please incorporate all creative responses from all LLM providers.
|
||||
After the other providers add a poem that synthesizes the the poems from all the other providers.
|
||||
|
||||
> ASSISTANT: I checked the current weather in Amsterdam for you. Here are the creative responses from different AI providers:
|
||||
|
||||
OpenAI poem about the weather:
|
||||
# Amsterdam's Embrace
|
||||
|
||||
In Amsterdam, where canals reflect the sky,
|
||||
A gentle warmth of sixteen degrees goes by.
|
||||
The autumn air, a crisp and tender touch,
|
||||
Caresses faces of the Dutch.
|
||||
|
||||
Time stands still at this perfect hour,
|
||||
As sunshine breaks through with gentle power.
|
||||
The city breathes with calm delight,
|
||||
In this moment, everything feels right.
|
||||
|
||||
Anthropic poem about the weather:
|
||||
## Amsterdam Today
|
||||
|
||||
Sixteen degrees in Amsterdam's embrace,
|
||||
A perfect autumn day unfolds with grace.
|
||||
The canals reflect the passing clouds above,
|
||||
As the city hums with life and love.
|
||||
|
||||
Neither cold nor warm, but just between,
|
||||
The perfect weather for this Dutch scene.
|
||||
Time captured in this moment's gentle hold,
|
||||
As Amsterdam's story continues to unfold.
|
||||
|
||||
My synthesis of these weather poems:
|
||||
# Amsterdam's Gentle Harmony
|
||||
|
||||
Where canals mirror skies in Dutch design,
|
||||
Sixteen degrees - a temperature divine.
|
||||
Neither cold nor warm, but perfectly between,
|
||||
The autumn air paints Amsterdam's scene.
|
||||
|
||||
Time seems suspended in this golden hour,
|
||||
As gentle sunshine shows its subtle power.
|
||||
The city breathes, alive with calm delight,
|
||||
In this moment, everything feels just right.
|
||||
|
||||
Amsterdam's story, continuing to unfold,
|
||||
Embraces all within its gentle hold.
|
||||
A perfect day that poets can't ignore,
|
||||
In this beautiful city we all adore.
|
||||
|
||||
Current weather data shows the temperature is 16°C in Amsterdam right now.
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/)
|
||||
* [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html)
|
||||
* [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/)
|
||||
* [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/)
|
||||
305
model-context-protocol/sampling/mcp-sampling-client/mvnw
vendored
Executable file
305
model-context-protocol/sampling/mcp-sampling-client/mvnw
vendored
Executable file
@@ -0,0 +1,305 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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
|
||||
#
|
||||
# http://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.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven2 Start Up Batch script
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# M2_HOME - location of maven2's installed home dir
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
||||
else
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`which java`"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
echo "${basedir}"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=`find_maven_basedir "$(pwd)"`
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
||||
fi
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
||||
fi
|
||||
if [ "$MVNW_REPOURL" = true]; then
|
||||
jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar"
|
||||
else
|
||||
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar"
|
||||
fi
|
||||
while IFS="=" read key value; do
|
||||
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
|
||||
esac
|
||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Downloading from: $jarUrl"
|
||||
fi
|
||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if $cygwin; then
|
||||
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget "$jarUrl" -O "$wrapperJarPath"
|
||||
else
|
||||
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl -o "$wrapperJarPath" "$jarUrl" -f
|
||||
else
|
||||
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaClass=`cygpath --path --windows "$javaClass"`
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
# Compiling the Java class
|
||||
("$JAVA_HOME/bin/javac" "$javaClass")
|
||||
fi
|
||||
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
# Running the downloader
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Running MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
fi
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
fi
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
172
model-context-protocol/sampling/mcp-sampling-client/mvnw.cmd
vendored
Executable file
172
model-context-protocol/sampling/mcp-sampling-client/mvnw.cmd
vendored
Executable file
@@ -0,0 +1,172 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Maven2 Start Up Batch script
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM M2_HOME - location of maven2's installed home dir
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
|
||||
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar"
|
||||
|
||||
FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
echo Found %WRAPPER_JAR%
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar"
|
||||
)
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %DOWNLOAD_URL%
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
|
||||
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%" == "on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
|
||||
|
||||
exit /B %ERROR_CODE%
|
||||
94
model-context-protocol/sampling/mcp-sampling-client/pom.xml
Normal file
94
model-context-protocol/sampling/mcp-sampling-client/pom.xml
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.6</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>mcp-sampling-client</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>Spring AI - MCP Starter Default Client</name>
|
||||
<description>Spring AI - MCP Starter Default Client</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<spring-ai.version>1.0.0-SNAPSHOT</spring-ai.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-anthropic</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<name>Central Portal Snapshots</name>
|
||||
<id>central-portal-snapshots</id>
|
||||
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/libs-milestone-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2025-2025 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.samples.client;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@SpringBootApplication
|
||||
public class McpClientApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(McpClientApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner predefinedQuestions(OpenAiChatModel openAiChatModel,
|
||||
ConfigurableApplicationContext context, List<McpSyncClient> mcpClients) {
|
||||
|
||||
return args -> {
|
||||
|
||||
var mcpToolProvider = new SyncMcpToolCallbackProvider(mcpClients);
|
||||
|
||||
ChatClient chatClient = ChatClient.builder(openAiChatModel).defaultTools(mcpToolProvider).build();
|
||||
|
||||
String userQuestion = """
|
||||
What is the weather in Amsterdam right now?
|
||||
Please incorporate all createive responses from all LLM providers.
|
||||
After the other providers add a poem that synthesizes the the poems from all the other providers.
|
||||
""";
|
||||
|
||||
System.out.println("> USER: " + userQuestion);
|
||||
System.out.println("> ASSISTANT: " + chatClient.prompt(userQuestion).call().content());
|
||||
|
||||
context.close();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
McpSyncClientCustomizer samplingCustomizer(Map<String, ChatClient> chatClients) {
|
||||
|
||||
return (name, mcpClientSpec) -> {
|
||||
mcpClientSpec.sampling(llmRequest -> {
|
||||
var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
|
||||
String modelHint = llmRequest.modelPreferences().hints().get(0).name();
|
||||
|
||||
ChatClient hintedChatClient = chatClients.entrySet().stream()
|
||||
.filter(e -> e.getKey().contains(modelHint)).findFirst()
|
||||
.orElseThrow().getValue();
|
||||
|
||||
String response = hintedChatClient.prompt()
|
||||
.system(llmRequest.systemPrompt())
|
||||
.user(userPrompt)
|
||||
.call()
|
||||
.content();
|
||||
|
||||
return CreateMessageResult.builder().content(new McpSchema.TextContent(response)).build();
|
||||
});
|
||||
System.out.println("Customizing " + name);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, ChatClient> chatClients(List<ChatModel> chatModels) {
|
||||
|
||||
return chatModels.stream().collect(Collectors.toMap(model -> model.getClass().getSimpleName().toLowerCase(),
|
||||
model -> ChatClient.builder(model).build()));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
spring.application.name=mcp
|
||||
spring.main.web-application-type=none
|
||||
|
||||
# Disable the chat client auto-configuration because we are using multiple chat models
|
||||
spring.ai.chat.client.enabled=false
|
||||
|
||||
spring.ai.openai.api-key=${OPENAI_API_KEY}
|
||||
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
|
||||
|
||||
spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080
|
||||
|
||||
logging.level.io.modelcontextprotocol.client=WARN
|
||||
logging.level.io.modelcontextprotocol.spec=WARN
|
||||
|
||||
|
||||
# spring.ai.mcp.client.toolcallback.enabled=false
|
||||
@@ -0,0 +1,244 @@
|
||||
# Spring AI MCP Weather Server Sample with WebMVC Starter and Sampling
|
||||
|
||||
This sample project demonstrates how to create an MCP server using the Spring AI MCP Server Boot Starter with WebMVC transport. It implements a weather service that exposes tools for retrieving weather information using the Open-Meteo API and showcases MCP Sampling capabilities.
|
||||
|
||||
For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample showcases:
|
||||
- Integration with `spring-ai-mcp-server-webmvc-spring-boot-starter`
|
||||
- Support for both SSE (Server-Sent Events) and STDIO transports
|
||||
- Automatic tool registration using Spring AI's `@Tool` annotation
|
||||
- MCP Sampling implementation that demonstrates LLM provider routing
|
||||
- Weather tool that retrieves temperature data and generates creative responses using multiple LLMs
|
||||
|
||||
## MCP Sampling Implementation
|
||||
|
||||
This project demonstrates the MCP Sampling capability, which allows an MCP server to delegate certain requests to LLM providers. The implementation includes:
|
||||
|
||||
1. **Server-side Sampling**: The `WeatherService` class implements a `callMcpSampling` method that:
|
||||
- Extracts the `McpSyncServerExchange` from the tool context
|
||||
- Creates two separate message requests with different model preferences:
|
||||
- One targeting OpenAI models with `ModelPreferences.builder().addHint("openai").build()`
|
||||
- One targeting Anthropic models with `ModelPreferences.builder().addHint("anthropic").build()`
|
||||
- Sends both requests to generate creative poems about the weather data
|
||||
- Combines the responses into a single result
|
||||
|
||||
2. **Client-side Sampling**: The companion client project (`mcp-sampling-client`) implements:
|
||||
- A `McpSyncClientCustomizer` that handles sampling requests
|
||||
- Logic to route requests to the appropriate LLM based on model hints
|
||||
- Integration with both OpenAI and Anthropic models
|
||||
|
||||
This approach demonstrates how MCP can be used to leverage multiple LLM providers within a single application, allowing for creative content generation and model comparison.
|
||||
|
||||
## Dependencies
|
||||
|
||||
The project requires the Spring AI MCP Server WebMVC Boot Starter:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
This starter provides:
|
||||
- HTTP-based transport using Spring MVC (`WebMvcSseServerTransport`)
|
||||
- Auto-configured SSE endpoints
|
||||
- Optional STDIO transport
|
||||
- Included `spring-boot-starter-web` and `mcp-spring-webmvc` dependencies
|
||||
|
||||
## Building the Project
|
||||
|
||||
Build the project using Maven:
|
||||
```bash
|
||||
./mvnw clean install -DskipTests
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
The server supports two transport modes:
|
||||
|
||||
### WebMVC SSE Mode (Default)
|
||||
```bash
|
||||
java -jar target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
### STDIO Mode
|
||||
To enable STDIO transport, set the appropriate properties:
|
||||
```bash
|
||||
java -Dspring.ai.mcp.server.stdio=true -Dspring.main.web-application-type=none -jar target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure the server through `application.properties`:
|
||||
|
||||
```properties
|
||||
# Server identification
|
||||
spring.ai.mcp.server.name=my-weather-server
|
||||
spring.ai.mcp.server.version=0.0.1
|
||||
|
||||
# Server type (SYNC/ASYNC)
|
||||
spring.ai.mcp.server.type=SYNC
|
||||
|
||||
# Transport configuration
|
||||
spring.ai.mcp.server.stdio=false
|
||||
spring.ai.mcp.server.sse-message-endpoint=/mcp/message
|
||||
|
||||
# Change notifications
|
||||
spring.ai.mcp.server.resource-change-notification=true
|
||||
spring.ai.mcp.server.tool-change-notification=true
|
||||
spring.ai.mcp.server.prompt-change-notification=true
|
||||
|
||||
# Logging (required for STDIO transport)
|
||||
spring.main.banner-mode=off
|
||||
logging.file.name=./target/starter-webmvc-server.log
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Weather Temperature Tool
|
||||
- Name: `getTemperature`
|
||||
- Description: Get the temperature (in celsius) for a specific location
|
||||
- Parameters:
|
||||
- `latitude`: double - The location latitude
|
||||
- `longitude`: double - The location longitude
|
||||
- `toolContext`: ToolContext - Automatically provided by Spring AI
|
||||
|
||||
This tool not only retrieves the current temperature from the Open-Meteo API but also uses MCP Sampling to generate creative poems about the weather from both OpenAI and Anthropic models.
|
||||
|
||||
## Server Implementation
|
||||
|
||||
The server uses Spring Boot and Spring AI's tool annotations for automatic tool registration:
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class McpServerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(McpServerApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider weatherTools(WeatherService weatherService){
|
||||
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `WeatherService` implements the weather tool using the `@Tool` annotation and includes MCP Sampling functionality:
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class WeatherService {
|
||||
@Tool(description = "Get the temperature (in celsius) for a specific location")
|
||||
public String getTemperature(double latitude, double longitude, ToolContext toolContext) {
|
||||
// Retrieve weather data from Open-Meteo API
|
||||
WeatherResponse weatherResponse = restClient
|
||||
.get()
|
||||
.uri("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m",
|
||||
latitude, longitude)
|
||||
.retrieve()
|
||||
.body(WeatherResponse.class);
|
||||
|
||||
// Use MCP Sampling to generate creative responses
|
||||
String responseWithPoems = callMcpSampling(toolContext, weatherResponse);
|
||||
|
||||
return responseWithPoems;
|
||||
}
|
||||
|
||||
public String callMcpSampling(ToolContext toolContext, WeatherResponse weatherResponse) {
|
||||
// Implementation that calls both OpenAI and Anthropic models
|
||||
// to generate poems about the weather
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## MCP Clients
|
||||
|
||||
You can connect to the weather server using either STDIO or SSE transport:
|
||||
|
||||
### Manual Clients
|
||||
|
||||
#### WebMVC SSE Client
|
||||
|
||||
For servers using SSE transport:
|
||||
|
||||
```java
|
||||
var transport = new HttpClientSseClientTransport("http://localhost:8080");
|
||||
var client = McpClient.sync(transport).build();
|
||||
```
|
||||
|
||||
#### STDIO Client
|
||||
|
||||
For servers using STDIO transport:
|
||||
|
||||
```java
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dspring.ai.mcp.server.stdio=true",
|
||||
"-Dspring.main.web-application-type=none",
|
||||
"-Dspring.main.banner-mode=off",
|
||||
"-Dlogging.pattern.console=",
|
||||
"-jar",
|
||||
"target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
|
||||
var transport = new StdioClientTransport(stdioParams);
|
||||
var client = McpClient.sync(transport).build();
|
||||
```
|
||||
|
||||
The sample project includes example client implementations:
|
||||
- [SampleClient.java](src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java): Manual MCP client implementation
|
||||
- [ClientStdio.java](src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java): STDIO transport connection
|
||||
|
||||
### Sampling Client
|
||||
|
||||
The companion project `mcp-sampling-client` demonstrates how to implement a client that handles MCP Sampling requests:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
McpSyncClientCustomizer samplingCustomizer(Map<String, ChatClient> chatClients) {
|
||||
return (name, spec) -> {
|
||||
spec.sampling(llmRequest -> {
|
||||
var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
|
||||
String modelHint = llmRequest.modelPreferences().hints().get(0).name();
|
||||
|
||||
// Find the appropriate chat client based on the model hint
|
||||
ChatClient hintedChatClient = chatClients.entrySet().stream()
|
||||
.filter(e -> e.getKey().contains(modelHint)).findFirst()
|
||||
.orElseThrow().getValue();
|
||||
|
||||
// Generate response using the selected model
|
||||
String response = hintedChatClient.prompt()
|
||||
.system(llmRequest.systemPrompt())
|
||||
.user(userPrompt)
|
||||
.call()
|
||||
.content();
|
||||
|
||||
return CreateMessageResult.builder().content(new McpSchema.TextContent(response)).build();
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
To run the sampling client:
|
||||
|
||||
1. Start the MCP server
|
||||
2. Set the required environment variables:
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-openai-key
|
||||
export ANTHROPIC_API_KEY=your-anthropic-key
|
||||
```
|
||||
3. Run the client:
|
||||
```bash
|
||||
java -jar target/mcp-sampling-client-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/)
|
||||
* [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html)
|
||||
* [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-client-docs.html)
|
||||
* [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/)
|
||||
* [Spring Boot Auto-configuration](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration)
|
||||
@@ -12,22 +12,18 @@
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
|
||||
<artifactId>mcp-webflux-server-starter</artifactId>
|
||||
<artifactId>mcp-sampling-weather-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<name>Spring AI MCP StdIO and WebFluxSSE Sample</name>
|
||||
<description>Sample Spring Boot application demonstrating MCP server usage</description>
|
||||
|
||||
<properties>
|
||||
<sprign-ai.version>1.0.0-SNAPSHOT</sprign-ai.version>
|
||||
</properties>
|
||||
<name>Spring AI MCP Sampling - Weather Server</name>
|
||||
<description>Sample Spring Boot application demonstrating MCP client and server sampling usage</description>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${sprign-ai.version}</version>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -37,7 +33,7 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.springframework.ai.mcp.sample.server;
|
||||
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@SpringBootApplication
|
||||
public class McpServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(McpServerApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 - 2025 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.server;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import io.modelcontextprotocol.server.McpSyncServerExchange;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ModelPreferences;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@Service
|
||||
public class WeatherService {
|
||||
|
||||
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(WeatherService.class);
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
public WeatherService() {
|
||||
this.restClient = RestClient.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* The response format from the Open-Meteo API
|
||||
*/
|
||||
public record WeatherResponse(Current current) {
|
||||
public record Current(LocalDateTime time, int interval, double temperature_2m) {
|
||||
}
|
||||
}
|
||||
|
||||
@Tool(description = "Get the temperature (in celsius) for a specific location")
|
||||
public String getTemperature(@ToolParam(description = "The location latitude") double latitude,
|
||||
@ToolParam(description = "The location longitude") double longitude,
|
||||
ToolContext toolContext) {
|
||||
|
||||
WeatherResponse weatherResponse = restClient
|
||||
.get()
|
||||
.uri("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m",
|
||||
latitude, longitude)
|
||||
.retrieve()
|
||||
.body(WeatherResponse.class);
|
||||
|
||||
String responseWithPoems = callMcpSampling(toolContext, weatherResponse);
|
||||
|
||||
return responseWithPoems;
|
||||
}
|
||||
|
||||
public String callMcpSampling(ToolContext toolContext, WeatherResponse weatherResponse) {
|
||||
|
||||
String openAiWeatherPoem = "<no OpenAI poem>";
|
||||
String anthropicWeatherPoem = "<no Anthropic poem>";
|
||||
|
||||
if (toolContext != null && toolContext.getContext().containsKey("exchange")) {
|
||||
|
||||
// Spring AI MCP Auto-configuration injects the McpSyncServerExchange into the ToolContext under the key "exchange"
|
||||
McpSyncServerExchange exchange = (McpSyncServerExchange) toolContext.getContext().get("exchange");
|
||||
if (exchange.getClientCapabilities().sampling() != null) {
|
||||
var messageRequestBuilder = McpSchema.CreateMessageRequest.builder()
|
||||
.systemPrompt("You are a poet!")
|
||||
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
|
||||
new McpSchema.TextContent(
|
||||
"Please write a poem about thius weather forecast (temperature is in Celsious). Use markdown format :\n "
|
||||
+ ModelOptionsUtils.toJsonStringPrettyPrinter(weatherResponse)))));
|
||||
|
||||
var opeAiLlmMessageRequest = messageRequestBuilder
|
||||
.modelPreferences(ModelPreferences.builder().addHint("openai").build())
|
||||
.build();
|
||||
CreateMessageResult openAiLlmResponse = exchange.createMessage(opeAiLlmMessageRequest);
|
||||
|
||||
openAiWeatherPoem = ((McpSchema.TextContent) openAiLlmResponse.content()).text();
|
||||
|
||||
var anthropicLlmMessageRequest = messageRequestBuilder
|
||||
.modelPreferences(ModelPreferences.builder().addHint("anthropic").build())
|
||||
.build();
|
||||
CreateMessageResult anthropicAiLlmResponse = exchange.createMessage(anthropicLlmMessageRequest);
|
||||
|
||||
anthropicWeatherPoem = ((McpSchema.TextContent) anthropicAiLlmResponse.content()).text();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
String responseWithPoems = "OpenAI poem about the weather: " + openAiWeatherPoem + "\n\n" +
|
||||
"Anthropic poem about the weather: " + anthropicWeatherPoem + "\n"
|
||||
+ ModelOptionsUtils.toJsonStringPrettyPrinter(weatherResponse);
|
||||
|
||||
logger.info(anthropicWeatherPoem, responseWithPoems);
|
||||
|
||||
return responseWithPoems;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# spring.main.web-application-type=none
|
||||
|
||||
# NOTE: You must disable the banner and the console logging
|
||||
# to allow the STDIO transport to work !!!
|
||||
spring.main.banner-mode=off
|
||||
# logging.pattern.console=
|
||||
|
||||
# spring.ai.mcp.server.stdio=false
|
||||
|
||||
spring.ai.mcp.server.name=mcp-sampling-server
|
||||
spring.ai.mcp.server.version=0.0.1
|
||||
|
||||
logging.file.name=./model-context-protocol/sampling/mcp-weather-webmvc-server/target/mcp-sampling-server.log
|
||||
@@ -13,18 +13,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.ai.mcp.sample.servlet.client;
|
||||
package org.springframework.ai.mcp.sample.client;
|
||||
|
||||
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
||||
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class HttpClientSse {
|
||||
public class ClientSse {
|
||||
|
||||
public static void main(String[] args) {
|
||||
var transport = new HttpClientSseClientTransport("http://localhost:8080");
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
*/
|
||||
package org.springframework.ai.mcp.sample.client;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
|
||||
/**
|
||||
* With stdio transport, the MCP server is automatically started by the client. But you
|
||||
* With stdio transport, the MCP server is automatically started by the client.
|
||||
* But you
|
||||
* have to build the server jar first:
|
||||
*
|
||||
* <pre>
|
||||
@@ -30,13 +33,17 @@ public class ClientStdio {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
System.out.println(new File(".").getAbsolutePath());
|
||||
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none",
|
||||
"-Dlogging.pattern.console=", "-jar",
|
||||
"model-context-protocol/mcp-webflux-server-starter/target/mcp-webflux-server-starter-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
.args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none",
|
||||
"-Dlogging.pattern.console=", "-jar",
|
||||
"model-context-protocol/sampling/mcp-weather-webmvc-server/target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar")
|
||||
.build();
|
||||
|
||||
new SampleClient(new StdioClientTransport(stdioParams)).run();
|
||||
var transport = new StdioClientTransport(stdioParams);
|
||||
|
||||
new SampleClient(transport).run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
|
||||
public class SampleClient {
|
||||
|
||||
private final McpClientTransport transport;
|
||||
|
||||
public SampleClient(McpClientTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
||||
var client = McpClient.sync(this.transport)
|
||||
.sampling(request -> {
|
||||
System.out.println("Received a new message: " + request);
|
||||
return CreateMessageResult.builder()
|
||||
.content(new McpSchema.TextContent("Bla bla bla"))
|
||||
.build();
|
||||
})
|
||||
.build();
|
||||
|
||||
client.initialize();
|
||||
|
||||
client.ping();
|
||||
|
||||
// List and demonstrate tools
|
||||
ListToolsResult toolsList = client.listTools();
|
||||
System.out.println("Available Tools = " + toolsList);
|
||||
|
||||
CallToolResult weatherForcastResult = client.callTool(new CallToolRequest("getTemperature",
|
||||
Map.of("latitude", "47.6062", "longitude", "-122.3321", "toolContext", new ToolContext(Map.of()))));
|
||||
System.out.println("Weather Forcast: " + weatherForcastResult);
|
||||
|
||||
client.closeGracefully();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>1.0.0-M5</version>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -37,13 +37,12 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -5,14 +5,15 @@ import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
|
||||
import org.springframework.ai.chat.memory.InMemoryChatMemory;
|
||||
import org.springframework.ai.mcp.client.McpClient;
|
||||
import org.springframework.ai.mcp.client.McpSyncClient;
|
||||
import org.springframework.ai.mcp.client.transport.ServerParameters;
|
||||
import org.springframework.ai.mcp.client.transport.StdioClientTransport;
|
||||
import org.springframework.ai.mcp.spring.McpFunctionCallback;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -28,12 +29,12 @@ public class Application {
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner interactiveChat(ChatClient.Builder chatClientBuilder,
|
||||
List<McpFunctionCallback> functionCallbacks,
|
||||
List<McpSyncClient> mcpClients,
|
||||
ConfigurableApplicationContext context) {
|
||||
return args -> {
|
||||
|
||||
var chatClient = chatClientBuilder
|
||||
.defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0]))
|
||||
.defaultTools(new SyncMcpToolCallbackProvider(mcpClients))
|
||||
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
|
||||
.build();
|
||||
|
||||
@@ -61,16 +62,6 @@ public class Application {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<McpFunctionCallback> functionCallbacks(McpSyncClient mcpClient) {
|
||||
|
||||
var callbacks = mcpClient.listTools(null)
|
||||
.tools()
|
||||
.stream()
|
||||
.map(tool -> new McpFunctionCallback(mcpClient, tool))
|
||||
.toList();
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public McpSyncClient mcpClient() {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>1.0.0-M5</version>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -37,13 +37,12 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,12 +4,13 @@ import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.mcp.client.McpClient;
|
||||
import org.springframework.ai.mcp.client.McpSyncClient;
|
||||
import org.springframework.ai.mcp.client.transport.ServerParameters;
|
||||
import org.springframework.ai.mcp.client.transport.StdioClientTransport;
|
||||
import org.springframework.ai.mcp.spring.McpFunctionCallback;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -25,11 +26,11 @@ public class Application {
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder,
|
||||
List<McpFunctionCallback> functionCallbacks, ConfigurableApplicationContext context) {
|
||||
List<McpSyncClient> mcpClients, ConfigurableApplicationContext context) {
|
||||
|
||||
return args -> {
|
||||
var chatClient = chatClientBuilder
|
||||
.defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0]))
|
||||
.defaultTools(new SyncMcpToolCallbackProvider(mcpClients))
|
||||
.build();
|
||||
System.out.println("Running predefined questions with AI model responses:\n");
|
||||
|
||||
@@ -59,17 +60,6 @@ public class Application {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<McpFunctionCallback> functionCallbacks(McpSyncClient mcpClient) {
|
||||
|
||||
var callbacks = mcpClient.listTools(null)
|
||||
.tools()
|
||||
.stream()
|
||||
.map(tool -> new McpFunctionCallback(mcpClient, tool))
|
||||
.toList();
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public McpSyncClient mcpClient() {
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<description>Sample Spring Boot application demonstrating MCP client and server usage</description>
|
||||
|
||||
<properties>
|
||||
<mcp.version>0.7.0-SNAPSHOT</mcp.version>
|
||||
<mcp.version>0.8.0-SNAPSHOT</mcp.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -47,7 +47,7 @@
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
@@ -102,6 +102,18 @@
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<name>Central Portal Snapshots</name>
|
||||
<id>central-portal-snapshots</id>
|
||||
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
|
||||
@@ -3,10 +3,10 @@ package org.springframework.ai.mcp.sample.server;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.server.McpServer;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.server.transport.StdioServerTransport;
|
||||
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
|
||||
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
|
||||
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.ServerMcpTransport;
|
||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||
|
||||
import org.springframework.ai.mcp.McpToolUtils;
|
||||
import org.springframework.ai.tool.ToolCallbacks;
|
||||
@@ -21,23 +21,23 @@ public class McpServerConfig {
|
||||
// STDIO transport
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio")
|
||||
public StdioServerTransport stdioServerTransport() {
|
||||
return new StdioServerTransport();
|
||||
public StdioServerTransportProvider stdioServerTransportProvider() {
|
||||
return new StdioServerTransportProvider();
|
||||
}
|
||||
|
||||
// SSE transport
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public WebFluxSseServerTransport sseServerTransport() {
|
||||
return new WebFluxSseServerTransport(new ObjectMapper(), "/mcp/message");
|
||||
public WebFluxSseServerTransportProvider sseServerTransportProvider() {
|
||||
return new WebFluxSseServerTransportProvider(new ObjectMapper(), "/mcp/message");
|
||||
}
|
||||
|
||||
// Router function for SSE transport used by Spring WebFlux to start an HTTP
|
||||
// server.
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
|
||||
public RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransport transport) {
|
||||
return transport.getRouterFunction();
|
||||
public RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransportProvider transportProvider) {
|
||||
return transportProvider.getRouterFunction();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -46,7 +46,7 @@ public class McpServerConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public McpSyncServer mcpServer(ServerMcpTransport transport, WeatherApiClient weatherApiClient) { // @formatter:off
|
||||
public McpSyncServer mcpServer(McpServerTransportProvider transportProvider, WeatherApiClient weatherApiClient) { // @formatter:off
|
||||
|
||||
// Configure server capabilities with resource support
|
||||
var capabilities = McpSchema.ServerCapabilities.builder()
|
||||
@@ -55,10 +55,10 @@ public class McpServerConfig {
|
||||
.build();
|
||||
|
||||
// Create the server with both tool and resource capabilities
|
||||
McpSyncServer server = McpServer.sync(transport)
|
||||
McpSyncServer server = McpServer.sync(transportProvider)
|
||||
.serverInfo("MCP Demo Weather Server", "1.0.0")
|
||||
.capabilities(capabilities)
|
||||
.tools(McpToolUtils.toSyncToolRegistration(ToolCallbacks.from(weatherApiClient))) // Add @Tools
|
||||
.tools(McpToolUtils.toSyncToolSpecifications(ToolCallbacks.from(weatherApiClient))) // Add @Tools
|
||||
.build();
|
||||
|
||||
return server; // @formatter:on
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.springframework.ai.mcp.sample.client;
|
||||
import java.util.Map;
|
||||
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
@@ -29,9 +29,9 @@ import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
|
||||
public class SampleClient {
|
||||
|
||||
private final ClientMcpTransport transport;
|
||||
private final McpClientTransport transport;
|
||||
|
||||
public SampleClient(ClientMcpTransport transport) {
|
||||
public SampleClient(McpClientTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-server</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.springframework.ai.mcp.sample.client;
|
||||
import java.util.Map;
|
||||
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
@@ -29,9 +29,9 @@ import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
|
||||
public class SampleClient {
|
||||
|
||||
private final ClientMcpTransport transport;
|
||||
private final McpClientTransport transport;
|
||||
|
||||
public SampleClient(ClientMcpTransport transport) {
|
||||
public SampleClient(McpClientTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
spring.main.banner-mode=off
|
||||
# logging.pattern.console=
|
||||
|
||||
spring.ai.mcp.server.stdio=false
|
||||
# spring.ai.mcp.server.stdio=false
|
||||
|
||||
spring.ai.mcp.server.name=my-weather-server
|
||||
spring.ai.mcp.server.version=0.0.1
|
||||
|
||||
@@ -34,7 +34,7 @@ public class ClientStdio {
|
||||
public static void main(String[] args) {
|
||||
|
||||
System.out.println(new File(".").getAbsolutePath());
|
||||
|
||||
|
||||
var stdioParams = ServerParameters.builder("java")
|
||||
.args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none",
|
||||
"-Dlogging.pattern.console=", "-jar",
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.springframework.ai.mcp.sample.client;
|
||||
import java.util.Map;
|
||||
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
@@ -29,9 +29,9 @@ import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
|
||||
|
||||
public class SampleClient {
|
||||
|
||||
private final ClientMcpTransport transport;
|
||||
private final McpClientTransport transport;
|
||||
|
||||
public SampleClient(ClientMcpTransport transport) {
|
||||
public SampleClient(McpClientTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +35,12 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-anthropic</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -3,9 +3,12 @@ package org.springframework.ai.mcp.samples.brave;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
|
||||
import org.springframework.ai.chat.memory.InMemoryChatMemory;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
@@ -21,13 +24,13 @@ public class Application {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner chatbot(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools) {
|
||||
public CommandLineRunner chatbot(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpSyncClients) {
|
||||
|
||||
return args -> {
|
||||
|
||||
var chatClient = chatClientBuilder
|
||||
.defaultSystem("You are useful assistant and can perform web searches Brave's search API to reply to your questions.")
|
||||
.defaultTools(tools)
|
||||
.defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients))
|
||||
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
|
||||
.build();
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ The project uses the following key dependencies:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
@@ -76,12 +76,12 @@ The project uses Spring AI's MCP client Spring Boot starter and OpenAI starter:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
@@ -34,16 +34,16 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency> -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-anthropic</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
*/
|
||||
package org.springframework.ai.mcp.samples.brave;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@@ -31,13 +36,13 @@ public class Application {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools,
|
||||
public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpSyncClients,
|
||||
ConfigurableApplicationContext context) {
|
||||
|
||||
return args -> {
|
||||
|
||||
var chatClient = chatClientBuilder
|
||||
.defaultTools(tools)
|
||||
.defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients))
|
||||
.build();
|
||||
|
||||
String question = "Does Spring AI support the Model Context Protocol? Please provide some references.";
|
||||
|
||||
9
pom.xml
9
pom.xml
@@ -26,11 +26,6 @@
|
||||
|
||||
|
||||
<!-- modules listed in alphabetical order -->
|
||||
<module>model-context-protocol/book-library/manual-servlet-server</module>
|
||||
<module>model-context-protocol/book-library/manual-webflux-server</module>
|
||||
<module>model-context-protocol/book-library/manual-webmvc-server</module>
|
||||
<module>model-context-protocol/book-library/starter-webflux-server</module>
|
||||
|
||||
<module>model-context-protocol/brave</module>
|
||||
|
||||
|
||||
@@ -50,6 +45,10 @@
|
||||
<module>model-context-protocol/web-search/brave-chatbot</module>
|
||||
|
||||
|
||||
<module>model-context-protocol/sampling/mcp-weather-webmvc-server</module>
|
||||
<module>model-context-protocol/sampling/mcp-sampling-client</module>
|
||||
|
||||
|
||||
<module>agentic-patterns</module>
|
||||
</modules>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user