Skip to content

Commit d7d0945

Browse files
olaservoclaude
andcommitted
feat(everything): add trigger-agentic-sampling tool
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 <[email protected]>
1 parent dcb47d2 commit d7d0945

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed

src/everything/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates
1414
import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js";
1515
import { registerTriggerLongRunningOperationTool } from "./trigger-long-running-operation.js";
1616
import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js";
17+
import { registerTriggerAgenticSamplingTool } from "./trigger-agentic-sampling.js";
1718

1819
/**
1920
* Register the tools with the MCP server.
@@ -42,4 +43,5 @@ export const registerConditionalTools = (server: McpServer) => {
4243
registerGetRootsListTool(server);
4344
registerTriggerElicitationRequestTool(server);
4445
registerTriggerSamplingRequestTool(server);
46+
registerTriggerAgenticSamplingTool(server);
4547
};
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
/**
2+
* Agentic Sampling Tool - Demonstrates sampling with tools (MCP 2025-11-25)
3+
*
4+
* This tool sends a prompt to the client's LLM with tools available,
5+
* handles tool_use responses, executes tools locally, and loops
6+
* until a final text response is received.
7+
*
8+
* Flow:
9+
* 1. Send sampling/createMessage with tools array
10+
* 2. If stopReason="toolUse", execute tools and continue
11+
* 3. Repeat until stopReason="endTurn" or iteration limit
12+
*/
13+
14+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15+
import {
16+
CallToolResult,
17+
CreateMessageRequest,
18+
CreateMessageResultWithToolsSchema,
19+
Tool,
20+
ToolUseContent,
21+
ToolResultContent,
22+
SamplingMessage,
23+
TextContent,
24+
} from "@modelcontextprotocol/sdk/types.js";
25+
import { z } from "zod";
26+
27+
// ============================================================================
28+
// INPUT SCHEMA
29+
// ============================================================================
30+
31+
const TriggerAgenticSamplingSchema = z.object({
32+
prompt: z.string().describe("The prompt to send to the LLM"),
33+
maxTokens: z
34+
.number()
35+
.default(1000)
36+
.describe("Maximum tokens per response"),
37+
maxIterations: z
38+
.number()
39+
.default(5)
40+
.describe("Maximum tool loop iterations (safety limit)"),
41+
availableTools: z
42+
.array(z.string())
43+
.default(["echo", "add"])
44+
.describe("Names of server tools to make available to the LLM (default: echo, add)"),
45+
});
46+
47+
// ============================================================================
48+
// TOOL DEFINITIONS
49+
// ============================================================================
50+
51+
/**
52+
* Tool definitions that we expose to the LLM during sampling.
53+
* These mirror the actual server tools but are executed locally.
54+
*/
55+
const AVAILABLE_TOOL_DEFINITIONS: Record<string, Tool> = {
56+
echo: {
57+
name: "echo",
58+
description: "Echoes back the input message",
59+
inputSchema: {
60+
type: "object",
61+
properties: {
62+
message: { type: "string", description: "Message to echo" },
63+
},
64+
required: ["message"],
65+
},
66+
},
67+
add: {
68+
name: "add",
69+
description: "Adds two numbers together",
70+
inputSchema: {
71+
type: "object",
72+
properties: {
73+
a: { type: "number", description: "First number" },
74+
b: { type: "number", description: "Second number" },
75+
},
76+
required: ["a", "b"],
77+
},
78+
},
79+
};
80+
81+
// ============================================================================
82+
// LOCAL TOOL EXECUTION
83+
// ============================================================================
84+
85+
/**
86+
* Execute a tool locally and return the result.
87+
* These implementations mirror the actual server tools.
88+
*/
89+
async function executeToolLocally(
90+
toolName: string,
91+
input: Record<string, unknown>
92+
): Promise<{ result: string; isError: boolean }> {
93+
try {
94+
switch (toolName) {
95+
case "echo":
96+
return { result: String(input.message), isError: false };
97+
98+
case "add": {
99+
const a = Number(input.a);
100+
const b = Number(input.b);
101+
if (isNaN(a) || isNaN(b)) {
102+
return { result: "Error: Both a and b must be numbers", isError: true };
103+
}
104+
return { result: String(a + b), isError: false };
105+
}
106+
107+
default:
108+
return { result: `Unknown tool: ${toolName}`, isError: true };
109+
}
110+
} catch (error) {
111+
return {
112+
result: `Error executing ${toolName}: ${error instanceof Error ? error.message : String(error)}`,
113+
isError: true,
114+
};
115+
}
116+
}
117+
118+
// ============================================================================
119+
// TOOL CONFIGURATION
120+
// ============================================================================
121+
122+
const name = "trigger-agentic-sampling";
123+
const config = {
124+
title: "Trigger Agentic Sampling Tool",
125+
description:
126+
"Demonstrates sampling with tools - sends a prompt to LLM with tools available, " +
127+
"handles tool calls in a loop until final response. " +
128+
"Requires client to support sampling.tools capability.",
129+
inputSchema: TriggerAgenticSamplingSchema,
130+
};
131+
132+
// ============================================================================
133+
// REGISTRATION
134+
// ============================================================================
135+
136+
/**
137+
* Registers the 'trigger-agentic-sampling' tool.
138+
*
139+
* Only registered if the client supports sampling.tools capability.
140+
*/
141+
export const registerTriggerAgenticSamplingTool = (server: McpServer) => {
142+
// Check if client supports sampling with tools
143+
const clientCapabilities = server.server.getClientCapabilities() || {};
144+
const samplingCapability = clientCapabilities.sampling;
145+
146+
// Need sampling.tools capability
147+
const clientSupportsSamplingWithTools =
148+
samplingCapability !== undefined &&
149+
typeof samplingCapability === "object" &&
150+
samplingCapability !== null &&
151+
"tools" in samplingCapability;
152+
153+
if (!clientSupportsSamplingWithTools) {
154+
console.log(
155+
"[trigger-agentic-sampling] Not registering - client does not support sampling.tools"
156+
);
157+
return;
158+
}
159+
160+
console.log("[trigger-agentic-sampling] Registering - client supports sampling.tools");
161+
162+
server.registerTool(name, config, async (args, extra): Promise<CallToolResult> => {
163+
const validatedArgs = TriggerAgenticSamplingSchema.parse(args);
164+
const { prompt, maxTokens, maxIterations, availableTools } = validatedArgs;
165+
166+
// Build tools array from requested tool names
167+
const tools: Tool[] = availableTools
168+
.filter((name) => name in AVAILABLE_TOOL_DEFINITIONS)
169+
.map((name) => AVAILABLE_TOOL_DEFINITIONS[name]);
170+
171+
if (tools.length === 0) {
172+
return {
173+
content: [
174+
{
175+
type: "text",
176+
text: `Error: No valid tools specified. Available tools: ${Object.keys(AVAILABLE_TOOL_DEFINITIONS).join(", ")}`,
177+
},
178+
],
179+
isError: true,
180+
};
181+
}
182+
183+
console.log(
184+
`[trigger-agentic-sampling] Starting with prompt: "${prompt.substring(0, 50)}..." ` +
185+
`(${tools.length} tools, max ${maxIterations} iterations)`
186+
);
187+
188+
// Initialize conversation
189+
let messages: SamplingMessage[] = [
190+
{
191+
role: "user",
192+
content: { type: "text", text: prompt },
193+
},
194+
];
195+
196+
let iteration = 0;
197+
let finalResponse = "";
198+
const toolCallLog: string[] = [];
199+
200+
// Agentic loop
201+
while (iteration < maxIterations) {
202+
iteration++;
203+
console.log(`[trigger-agentic-sampling] Iteration ${iteration}/${maxIterations}`);
204+
205+
// Build and send sampling request
206+
// On last iteration, use toolChoice: none to force final response
207+
const isLastIteration = iteration >= maxIterations;
208+
const request: CreateMessageRequest = {
209+
method: "sampling/createMessage",
210+
params: {
211+
messages,
212+
tools,
213+
toolChoice: isLastIteration ? { mode: "none" } : { mode: "auto" },
214+
systemPrompt:
215+
"You are a helpful assistant with access to tools. " +
216+
"Use them when needed to answer questions accurately. " +
217+
"When you have the final answer, respond with just the answer.",
218+
maxTokens,
219+
temperature: 0.7,
220+
},
221+
};
222+
223+
// Send the sampling request to the client
224+
const result = await extra.sendRequest(request, CreateMessageResultWithToolsSchema);
225+
226+
console.log(`[trigger-agentic-sampling] Got response with stopReason: ${result.stopReason}`);
227+
228+
// Check if LLM wants to use tools
229+
if (result.stopReason === "toolUse") {
230+
// Extract tool_use blocks from content
231+
const content = Array.isArray(result.content) ? result.content : [result.content];
232+
const toolUseBlocks = content.filter(
233+
(block): block is ToolUseContent => block.type === "tool_use"
234+
);
235+
236+
if (toolUseBlocks.length === 0) {
237+
console.log(
238+
"[trigger-agentic-sampling] stopReason=toolUse but no tool_use blocks found"
239+
);
240+
finalResponse = "Error: Received toolUse stop reason but no tool_use blocks";
241+
break;
242+
}
243+
244+
// Add assistant message with tool_use to history
245+
messages.push({
246+
role: "assistant",
247+
content: toolUseBlocks,
248+
});
249+
250+
// Execute each tool and collect results
251+
const toolResults: ToolResultContent[] = [];
252+
for (const toolUse of toolUseBlocks) {
253+
console.log(
254+
`[trigger-agentic-sampling] Executing tool: ${toolUse.name}(${JSON.stringify(toolUse.input)})`
255+
);
256+
257+
const execResult = await executeToolLocally(
258+
toolUse.name,
259+
toolUse.input as Record<string, unknown>
260+
);
261+
262+
toolCallLog.push(
263+
`${toolUse.name}(${JSON.stringify(toolUse.input)}) => ${execResult.result}`
264+
);
265+
266+
toolResults.push({
267+
type: "tool_result",
268+
toolUseId: toolUse.id,
269+
content: [{ type: "text", text: execResult.result } as TextContent],
270+
isError: execResult.isError,
271+
});
272+
}
273+
274+
// Add user message with tool_results (MUST only contain tool_results per MCP spec)
275+
messages.push({
276+
role: "user",
277+
content: toolResults,
278+
});
279+
} else {
280+
// Final response (endTurn, maxTokens, stopSequence)
281+
const content = Array.isArray(result.content) ? result.content : [result.content];
282+
const textBlock = content.find((block) => block.type === "text");
283+
finalResponse =
284+
textBlock?.type === "text"
285+
? (textBlock as TextContent).text
286+
: JSON.stringify(result.content);
287+
console.log(
288+
`[trigger-agentic-sampling] Final response received (stopReason: ${result.stopReason})`
289+
);
290+
break;
291+
}
292+
}
293+
294+
// Handle iteration limit reached
295+
if (iteration >= maxIterations && !finalResponse) {
296+
finalResponse = `[Reached maximum iterations (${maxIterations}) without final response]`;
297+
}
298+
299+
// Build response with tool call log
300+
let responseText = `Agentic sampling completed in ${iteration} iteration(s).\n`;
301+
302+
if (toolCallLog.length > 0) {
303+
responseText += `\nTool calls:\n${toolCallLog.map((log) => ` - ${log}`).join("\n")}\n`;
304+
}
305+
306+
responseText += `\nFinal response:\n${finalResponse}`;
307+
308+
return {
309+
content: [{ type: "text", text: responseText }],
310+
};
311+
});
312+
};

0 commit comments

Comments
 (0)