feat: Improve Tool schema handling
- Make McpSchema final and add private constructor - Add JsonSchema record for structured schema representation - Change Tool.inputSchema type from Map<String, Object> to JsonSchema. - Use the String JSON Schema constructor when creating the Tool programaticaly. - Enhance StdioClientTransport error logging with more context - Add ToolHelper utility class for: - Converting Spring AI FunctionCallbacks to MCP tools - Generating JSON schemas for tool input validation - Facilitating integration between Spring AI functions and MCP tools - Update tests and samples to use the new Tool schema format Resolves #52
This commit is contained in:
parent
8334e2ee96
commit
aa26cfa848
5 changed files with 392 additions and 51 deletions
|
|
@ -20,7 +20,7 @@
|
|||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp</artifactId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ public class SampleClient {
|
|||
.callTool(new CallToolRequest("calculator", Map.of("operation", "multiply", "a", 2.0, "b", 3.0)));
|
||||
System.out.println("Calculator Response = " + calcResponse);
|
||||
|
||||
CallToolResult paymentStatus = client.callTool(
|
||||
new CallToolRequest("paymentTransactionStatus", Map.of("transactionId", "006", "accountName", "John")));
|
||||
System.out.println("Payment Status Response = " + paymentStatus);
|
||||
|
||||
CallToolResult parks = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework")));
|
||||
System.out.println("Books Response = " + parks);
|
||||
|
||||
// List and demonstrate resources
|
||||
var resourcesList = client.listResources();
|
||||
System.out.println("\nAvailable Resources = " + resourcesList);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
|
@ -21,10 +22,14 @@ import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
|
|||
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.McpSchema.Tool;
|
||||
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
|
||||
|
|
@ -55,7 +60,7 @@ public class McpServerConfig {
|
|||
}
|
||||
|
||||
@Bean
|
||||
public McpAsyncServer mcpServer(ServerMcpTransport transport) { // @formatter:off
|
||||
public McpAsyncServer mcpServer(ServerMcpTransport transport, OpenLibrary openLibrary) { // @formatter:off
|
||||
|
||||
// Configure server capabilities with resource support
|
||||
var capabilities = McpSchema.ServerCapabilities.builder()
|
||||
|
|
@ -71,13 +76,48 @@ public class McpServerConfig {
|
|||
.capabilities(capabilities)
|
||||
.resources(systemInfoResourceRegistration())
|
||||
.prompts(greetingPromptRegistration())
|
||||
.tools(calculatorToolRegistration())
|
||||
.tools(calculatorToolRegistration(),
|
||||
ToolHelper.toToolRegistration(
|
||||
FunctionCallback.builder()
|
||||
.description("Get transaction payment status")
|
||||
.method("paymentTransactionStatus",String.class, String.class)
|
||||
.targetClass(McpServerConfig.class)
|
||||
.build()),
|
||||
ToolHelper.toToolRegistration(
|
||||
FunctionCallback.builder()
|
||||
.description("To upper case")
|
||||
.function("toUpperCase", new Function<String, String>() {
|
||||
@Override
|
||||
public String apply(String s) {
|
||||
return s.toUpperCase();
|
||||
}
|
||||
})
|
||||
.inputType(String.class)
|
||||
.build()))
|
||||
.tools(openLibraryToolRegistrations(openLibrary))
|
||||
.async();
|
||||
|
||||
server.addTool(weatherToolRegistration(server));
|
||||
return server; // @formatter:on
|
||||
} // @formatter:on
|
||||
|
||||
public static List<ToolRegistration> openLibraryToolRegistrations(OpenLibrary openLibrary) {
|
||||
|
||||
var books = FunctionCallback.builder()
|
||||
.description("Get list of Books by title")
|
||||
.method("getBooks", String.class)
|
||||
.targetObject(openLibrary)
|
||||
.build();
|
||||
|
||||
var bookTitlesByAuthor = FunctionCallback.builder()
|
||||
.description("Get book titles by author")
|
||||
.method("getBookTitlesByAuthor", String.class)
|
||||
.targetObject(openLibrary)
|
||||
.build();
|
||||
|
||||
return ToolHelper.toToolRegistration(books, bookTitlesByAuthor);
|
||||
}
|
||||
|
||||
private static ResourceRegistration systemInfoResourceRegistration() {
|
||||
|
||||
// Create a resource registration for system information
|
||||
|
|
@ -131,8 +171,14 @@ public class McpServerConfig {
|
|||
}
|
||||
|
||||
private static ToolRegistration weatherToolRegistration(McpAsyncServer server) {
|
||||
return new ToolRegistration(
|
||||
new McpSchema.Tool("weather", "Weather forecast tool by location", Map.of("city", "String")),
|
||||
String emptyJsonSchema = """
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {"city" : "string"}
|
||||
}
|
||||
""";
|
||||
return new ToolRegistration(new McpSchema.Tool("weather", "Weather forecast tool by location", emptyJsonSchema),
|
||||
(arguments) -> {
|
||||
String city = (String) arguments.get("city");
|
||||
|
||||
|
|
@ -210,4 +256,17 @@ public class McpServerConfig {
|
|||
});
|
||||
}
|
||||
|
||||
public static String paymentTransactionStatus(String transactionId, String accountName) {
|
||||
return "The status for " + transactionId + ", by " + accountName + " is PENDING";
|
||||
}
|
||||
|
||||
public Function<String, String> toUpperCase() {
|
||||
return String::toUpperCase;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenLibrary openLibrary() {
|
||||
return new OpenLibrary(RestClient.builder());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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,16 +1,8 @@
|
|||
# Server port
|
||||
# server.port=8080
|
||||
|
||||
# Logging
|
||||
# logging.level.org.springframework.ai.mcp=DEBUG
|
||||
# logging.level.org.springframework.web=INFO
|
||||
|
||||
spring.main.banner-mode=off
|
||||
# logging.level.root=OFF
|
||||
# spring.main.web-application-type=none
|
||||
# logging.pattern.console=
|
||||
logging.level.org.springframework.ai.mcp.*=OFF
|
||||
logging.level.org.springframework.ai.mcp.spec.*=OFF
|
||||
logging.level.org.springframework.**=OFF
|
||||
|
||||
# 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
|
||||
Loading…
Reference in a new issue