list_folders
Retrieve a nested tree of folders of specified type (TESTCASE, TESTCYCLE, or TESTPLAN) for a Jira project. Optionally provide a folder ID to return only that subtree, useful for large projects.
Instructions
List folders of a given type in a project as a nested tree with id, name, parentId, and children. Provide folderId to return only that subtree instead of the full project tree (recommended for large projects).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| projectId | Yes | Jira project numeric ID (e.g. 10011) | |
| folderType | Yes | Folder type to list | |
| folderId | No | Return only this folder and its children (subtree). Omit to get the full project tree. |
Implementation Reference
- src/index.ts:633-655 (handler)The handler function for the list_folders tool. It fetches folder data from the QMetry API, optionally filtering to a specific subtree by folderId using a recursive findSubtree helper.
async ({ projectId, folderType, folderId }) => { const folderSegments: Record<string, string> = { TESTCASE: "testcase-folders", TESTCYCLE: "testcycle-folders", TESTPLAN: "testplan-folders", }; const typeSegment = folderSegments[folderType]; const data = await qtmFetch(`/projects/${projectId}/${typeSegment}`) as any; if (folderId === undefined) return ok(data); const nodes = data?.data ?? data ?? []; // Find and return only the subtree rooted at folderId function findSubtree(items: any[]): any | null { for (const item of items) { if (item.id === folderId) return item; const found = findSubtree(item.children ?? []); if (found) return found; } return null; } const subtree = findSubtree(nodes); return ok(subtree ?? { error: `Folder ${folderId} not found` }); } ); - src/index.ts:626-632 (schema)Input schema for list_folders: requires projectId (string or number), folderType (enum: TESTCASE/TESTCYCLE/TESTPLAN), and optional folderId for subtree filtering.
{ projectId: z.union([z.string(), z.number()]).describe("Jira project numeric ID (e.g. 10011)"), folderType: z .enum(["TESTCASE", "TESTCYCLE", "TESTPLAN"]) .describe("Folder type to list"), folderId: z.number().int().optional().describe("Return only this folder and its children (subtree). Omit to get the full project tree."), }, - src/index.ts:623-655 (registration)Registration of the list_folders tool via the local 'tool' wrapper, which calls server.registerTool on the McpServer instance.
tool( "list_folders", "List folders of a given type in a project as a nested tree with id, name, parentId, and children. Provide folderId to return only that subtree instead of the full project tree (recommended for large projects).", { projectId: z.union([z.string(), z.number()]).describe("Jira project numeric ID (e.g. 10011)"), folderType: z .enum(["TESTCASE", "TESTCYCLE", "TESTPLAN"]) .describe("Folder type to list"), folderId: z.number().int().optional().describe("Return only this folder and its children (subtree). Omit to get the full project tree."), }, async ({ projectId, folderType, folderId }) => { const folderSegments: Record<string, string> = { TESTCASE: "testcase-folders", TESTCYCLE: "testcycle-folders", TESTPLAN: "testplan-folders", }; const typeSegment = folderSegments[folderType]; const data = await qtmFetch(`/projects/${projectId}/${typeSegment}`) as any; if (folderId === undefined) return ok(data); const nodes = data?.data ?? data ?? []; // Find and return only the subtree rooted at folderId function findSubtree(items: any[]): any | null { for (const item of items) { if (item.id === folderId) return item; const found = findSubtree(item.children ?? []); if (found) return found; } return null; } const subtree = findSubtree(nodes); return ok(subtree ?? { error: `Folder ${folderId} not found` }); } ); - src/index.ts:25-66 (helper)The qtmFetch helper used by the list_folders handler to make HTTP requests to the QMetry API.
async function qtmFetch( path: string, options: RequestInit = {}, attempt = 1 ): Promise<unknown> { const url = `${BASE_URL}${path}`; const headers: Record<string, string> = { apiKey: API_KEY ?? "", "Content-Type": "application/json", Accept: "application/json", ...(options.headers as Record<string, string> | undefined), }; const response = await fetch(url, { ...options, headers }); // Exponential back-off for rate limiting (max 3 attempts) if (response.status === 429 && attempt < 3) { const retryAfter = Number.parseInt( response.headers.get("Retry-After") ?? "1", 10 ); const delay = Math.max(retryAfter * 1000, 1000) * attempt; await new Promise((r) => setTimeout(r, delay)); return qtmFetch(path, options, attempt + 1); } const text = await response.text(); let body: unknown; try { body = text ? JSON.parse(text) : null; } catch { body = text; } if (!response.ok) { throw new Error( `HTTP ${response.status} ${response.statusText}: ${JSON.stringify(body)}` ); } return body; } - src/index.ts:172-184 (helper)The 'tool' wrapper function that registers tools on the MCP server, used to register list_folders.
const tool = <Shape extends z.ZodRawShape>( name: string, description: string, inputSchema: Shape, // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (args: z.infer<z.ZodObject<Shape>>) => Promise<any> ) => server.registerTool( name, { description, inputSchema }, // eslint-disable-next-line @typescript-eslint/no-explicit-any callback as any );