fix(tools): address final code review findings on validation, concurrency, and testing
This commit is contained in:
+4
-2
@@ -30,6 +30,7 @@ export const allTools: AnyToolDefinition[] = [
|
||||
|
||||
export interface RegisterToolsOptions {
|
||||
readOnly?: boolean;
|
||||
tools?: AnyToolDefinition[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,12 +42,13 @@ export function registerAllTools(
|
||||
options?: RegisterToolsOptions
|
||||
): void {
|
||||
const isReadOnly = options?.readOnly ?? false;
|
||||
const toolsToRegister = options?.tools ?? allTools;
|
||||
|
||||
for (const tool of allTools) {
|
||||
for (const tool of toolsToRegister) {
|
||||
// If server is in read-only mode, skip any non-read-only tools
|
||||
if (isReadOnly && !tool.readOnly) {
|
||||
continue;
|
||||
}
|
||||
registerTool(server, tool);
|
||||
registerTool(server, tool as unknown as ToolDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,13 @@ export async function handleListProjects(
|
||||
try {
|
||||
let filters: FilterElement[] | undefined;
|
||||
if (args.filters) {
|
||||
filters = JSON.parse(args.filters) as FilterElement[];
|
||||
const parsed = JSON.parse(args.filters);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return formatToolError(
|
||||
new Error("filters parameter must be a JSON array string")
|
||||
);
|
||||
}
|
||||
filters = parsed as FilterElement[];
|
||||
}
|
||||
|
||||
const result = await listProjects({
|
||||
@@ -71,7 +77,7 @@ export async function handleListProjects(
|
||||
|
||||
export const getProjectShape = {
|
||||
projectId: z
|
||||
.union([z.number().int().positive(), z.string()])
|
||||
.union([z.number().int().positive(), z.string().min(1)])
|
||||
.describe("Numeric project ID or string identifier/slug"),
|
||||
};
|
||||
|
||||
|
||||
@@ -93,11 +93,13 @@ export async function handleGetQuery(
|
||||
args: GetQueryArgs
|
||||
): Promise<McpToolResponse> {
|
||||
try {
|
||||
const query = await getQuery(args.queryId);
|
||||
const results = await getQueryResults(args.queryId, {
|
||||
const [query, results] = await Promise.all([
|
||||
getQuery(args.queryId),
|
||||
getQueryResults(args.queryId, {
|
||||
pageSize: args.pageSize,
|
||||
offset: args.offset,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
|
||||
return formatToolSuccess({
|
||||
query,
|
||||
|
||||
+38
-1
@@ -252,6 +252,17 @@ describe("Project Tools", () => {
|
||||
expect(response.content[0]?.text).toContain("Error");
|
||||
});
|
||||
|
||||
test("handleListProjects catches non-array JSON filters and returns error response", async () => {
|
||||
const response = await runWithContext(
|
||||
{ client: dummyClient, isReadOnly: false },
|
||||
() => handleListProjects({ filters: '{"not":"an array"}' })
|
||||
);
|
||||
expect(response.isError).toBe(true);
|
||||
expect(response.content[0]?.text).toContain(
|
||||
"filters parameter must be a JSON array string"
|
||||
);
|
||||
});
|
||||
|
||||
test("getProject validates schema and retrieves single project", async () => {
|
||||
const schema = z.object(getProjectShape);
|
||||
const parsed = schema.parse({ projectId: 4 });
|
||||
@@ -266,6 +277,13 @@ describe("Project Tools", () => {
|
||||
expect(result.identifier).toBe("mcp-test-project");
|
||||
});
|
||||
|
||||
test("getProject rejects empty string projectId", () => {
|
||||
const schema = z.object(getProjectShape);
|
||||
expect(() => schema.parse({ projectId: "" })).toThrow();
|
||||
expect(() => schema.parse({ projectId: 0 })).toThrow();
|
||||
expect(() => schema.parse({ projectId: -1 })).toThrow();
|
||||
});
|
||||
|
||||
test("getProject catches errors and formats error response", async () => {
|
||||
const failingClient = {
|
||||
get: async () => {
|
||||
@@ -977,7 +995,20 @@ describe("Tool Registry", () => {
|
||||
|
||||
test("registerAllTools filters out non-readOnly tools when readOnly is true", () => {
|
||||
const server = new McpServer({ name: "test-mcp", version: "1.0.0" });
|
||||
registerAllTools(server, { readOnly: true });
|
||||
const syntheticMutatingTool: ToolDefinition = {
|
||||
name: "openproject_synthetic_mutating_tool",
|
||||
description: "A synthetic mutating tool for testing readOnly filtering",
|
||||
readOnly: false,
|
||||
execute: async () => formatToolSuccess({ mutated: true }),
|
||||
};
|
||||
|
||||
registerAllTools(server, {
|
||||
readOnly: true,
|
||||
tools: [
|
||||
...allTools,
|
||||
syntheticMutatingTool as unknown as AnyToolDefinition,
|
||||
],
|
||||
});
|
||||
|
||||
const registeredTools = (
|
||||
server as unknown as {
|
||||
@@ -985,7 +1016,13 @@ describe("Tool Registry", () => {
|
||||
}
|
||||
)._registeredTools;
|
||||
|
||||
expect(
|
||||
registeredTools["openproject_synthetic_mutating_tool"]
|
||||
).toBeUndefined();
|
||||
expect(Object.keys(registeredTools)).toHaveLength(10);
|
||||
for (const tool of allTools) {
|
||||
expect(registeredTools[tool.name]).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user