Skip to content

Commit 84a1869

Browse files
committed
fix: update tool configurations and improve search plugin error handling
1 parent 9d70997 commit 84a1869

File tree

4 files changed

+66
-13
lines changed

4 files changed

+66
-13
lines changed

packages/cli/src/app.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ export const createServer = async (): Promise<ComposableMCPServer> =>
77
await mcpc(
88
[
99
{
10-
name: "capi-mcp",
10+
name: "large-result-plugin-example",
1111
version: "0.1.0",
1212
},
1313
{ capabilities: { tools: {}, sampling: {} } },
1414
],
15-
// TODO: Move example tool definitions here or make configurable
16-
[],
15+
[
16+
{
17+
name: null,
18+
description: "",
19+
plugins: ["./plugins/large-result.ts?maxSize=8000&previewSize=4000"],
20+
},
21+
],
1722
);
1823

1924
export const createApp = (): OpenAPIHono => {

packages/core/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mcpc/core",
3-
"version": "0.2.0-beta.7",
3+
"version": "0.2.0-beta.8",
44
"exports": {
55
".": "./mod.ts",
66
"./plugins": "./plugins.ts",

packages/core/src/plugins/search-tool.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import rg from "@mcpc-tech/ripgrep-napi";
77
import { tmpdir } from "node:os";
88
import { jsonSchema } from "ai";
99
import type { ToolPlugin } from "../plugin-types.ts";
10+
import { resolve } from "node:path";
11+
import { relative } from "node:path";
1012

1113
/**
1214
* Configuration options for the search plugin
@@ -42,21 +44,25 @@ export function createSearchPlugin(options: SearchOptions = {}): ToolPlugin {
4244
// Register the search tool once during plugin initialization
4345
server.tool(
4446
"search-tool-result",
45-
`Search for text patterns in files and directories. Use this to find specific content, code, or information within files.`,
47+
`Search for text patterns in files and directories. Use this to find specific content, code, or information within files. Provide a simple literal string or a regular expression. If your pattern is a regex, ensure it's valid; otherwise use quotes or escape special characters to treat it as a literal string.
48+
Only search within the allowed directory: ${allowedSearchDir}`,
4649
jsonSchema<{ pattern: string; path?: string; maxResults?: number }>({
4750
type: "object",
4851
properties: {
4952
pattern: {
5053
type: "string",
51-
description: "Text to search for",
54+
description:
55+
"Text to search for. Can be a plain string or a regular expression. For regexes, don't include delimiters (e.g. use `^foo` not `/^foo/`). If you get a regex parse error, try escaping special chars or using a simpler literal search.",
5256
},
5357
path: {
5458
type: "string",
55-
description: "File or folder path (optional)",
59+
description:
60+
"File or folder path to limit the search (optional). Must be within the allowed directory.",
5661
},
5762
maxResults: {
5863
type: "number",
59-
description: "Max results (optional)",
64+
description:
65+
"Maximum number of matches to return (optional). Lower this to reduce output size and runtime.",
6066
},
6167
},
6268
required: ["pattern"],
@@ -66,13 +72,34 @@ export function createSearchPlugin(options: SearchOptions = {}): ToolPlugin {
6672
path?: string;
6773
maxResults?: number;
6874
}) => {
75+
const isBroad = (raw: string) => {
76+
const t = (raw ?? "").trim();
77+
if (!t) return true;
78+
// only-wildcards when length >=2 (single '*' allowed)
79+
if (/^[*.\s]{2,}$/.test(t)) return true;
80+
if (t === ".*" || t === "." || t === "^.*$") return true;
81+
if (/^\^?\.\*\$?$/.test(t)) return true;
82+
if (/^\\s?\*+$/.test(t)) return true;
83+
return false;
84+
};
85+
86+
const appendMatchSafely = (
87+
current: string,
88+
addition: string,
89+
limit: number,
90+
) => {
91+
if ((current + addition).length > limit) {
92+
return { current, added: false };
93+
}
94+
return { current: current + addition, added: true };
95+
};
96+
6997
try {
7098
const requestedPath = args.path || allowedSearchDir;
7199
const limit = args.maxResults || maxResults;
72100

73101
// Security check: Validate that the requested path is within allowed directory
74102
if (args.path) {
75-
const { resolve, relative } = await import("node:path");
76103
const resolvedRequested = resolve(args.path);
77104
const resolvedAllowed = resolve(allowedSearchDir);
78105
const relativePath = relative(resolvedAllowed, resolvedRequested);
@@ -88,12 +115,28 @@ export function createSearchPlugin(options: SearchOptions = {}): ToolPlugin {
88115
`❌ Path "${args.path}" not allowed. Must be within: ${allowedSearchDir}`,
89116
},
90117
],
118+
isError: true,
91119
};
92120
}
93121
}
94122

95123
const searchPath = requestedPath;
96124

125+
// Reject overly-broad patterns to avoid expensive/unsafe searches
126+
const rawPattern = args.pattern ?? "";
127+
if (isBroad(rawPattern)) {
128+
return {
129+
content: [
130+
{
131+
type: "text",
132+
text:
133+
`❌ Search pattern too broad: "${rawPattern}"\nProvide a more specific pattern (e.g. include a filename fragment, a keyword, or limit with the "path" parameter). Avoid patterns that only contain wildcards like "*" or ".*".`,
134+
},
135+
],
136+
isError: true,
137+
};
138+
}
139+
97140
// Create timeout promise and keep reference to clear it later
98141
let timeoutId: ReturnType<typeof setTimeout> | undefined;
99142
const timeoutPromise = new Promise((_, reject) => {
@@ -102,6 +145,7 @@ export function createSearchPlugin(options: SearchOptions = {}): ToolPlugin {
102145
}, timeoutMs);
103146
});
104147

148+
console.log(`Searching for "${args.pattern}" in ${searchPath}`);
105149
// Create search promise
106150
const searchPromise = new Promise((resolve, reject) => {
107151
try {
@@ -144,13 +188,16 @@ export function createSearchPlugin(options: SearchOptions = {}): ToolPlugin {
144188
const fullMatchText =
145189
`${baseMatchText}\`\`\`\n${match.line}\n\`\`\`\n\n`;
146190

147-
// Check if adding this match would exceed size limit
148-
if ((output + fullMatchText).length > maxOutputSize) {
191+
const res = appendMatchSafely(
192+
output,
193+
fullMatchText,
194+
maxOutputSize,
195+
);
196+
if (!res.added) {
149197
// If we haven't shown any matches yet, show a truncated version
150198
if (matchesIncluded === 0) {
151199
const remainingSpace = maxOutputSize - output.length - 100; // Reserve 100 chars for warning
152200
if (remainingSpace > 50) {
153-
// Only truncate if we have reasonable space
154201
const truncatedLine = match.line.slice(0, remainingSpace);
155202
output +=
156203
`${baseMatchText}\`\`\`\n${truncatedLine}...\n\`\`\`\n\n`;
@@ -163,7 +210,7 @@ export function createSearchPlugin(options: SearchOptions = {}): ToolPlugin {
163210
break;
164211
}
165212

166-
output += fullMatchText;
213+
output = res.current;
167214
matchesIncluded++;
168215
}
169216

packages/core/src/service/tools.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const SseConfigSchema: z.ZodObject<Record<string, z.ZodTypeAny>> =
2020
export const StreamableHTTPSchema: z.ZodObject<Record<string, z.ZodTypeAny>> =
2121
BaseConfigSchema.extend({
2222
url: z.string().url(),
23+
transportType: z.literal("streamable-http").optional(),
2324
headers: z.record(z.string()).optional(),
2425
});
2526

0 commit comments

Comments
 (0)