Skip to content

feat: MCP tools support ToolContext #3831

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

YunKuiLu
Copy link
Contributor

@YunKuiLu YunKuiLu commented Jul 16, 2025

  • Add excludedToolContextKeys to McpClientCommonProperties.ToolCallback. This will let us control which ToolContext fields should not be sent to the server. By default, excludedToolContextKeys is set to TOOL_CALL_HISTORY.
  • Add AbstractMcpToolCallback to hold common code for McpToolCallback
  • Update AsyncMcpToolCallback and SyncMcpToolCallback to pass ToolContext via _meta using key ai.springframework.org/tool_context
  • Update McpToolUtils to help server tools extract ToolContext from MCP Client requests
  • Update AsyncMcpToolCallbackProvider and SyncMcpToolCallbackProvider to use the Builder design pattern and add the excludedToolContextKeys field
  • Update the corresponding test cases

PR description

  1. The Spring AI MCP client now sends ToolContext data inside the _meta field of CallToolRequest, using the key ai.springframework.org/tool_context.

  2. By default, the TOOL_CALL_HISTORY entry in ToolContext is excluded from being sent to the server. This can be customized using the config spring.ai.mcp.client.toolcallback.excluded-tool-context-keys.

  3. On the server side, Spring AI reads the ToolContext from the _meta field using the same key ai.springframework.org/tool_context, and passes it into the tool method.

Example

Client

  • application.yml
spring:
  ai:
    mcp:
      client:
        type: sync
        sse:
          connections:
            server1:
              url: http://localhost:8080
        toolcallback:
          excluded-tool-context-keys: TOOL_CALL_HISTORY,foo
  • code
chatClient.prompt()
    .toolCallbacks(syncMcpToolCallbackProvider)
    .toolContext(Map.of("userId", "123", "person", new Person("username1", 18), "foo", "bar"))
    .user(userMessage)
    .call()
    .content();

public record Person(String name, int age) {
}

Server

private final ObjectMapper objectMapper = new ObjectMapper();
@Tool(description = "Query the number of users for the specified system.")
public String querySystemPersonNum(@ToolParam(description = "systemName") String param, ToolContext toolContext) {
    log.info("param = {}", param);
    log.info("toolContext = {}", toolContext.getContext());
    log.info("userId = {}", toolContext.getContext().get("userId"));
    log.info("person = {}", objectMapper.convertValue(toolContext.getContext().getOrDefault("person", null), Person.class));
    log.info("_meta = {}", toolContext.getContext().get(TOOL_CONTEXT_MCP_META_KEY));
    return "102";
}

public record Person(String name, int age) {
}
  • print:
INFO 28100 --- [oundedElastic-1] t.l.a.s.d.mcp.server.DemoMcpServerTools  : param = User Center
INFO 28100 --- [oundedElastic-2] t.l.a.s.d.mcp.server.DemoMcpServerTools  : toolContext = {_meta={ai.springframework.org/tool_context={userId=123, person={name=username1, age=18}}}, exchange=io.modelcontextprotocol.server.McpSyncServerExchange@505a4e46, userId=123, person={name=username1, age=18}}
INFO 28100 --- [oundedElastic-2] t.l.a.s.d.mcp.server.DemoMcpServerTools  : userId = 123
INFO 28100 --- [oundedElastic-2] t.l.a.s.d.mcp.server.DemoMcpServerTools  : person = Person[name=username1, age=18]
INFO 28100 --- [oundedElastic-2] t.l.a.s.d.mcp.server.DemoMcpServerTools  : _meta = {ai.springframework.org/tool_context={userId=123, person={name=username1, age=18}}}

Resolves #3505

@msievers
Copy link

msievers commented Jul 17, 2025

Just an idea regarding the excludedToolContextKeys ... Another option would be to have this configurable the other way around. What I mean is to exclude the TOOL_CALL_HISTORY by default and instead make it configurable to include it in the ToolContext only if explicitly enabled. I don't know, something like the following, for example:

spring.ai.mcp.client.toolcallback.tool-call-history.enabled=true

That way, the out-of-the-box user experience would be that the server only receives what the user explicitly writes into the ToolContext. And if the user wants to transmit additional metadata (such as TOOL_CALL_HISTORY), they can explicitly enable that on the client side.

On the other hand, one could argue that the default behavior of the ToolContext would then change depending on whether the tool call is executed locally or via MCP, which in turn would be an argument for including it by default.

Tough decision, but I just thought I’d think out loud about it for a moment 🤔

Comment on lines 196 to 202
/**
* The keys that will not be sent to the MCP Server inside * the `_meta` field of
* {@link io.modelcontextprotocol.spec.McpSchema.CallToolRequest}
*/
// Remember to update META-INF/additional-spring-configuration-metadata.json if
// you change this default values.
private Set<String> excludedToolContextKeys = Set.of(TOOL_CALL_HISTORY);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That way, the out-of-the-box user experience would be that the server only receives what the user explicitly writes into the ToolContext. And if the user wants to transmit additional metadata (such as TOOL_CALL_HISTORY), they can explicitly enable that on the client side.

@msievers I did think about the scenario you mentioned.

The default value is spring.ai.mcp.client.toolcallback.excluded-tool-context-keys=TOOL_CALL_HISTORY, and users can override it. It also makes it easier for us to add more defaults in the future.

I'm not sure if we need a separate field just for handling TOOL_CALL_HISTORY.

@YunKuiLu YunKuiLu force-pushed the tool_context_meta branch 2 times, most recently from 5fac03f to 7db214a Compare July 18, 2025 02:25
- Added `excludedToolContextKeys` to `McpClientCommonProperties.ToolCallback`. This will let us control which `ToolContext` fields should not be sent to the server. By default, `excludedToolContextKeys` is set to `TOOL_CALL_HISTORY`.
- Added `AbstractMcpToolCallback` to hold common code for `McpToolCallback`
- Updated `AsyncMcpToolCallback` and `SyncMcpToolCallback` to pass `ToolContext` via `_meta` using key `ai.springframework.org/tool_context`
- Updated `McpToolUtils` to help server tools extract `ToolContext` from MCP Client requests
- Updated `AsyncMcpToolCallbackProvider` and `SyncMcpToolCallbackProvider` to use the Builder design pattern and add the `excludedToolContextKeys` field
- Updated the corresponding test cases

Signed-off-by: YunKui Lu <[email protected]>
@YunKuiLu YunKuiLu force-pushed the tool_context_meta branch from 7db214a to 9053c65 Compare July 18, 2025 03:50
if (request.meta().containsKey(DEFAULT_MCP_META_TOOL_CONTEXT_KEY)) {
// Get the McpClient tool context from the
// `ai.springframework.org/tool_context` key in the _meta.
Map<String, Object> toolContext = OBJECT_MAPPER.convertValue(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could ModelOptionsUtils.objectToMap perhaps be used here instead of a dedicated ObjectMapper? I'm just asking because I noticed that in other parts, ModelOptionsUtils is often used when it comes to parameter conversions.

Map<String, Object> toolContext = ModelOptionsUtils.objectToMap(request.meta().get(DEFAULT_MCP_META_TOOL_CONTEXT_KEY));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ToolContext is not supported by the MCP tools
2 participants