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 {
|
export interface RegisterToolsOptions {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
tools?: AnyToolDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,12 +42,13 @@ export function registerAllTools(
|
|||||||
options?: RegisterToolsOptions
|
options?: RegisterToolsOptions
|
||||||
): void {
|
): void {
|
||||||
const isReadOnly = options?.readOnly ?? false;
|
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 server is in read-only mode, skip any non-read-only tools
|
||||||
if (isReadOnly && !tool.readOnly) {
|
if (isReadOnly && !tool.readOnly) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
registerTool(server, tool);
|
registerTool(server, tool as unknown as ToolDefinition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,13 @@ export async function handleListProjects(
|
|||||||
try {
|
try {
|
||||||
let filters: FilterElement[] | undefined;
|
let filters: FilterElement[] | undefined;
|
||||||
if (args.filters) {
|
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({
|
const result = await listProjects({
|
||||||
@@ -71,7 +77,7 @@ export async function handleListProjects(
|
|||||||
|
|
||||||
export const getProjectShape = {
|
export const getProjectShape = {
|
||||||
projectId: z
|
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"),
|
.describe("Numeric project ID or string identifier/slug"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -93,11 +93,13 @@ export async function handleGetQuery(
|
|||||||
args: GetQueryArgs
|
args: GetQueryArgs
|
||||||
): Promise<McpToolResponse> {
|
): Promise<McpToolResponse> {
|
||||||
try {
|
try {
|
||||||
const query = await getQuery(args.queryId);
|
const [query, results] = await Promise.all([
|
||||||
const results = await getQueryResults(args.queryId, {
|
getQuery(args.queryId),
|
||||||
pageSize: args.pageSize,
|
getQueryResults(args.queryId, {
|
||||||
offset: args.offset,
|
pageSize: args.pageSize,
|
||||||
});
|
offset: args.offset,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return formatToolSuccess({
|
return formatToolSuccess({
|
||||||
query,
|
query,
|
||||||
|
|||||||
+38
-1
@@ -252,6 +252,17 @@ describe("Project Tools", () => {
|
|||||||
expect(response.content[0]?.text).toContain("Error");
|
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 () => {
|
test("getProject validates schema and retrieves single project", async () => {
|
||||||
const schema = z.object(getProjectShape);
|
const schema = z.object(getProjectShape);
|
||||||
const parsed = schema.parse({ projectId: 4 });
|
const parsed = schema.parse({ projectId: 4 });
|
||||||
@@ -266,6 +277,13 @@ describe("Project Tools", () => {
|
|||||||
expect(result.identifier).toBe("mcp-test-project");
|
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 () => {
|
test("getProject catches errors and formats error response", async () => {
|
||||||
const failingClient = {
|
const failingClient = {
|
||||||
get: async () => {
|
get: async () => {
|
||||||
@@ -977,7 +995,20 @@ describe("Tool Registry", () => {
|
|||||||
|
|
||||||
test("registerAllTools filters out non-readOnly tools when readOnly is true", () => {
|
test("registerAllTools filters out non-readOnly tools when readOnly is true", () => {
|
||||||
const server = new McpServer({ name: "test-mcp", version: "1.0.0" });
|
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 = (
|
const registeredTools = (
|
||||||
server as unknown as {
|
server as unknown as {
|
||||||
@@ -985,7 +1016,13 @@ describe("Tool Registry", () => {
|
|||||||
}
|
}
|
||||||
)._registeredTools;
|
)._registeredTools;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
registeredTools["openproject_synthetic_mutating_tool"]
|
||||||
|
).toBeUndefined();
|
||||||
expect(Object.keys(registeredTools)).toHaveLength(10);
|
expect(Object.keys(registeredTools)).toHaveLength(10);
|
||||||
|
for (const tool of allTools) {
|
||||||
|
expect(registeredTools[tool.name]).toBeDefined();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user