6
6
7
7
Usage:
8
8
npx -y @modelcontextprotocol/inspector \
9
- npx -y --silent tsx src/examples/backfill/backfillSampling.ts -- \
9
+ npx -- - y --silent tsx src/examples/backfill/backfillSampling.ts \
10
10
npx -y --silent tsx src/examples/server/toolLoopSampling.ts
11
11
12
12
Then connect with an MCP client and call the "localResearch" tool with a query like:
@@ -25,7 +25,8 @@ import type {
25
25
ToolCallContent ,
26
26
CreateMessageResult ,
27
27
CreateMessageRequest ,
28
- ToolResultContent ,
28
+ ToolResultContent ,
29
+ CallToolResult ,
29
30
} from "../../types.js" ;
30
31
31
32
const CWD = process . cwd ( ) ;
@@ -71,6 +72,19 @@ function ensureSafePath(inputPath: string): string {
71
72
return resolved ;
72
73
}
73
74
75
+
76
+ function makeErrorCallToolResult ( error : any ) : CallToolResult {
77
+ return {
78
+ content : [
79
+ {
80
+ type : "text" ,
81
+ text : error instanceof Error ? `${ error . message } \n${ error . stack } ` : `${ error } ` ,
82
+ } ,
83
+ ] ,
84
+ isError : true ,
85
+ }
86
+ }
87
+
74
88
/**
75
89
* Executes ripgrep to search for a pattern in files.
76
90
* Returns search results as a string.
@@ -79,7 +93,7 @@ async function executeRipgrep(
79
93
server : McpServer ,
80
94
pattern : string ,
81
95
path : string
82
- ) : Promise < { output ?: string ; error ?: string } > {
96
+ ) : Promise < CallToolResult > {
83
97
try {
84
98
await server . sendLoggingMessage ( {
85
99
level : "info" ,
@@ -88,46 +102,35 @@ async function executeRipgrep(
88
102
89
103
const safePath = ensureSafePath ( path ) ;
90
104
91
- return new Promise ( ( resolve ) => {
92
- const rg = spawn ( "rg" , [
93
- "--json" ,
94
- "--max-count" , "50" ,
95
- "--" ,
96
- pattern ,
97
- safePath ,
98
- ] ) ;
105
+ const output = await new Promise < string > ( ( resolve , reject ) => {
106
+ const command = [ "rg" , "--json" , "--max-count" , "50" , "--" , pattern , safePath ] ;
107
+ const rg = spawn ( command [ 0 ] , command . slice ( 1 ) ) ;
99
108
100
109
let stdout = "" ;
101
110
let stderr = "" ;
102
-
103
- rg . stdout . on ( "data" , ( data ) => {
104
- stdout += data . toString ( ) ;
105
- } ) ;
106
-
107
- rg . stderr . on ( "data" , ( data ) => {
108
- stderr += data . toString ( ) ;
109
- } ) ;
110
-
111
+ rg . stdout . on ( "data" , ( data ) => stdout += data . toString ( ) ) ;
112
+ rg . stderr . on ( "data" , ( data ) => stderr += data . toString ( ) ) ;
111
113
rg . on ( "close" , ( code ) => {
112
114
if ( code === 0 || code === 1 ) {
113
115
// code 1 means no matches, which is fine
114
- resolve ( { output : stdout || "No matches found" } ) ;
116
+ resolve ( stdout || "No matches found" ) ;
115
117
} else {
116
- resolve ( { error : stderr || `ripgrep exited with code ${ code } ` } ) ;
118
+ reject ( new Error ( `ripgrep exited with code ${ code } :\n ${ stderr } ` ) ) ;
117
119
}
118
120
} ) ;
119
-
120
- rg . on ( "error" , ( err ) => {
121
- resolve ( { error : `Failed to execute ripgrep: ${ err . message } ` } ) ;
122
- } ) ;
121
+ rg . on ( "error" , err => reject ( new Error ( `Failed to start \`${ command . map ( a => a . indexOf ( ' ' ) >= 0 ? `"${ a } "` : a ) . join ( ' ' ) } \`: ${ err . message } \n${ stderr } ` ) ) ) ;
123
122
} ) ;
124
- } catch ( error ) {
123
+ const structuredContent = { output } ;
125
124
return {
126
- error : error instanceof Error ? error . message : "Unknown error" ,
125
+ content : [ { type : "text" , text : JSON . stringify ( structuredContent ) } ] ,
126
+ structuredContent,
127
127
} ;
128
+ } catch ( error ) {
129
+ return makeErrorCallToolResult ( error ) ;
128
130
}
129
131
}
130
132
133
+
131
134
/**
132
135
* Reads a file from the filesystem, optionally within a line range.
133
136
* Returns file contents as a string.
@@ -137,7 +140,7 @@ async function executeRead(
137
140
path : string ,
138
141
startLineInclusive ?: number ,
139
142
endLineInclusive ?: number
140
- ) : Promise < { content ?: string ; error ?: string } > {
143
+ ) : Promise < CallToolResult > {
141
144
try {
142
145
// Log the read operation
143
146
if ( startLineInclusive !== undefined || endLineInclusive !== undefined ) {
@@ -153,35 +156,39 @@ async function executeRead(
153
156
}
154
157
155
158
const safePath = ensureSafePath ( path ) ;
156
- const content = await readFile ( safePath , "utf-8" ) ;
157
- const lines = content . split ( "\n" ) ;
159
+ const fileContent = await readFile ( safePath , "utf-8" ) ;
160
+ if ( typeof fileContent !== "string" ) {
161
+ throw new Error ( `Result of reading file ${ path } is not text: ${ fileContent } ` ) ;
162
+ }
163
+
164
+ let content = fileContent ;
158
165
159
166
// If line range specified, extract only those lines
160
167
if ( startLineInclusive !== undefined || endLineInclusive !== undefined ) {
168
+ const lines = fileContent . split ( "\n" ) ;
169
+
161
170
const start = ( startLineInclusive ?? 1 ) - 1 ; // Convert to 0-indexed
162
171
const end = endLineInclusive ?? lines . length ; // Default to end of file
163
172
164
173
if ( start < 0 || start >= lines . length ) {
165
- return { error : `Start line ${ startLineInclusive } is out of bounds (file has ${ lines . length } lines)` } ;
174
+ throw new Error ( `Start line ${ startLineInclusive } is out of bounds (file has ${ lines . length } lines)` ) ;
166
175
}
167
176
if ( end < start ) {
168
- return { error : `End line ${ endLineInclusive } is before start line ${ startLineInclusive } ` } ;
177
+ throw new Error ( `End line ${ endLineInclusive } is before start line ${ startLineInclusive } ` ) ;
169
178
}
170
179
171
- const selectedLines = lines . slice ( start , end ) ;
172
- // Add line numbers to output
173
- const numberedContent = selectedLines
174
- . map ( ( line , idx ) => `${ start + idx + 1 } : ${ line } ` )
175
- . join ( "\n" ) ;
176
-
177
- return { content : numberedContent } ;
180
+ content = lines . slice ( start , end )
181
+ . map ( ( line , idx ) => `${ start + idx + 1 } : ${ line } ` )
182
+ . join ( "\n" ) ;
178
183
}
179
184
180
- return { content } ;
181
- } catch ( error ) {
185
+ const structuredContent = { content }
182
186
return {
183
- error : error instanceof Error ? error . message : "Unknown error" ,
187
+ content : [ { type : "text" , text : JSON . stringify ( structuredContent ) } ] ,
188
+ structuredContent,
184
189
} ;
190
+ } catch ( error ) {
191
+ return makeErrorCallToolResult ( error ) ;
185
192
}
186
193
}
187
194
@@ -242,7 +249,7 @@ async function executeLocalTool(
242
249
server : McpServer ,
243
250
toolName : string ,
244
251
toolInput : Record < string , unknown >
245
- ) : Promise < Record < string , unknown > > {
252
+ ) : Promise < CallToolResult > {
246
253
try {
247
254
switch ( toolName ) {
248
255
case "ripgrep" : {
@@ -259,17 +266,13 @@ async function executeLocalTool(
259
266
) ;
260
267
}
261
268
default :
262
- return { error : `Unknown tool: ${ toolName } ` } ;
269
+ return makeErrorCallToolResult ( `Unknown tool: ${ toolName } ` ) ;
263
270
}
264
271
} catch ( error ) {
265
272
if ( error instanceof z . ZodError ) {
266
- return {
267
- error : `Invalid input for tool '${ toolName } ': ${ error . errors . map ( e => e . message ) . join ( ", " ) } ` ,
268
- } ;
273
+ return makeErrorCallToolResult ( `Invalid input for tool '${ toolName } ': ${ error . errors . map ( e => e . message ) . join ( ", " ) } ` ) ;
269
274
}
270
- return {
271
- error : error instanceof Error ? error . message : "Unknown error during tool execution" ,
272
- } ;
275
+ return makeErrorCallToolResult ( error ) ;
273
276
}
274
277
}
275
278
@@ -356,10 +359,12 @@ async function runToolLoop(
356
359
357
360
const toolResults : ToolResultContent [ ] = await Promise . all ( toolCalls . map ( async ( toolCall ) => {
358
361
const result = await executeLocalTool ( server , toolCall . name , toolCall . input ) ;
359
- return {
362
+ return < ToolResultContent > {
360
363
type : "tool_result" ,
361
364
toolUseId : toolCall . id ,
362
- content : result ,
365
+ content : result . content ,
366
+ structuredContent : result . structuredContent ,
367
+ isError : result . isError ,
363
368
}
364
369
} ) )
365
370
@@ -416,12 +421,14 @@ mcpServer.registerTool(
416
421
inputSchema : {
417
422
query : z
418
423
. string ( )
424
+ . default ( "describe main classes" )
419
425
. describe (
420
426
"A natural language query describing what to search for (e.g., 'Find all TypeScript files that export a Server class')"
421
427
) ,
428
+ maxIterations : z . number ( ) . int ( ) . positive ( ) . optional ( ) . default ( 20 ) . describe ( "Maximum number of tool use iterations (default 20)" ) ,
422
429
} ,
423
430
} ,
424
- async ( { query } ) => {
431
+ async ( { query, maxIterations } ) => {
425
432
try {
426
433
const { answer, transcript, usage } = await runToolLoop ( mcpServer , query ) ;
427
434
@@ -459,15 +466,7 @@ mcpServer.registerTool(
459
466
] ,
460
467
} ;
461
468
} catch ( error ) {
462
- return {
463
- content : [
464
- {
465
- type : "text" ,
466
- text : error instanceof Error ? error . message : `${ error } ` ,
467
- isError : true ,
468
- } ,
469
- ] ,
470
- } ;
469
+ return makeErrorCallToolResult ( error ) ;
471
470
}
472
471
}
473
472
) ;
0 commit comments