Expected Behavior

Enable the use of ToolContext together with MCP.

Current Behavior

I looked in the documentation but I didn't find anything referring to how to use ToolContext together with MCP.

Context

I would like it to be possible to use the values ​​available in the ToolContext to call tools provided by MCP servers.

Comment From: henningSaulCM

Related to https://github.com/spring-projects/spring-ai/issues/2378

Comment From: He-Pin

I think we need pass the ServerRequest to the ToolContext

Image

currently, only the body is passed.

Comment From: rafaelrddc

Hello everyone!

Since the MCP specification does not yet provide for this use case, I was able to solve the problem by creating an Advisor that adds the context to the prompt. And so far it has worked well.

class ToolContextPromptAdvisor : AbstractAdvisor(Ordered.HIGHEST_PRECEDENCE) {
    private companion object {
        const val TOOL_CONTEXT_KEY = "tool_context"

        const val PROMPT_TOOL_CONTEXT =
            """
                # TOOL CONTEXT:
                Below is the tool context data provided for your use. These values are available to support your task 
                but should NOT be included or mentioned in your responses:

                {$TOOL_CONTEXT_KEY}  
            """
    }

    override fun aroundCall(
        advisedRequest: AdvisedRequest,
        chain: CallAroundAdvisorChain,
    ): AdvisedResponse = chain.nextAroundCall(addToolContextInPrompt(advisedRequest))

    private fun addToolContextInPrompt(advisedRequest: AdvisedRequest): AdvisedRequest {
        val toolContext = advisedRequest.toolContext
            .map { "- *${it.key}*: ${it.value}" }
            .joinToString(separator = System.lineSeparator())

        val systemParams = advisedRequest.systemParams.toMutableMap()
        systemParams[TOOL_CONTEXT_KEY] = toolContext
        systemParams[CURRENT_DATE_TIME] = LocalDateTime.now().toString()

        return AdvisedRequest
            .from(advisedRequest)
            .toolContext(emptyMap())
            .systemParams(systemParams)
            .systemText(advisedRequest.systemText + System.lineSeparator() + PROMPT_TOOL_CONTEXT)
            .build()
    }
}

Comment From: cidd04

@rafaelrddc This is great. Is it prone to prompt injection though? Have you tested prompts that could overwrite values in your toolcontext?

Comment From: rafaelrddc

@cidd04 This approach is indeed prone to prompt injection, as the user can send this information in the message and the AI ​​uses it instead of the data added by the Advisor.

Comment From: jeweis

I am also facing this issue, is there any alternative solution?

Comment From: rafaelrddc

Hello!

I'm testing a second way to pass context information to the function, which I believe removes the prompt inject problem. I am doing it as follows. On the server side I declare a parameter as optional. Example

Server

@Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
void setAlarm(String time, @ToolParam(required = false) UUID userId) {
   if (Objects.isNull(userId)) {
       return "Failed to process the request." // Generic error for AI not informing the user what it needs
   }

   LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
   System.out.println("Alarm set for " + alarmTime);
}

On the client side, the ToolCallback class was implemented where I add context information to the call to the MCP.

Client

class AsyncMcpToolCallback(
    private val mcpAsyncClient: McpAsyncClient,
    private val tool: McpSchema.Tool,
): ToolCallback {
    override fun call(toolInput: String): String {
        return call(toolInput, null)
    }

    override fun call(toolInput: String, tooContext: ToolContext?): String {
        val arguments = ModelOptionsUtils.jsonToMap(toolInput) +
                (tooContext?.context ?: emptyMap())

        return mcpAsyncClient.callTool(McpSchema.CallToolRequest(tool.name, arguments))
            .map {
                ModelOptionsUtils.toJsonString(it.content)
            }.block().toString()
    }

    override fun getToolDefinition(): ToolDefinition {
        return ToolDefinition.builder().name(
            McpToolUtils.prefixedToolName(
                this.mcpAsyncClient.clientInfo.name(),
                tool.name()
            )
        ).description(tool.description()).inputSchema(
            ModelOptionsUtils.toJsonString(
                tool.inputSchema()
            )
        ).build()
    }
}