Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> 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()
Expand Down Expand Up @@ -107,85 +107,85 @@ public class DocumentServer {
private final Map<String, Document> 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))));
}
Expand Down Expand Up @@ -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()))
Expand All @@ -270,21 +270,21 @@ public class ClientHandlers {
public ElicitResult handleElicitation(ElicitRequest request) {
// In a real application, this would show a UI dialog
Map<String, Object> userData = new HashMap<>();

logger.info("Elicitation requested: {}", request.message());

// Simulate user input based on schema
Map<String, Object> schema = request.requestedSchema();
if (schema != null && schema.containsKey("properties")) {
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) 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);
}

Expand All @@ -296,31 +296,31 @@ public class ClientHandlers {
notification.total(),
notification.message()
);

// Update UI or send websocket notification
broadcastProgress(notification);
}

@McpToolListChanged(clients = "server1")
public void handleServer1ToolsChanged(List<McpSchema.Tool> 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));
}

@McpResourceListChanged(clients = "server1")
public void handleServer1ResourcesChanged(List<McpSchema.Resource> 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));
}
}
Expand Down Expand Up @@ -364,9 +364,9 @@ public class AsyncDataProcessor {
public Mono<DataResult> 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)
Expand All @@ -381,10 +381,10 @@ public class AsyncDataProcessor {
public Flux<String> 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 -> {
Expand All @@ -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)
)));
}
Expand Down Expand Up @@ -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();
Expand All @@ -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))
Expand All @@ -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))));
}
Expand Down Expand Up @@ -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]
----
Expand All @@ -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)
Expand Down Expand Up @@ -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*
Expand All @@ -771,7 +772,7 @@ Weather Data:
"temperature_2m": 4.2
}
}
```
----

== Integration with Spring AI

Expand All @@ -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;
Expand All @@ -796,15 +797,15 @@ 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(),
ChatOptionsBuilder.builder()
.withTools(mcpTools)
.build()
);

// Call chat model with MCP tools available
return chatModel.call(prompt);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
}
Expand Down
Loading