Skip to content

Commit 08820b3

Browse files
authored
feat: add unified request context interfaces for MCP operations (#70)
feat: Add unified request context interfaces for MCP operations Introduce McpSyncRequestContext and McpAsyncRequestContext as unified interfaces that provide convenient access to MCP request context, server exchange, transport context, and helper methods for common operations. Key Changes: Core Context Interfaces: - Add McpSyncRequestContext interface for synchronous operations - Add McpAsyncRequestContext interface for asynchronous operations (Mono-based) - Add McpRequestContextTypes interface with shared type definitions - Add StructuredElicitResult<T> record for typed elicitation responses Default Implementations: - Add DefaultMcpSyncRequestContext with stateful and stateless support - Add DefaultMcpAsyncRequestContext with stateful and stateless support - Implement builder pattern for context creation - Support automatic context injection in tool/resource/prompt methods Specification Classes: - Add DefaultElicitationSpec for elicitation configuration - Add DefaultLoggingSpec for logging configuration - Add DefaultProgressSpec for progress notification configuration - Add DefaultSamplingSpec for sampling request configuration - Add DefaultSamplingSpec.DefaultModelPreferenceSpec for model preferences Context Features: - Unified API for both stateful and stateless operations - Convenience methods: debug(), info(), warn(), error() for logging - Progress tracking: progress(int), progress(Consumer<ProgressSpec>) - Elicitation support with typed responses and custom configuration - Sampling support with flexible message and model configuration - Access to roots, ping, and request metadata Method Callback Updates: - Update AbstractMcpToolMethodCallback to support context injection - Add createRequestContext() abstract method for context creation - Update all tool callback implementations (Sync/Async, Stateful/Stateless) - Add context type checking in isExchangeOrContextType() - Update JSON schema generation to exclude context parameters Documentation: - Update README.md with context usage examples - Add synchronous and asynchronous context examples - Document all available context methods and their return types - Mark @McpProgressToken and McpServerExchange as deprecated - Add migration notes for using new context interfaces Testing: - Add DefaultMcpSyncRequestContextTests with test coverage - Add DefaultMcpAsyncRequestContextTests with reactive test coverage - Add DefaultLoggingSpecTests for logging specification - Add DefaultProgressSpecTests for progress specification - Add DefaultSamplingSpecTests for sampling specification - Update tool callback tests to verify context parameter support Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent 1176e6b commit 08820b3

26 files changed

+3963
-27
lines changed

README.md

Lines changed: 210 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,15 @@ Each operation type has both synchronous and asynchronous implementations, allow
120120
- **`@McpToolParam`** - Annotates tool method parameters with descriptions and requirement specifications
121121

122122
#### Special Parameters and Annotations
123-
- **`@McpProgressToken`** - Marks a method parameter to receive the progress token from the request. This parameter is automatically injected and excluded from the generated JSON schema
124-
- **`McpMeta`** - Special parameter type that provides access to metadata from MCP requests, notifications, and results. This parameter is automatically injected and excluded from parameter count limits and JSON schema generation
125-
- **`McpSyncServerExchange`** - Special parameter type for stateful synchronous operations that provides access to server exchange functionality including logging notifications, progress updates, and other server-side operations. This parameter is automatically injected and excluded from JSON schema generation
126-
- **`McpAsyncServerExchange`** - Special parameter type for stateful asynchronous operations that provides access to server exchange functionality with reactive support. This parameter is automatically injected and excluded from JSON schema generation
123+
- **`McpSyncRequestContext`** - Special parameter type for synchronous operations that provides a unified interface for accessing MCP request context, including the original request, server exchange (for stateful operations), transport context (for stateless operations), and convenient methods for logging, progress, sampling, and elicitation. This parameter is automatically injected and excluded from JSON schema generation
124+
- **`McpAsyncRequestContext`** - Special parameter type for asynchronous operations that provides the same unified interface as `McpSyncRequestContext` but with reactive (Mono-based) return types. This parameter is automatically injected and excluded from JSON schema generation
125+
- **(Deprecated and replaced by `McpSyncRequestContext`) `McpSyncServerExchange`** - Special parameter type for stateful synchronous operations that provides access to server exchange functionality including logging notifications, progress updates, and other server-side operations. This parameter is automatically injected and excluded from JSON schema generation.
126+
- **(Deprecated and replaced by `McpAsyncRequestContext`) `McpAsyncServerExchange`** - Special parameter type for stateful asynchronous operations that provides access to server exchange functionality with reactive support. This parameter is automatically injected and excluded from JSON schema generation
127127
- **`McpTransportContext`** - Special parameter type for stateless operations that provides lightweight access to transport-level context without full server exchange functionality. This parameter is automatically injected and excluded from JSON schema generation
128+
- **(Deprecated. Handled internally by `McpSyncRequestContext` and `McpAsyncRequestContext`)`@McpProgressToken`** - Marks a method parameter to receive the progress token from the request. This parameter is automatically injected and excluded from the generated JSON schema
129+
**Note:** if using the `McpSyncRequestContext` or `McpAsyncRequestContext` the progress token is handled internally.
130+
- **`McpMeta`** - Special parameter type that provides access to metadata from MCP requests, notifications, and results. This parameter is automatically injected and excluded from parameter count limits and JSON schema generation.
131+
**Note:** if using the McpSyncRequestContext or McpAsyncRequestContext the meta can be obatined via `requestMeta()` instead.
128132

129133
### Method Callbacks
130134

@@ -870,6 +874,204 @@ public List<String> smartComplete(
870874

871875
This feature enables context-aware MCP operations where the behavior can be customized based on client-provided metadata such as user identity, preferences, session information, or any other contextual data.
872876

877+
#### McpRequestContext Support
878+
879+
The library provides unified request context interfaces (`McpSyncRequestContext` and `McpAsyncRequestContext`) that offer a higher-level abstraction over the underlying MCP infrastructure. These context objects provide convenient access to:
880+
881+
- The original request (CallToolRequest, ReadResourceRequest, etc.)
882+
- Server exchange (for stateful operations) or transport context (for stateless operations)
883+
- Convenient methods for logging, progress updates, sampling, elicitation, and more
884+
885+
**Key Benefits:**
886+
- **Unified API**: Single parameter type works for both stateful and stateless operations
887+
- **Convenience Methods**: Built-in helpers for common operations like logging and progress tracking
888+
- **Type Safety**: Strongly-typed access to request data and context
889+
- **Automatic Injection**: Context is automatically created and injected by the framework
890+
891+
When a method parameter is of type `McpSyncRequestContext` or `McpAsyncRequestContext`:
892+
- The parameter is automatically injected with the appropriate context implementation
893+
- The parameter is excluded from JSON schema generation
894+
- For stateful operations, the context provides access to `McpSyncServerExchange` or `McpAsyncServerExchange`
895+
- For stateless operations, the context provides access to `McpTransportContext`
896+
897+
**Synchronous Context Example:**
898+
899+
```java
900+
public record UserInfo(String name, String email, Number age) {}
901+
902+
@McpTool(name = "process-with-context", description = "Process data with unified context")
903+
public String processWithContext(
904+
McpSyncRequestContext context,
905+
@McpToolParam(description = "Data to process", required = true) String data) {
906+
907+
// Access the original request
908+
CallToolRequest request = (CallToolRequest) context.request();
909+
910+
// Log information
911+
context.info("Processing data: " + data);
912+
913+
// Send progress updates
914+
context.progress(50); // 50% complete
915+
916+
// Check if running in stateful mode
917+
if (!context.isStateless()) {
918+
// Access server exchange for stateful operations
919+
McpSyncServerExchange exchange = context.exchange().orElseThrow();
920+
// Use exchange for additional operations...
921+
}
922+
923+
// Perform elicitation with default message - returns StructuredElicitResult
924+
Optional<StructuredElicitResult<UserInfo>> result = context.elicit(new TypeReference<UserInfo>() {});
925+
926+
// Or perform elicitation with custom configuration - returns StructuredElicitResult
927+
Optional<StructuredElicitResult<UserInfo>> structuredResult = context.elicit(
928+
e -> e.message("Please provide your information").meta("context", "user-registration"),
929+
new TypeReference<UserInfo>() {}
930+
);
931+
932+
if (structuredResult.isPresent() && structuredResult.get().action() == ElicitResult.Action.ACCEPT) {
933+
UserInfo info = structuredResult.get().structuredContent();
934+
return "Processed: " + data + " for user " + info.name();
935+
}
936+
937+
return "Processed: " + data;
938+
}
939+
940+
@McpResource(uri = "data://{id}", name = "Data Resource", description = "Resource with context")
941+
public ReadResourceResult getDataWithContext(
942+
McpSyncRequestContext context,
943+
String id) {
944+
945+
// Log the resource access
946+
context.debug("Accessing resource: " + id);
947+
948+
// Access metadata from the request
949+
Map<String, Object> metadata = context.request()._meta();
950+
951+
String content = "Data for " + id;
952+
return new ReadResourceResult(List.of(
953+
new TextResourceContents("data://" + id, "text/plain", content)
954+
));
955+
}
956+
957+
@McpPrompt(name = "generate-with-context", description = "Generate prompt with context")
958+
public GetPromptResult generateWithContext(
959+
McpSyncRequestContext context,
960+
@McpArg(name = "topic", required = true) String topic) {
961+
962+
// Log prompt generation
963+
context.info("Generating prompt for topic: " + topic);
964+
965+
// Perform sampling if needed
966+
Optional<CreateMessageResult> samplingResult = context.sample(
967+
"What are the key points about " + topic + "?"
968+
);
969+
970+
String message = "Let's discuss " + topic;
971+
return new GetPromptResult("Generated Prompt",
972+
List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message))));
973+
}
974+
```
975+
976+
**Asynchronous Context Example:**
977+
978+
```java
979+
public record UserInfo(String name, String email, int age) {}
980+
981+
@McpTool(name = "async-process-with-context", description = "Async process with unified context")
982+
public Mono<String> asyncProcessWithContext(
983+
McpAsyncRequestContext context,
984+
@McpToolParam(description = "Data to process", required = true) String data) {
985+
986+
return Mono.fromCallable(() -> {
987+
// Access the original request
988+
CallToolRequest request = (CallToolRequest) context.request();
989+
return data;
990+
})
991+
.flatMap(processedData -> {
992+
// Log information (returns Mono<Void>)
993+
return context.info("Processing data: " + processedData)
994+
.thenReturn(processedData);
995+
})
996+
.flatMap(processedData -> {
997+
// Send progress updates (returns Mono<Void>)
998+
return context.progress(50)
999+
.thenReturn(processedData);
1000+
})
1001+
.flatMap(processedData -> {
1002+
// Perform elicitation with default message - returns Mono<UserInfo>
1003+
return context.elicitation(new TypeReference<UserInfo>() {})
1004+
.map(userInfo -> "Processed: " + processedData + " for user " + userInfo.name());
1005+
})
1006+
.switchIfEmpty(Mono.fromCallable(() -> {
1007+
// Or perform elicitation with custom message and metadata - returns Mono<StructuredElicitResult<UserInfo>>
1008+
return context.elicitation(
1009+
new TypeReference<UserInfo>() {},
1010+
"Please provide your information",
1011+
Map.of("context", "user-registration")
1012+
)
1013+
.filter(result -> result.action() == ElicitResult.Action.ACCEPT)
1014+
.map(result -> "Processed: " + data + " for user " + result.structuredContent().name())
1015+
.defaultIfEmpty("Processed: " + data);
1016+
}).flatMap(mono -> mono));
1017+
}
1018+
1019+
@McpResource(uri = "async-data://{id}", name = "Async Data Resource",
1020+
description = "Async resource with context")
1021+
public Mono<ReadResourceResult> getAsyncDataWithContext(
1022+
McpAsyncRequestContext context,
1023+
String id) {
1024+
1025+
// Log the resource access (returns Mono<Void>)
1026+
return context.debug("Accessing async resource: " + id)
1027+
.then(Mono.fromCallable(() -> {
1028+
String content = "Async data for " + id;
1029+
return new ReadResourceResult(List.of(
1030+
new TextResourceContents("async-data://" + id, "text/plain", content)
1031+
));
1032+
}));
1033+
}
1034+
1035+
@McpPrompt(name = "async-generate-with-context",
1036+
description = "Async generate prompt with context")
1037+
public Mono<GetPromptResult> asyncGenerateWithContext(
1038+
McpAsyncRequestContext context,
1039+
@McpArg(name = "topic", required = true) String topic) {
1040+
1041+
// Log prompt generation and perform sampling
1042+
return context.info("Generating async prompt for topic: " + topic)
1043+
.then(context.sampling("What are the key points about " + topic + "?"))
1044+
.map(samplingResult -> {
1045+
String message = "Let's discuss " + topic;
1046+
return new GetPromptResult("Generated Async Prompt",
1047+
List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message))));
1048+
});
1049+
}
1050+
```
1051+
1052+
**Available Context Methods:**
1053+
1054+
`McpSyncRequestContext` provides:
1055+
- `request()` - Access the original request object
1056+
- `exchange()` - Access the server exchange (for stateful operations)
1057+
- `transportContext()` - Access the transport context (for stateless operations)
1058+
- `isStateless()` - Check if running in stateless mode
1059+
- `log(Consumer<LoggingSpec>)` - Send log messages with custom configuration
1060+
- `debug(String)`, `info(String)`, `warn(String)`, `error(String)` - Convenience logging methods
1061+
- `progress(int)`, `progress(Consumer<ProgressSpec>)` - Send progress updates
1062+
- `elicit(TypeReference<T>)` - Request user input with default message, returns `StructuredElicitResult<T>` with action, typed content, and metadata
1063+
- `elicit(Class<T>)` - Request user input with default message using Class type, returns `StructuredElicitResult<T>`
1064+
- `elicit(Consumer<ElicitationSpec>, TypeReference<T>)` - Request user input with custom configuration, returns `StructuredElicitResult<T>`
1065+
- `elicit(Consumer<ElicitationSpec>, Class<T>)` - Request user input with custom configuration using Class type, returns `StructuredElicitResult<T>`
1066+
- `elicit(ElicitRequest)` - Request user input with full control over the elicitation request
1067+
- `sample(...)` - Request LLM sampling with various configuration options
1068+
- `roots()` - Access root directories (returns `Optional<ListRootsResult>`)
1069+
- `ping()` - Send ping to check connection
1070+
1071+
`McpAsyncRequestContext` provides the same methods but with reactive return types (`Mono<T>` instead of `T` or `Optional<T>`).
1072+
1073+
This unified context approach simplifies method signatures and provides a consistent API across different operation types and execution modes (stateful vs stateless, sync vs async).
1074+
8731075
### Async Tool Example
8741076

8751077
```java
@@ -1771,30 +1973,30 @@ public class AsyncElicitationHandler {
17711973
public class MyMcpClient {
17721974

17731975
public static McpSyncClient createSyncClientWithElicitation(ElicitationHandler elicitationHandler) {
1774-
Function<ElicitRequest, ElicitResult> elicitationHandler =
1976+
Function<ElicitRequest, ElicitResult> elicitationHandlerFunc =
17751977
new SyncMcpElicitationProvider(List.of(elicitationHandler)).getElicitationHandler();
17761978

17771979
McpSyncClient client = McpClient.sync(transport)
17781980
.capabilities(ClientCapabilities.builder()
17791981
.elicitation() // Enable elicitation support
17801982
// Other capabilities...
17811983
.build())
1782-
.elicitationHandler(elicitationHandler)
1984+
.elicitationHandler(elicitationHandlerFunc)
17831985
.build();
17841986

17851987
return client;
17861988
}
17871989

17881990
public static McpAsyncClient createAsyncClientWithElicitation(AsyncElicitationHandler asyncElicitationHandler) {
1789-
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler =
1991+
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandlerFunc =
17901992
new AsyncMcpElicitationProvider(List.of(asyncElicitationHandler)).getElicitationHandler();
17911993

17921994
McpAsyncClient client = McpClient.async(transport)
17931995
.capabilities(ClientCapabilities.builder()
17941996
.elicitation() // Enable elicitation support
17951997
// Other capabilities...
17961998
.build())
1797-
.elicitationHandler(elicitationHandler)
1999+
.elicitationHandler(elicitationHandlerFunc)
17982000
.build();
17992001

18002002
return client;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package org.springaicommunity.mcp.context;
6+
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
10+
import org.springaicommunity.mcp.context.McpRequestContextTypes.ElicitationSpec;
11+
12+
public class DefaultElicitationSpec implements ElicitationSpec {
13+
14+
protected String message;
15+
16+
protected Map<String, Object> meta = new HashMap<>();
17+
18+
protected String message() {
19+
return message;
20+
}
21+
22+
protected Map<String, Object> meta() {
23+
return meta;
24+
}
25+
26+
@Override
27+
public ElicitationSpec message(String message) {
28+
this.message = message;
29+
return this;
30+
}
31+
32+
@Override
33+
public ElicitationSpec meta(Map<String, Object> m) {
34+
if (m != null) {
35+
this.meta.putAll(m);
36+
}
37+
return this;
38+
}
39+
40+
@Override
41+
public ElicitationSpec meta(String k, Object v) {
42+
if (k != null && v != null) {
43+
this.meta.put(k, v);
44+
}
45+
return this;
46+
}
47+
48+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package org.springaicommunity.mcp.context;
6+
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
10+
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
11+
import org.springaicommunity.mcp.context.McpRequestContextTypes.LoggingSpec;
12+
13+
/**
14+
* @author Christian Tzolov
15+
*/
16+
public class DefaultLoggingSpec implements LoggingSpec {
17+
18+
protected String message;
19+
20+
protected String logger;
21+
22+
protected LoggingLevel level = LoggingLevel.INFO;
23+
24+
protected Map<String, Object> meta = new HashMap<>();
25+
26+
@Override
27+
public LoggingSpec message(String message) {
28+
this.message = message;
29+
return this;
30+
}
31+
32+
@Override
33+
public LoggingSpec logger(String logger) {
34+
this.logger = logger;
35+
return this;
36+
}
37+
38+
@Override
39+
public LoggingSpec level(LoggingLevel level) {
40+
this.level = level;
41+
return this;
42+
}
43+
44+
@Override
45+
public LoggingSpec meta(Map<String, Object> m) {
46+
if (m != null) {
47+
this.meta.putAll(m);
48+
}
49+
return this;
50+
}
51+
52+
@Override
53+
public LoggingSpec meta(String k, Object v) {
54+
if (k != null && v != null) {
55+
this.meta.put(k, v);
56+
}
57+
return this;
58+
}
59+
60+
}

0 commit comments

Comments
 (0)