-
Notifications
You must be signed in to change notification settings - Fork 9.3k
feat(everything): add trigger-agentic-sampling tool #3163
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
olaservo
wants to merge
6
commits into
modelcontextprotocol:main
Choose a base branch
from
olaservo:add-sampling-with-tools
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+314
−1
Open
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d7d0945
feat(everything): add trigger-agentic-sampling tool
olaservo da7ffc9
docs(everything): add trigger-agentic-sampling to docs
olaservo 228bfea
fix(everything): remove console.log from trigger-agentic-sampling
olaservo 89a154f
fix(everything): use positive conditional pattern for tool registration
olaservo 53f38cc
Merge branch 'main' into add-sampling-with-tools
cliffhall 23edebe
Merge branch 'main' into add-sampling-with-tools
olaservo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,312 @@ | ||
| /** | ||
| * Agentic Sampling Tool - Demonstrates sampling with tools (MCP 2025-11-25) | ||
| * | ||
| * This tool sends a prompt to the client's LLM with tools available, | ||
| * handles tool_use responses, executes tools locally, and loops | ||
| * until a final text response is received. | ||
| * | ||
| * Flow: | ||
| * 1. Send sampling/createMessage with tools array | ||
| * 2. If stopReason="toolUse", execute tools and continue | ||
| * 3. Repeat until stopReason="endTurn" or iteration limit | ||
| */ | ||
|
|
||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { | ||
| CallToolResult, | ||
| CreateMessageRequest, | ||
| CreateMessageResultWithToolsSchema, | ||
| Tool, | ||
| ToolUseContent, | ||
| ToolResultContent, | ||
| SamplingMessage, | ||
| TextContent, | ||
| } from "@modelcontextprotocol/sdk/types.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| // ============================================================================ | ||
| // INPUT SCHEMA | ||
| // ============================================================================ | ||
|
|
||
| const TriggerAgenticSamplingSchema = z.object({ | ||
| prompt: z.string().describe("The prompt to send to the LLM"), | ||
| maxTokens: z | ||
| .number() | ||
| .default(1000) | ||
| .describe("Maximum tokens per response"), | ||
| maxIterations: z | ||
| .number() | ||
| .default(5) | ||
| .describe("Maximum tool loop iterations (safety limit)"), | ||
| availableTools: z | ||
| .array(z.string()) | ||
| .default(["echo", "add"]) | ||
| .describe("Names of server tools to make available to the LLM (default: echo, add)"), | ||
| }); | ||
|
|
||
| // ============================================================================ | ||
| // TOOL DEFINITIONS | ||
| // ============================================================================ | ||
|
|
||
| /** | ||
| * Tool definitions that we expose to the LLM during sampling. | ||
| * These mirror the actual server tools but are executed locally. | ||
| */ | ||
| const AVAILABLE_TOOL_DEFINITIONS: Record<string, Tool> = { | ||
| echo: { | ||
| name: "echo", | ||
| description: "Echoes back the input message", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| message: { type: "string", description: "Message to echo" }, | ||
| }, | ||
| required: ["message"], | ||
| }, | ||
| }, | ||
| add: { | ||
| name: "add", | ||
| description: "Adds two numbers together", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| a: { type: "number", description: "First number" }, | ||
| b: { type: "number", description: "Second number" }, | ||
| }, | ||
| required: ["a", "b"], | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| // ============================================================================ | ||
| // LOCAL TOOL EXECUTION | ||
| // ============================================================================ | ||
|
|
||
| /** | ||
| * Execute a tool locally and return the result. | ||
| * These implementations mirror the actual server tools. | ||
| */ | ||
| async function executeToolLocally( | ||
| toolName: string, | ||
| input: Record<string, unknown> | ||
| ): Promise<{ result: string; isError: boolean }> { | ||
| try { | ||
| switch (toolName) { | ||
| case "echo": | ||
| return { result: String(input.message), isError: false }; | ||
|
|
||
| case "add": { | ||
| const a = Number(input.a); | ||
| const b = Number(input.b); | ||
| if (isNaN(a) || isNaN(b)) { | ||
| return { result: "Error: Both a and b must be numbers", isError: true }; | ||
| } | ||
| return { result: String(a + b), isError: false }; | ||
| } | ||
|
|
||
| default: | ||
| return { result: `Unknown tool: ${toolName}`, isError: true }; | ||
| } | ||
| } catch (error) { | ||
| return { | ||
| result: `Error executing ${toolName}: ${error instanceof Error ? error.message : String(error)}`, | ||
| isError: true, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| // ============================================================================ | ||
| // TOOL CONFIGURATION | ||
| // ============================================================================ | ||
|
|
||
| const name = "trigger-agentic-sampling"; | ||
| const config = { | ||
| title: "Trigger Agentic Sampling Tool", | ||
| description: | ||
| "Demonstrates sampling with tools - sends a prompt to LLM with tools available, " + | ||
| "handles tool calls in a loop until final response. " + | ||
| "Requires client to support sampling.tools capability.", | ||
| inputSchema: TriggerAgenticSamplingSchema, | ||
| }; | ||
|
|
||
| // ============================================================================ | ||
| // REGISTRATION | ||
| // ============================================================================ | ||
|
|
||
| /** | ||
| * Registers the 'trigger-agentic-sampling' tool. | ||
| * | ||
| * Only registered if the client supports sampling.tools capability. | ||
| */ | ||
| export const registerTriggerAgenticSamplingTool = (server: McpServer) => { | ||
| // Check if client supports sampling with tools | ||
| const clientCapabilities = server.server.getClientCapabilities() || {}; | ||
| const samplingCapability = clientCapabilities.sampling; | ||
|
|
||
| // Need sampling.tools capability | ||
| const clientSupportsSamplingWithTools = | ||
| samplingCapability !== undefined && | ||
| typeof samplingCapability === "object" && | ||
| samplingCapability !== null && | ||
| "tools" in samplingCapability; | ||
|
|
||
| if (!clientSupportsSamplingWithTools) { | ||
| console.log( | ||
| "[trigger-agentic-sampling] Not registering - client does not support sampling.tools" | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| console.log("[trigger-agentic-sampling] Registering - client supports sampling.tools"); | ||
|
|
||
| server.registerTool(name, config, async (args, extra): Promise<CallToolResult> => { | ||
| const validatedArgs = TriggerAgenticSamplingSchema.parse(args); | ||
| const { prompt, maxTokens, maxIterations, availableTools } = validatedArgs; | ||
|
|
||
| // Build tools array from requested tool names | ||
| const tools: Tool[] = availableTools | ||
| .filter((name) => name in AVAILABLE_TOOL_DEFINITIONS) | ||
| .map((name) => AVAILABLE_TOOL_DEFINITIONS[name]); | ||
|
|
||
| if (tools.length === 0) { | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: `Error: No valid tools specified. Available tools: ${Object.keys(AVAILABLE_TOOL_DEFINITIONS).join(", ")}`, | ||
| }, | ||
| ], | ||
| isError: true, | ||
| }; | ||
| } | ||
|
|
||
| console.log( | ||
| `[trigger-agentic-sampling] Starting with prompt: "${prompt.substring(0, 50)}..." ` + | ||
| `(${tools.length} tools, max ${maxIterations} iterations)` | ||
| ); | ||
|
|
||
| // Initialize conversation | ||
| let messages: SamplingMessage[] = [ | ||
| { | ||
| role: "user", | ||
| content: { type: "text", text: prompt }, | ||
| }, | ||
| ]; | ||
|
|
||
| let iteration = 0; | ||
| let finalResponse = ""; | ||
| const toolCallLog: string[] = []; | ||
|
|
||
| // Agentic loop | ||
| while (iteration < maxIterations) { | ||
| iteration++; | ||
| console.log(`[trigger-agentic-sampling] Iteration ${iteration}/${maxIterations}`); | ||
olaservo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Build and send sampling request | ||
| // On last iteration, use toolChoice: none to force final response | ||
| const isLastIteration = iteration >= maxIterations; | ||
| const request: CreateMessageRequest = { | ||
| method: "sampling/createMessage", | ||
| params: { | ||
| messages, | ||
| tools, | ||
| toolChoice: isLastIteration ? { mode: "none" } : { mode: "auto" }, | ||
| systemPrompt: | ||
| "You are a helpful assistant with access to tools. " + | ||
| "Use them when needed to answer questions accurately. " + | ||
| "When you have the final answer, respond with just the answer.", | ||
| maxTokens, | ||
| temperature: 0.7, | ||
| }, | ||
| }; | ||
|
|
||
| // Send the sampling request to the client | ||
| const result = await extra.sendRequest(request, CreateMessageResultWithToolsSchema); | ||
|
|
||
| console.log(`[trigger-agentic-sampling] Got response with stopReason: ${result.stopReason}`); | ||
olaservo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Check if LLM wants to use tools | ||
| if (result.stopReason === "toolUse") { | ||
| // Extract tool_use blocks from content | ||
| const content = Array.isArray(result.content) ? result.content : [result.content]; | ||
| const toolUseBlocks = content.filter( | ||
| (block): block is ToolUseContent => block.type === "tool_use" | ||
| ); | ||
|
|
||
| if (toolUseBlocks.length === 0) { | ||
| console.log( | ||
olaservo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "[trigger-agentic-sampling] stopReason=toolUse but no tool_use blocks found" | ||
| ); | ||
| finalResponse = "Error: Received toolUse stop reason but no tool_use blocks"; | ||
| break; | ||
| } | ||
|
|
||
| // Add assistant message with tool_use to history | ||
| messages.push({ | ||
| role: "assistant", | ||
| content: toolUseBlocks, | ||
| }); | ||
|
|
||
| // Execute each tool and collect results | ||
| const toolResults: ToolResultContent[] = []; | ||
| for (const toolUse of toolUseBlocks) { | ||
| console.log( | ||
olaservo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| `[trigger-agentic-sampling] Executing tool: ${toolUse.name}(${JSON.stringify(toolUse.input)})` | ||
| ); | ||
|
|
||
| const execResult = await executeToolLocally( | ||
| toolUse.name, | ||
| toolUse.input as Record<string, unknown> | ||
| ); | ||
|
|
||
| toolCallLog.push( | ||
| `${toolUse.name}(${JSON.stringify(toolUse.input)}) => ${execResult.result}` | ||
| ); | ||
|
|
||
| toolResults.push({ | ||
| type: "tool_result", | ||
| toolUseId: toolUse.id, | ||
| content: [{ type: "text", text: execResult.result } as TextContent], | ||
| isError: execResult.isError, | ||
| }); | ||
| } | ||
|
|
||
| // Add user message with tool_results (MUST only contain tool_results per MCP spec) | ||
| messages.push({ | ||
| role: "user", | ||
| content: toolResults, | ||
| }); | ||
| } else { | ||
| // Final response (endTurn, maxTokens, stopSequence) | ||
| const content = Array.isArray(result.content) ? result.content : [result.content]; | ||
| const textBlock = content.find((block) => block.type === "text"); | ||
| finalResponse = | ||
| textBlock?.type === "text" | ||
| ? (textBlock as TextContent).text | ||
| : JSON.stringify(result.content); | ||
| console.log( | ||
olaservo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| `[trigger-agentic-sampling] Final response received (stopReason: ${result.stopReason})` | ||
| ); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // Handle iteration limit reached | ||
| if (iteration >= maxIterations && !finalResponse) { | ||
| finalResponse = `[Reached maximum iterations (${maxIterations}) without final response]`; | ||
| } | ||
|
|
||
| // Build response with tool call log | ||
| let responseText = `Agentic sampling completed in ${iteration} iteration(s).\n`; | ||
|
|
||
| if (toolCallLog.length > 0) { | ||
| responseText += `\nTool calls:\n${toolCallLog.map((log) => ` - ${log}`).join("\n")}\n`; | ||
| } | ||
|
|
||
| responseText += `\nFinal response:\n${finalResponse}`; | ||
|
|
||
| return { | ||
| content: [{ type: "text", text: responseText }], | ||
| }; | ||
| }); | ||
| }; | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.