From d7d0945d1c3cf846ecec12be0aa6072609495f36 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sun, 28 Dec 2025 21:29:10 -0700 Subject: [PATCH 1/6] feat(everything): add trigger-agentic-sampling tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new tool that demonstrates sampling with tools capability (MCP 2025-11-25). The tool sends prompts to the LLM with tools available, handles tool_use responses in an agentic loop, and executes tools locally until a final response is received. πŸ¦‰ Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/everything/tools/index.ts | 2 + .../tools/trigger-agentic-sampling.ts | 312 ++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 src/everything/tools/trigger-agentic-sampling.ts diff --git a/src/everything/tools/index.ts b/src/everything/tools/index.ts index d3bd2aaff7..14edebfd24 100644 --- a/src/everything/tools/index.ts +++ b/src/everything/tools/index.ts @@ -14,6 +14,7 @@ import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js"; import { registerTriggerLongRunningOperationTool } from "./trigger-long-running-operation.js"; import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js"; +import { registerTriggerAgenticSamplingTool } from "./trigger-agentic-sampling.js"; /** * Register the tools with the MCP server. @@ -42,4 +43,5 @@ export const registerConditionalTools = (server: McpServer) => { registerGetRootsListTool(server); registerTriggerElicitationRequestTool(server); registerTriggerSamplingRequestTool(server); + registerTriggerAgenticSamplingTool(server); }; diff --git a/src/everything/tools/trigger-agentic-sampling.ts b/src/everything/tools/trigger-agentic-sampling.ts new file mode 100644 index 0000000000..16b46f575d --- /dev/null +++ b/src/everything/tools/trigger-agentic-sampling.ts @@ -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 = { + 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 +): 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 => { + 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}`); + + // 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}`); + + // 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( + "[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( + `[trigger-agentic-sampling] Executing tool: ${toolUse.name}(${JSON.stringify(toolUse.input)})` + ); + + const execResult = await executeToolLocally( + toolUse.name, + toolUse.input as Record + ); + + 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( + `[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 }], + }; + }); +}; From da7ffc920a90df46e686804ece208377e1dee682 Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 29 Dec 2025 20:58:07 -0700 Subject: [PATCH 2/6] docs(everything): add trigger-agentic-sampling to docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update features.md and structure.md to document the new trigger-agentic-sampling tool. Also adds missing documentation for trigger-elicitation-request. πŸ¦‰ Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/everything/docs/features.md | 4 +++- src/everything/docs/structure.md | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/everything/docs/features.md b/src/everything/docs/features.md index c10f311fa4..f1c1c6c88e 100644 --- a/src/everything/docs/features.md +++ b/src/everything/docs/features.md @@ -22,7 +22,9 @@ - `trigger-long-running-operation` (tools/trigger-trigger-long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client. - `toggle-simulated-logging` (tools/toggle-simulated-logging.ts): Starts or stops simulated, random‑leveled logging for the invoking session. Respects the client’s selected minimum logging level. - `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to. -- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM’s response payload. +- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload. Only registered if the client supports the `sampling` capability. +- `trigger-agentic-sampling` (tools/trigger-agentic-sampling.ts): Demonstrates SEP-1577 sampling with tools. Sends a `prompt` to the LLM with tools available (`echo`, `add`), handles `tool_use` responses by executing tools locally, and loops until a final response or `maxIterations` is reached. Only registered if the client supports the `sampling.tools` capability. +- `trigger-elicitation-request` (tools/trigger-elicitation-request.ts): Demonstrates user input elicitation by requesting a form with various field types (string, boolean, number, enum). Only registered if the client supports the `elicitation` capability. ## Prompts diff --git a/src/everything/docs/structure.md b/src/everything/docs/structure.md index 6bcedcd425..a101b99b1b 100644 --- a/src/everything/docs/structure.md +++ b/src/everything/docs/structure.md @@ -50,6 +50,7 @@ src/everything β”‚ β”œβ”€β”€ gzip-file-as-resource.ts β”‚ β”œβ”€β”€ toggle-simulated-logging.ts β”‚ β”œβ”€β”€ toggle-subscriber-updates.ts + β”‚ β”œβ”€β”€ trigger-agentic-sampling.ts β”‚ β”œβ”€β”€ trigger-elicitation-request.ts β”‚ β”œβ”€β”€ trigger-long-running-operation.ts β”‚ └── trigger-sampling-request.ts @@ -151,6 +152,8 @@ src/everything - Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result. - `trigger-sampling-request.ts` - Registers a `trigger-sampling-request` tool that sends a `sampling/createMessage` request to the client/LLM and returns the sampling result. +- `trigger-agentic-sampling.ts` + - Registers a `trigger-agentic-sampling` tool that demonstrates SEP-1577 sampling with tools. Sends a prompt to the LLM with tools available (`echo`, `add`), handles `tool_use` responses by executing tools locally, and loops until a final response or max iterations is reached. Only registered if the client supports the `sampling.tools` capability. - `get-structured-content.ts` - Registers a `get-structured-content` tool that demonstrates structuredContent block responses. - `get-sum.ts` From 228bfea7fb570b3aa53fec9d708b145f42bf3239 Mon Sep 17 00:00:00 2001 From: olaservo Date: Thu, 8 Jan 2026 21:14:33 -0700 Subject: [PATCH 3/6] fix(everything): remove console.log from trigger-agentic-sampling Address PR review feedback: remove console.log statements that would interfere with STDIO JSON-RPC output. Registration now silently skips when client doesn't support sampling.tools (matching other conditional tools). Remaining debug logs changed to console.error. Co-Authored-By: Claude Opus 4.5 --- .../tools/trigger-agentic-sampling.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/everything/tools/trigger-agentic-sampling.ts b/src/everything/tools/trigger-agentic-sampling.ts index 16b46f575d..b146e47351 100644 --- a/src/everything/tools/trigger-agentic-sampling.ts +++ b/src/everything/tools/trigger-agentic-sampling.ts @@ -151,14 +151,9 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { "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 => { const validatedArgs = TriggerAgenticSamplingSchema.parse(args); const { prompt, maxTokens, maxIterations, availableTools } = validatedArgs; @@ -180,7 +175,7 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { }; } - console.log( + console.error( `[trigger-agentic-sampling] Starting with prompt: "${prompt.substring(0, 50)}..." ` + `(${tools.length} tools, max ${maxIterations} iterations)` ); @@ -200,7 +195,7 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { // Agentic loop while (iteration < maxIterations) { iteration++; - console.log(`[trigger-agentic-sampling] Iteration ${iteration}/${maxIterations}`); + console.error(`[trigger-agentic-sampling] Iteration ${iteration}/${maxIterations}`); // Build and send sampling request // On last iteration, use toolChoice: none to force final response @@ -223,7 +218,7 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { // 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}`); + console.error(`[trigger-agentic-sampling] Got response with stopReason: ${result.stopReason}`); // Check if LLM wants to use tools if (result.stopReason === "toolUse") { @@ -234,7 +229,7 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { ); if (toolUseBlocks.length === 0) { - console.log( + console.error( "[trigger-agentic-sampling] stopReason=toolUse but no tool_use blocks found" ); finalResponse = "Error: Received toolUse stop reason but no tool_use blocks"; @@ -250,7 +245,7 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { // Execute each tool and collect results const toolResults: ToolResultContent[] = []; for (const toolUse of toolUseBlocks) { - console.log( + console.error( `[trigger-agentic-sampling] Executing tool: ${toolUse.name}(${JSON.stringify(toolUse.input)})` ); @@ -284,7 +279,7 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { textBlock?.type === "text" ? (textBlock as TextContent).text : JSON.stringify(result.content); - console.log( + console.error( `[trigger-agentic-sampling] Final response received (stopReason: ${result.stopReason})` ); break; From 89a154ffaeb3c5e1aad8f8d54ac5393fed70ef2b Mon Sep 17 00:00:00 2001 From: olaservo Date: Thu, 8 Jan 2026 21:28:46 -0700 Subject: [PATCH 4/6] fix(everything): use positive conditional pattern for tool registration Match the pattern used by other conditional tools: wrap registerTool in `if (clientSupports...) { }` instead of early return. Co-Authored-By: Claude Opus 4.5 --- .../tools/trigger-agentic-sampling.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/everything/tools/trigger-agentic-sampling.ts b/src/everything/tools/trigger-agentic-sampling.ts index b146e47351..be14a01e27 100644 --- a/src/everything/tools/trigger-agentic-sampling.ts +++ b/src/everything/tools/trigger-agentic-sampling.ts @@ -150,11 +150,9 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { samplingCapability !== null && "tools" in samplingCapability; - if (!clientSupportsSamplingWithTools) { - return; - } - - server.registerTool(name, config, async (args, extra): Promise => { + // If so, register tool + if (clientSupportsSamplingWithTools) { + server.registerTool(name, config, async (args, extra): Promise => { const validatedArgs = TriggerAgenticSamplingSchema.parse(args); const { prompt, maxTokens, maxIterations, availableTools } = validatedArgs; @@ -300,8 +298,9 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { responseText += `\nFinal response:\n${finalResponse}`; - return { - content: [{ type: "text", text: responseText }], - }; - }); + return { + content: [{ type: "text", text: responseText }], + }; + }); + } }; From 1984d5c20373ee444d7a6a36253779a22a014ece Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 24 Jan 2026 13:40:02 -0700 Subject: [PATCH 5/6] fix(everything): preserve full assistant content in agentic sampling history Retain complete LLM response (text + tool_use blocks) in message history instead of filtering to only tool_use blocks. This preserves chain-of-thought reasoning context for subsequent turns in multi-step agentic loops. Co-Authored-By: Claude Opus 4.5 --- src/everything/tools/trigger-agentic-sampling.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/everything/tools/trigger-agentic-sampling.ts b/src/everything/tools/trigger-agentic-sampling.ts index be14a01e27..2e55d67764 100644 --- a/src/everything/tools/trigger-agentic-sampling.ts +++ b/src/everything/tools/trigger-agentic-sampling.ts @@ -234,10 +234,11 @@ export const registerTriggerAgenticSamplingTool = (server: McpServer) => { break; } - // Add assistant message with tool_use to history + // Add assistant message with full content to history + // Preserves any reasoning text alongside tool_use blocks for better context messages.push({ role: "assistant", - content: toolUseBlocks, + content: content, }); // Execute each tool and collect results From da6e82b8e2dacd373324c43977823eb5f5957cab Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 24 Jan 2026 15:24:22 -0700 Subject: [PATCH 6/6] feat(everything): add tool annotations to agentic sampling demo tools Add MCP-spec compliant annotations to echo and add tools indicating they are read-only, non-destructive, idempotent, and closed-world. Co-Authored-By: Claude Opus 4.5 --- src/everything/tools/trigger-agentic-sampling.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/everything/tools/trigger-agentic-sampling.ts b/src/everything/tools/trigger-agentic-sampling.ts index 2e55d67764..a91e32cf30 100644 --- a/src/everything/tools/trigger-agentic-sampling.ts +++ b/src/everything/tools/trigger-agentic-sampling.ts @@ -63,6 +63,12 @@ const AVAILABLE_TOOL_DEFINITIONS: Record = { }, required: ["message"], }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }, add: { name: "add", @@ -75,6 +81,12 @@ const AVAILABLE_TOOL_DEFINITIONS: Record = { }, required: ["a", "b"], }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }, };