Skip to content

Commit 27d36a1

Browse files
committed
Update toolLoopSampling.ts
1 parent daedb61 commit 27d36a1

File tree

1 file changed

+62
-63
lines changed

1 file changed

+62
-63
lines changed

src/examples/server/toolLoopSampling.ts

Lines changed: 62 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
Usage:
88
npx -y @modelcontextprotocol/inspector \
9-
npx -y --silent tsx src/examples/backfill/backfillSampling.ts -- \
9+
npx -- -y --silent tsx src/examples/backfill/backfillSampling.ts \
1010
npx -y --silent tsx src/examples/server/toolLoopSampling.ts
1111
1212
Then connect with an MCP client and call the "localResearch" tool with a query like:
@@ -25,7 +25,8 @@ import type {
2525
ToolCallContent,
2626
CreateMessageResult,
2727
CreateMessageRequest,
28-
ToolResultContent,
28+
ToolResultContent,
29+
CallToolResult,
2930
} from "../../types.js";
3031

3132
const CWD = process.cwd();
@@ -71,6 +72,19 @@ function ensureSafePath(inputPath: string): string {
7172
return resolved;
7273
}
7374

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+
7488
/**
7589
* Executes ripgrep to search for a pattern in files.
7690
* Returns search results as a string.
@@ -79,7 +93,7 @@ async function executeRipgrep(
7993
server: McpServer,
8094
pattern: string,
8195
path: string
82-
): Promise<{ output?: string; error?: string }> {
96+
): Promise<CallToolResult> {
8397
try {
8498
await server.sendLoggingMessage({
8599
level: "info",
@@ -88,46 +102,35 @@ async function executeRipgrep(
88102

89103
const safePath = ensureSafePath(path);
90104

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));
99108

100109
let stdout = "";
101110
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());
111113
rg.on("close", (code) => {
112114
if (code === 0 || code === 1) {
113115
// code 1 means no matches, which is fine
114-
resolve({ output: stdout || "No matches found" });
116+
resolve(stdout || "No matches found");
115117
} else {
116-
resolve({ error: stderr || `ripgrep exited with code ${code}` });
118+
reject(new Error(`ripgrep exited with code ${code}:\n${stderr}`));
117119
}
118120
});
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}`)));
123122
});
124-
} catch (error) {
123+
const structuredContent = { output };
125124
return {
126-
error: error instanceof Error ? error.message : "Unknown error",
125+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
126+
structuredContent,
127127
};
128+
} catch (error) {
129+
return makeErrorCallToolResult(error);
128130
}
129131
}
130132

133+
131134
/**
132135
* Reads a file from the filesystem, optionally within a line range.
133136
* Returns file contents as a string.
@@ -137,7 +140,7 @@ async function executeRead(
137140
path: string,
138141
startLineInclusive?: number,
139142
endLineInclusive?: number
140-
): Promise<{ content?: string; error?: string }> {
143+
): Promise<CallToolResult> {
141144
try {
142145
// Log the read operation
143146
if (startLineInclusive !== undefined || endLineInclusive !== undefined) {
@@ -153,35 +156,39 @@ async function executeRead(
153156
}
154157

155158
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;
158165

159166
// If line range specified, extract only those lines
160167
if (startLineInclusive !== undefined || endLineInclusive !== undefined) {
168+
const lines = fileContent.split("\n");
169+
161170
const start = (startLineInclusive ?? 1) - 1; // Convert to 0-indexed
162171
const end = endLineInclusive ?? lines.length; // Default to end of file
163172

164173
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)`);
166175
}
167176
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}`);
169178
}
170179

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");
178183
}
179184

180-
return { content };
181-
} catch (error) {
185+
const structuredContent = { content }
182186
return {
183-
error: error instanceof Error ? error.message : "Unknown error",
187+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
188+
structuredContent,
184189
};
190+
} catch (error) {
191+
return makeErrorCallToolResult(error);
185192
}
186193
}
187194

@@ -242,7 +249,7 @@ async function executeLocalTool(
242249
server: McpServer,
243250
toolName: string,
244251
toolInput: Record<string, unknown>
245-
): Promise<Record<string, unknown>> {
252+
): Promise<CallToolResult> {
246253
try {
247254
switch (toolName) {
248255
case "ripgrep": {
@@ -259,17 +266,13 @@ async function executeLocalTool(
259266
);
260267
}
261268
default:
262-
return { error: `Unknown tool: ${toolName}` };
269+
return makeErrorCallToolResult(`Unknown tool: ${toolName}`);
263270
}
264271
} catch (error) {
265272
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(", ")}`);
269274
}
270-
return {
271-
error: error instanceof Error ? error.message : "Unknown error during tool execution",
272-
};
275+
return makeErrorCallToolResult(error);
273276
}
274277
}
275278

@@ -356,10 +359,12 @@ async function runToolLoop(
356359

357360
const toolResults: ToolResultContent[] = await Promise.all(toolCalls.map(async (toolCall) => {
358361
const result = await executeLocalTool(server, toolCall.name, toolCall.input);
359-
return {
362+
return <ToolResultContent>{
360363
type: "tool_result",
361364
toolUseId: toolCall.id,
362-
content: result,
365+
content: result.content,
366+
structuredContent: result.structuredContent,
367+
isError: result.isError,
363368
}
364369
}))
365370

@@ -416,12 +421,14 @@ mcpServer.registerTool(
416421
inputSchema: {
417422
query: z
418423
.string()
424+
.default("describe main classes")
419425
.describe(
420426
"A natural language query describing what to search for (e.g., 'Find all TypeScript files that export a Server class')"
421427
),
428+
maxIterations: z.number().int().positive().optional().default(20).describe("Maximum number of tool use iterations (default 20)"),
422429
},
423430
},
424-
async ({ query }) => {
431+
async ({ query, maxIterations }) => {
425432
try {
426433
const { answer, transcript, usage } = await runToolLoop(mcpServer, query);
427434

@@ -459,15 +466,7 @@ mcpServer.registerTool(
459466
],
460467
};
461468
} 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);
471470
}
472471
}
473472
);

0 commit comments

Comments
 (0)