77 Usage:
88 npx -y @modelcontextprotocol/inspector \
99 npx -- -y --silent tsx src/examples/backfill/backfillSampling.ts \
10- npx -y --silent tsx src/examples/server/toolLoopSampling .ts
10+ npx -y --silent tsx src/examples/server/simpleLocalResearcher .ts
1111
1212 claude mcp add sampling_with_tools -- \
1313 npx -y --silent tsx src/examples/backfill/backfillSampling.ts \
14- npx -y --silent tsx src/examples/server/toolLoopSampling .ts
14+ npx -y --silent tsx src/examples/server/simpleLocalResearcher .ts
1515
1616 # Or dockerized:
1717 rm -fR node_modules
@@ -48,71 +48,11 @@ import type {
4848 ServerRequest ,
4949 ServerNotification ,
5050} from "../../types.js" ;
51- import { zodToJsonSchema } from "zod-to-json-schema" ;
52-
53-
54- class ToolRegistry {
55- readonly tools : Tool [ ]
56-
57- constructor ( private toolDefinitions : { [ name : string ] : Pick < RegisteredTool , 'title' | 'description' | 'inputSchema' | 'outputSchema' | 'annotations' | '_meta' | 'callback' > } ) {
58- this . tools = Object . entries ( this . toolDefinitions ) . map ( ( [ name , tool ] ) => ( < Tool > {
59- name,
60- title : tool . title ,
61- description : tool . description ,
62- inputSchema : tool . inputSchema ? zodToJsonSchema ( tool . inputSchema ) : undefined ,
63- outputSchema : tool . outputSchema ? zodToJsonSchema ( tool . outputSchema ) : undefined ,
64- annotations : tool . annotations ,
65- _meta : tool . _meta ,
66- } ) ) ;
67- }
68-
69- register ( server : McpServer ) {
70- for ( const [ name , tool ] of Object . entries ( this . toolDefinitions ) ) {
71- server . registerTool ( name , {
72- title : tool . title ,
73- description : tool . description ,
74- inputSchema : tool . inputSchema ?. shape ,
75- outputSchema : tool . outputSchema ?. shape ,
76- annotations : tool . annotations ,
77- _meta : tool . _meta ,
78- } , tool . callback ) ;
79- }
80- }
81-
82- async callTools ( toolCalls : ToolCallContent [ ] , extra : RequestHandlerExtra < ServerRequest , ServerNotification > ) : Promise < ToolResultContent [ ] > {
83- return Promise . all ( toolCalls . map ( async ( { name, id, input } ) => {
84- const tool = this . toolDefinitions [ name ] ;
85- if ( ! tool ) {
86- throw new Error ( `Tool ${ name } not found` ) ;
87- }
88- try {
89- return < ToolResultContent > {
90- type : "tool_result" ,
91- toolUseId : id ,
92- // Copies fields: content, structuredContent?, isError?
93- ...await tool . callback ( input as any , extra ) ,
94- } ;
95- } catch ( error ) {
96- throw new Error ( `Tool ${ name } failed: ${ error instanceof Error ? `${ error . message } \n${ error . stack } ` : error } ` ) ;
97- }
98- } ) ) ;
99- }
100- }
101-
51+ import { ToolRegistry } from "./toolRegistry.js" ;
52+ import { runToolLoop } from './toolLoop.js' ;
10253
10354const CWD = process . cwd ( ) ;
10455
105- /**
106- * Interface for tracking aggregated token usage across API calls.
107- */
108- interface AggregatedUsage {
109- input_tokens : number ;
110- output_tokens : number ;
111- cache_creation_input_tokens : number ;
112- cache_read_input_tokens : number ;
113- api_calls : number ;
114- }
115-
11656/**
11757 * Zod schemas for validating tool inputs
11858 */
@@ -247,128 +187,6 @@ const registry = new ToolRegistry({
247187 }
248188} ) ;
249189
250- /**
251- * Runs a tool loop using sampling.
252- * Continues until the LLM provides a final answer.
253- */
254- async function runToolLoop (
255- server : McpServer ,
256- initialQuery : string ,
257- registry : ToolRegistry ,
258- extra : RequestHandlerExtra < ServerRequest , ServerNotification >
259- ) : Promise < { answer : string ; transcript : SamplingMessage [ ] ; usage : AggregatedUsage } > {
260- const messages : SamplingMessage [ ] = [
261- {
262- role : "user" ,
263- content : {
264- type : "text" ,
265- text : initialQuery ,
266- } ,
267- } ,
268- ] ;
269-
270- // Initialize usage tracking
271- const aggregatedUsage : AggregatedUsage = {
272- input_tokens : 0 ,
273- output_tokens : 0 ,
274- cache_creation_input_tokens : 0 ,
275- cache_read_input_tokens : 0 ,
276- api_calls : 0 ,
277- } ;
278-
279- const MAX_ITERATIONS = 20 ;
280- let iteration = 0 ;
281-
282- const systemPrompt =
283- "You are a helpful assistant that searches through files to answer questions. " +
284- "You have access to ripgrep (for searching) and read (for reading file contents). " +
285- "Use ripgrep to find relevant files, then read them to provide accurate answers. " +
286- "All paths are relative to the current working directory. " +
287- "Be concise and focus on providing the most relevant information." +
288- "You will be allowed up to " + MAX_ITERATIONS + " iterations of tool use to find the information needed. When you have enough information or reach the last iteration, provide a final answer." ;
289-
290- let request : CreateMessageRequest [ "params" ] | undefined
291- let response : CreateMessageResult | undefined
292- while ( iteration < MAX_ITERATIONS ) {
293- iteration ++ ;
294-
295- // Request message from LLM with available tools
296- response = await server . server . createMessage ( request = {
297- messages,
298- systemPrompt,
299- maxTokens : 4000 ,
300- tools : iteration < MAX_ITERATIONS ? registry . tools : undefined ,
301- // Don't allow tool calls at the last iteration: finish with an answer no matter what!
302- tool_choice : { mode : iteration < MAX_ITERATIONS ? "auto" : "none" } ,
303- } ) ;
304-
305- // Aggregate usage statistics from the response
306- if ( response . _meta ?. usage ) {
307- const usage = response . _meta . usage as any ;
308- aggregatedUsage . input_tokens += usage . input_tokens || 0 ;
309- aggregatedUsage . output_tokens += usage . output_tokens || 0 ;
310- aggregatedUsage . cache_creation_input_tokens += usage . cache_creation_input_tokens || 0 ;
311- aggregatedUsage . cache_read_input_tokens += usage . cache_read_input_tokens || 0 ;
312- aggregatedUsage . api_calls += 1 ;
313- }
314-
315- // Add assistant's response to message history
316- // SamplingMessage now supports arrays of content
317- messages . push ( {
318- role : "assistant" ,
319- content : response . content ,
320- } ) ;
321-
322- if ( response . stopReason === "toolUse" ) {
323- const contentArray = Array . isArray ( response . content ) ? response . content : [ response . content ] ;
324- const toolCalls = contentArray . filter (
325- ( content ) : content is ToolCallContent => content . type === "tool_use"
326- ) ;
327-
328- await server . sendLoggingMessage ( {
329- level : "info" ,
330- data : `Loop iteration ${ iteration } : ${ toolCalls . length } tool invocation(s) requested` ,
331- } ) ;
332-
333- const toolResults = await registry . callTools ( toolCalls , extra ) ;
334-
335- messages . push ( {
336- role : "user" ,
337- content : iteration < MAX_ITERATIONS ? toolResults : [
338- ...toolResults ,
339- {
340- type : "text" ,
341- text : "Using the information retrieved from the tools, please now provide a concise final answer to the original question (last iteration of the tool loop)." ,
342- }
343- ] ,
344- } ) ;
345- } else if ( response . stopReason === "endTurn" ) {
346- const contentArray = Array . isArray ( response . content ) ? response . content : [ response . content ] ;
347- const unexpectedBlocks = contentArray . filter ( content => content . type !== "text" ) ;
348- if ( unexpectedBlocks . length > 0 ) {
349- throw new Error ( `Expected text content in final answer, but got: ${ unexpectedBlocks . map ( b => b . type ) . join ( ", " ) } ` ) ;
350- }
351-
352- await server . sendLoggingMessage ( {
353- level : "info" ,
354- data : `Tool loop completed after ${ iteration } iteration(s)` ,
355- } ) ;
356-
357- return {
358- answer : contentArray . map ( block => block . text ) . join ( "\n\n" ) ,
359- transcript : messages ,
360- usage : aggregatedUsage
361- } ;
362- } else if ( response ?. stopReason === "maxTokens" ) {
363- throw new Error ( "LLM response hit max tokens limit" ) ;
364- } else {
365- throw new Error ( `Unsupported stop reason: ${ response . stopReason } ` ) ;
366- }
367- }
368-
369- throw new Error ( `Tool loop exceeded maximum iterations (${ MAX_ITERATIONS } ); request: ${ JSON . stringify ( request ) } \nresponse: ${ JSON . stringify ( response ) } ` ) ;
370- }
371-
372190// Create and configure MCP server
373191const mcpServer = new McpServer ( {
374192 name : "tool-loop-sampling-server" ,
@@ -394,7 +212,26 @@ mcpServer.registerTool(
394212 } ,
395213 async ( { query, maxIterations } , extra ) => {
396214 try {
397- const { answer, transcript, usage } = await runToolLoop ( mcpServer , query , registry , extra ) ;
215+ const MAX_ITERATIONS = 20 ;
216+ const { answer, transcript, usage } = await runToolLoop ( {
217+ initialMessages : [ {
218+ role : "user" ,
219+ content : {
220+ type : "text" ,
221+ text : query ,
222+ } ,
223+ } ] ,
224+ systemPrompt :
225+ "You are a helpful assistant that searches through files to answer questions. " +
226+ "You have access to ripgrep (for searching) and read (for reading file contents). " +
227+ "Use ripgrep to find relevant files, then read them to provide accurate answers. " +
228+ "All paths are relative to the current working directory. " +
229+ "Be concise and focus on providing the most relevant information." +
230+ "You will be allowed up to " + MAX_ITERATIONS + " iterations of tool use to find the information needed. When you have enough information or reach the last iteration, provide a final answer." ,
231+ maxIterations : MAX_ITERATIONS ,
232+ server : mcpServer ,
233+ registry,
234+ } , extra ) ;
398235
399236 // Calculate total input tokens
400237 const totalInputTokens =
0 commit comments