diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/postgresml-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/postgresml-embeddings.adoc index 808c2ae9f22..f472928a911 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/postgresml-embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/postgresml-embeddings.adoc @@ -69,7 +69,7 @@ The prefix `spring.ai.postgresml.embedding` is property prefix that configures t | Property | Description | Default | spring.ai.postgresml.embedding.enabled (Removed and no longer valid) | Enable PostgresML embedding model. | true | spring.ai.model.embedding | Enable PostgresML embedding model. | postgresml -| spring.ai.postgresml.embedding.create-extension | Execute the SQL 'CREATE EXTENSION IF NOT EXISTS pgml' to enable the extesnion | false +| spring.ai.postgresml.embedding.create-extension | Execute the SQL 'CREATE EXTENSION IF NOT EXISTS pgml' to enable the extension | false | spring.ai.postgresml.embedding.options.transformer | The Hugging Face transformer model to use for the embedding. | distilbert-base-uncased | spring.ai.postgresml.embedding.options.kwargs | Additional transformer specific options. | empty map | spring.ai.postgresml.embedding.options.vectorType | PostgresML vector type to use for the embedding. Two options are supported: `PG_ARRAY` and `PG_VECTOR`. | PG_ARRAY diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-annotations-examples.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-annotations-examples.adoc index a623f9d230c..39922ae2d99 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-annotations-examples.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-annotations-examples.adoc @@ -51,18 +51,18 @@ public class CalculatorTools { return dividend / divisor; } - @McpTool(name = "calculate-expression", + @McpTool(name = "calculate-expression", description = "Calculate a complex mathematical expression") public CallToolResult calculateExpression( CallToolRequest request, McpSyncRequestContext context) { - + Map args = request.arguments(); String expression = (String) args.get("expression"); - + // Use convenient logging method context.info("Calculating: " + expression); - + try { double result = evaluateExpression(expression); return CallToolResult.builder() @@ -107,85 +107,85 @@ public class DocumentServer { private final Map documents = new ConcurrentHashMap<>(); @McpResource( - uri = "document://{id}", - name = "Document", + uri = "document://{id}", + name = "Document", description = "Access stored documents") public ReadResourceResult getDocument(String id, McpMeta meta) { Document doc = documents.get(id); - + if (doc == null) { return new ReadResourceResult(List.of( - new TextResourceContents("document://" + id, + new TextResourceContents("document://" + id, "text/plain", "Document not found") )); } - + // Check access permissions from metadata String accessLevel = (String) meta.get("accessLevel"); - if ("restricted".equals(doc.getClassification()) && + if ("restricted".equals(doc.getClassification()) && !"admin".equals(accessLevel)) { return new ReadResourceResult(List.of( - new TextResourceContents("document://" + id, + new TextResourceContents("document://" + id, "text/plain", "Access denied") )); } - + return new ReadResourceResult(List.of( - new TextResourceContents("document://" + id, + new TextResourceContents("document://" + id, doc.getMimeType(), doc.getContent()) )); } - @McpTool(name = "analyze-document", + @McpTool(name = "analyze-document", description = "Analyze document content") public String analyzeDocument( McpSyncRequestContext context, @McpToolParam(description = "Document ID", required = true) String docId, @McpToolParam(description = "Analysis type", required = false) String type) { - + Document doc = documents.get(docId); if (doc == null) { return "Document not found"; } - + // Access progress token from context String progressToken = context.request().progressToken(); - + if (progressToken != null) { context.progress(p -> p.progress(0.0).total(1.0).message("Starting analysis")); } - + // Perform analysis String analysisType = type != null ? type : "summary"; String result = performAnalysis(doc, analysisType); - + if (progressToken != null) { context.progress(p -> p.progress(1.0).total(1.0).message("Analysis complete")); } - + return result; } @McpPrompt( - name = "document-summary", + name = "document-summary", description = "Generate document summary prompt") public GetPromptResult documentSummaryPrompt( @McpArg(name = "docId", required = true) String docId, @McpArg(name = "length", required = false) String length) { - + Document doc = documents.get(docId); if (doc == null) { return new GetPromptResult("Error", - List.of(new PromptMessage(Role.SYSTEM, + List.of(new PromptMessage(Role.SYSTEM, new TextContent("Document not found")))); } - + String promptText = String.format( "Please summarize the following document in %s:\n\n%s", length != null ? length : "a few paragraphs", doc.getContent() ); - + return new GetPromptResult("Document Summary", List.of(new PromptMessage(Role.USER, new TextContent(promptText)))); } @@ -256,9 +256,9 @@ public class ClientHandlers { } }) .toList(); - + ChatResponse response = chatModel.call(new Prompt(messages)); - + return CreateMessageResult.builder() .role(Role.ASSISTANT) .content(new TextContent(response.getResult().getOutput().getContent())) @@ -270,21 +270,21 @@ public class ClientHandlers { public ElicitResult handleElicitation(ElicitRequest request) { // In a real application, this would show a UI dialog Map userData = new HashMap<>(); - + logger.info("Elicitation requested: {}", request.message()); - + // Simulate user input based on schema Map schema = request.requestedSchema(); if (schema != null && schema.containsKey("properties")) { @SuppressWarnings("unchecked") Map properties = (Map) schema.get("properties"); - + properties.forEach((key, value) -> { // In real app, prompt user for each field userData.put(key, getDefaultValueForProperty(key, value)); }); } - + return new ElicitResult(ElicitResult.Action.ACCEPT, userData); } @@ -296,7 +296,7 @@ public class ClientHandlers { notification.total(), notification.message() ); - + // Update UI or send websocket notification broadcastProgress(notification); } @@ -304,10 +304,10 @@ public class ClientHandlers { @McpToolListChanged(clients = "server1") public void handleServer1ToolsChanged(List tools) { logger.info("Server1 tools updated: {} tools available", tools.size()); - + // Update tool registry toolRegistry.updateServerTools("server1", tools); - + // Notify UI to refresh tool list eventBus.publish(new ToolsUpdatedEvent("server1", tools)); } @@ -315,12 +315,12 @@ public class ClientHandlers { @McpResourceListChanged(clients = "server1") public void handleServer1ResourcesChanged(List resources) { logger.info("Server1 resources updated: {} resources available", resources.size()); - + // Clear resource cache for this server resourceCache.clearServer("server1"); - + // Register new resources - resources.forEach(resource -> + resources.forEach(resource -> resourceCache.register("server1", resource)); } } @@ -364,9 +364,9 @@ public class AsyncDataProcessor { public Mono fetchData( @McpToolParam(description = "Data source URL", required = true) String url, @McpToolParam(description = "Timeout in seconds", required = false) Integer timeout) { - + Duration timeoutDuration = Duration.ofSeconds(timeout != null ? timeout : 30); - + return WebClient.create() .get() .uri(url) @@ -381,10 +381,10 @@ public class AsyncDataProcessor { public Flux processStream( McpAsyncRequestContext context, @McpToolParam(description = "Item count", required = true) int count) { - + // Access progress token from context String progressToken = context.request().progressToken(); - + return Flux.range(1, count) .delayElements(Duration.ofMillis(100)) .flatMap(i -> { @@ -402,7 +402,7 @@ public class AsyncDataProcessor { return Mono.fromCallable(() -> loadDataAsync(id)) .subscribeOn(Schedulers.boundedElastic()) .map(data -> new ReadResourceResult(List.of( - new TextResourceContents("async-data://" + id, + new TextResourceContents("async-data://" + id, "application/json", data) ))); } @@ -470,7 +470,7 @@ public class StatelessTools { public String formatText( @McpToolParam(description = "Text to format", required = true) String text, @McpToolParam(description = "Format type", required = true) String format) { - + return switch (format.toLowerCase()) { case "uppercase" -> text.toUpperCase(); case "lowercase" -> text.toLowerCase(); @@ -485,11 +485,11 @@ public class StatelessTools { public CallToolResult validateJson( McpTransportContext context, @McpToolParam(description = "JSON string", required = true) String json) { - + try { ObjectMapper mapper = new ObjectMapper(); mapper.readTree(json); - + return CallToolResult.builder() .addTextContent("Valid JSON") .structuredContent(Map.of("valid", true)) @@ -512,12 +512,12 @@ public class StatelessTools { public GetPromptResult templatePrompt( @McpArg(name = "template", required = true) String templateName, @McpArg(name = "variables", required = false) String variables) { - + String template = loadTemplate(templateName); if (variables != null) { template = substituteVariables(template, variables); } - + return new GetPromptResult("Template: " + templateName, List.of(new PromptMessage(Role.USER, new TextContent(template)))); } @@ -662,7 +662,7 @@ public class McpClientHandlers { === Client Application Setup -Regisster the MCP tools and handlers in the client application: +Register the MCP tools and handlers in the client application: [source,java] ---- @@ -677,7 +677,7 @@ public class McpClientApplication { public CommandLineRunner predefinedQuestions(OpenAiChatModel openAiChatModel, ToolCallbackProvider mcpToolProvider) { - return args -> { + return args -> { ChatClient chatClient = ChatClient.builder(openAiChatModel) .defaultToolCallbacks(mcpToolProvider) @@ -743,12 +743,13 @@ spring.ai.mcp.client.toolcallback.enabled=false When running the client, you'll see output like: -``` +[source] +---- > 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 poems from all the other providers. -> ASSISTANT: +> ASSISTANT: OpenAI poem about the weather: **Amsterdam's Winter Whisper** *Temperature: 4.2°C* @@ -771,7 +772,7 @@ Weather Data: "temperature_2m": 4.2 } } -``` +---- == Integration with Spring AI @@ -786,7 +787,7 @@ public class ChatController { private final ChatModel chatModel; private final SyncMcpToolCallbackProvider toolCallbackProvider; - public ChatController(ChatModel chatModel, + public ChatController(ChatModel chatModel, SyncMcpToolCallbackProvider toolCallbackProvider) { this.chatModel = chatModel; this.toolCallbackProvider = toolCallbackProvider; @@ -796,7 +797,7 @@ public class ChatController { public ChatResponse chat(@RequestBody ChatRequest request) { // Get MCP tools as Spring AI function callbacks ToolCallback[] mcpTools = toolCallbackProvider.getToolCallbacks(); - + // Create prompt with MCP tools Prompt prompt = new Prompt( request.getMessage(), @@ -804,7 +805,7 @@ public class ChatController { .withTools(mcpTools) .build() ); - + // Call chat model with MCP tools available return chatModel.call(prompt); } @@ -817,9 +818,9 @@ public class WeatherTools { public WeatherInfo getWeather( @McpToolParam(description = "City name", required = true) String city, @McpToolParam(description = "Units (metric/imperial)", required = false) String units) { - + String unit = units != null ? units : "metric"; - + // Call weather API return weatherService.getCurrentWeather(city, unit); } @@ -828,9 +829,9 @@ public class WeatherTools { public ForecastInfo getForecast( @McpToolParam(description = "City name", required = true) String city, @McpToolParam(description = "Days (1-7)", required = false) Integer days) { - + int forecastDays = days != null ? days : 3; - + return weatherService.getForecast(city, forecastDays); } } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc index b9bbdd25561..14d3842d0b2 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc @@ -1,6 +1,6 @@ = MCP Client Boot Starter -The Spring AI MCP (Model Context Protocol) Client Boot Starter provides auto-configuration for MCP client functionality in Spring Boot applications. +The Spring AI MCP (Model Context Protocol) Client Boot Starter provides auto-configuration for MCP client functionality in Spring Boot applications. It supports both synchronous and asynchronous client implementations with various transport options. The MCP Client Boot Starter provides: @@ -356,17 +356,17 @@ The auto-configuration provides extensive client spec customization capabilities The following customization options are available: * *Request Configuration* - Set custom request timeouts -* link:https://modelcontextprotocol.io/specification/2025-06-18/client/sampling[*Custom Sampling Handlers*] - standardized way for servers to request LLM sampling (`completions` or `generations`) from LLMs via clients. This flow allows clients to maintain control over model access, selection, and permissions while enabling servers to leverage AI capabilities — with no server API keys necessary. -* link:https://modelcontextprotocol.io/specification/2025-06-18/client/roots[*File system (Roots) Access*] - standardized way for clients to expose filesystem `roots` to servers. -Roots define the boundaries of where servers can operate within the filesystem, allowing them to understand which directories and files they have access to. +* link:https://modelcontextprotocol.io/specification/2025-06-18/client/sampling[*Custom Sampling Handlers*] - standardized way for servers to request LLM sampling (`completions` or `generations`) from LLMs via clients. This flow allows clients to maintain control over model access, selection, and permissions while enabling servers to leverage AI capabilities — with no server API keys necessary. +* link:https://modelcontextprotocol.io/specification/2025-06-18/client/roots[*File system (Roots) Access*] - standardized way for clients to expose filesystem `roots` to servers. +Roots define the boundaries of where servers can operate within the filesystem, allowing them to understand which directories and files they have access to. Servers can request the list of roots from supporting clients and receive notifications when that list changes. * link:https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation[*Elicitation Handlers*] - standardized way for servers to request additional information from users through the client during interactions. * *Event Handlers* - client's handler to be notified when a certain server event occurs: - Tools change notifications - when the list of available server tools changes - Resources change notifications - when the list of available server resources changes. - Prompts change notifications - when the list of available server prompts changes. - - link:https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging[*Logging Handlers*] - standardized way for servers to send structured log messages to clients. - - link:https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress[*Progress Handlers*] - standardized way for servers to send structured progress messages to clients. + - link:https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging[*Logging Handlers*] - standardized way for servers to send structured log messages to clients. + - link:https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress[*Progress Handlers*] - standardized way for servers to send structured progress messages to clients. Clients can control logging verbosity by setting minimum log levels @@ -472,28 +472,28 @@ To implement tool filtering, create a bean that implements the `McpToolFilter` i ---- @Component public class CustomMcpToolFilter implements McpToolFilter { - + @Override public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) { // Filter logic based on connection information and tool properties // Return true to include the tool, false to exclude it - + // Example: Exclude tools from a specific client if (connectionInfo.clientInfo().name().equals("restricted-client")) { return false; } - + // Example: Only include tools with specific names if (tool.name().startsWith("allowed_")) { return true; } - + // Example: Filter based on tool description or other properties - if (tool.description() != null && + if (tool.description() != null && tool.description().contains("experimental")) { return false; } - + return true; // Include all other tools by default } } @@ -505,10 +505,10 @@ The `McpConnectionInfo` record provides access to: * `clientInfo` - Information about the MCP client (name and version) * `initializeResult` - The initialization result from the MCP server -The filter is automatically detected and applied to both synchronous and asynchronous MCP tool callback providers. +The filter is automatically detected and applied to both synchronous and asynchronous MCP tool callback providers. If no custom filter is provided, all discovered tools are included by default. -Note: Only one `McpToolFilter` bean should be defined in the application context. +Note: Only one `McpToolFilter` bean should be defined in the application context. If multiple filters are needed, combine them into a single composite filter implementation. === Tool Name Prefix Generation @@ -534,11 +534,11 @@ You can customize this behavior by providing your own implementation: ---- @Component public class CustomToolNamePrefixGenerator implements McpToolNamePrefixGenerator { - + @Override public String prefixedToolName(McpConnectionInfo connectionInfo, Tool tool) { // Custom logic to generate prefixed tool names - + // Example: Use server name and version as prefix String serverName = connectionInfo.initializeResult().serverInfo().name(); String serverVersion = connectionInfo.initializeResult().serverInfo().version(); @@ -566,7 +566,7 @@ To disable prefixing entirely and use raw tool names (not recommended if using m ---- @Configuration public class McpConfiguration { - + @Bean public McpToolNamePrefixGenerator mcpToolNamePrefixGenerator() { return McpToolNamePrefixGenerator.noPrefix(); @@ -581,7 +581,7 @@ WARNING: When using `McpToolNamePrefixGenerator.noPrefix()` with multiple MCP se === Tool Context to MCP Meta Converter -The MCP Client Boot Starter supports customizable conversion of Spring AI's xref:api/tools.adoc#_tool_context[ToolContext] to MCP tool-call metadata through the `ToolContextToMcpMetaConverter` interface. +The MCP Client Boot Starter supports customizable conversion of Spring AI's xref:api/tools.adoc#_tool_context[ToolContext] to MCP tool-call metadata through the `ToolContextToMcpMetaConverter` interface. This feature allows you to pass additional contextual information (e.g. user id, secrets token) as metadata along with the LLM's generated call arguments. For example you can pass the MCP `progressToken` to your link:https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow[MCP Progress Flow] in the tool context to track the progress of long-running operations: @@ -612,27 +612,27 @@ You can customize this behavior by providing your own implementation: ---- @Component public class CustomToolContextToMcpMetaConverter implements ToolContextToMcpMetaConverter { - + @Override public Map convert(ToolContext toolContext) { if (toolContext == null || toolContext.getContext() == null) { return Map.of(); } - + // Custom logic to convert tool context to MCP metadata Map metadata = new HashMap<>(); - + // Example: Add custom prefix to all keys for (Map.Entry entry : toolContext.getContext().entrySet()) { if (entry.getValue() != null) { metadata.put("app_" + entry.getKey(), entry.getValue()); } } - + // Example: Add additional metadata metadata.put("timestamp", System.currentTimeMillis()); metadata.put("source", "spring-ai"); - + return metadata; } } @@ -651,7 +651,7 @@ To disable context-to-metadata conversion entirely: ---- @Configuration public class McpConfiguration { - + @Bean public ToolContextToMcpMetaConverter toolContextToMcpMetaConverter() { return ToolContextToMcpMetaConverter.noOp(); @@ -667,7 +667,7 @@ If no custom converter bean is provided, the default converter is used automatic The MCP ToolCallback auto-configuration is enabled by default, but can be disabled with the `spring.ai.mcp.client.toolcallback.enabled=false` property. When disabled, no `ToolCallbackProvider` bean is created from the available MCP tools. -bsafdsa + == MCP Client Annotations The MCP Client Boot Starter automatically detects and registers annotated methods for handling various MCP client operations: @@ -689,7 +689,7 @@ public class McpClientHandlers { @McpLogging(clients = "server1") public void handleLoggingMessage(LoggingMessageNotification notification) { - System.out.println("Received log: " + notification.level() + + System.out.println("Received log: " + notification.level() + " - " + notification.data()); } @@ -697,7 +697,7 @@ public class McpClientHandlers { public CreateMessageResult handleSamplingRequest(CreateMessageRequest request) { // Process the request and generate a response String response = generateLLMResponse(request); - + return CreateMessageResult.builder() .role(Role.ASSISTANT) .content(new TextContent(response)) @@ -708,7 +708,7 @@ public class McpClientHandlers { @McpProgress(clients = "server1") public void handleProgressNotification(ProgressNotification notification) { double percentage = notification.progress() * 100; - System.out.println(String.format("Progress: %.2f%% - %s", + System.out.println(String.format("Progress: %.2f%% - %s", percentage, notification.message())); } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc index 69617ba6ed0..876de904401 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc @@ -24,7 +24,7 @@ Use the `spring-ai-starter-mcp-server-webmvc` dependency: and set the `spring.ai.mcp.server.protocol` property to `STREAMABLE`. * Full MCP server capabilities with Spring MVC Streamable transport -* Suppport for tools, resources, prompts, completion, logging, progression, ping, root-changes capabilities +* Support for tools, resources, prompts, completion, logging, progression, ping, root-changes capabilities * Persistent connection management === Streamable-HTTP WebFlux Server @@ -42,7 +42,7 @@ Use the `spring-ai-starter-mcp-server-webflux` dependency: and set the `spring.ai.mcp.server.protocol` property to `STREAMABLE`. * Reactive MCP server with WebFlux Streamable transport -* Suppport for tools, resources, prompts, completion, logging, progression, ping, root-changes capabilities +* Support for tools, resources, prompts, completion, logging, progression, ping, root-changes capabilities * Non-blocking, persistent connection management == Configuration Properties @@ -239,7 +239,7 @@ public List myCompletions() { === link:https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging/[Logging] -Provides a standardized way for servers to send structured log messages to clients. +Provides a standardized way for servers to send structured log messages to clients. From within the tool, resource, prompt or completion call handler use the provided `McpSyncServerExchange`/`McpAsyncServerExchange` `exchange` object to send logging messages: [source,java] @@ -323,19 +323,20 @@ From within the tool, resource, prompt or completion call handler use the provid Server can optionally, periodically issue pings to connected clients to verify connection health. -By default, keep-alive is disabled. +By default, keep-alive is disabled. To enable keep-alive, set the `keep-alive-interval` property in your configuration: -```yaml +[source,yaml] +---- spring: ai: mcp: server: streamable-http: keep-alive-interval: 30s -``` +---- -NOTE: Currently, for streamable-http servers, the keep-alive mechanism is available only for the link:https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server[Listening for Messages from the Server (SSE)] connection. +NOTE: Currently, for streamable-http servers, the keep-alive mechanism is available only for the link:https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server[Listening for Messages from the Server (SSE)] connection. == Usage Examples