Skip to main content
Glama
TCSoftInc

TestCollab MCP Server

by TCSoftInc

list_test_plans

Retrieve test plans from a TestCollab project with filtering, sorting, and pagination options to organize testing workflows.

Instructions

List test plans from a TestCollab project with optional filtering, sorting, and pagination.

Optional filters:

  • title_contains

  • status: 0/1/2/3 or draft/ready/finished/finished_with_failures

  • priority: 0/1/2 or low/normal/high

  • archived: true/false

  • created_by: creator user ID

  • test_plan_folder: folder ID or folder title

  • release: release ID or release title

  • created_at_from/to, updated_at_from/to, start_date_from/to, end_date_from/to, last_run_from/to

  • filter: raw filter object for advanced keys (merged with explicit filters)

Example: { "project_id": 16, "title_contains": "Release", "status": "ready", "priority": "high", "created_by": 27, "sort_by": "updated_at", "sort_order": "desc", "limit": 25, "offset": 0 }

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idNoProject ID (uses TC_DEFAULT_PROJECT env var if not specified)
limitNoMaximum results to return (1-100, default: 25)
offsetNoNumber of results to skip (default: 0)
sort_byNoSort field (default: updated_at)updated_at
sort_orderNoSort order (default: desc)desc
title_containsNoFilter plans whose title contains this string
statusNoFilter by status: 0/"draft", 1/"ready", 2/"finished", 3/"finished_with_failures"
priorityNoFilter by priority: 0/"low", 1/"normal", 2/"high"
archivedNoFilter by archived state
created_byNoFilter by creator user ID
test_plan_folderNoFilter by test plan folder ID or folder title
releaseNoFilter by release ID or release title
created_at_fromNoFilter by created_at >= this ISO date/time
created_at_toNoFilter by created_at <= this ISO date/time
updated_at_fromNoFilter by updated_at >= this ISO date/time
updated_at_toNoFilter by updated_at <= this ISO date/time
start_date_fromNoFilter by start_date >= this date (YYYY-MM-DD)
start_date_toNoFilter by start_date <= this date (YYYY-MM-DD)
end_date_fromNoFilter by end_date >= this date (YYYY-MM-DD)
end_date_toNoFilter by end_date <= this date (YYYY-MM-DD)
last_run_fromNoFilter by last_run >= this ISO date/time
last_run_toNoFilter by last_run <= this ISO date/time
filterNoAdvanced raw filter object (Strapi-style query keys, e.g. title_contains, created_at_gte, created_by, test_plan_folder, release)

Implementation Reference

  • The main handler function for the "list_test_plans" tool, which processes arguments, applies filters, calls the API client, and formats the result.
    export async function handleListTestPlans(
      args: unknown
    ): Promise<{ content: Array<{ type: "text"; text: string }> }> {
      const parsed = listTestPlansSchema.safeParse(args);
      if (!parsed.success) {
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                error: {
                  code: "VALIDATION_ERROR",
                  message: "Invalid input parameters",
                  details: parsed.error.errors,
                },
              }),
            },
          ],
        };
      }
    
      const {
        project_id,
        limit,
        offset,
        sort_by,
        sort_order,
        title_contains,
        status,
        priority,
        archived,
        created_by,
        test_plan_folder,
        release,
        created_at_from,
        created_at_to,
        updated_at_from,
        updated_at_to,
        start_date_from,
        start_date_to,
        end_date_from,
        end_date_to,
        last_run_from,
        last_run_to,
        filter,
      } = parsed.data;
    
      const requestContext = getRequestContext();
      const envConfig = requestContext ? null : getConfig();
      const resolvedProjectId =
        project_id ?? requestContext?.defaultProjectId ?? envConfig?.defaultProjectId;
    
      if (!resolvedProjectId) {
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                error: {
                  code: "MISSING_PROJECT_ID",
                  message:
                    "project_id is required. Either provide it in the request or set TC_DEFAULT_PROJECT environment variable.",
                },
              }),
            },
          ],
        };
      }
    
      try {
        const client = getApiClient();
        const mergedFilter: Record<string, unknown> = filter
          ? { ...filter }
          : {};
    
        if (test_plan_folder !== undefined) {
          const numericFolderId = toNumberId(test_plan_folder);
          if (numericFolderId !== undefined) {
            mergedFilter.test_plan_folder = numericFolderId;
          } else {
            const folderTitle = normalizeString(test_plan_folder);
            if (!folderTitle) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: {
                        code: "INVALID_TEST_PLAN_FOLDER",
                        message:
                          "test_plan_folder must be a numeric ID or non-empty folder title.",
                      },
                    }),
                  },
                ],
              };
            }
    
            const cachedContext = getCachedProjectContext(resolvedProjectId);
            const cachedFolders = mapTestPlanFoldersForLookup(
              Array.isArray(cachedContext?.test_plan_folders)
                ? cachedContext.test_plan_folders
                : []
            );
    
            let matchedFolders = findFoldersByTitle(cachedFolders, folderTitle);
    
            if (matchedFolders.length !== 1) {
              const folders = await client.listTestPlanFolders(resolvedProjectId);
              const liveFolders = mapTestPlanFoldersForLookup(
                Array.isArray(folders) ? folders : []
              );
              matchedFolders = findFoldersByTitle(liveFolders, folderTitle);
            }
    
            if (matchedFolders.length === 0) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: {
                        code: "TEST_PLAN_FOLDER_NOT_FOUND",
                        message: `Test plan folder not found with title "${folderTitle}" in that project.`,
                      },
                    }),
                  },
                ],
              };
            }
    
            if (matchedFolders.length > 1) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: {
                        code: "AMBIGUOUS_TEST_PLAN_FOLDER",
                        message: `Multiple folders matched "${folderTitle}". Provide folder ID instead.`,
                        details: {
                          matching_ids: matchedFolders.map((folder) => folder.id),
                        },
                      },
                    }),
                  },
                ],
              };
            }
    
            mergedFilter.test_plan_folder = matchedFolders[0].id;
          }
        }
    
        if (release !== undefined) {
          const numericReleaseId = toNumberId(release);
          if (numericReleaseId !== undefined) {
            mergedFilter.release = numericReleaseId;
          } else {
            const releaseTitle = normalizeString(release);
            if (!releaseTitle) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: {
                        code: "INVALID_RELEASE",
                        message: "release must be a numeric ID or non-empty release title.",
                      },
                    }),
                  },
                ],
              };
            }
    
            const cachedContext = getCachedProjectContext(resolvedProjectId);
            const cachedReleases = mapReleasesForLookup(
              Array.isArray(cachedContext?.releases) ? cachedContext.releases : []
            );
    
            let matchedReleases = findReleasesByTitle(cachedReleases, releaseTitle);
    
            if (matchedReleases.length !== 1) {
              const releases = await client.listReleases(resolvedProjectId);
              const liveReleases = mapReleasesForLookup(
                Array.isArray(releases) ? releases : []
              );
              matchedReleases = findReleasesByTitle(liveReleases, releaseTitle);
            }
    
            if (matchedReleases.length === 0) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: {
                        code: "RELEASE_NOT_FOUND",
                        message: `Release not found with title "${releaseTitle}" in that project.`,
                      },
                    }),
                  },
                ],
              };
            }
    
            if (matchedReleases.length > 1) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: {
                        code: "AMBIGUOUS_RELEASE",
                        message: `Multiple releases matched "${releaseTitle}". Provide release ID instead.`,
                        details: {
                          matching_ids: matchedReleases.map((item) => item.id),
                        },
                      },
                    }),
                  },
                ],
              };
            }
    
            mergedFilter.release = matchedReleases[0].id;
          }
        }
    
        if (title_contains !== undefined) {
          mergedFilter.title_contains = title_contains;
        }
    
        const statusCode = toStatusCode(status);
        if (statusCode !== undefined) {
          mergedFilter.status = statusCode;
        }
    
        const priorityCode = toPriorityCode(priority);
        if (priorityCode !== undefined) {
          mergedFilter.priority = priorityCode;
        }
    
        if (archived !== undefined) {
          mergedFilter.archived = archived;
        }
    
        if (created_by !== undefined) {
          mergedFilter.created_by = created_by;
        }
    
        if (created_at_from !== undefined) {
          mergedFilter.created_at_gte = created_at_from;
        }
        if (created_at_to !== undefined) {
          mergedFilter.created_at_lte = created_at_to;
        }
        if (updated_at_from !== undefined) {
          mergedFilter.updated_at_gte = updated_at_from;
        }
        if (updated_at_to !== undefined) {
          mergedFilter.updated_at_lte = updated_at_to;
        }
        if (start_date_from !== undefined) {
          mergedFilter.start_date_gte = start_date_from;
        }
        if (start_date_to !== undefined) {
          mergedFilter.start_date_lte = start_date_to;
        }
        if (end_date_from !== undefined) {
          mergedFilter.end_date_gte = end_date_from;
        }
        if (end_date_to !== undefined) {
          mergedFilter.end_date_lte = end_date_to;
        }
        if (last_run_from !== undefined) {
          mergedFilter.last_run_gte = last_run_from;
        }
        if (last_run_to !== undefined) {
          mergedFilter.last_run_lte = last_run_to;
        }
    
        const rows = await client.listTestPlans({
          projectId: resolvedProjectId,
          limit,
          offset,
          sort: `${sort_by}:${sort_order}`,
          ...(Object.keys(mergedFilter).length > 0 ? { filter: mergedFilter } : {}),
        });
    
        const testPlans = rows.map((plan) => {
          const planStatus =
            typeof plan.status === "number" ? statusCodeToLabel[plan.status] : undefined;
          const planPriority =
            typeof plan.priority === "number"
              ? priorityCodeToLabel[plan.priority]
              : undefined;
    
          return {
            id: plan.id,
            title: plan.title,
            description: plan.description,
            status: plan.status,
            statusLabel: planStatus ?? "Unknown",
            priority: plan.priority,
            priorityLabel: planPriority ?? "Unknown",
            archived: plan.archived,
            testPlanFolder: plan.testPlanFolder
              ? {
                  id: plan.testPlanFolder.id,
                  title: plan.testPlanFolder.title,
                }
              : null,
            release: plan.release
              ? {
                  id: plan.release.id,
                  title: plan.release.name,
                }
              : null,
            createdBy: plan.createdBy
              ? {
                  id: plan.createdBy.id,
                  name: plan.createdBy.name,
                  ...(plan.createdBy.username
                    ? { username: plan.createdBy.username }
                    : {}),
                }
              : null,
            assignedTo: (plan.assignedTo ?? []).map((user) => ({
              id: user.id,
              name: user.name,
              ...(user.username ? { username: user.username } : {}),
            })),
            startDate: plan.startDate,
            endDate: plan.endDate,
            actualStartDate: plan.actualStartDate,
            lastRun: plan.lastRun,
            createdAt: plan.createdAt,
            updatedAt: plan.updatedAt,
          };
        });
    
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(
                {
                  testPlans,
                  returned: testPlans.length,
                  limit,
                  offset,
                  hasMore: testPlans.length === limit,
                },
                null,
                2
              ),
            },
          ],
        };
      } catch (error) {
        const message = error instanceof Error ? error.message : "Unknown error";
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                error: {
                  code: "API_ERROR",
                  message,
                },
              }),
            },
          ],
        };
      }
    }
  • The Zod schema definition for input validation of the "list_test_plans" tool.
    export const listTestPlansSchema = z.object({
      project_id: z
        .number()
        .optional()
        .describe("Project ID (uses TC_DEFAULT_PROJECT env var if not specified)"),
      limit: z
        .number()
        .min(1)
        .max(100)
        .default(25)
        .describe("Maximum results to return (1-100, default: 25)"),
      offset: z
        .number()
        .min(0)
        .default(0)
        .describe("Number of results to skip (default: 0)"),
      sort_by: sortBySchema
        .default("updated_at")
        .describe("Sort field (default: updated_at)"),
      sort_order: z
        .enum(["asc", "desc"])
        .default("desc")
        .describe("Sort order (default: desc)"),
      title_contains: z
        .string()
        .min(1)
        .optional()
        .describe("Filter plans whose title contains this string"),
      status: statusInputSchema
        .optional()
        .describe(
          'Filter by status: 0/"draft", 1/"ready", 2/"finished", 3/"finished_with_failures"'
        ),
      priority: priorityInputSchema
        .optional()
        .describe('Filter by priority: 0/"low", 1/"normal", 2/"high"'),
      archived: z
        .boolean()
        .optional()
        .describe("Filter by archived state"),
      created_by: z
        .number()
        .optional()
        .describe("Filter by creator user ID"),
      test_plan_folder: z
        .union([z.number(), z.string()])
        .optional()
        .describe("Filter by test plan folder ID or folder title"),
      release: z
        .union([z.number(), z.string()])
        .optional()
        .describe("Filter by release ID or release title"),
      created_at_from: z
        .string()
        .optional()
        .describe("Filter by created_at >= this ISO date/time"),
      created_at_to: z
        .string()
        .optional()
        .describe("Filter by created_at <= this ISO date/time"),
      updated_at_from: z
        .string()
        .optional()
        .describe("Filter by updated_at >= this ISO date/time"),
      updated_at_to: z
        .string()
        .optional()
        .describe("Filter by updated_at <= this ISO date/time"),
      start_date_from: z
        .string()
        .optional()
        .describe("Filter by start_date >= this date (YYYY-MM-DD)"),
      start_date_to: z
        .string()
        .optional()
        .describe("Filter by start_date <= this date (YYYY-MM-DD)"),
      end_date_from: z
        .string()
        .optional()
        .describe("Filter by end_date >= this date (YYYY-MM-DD)"),
      end_date_to: z
        .string()
        .optional()
        .describe("Filter by end_date <= this date (YYYY-MM-DD)"),
      last_run_from: z
        .string()
        .optional()
        .describe("Filter by last_run >= this ISO date/time"),
      last_run_to: z
        .string()
        .optional()
        .describe("Filter by last_run <= this ISO date/time"),
      filter: z
        .record(z.unknown())
        .optional()
        .describe(
          "Advanced raw filter object (Strapi-style query keys, e.g. title_contains, created_at_gte, created_by, test_plan_folder, release)"
        ),
    });
  • Tool registration object including the tool name and schema definition.
    export const listTestPlansTool = {
      name: "list_test_plans",
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description carries full burden. Documents pagination controls and filter merging behavior ('merged with explicit filters'), but omits safety profile (read-only vs mutation), rate limits, empty result behavior, or required project permissions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Information-dense with clear hierarchy: one-sentence purpose summary, bulleted filter categories with specific enum values, and terminal JSON example. No redundant prose; every line provides actionable parameter guidance or syntax examples.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Excellent coverage of 23 input parameters with complex filtering logic, though no output schema exists and description doesn't specify return structure (e.g., array of test plan objects, total count). Given input complexity, this is acceptable but not ideal.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema has 100% coverage (baseline 3). Description adds significant value through concrete JSON example showing realistic parameter combination, and clarifies enum mappings (e.g., '0/1/2/3 or draft/ready/finished/finished_with_failures') in organized list format. Explains filter merging behavior not obvious from schema alone.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description opens with specific verb 'List' + resource 'test plans' + source 'TestCollab project', clearly distinguishing from sibling 'get_test_plan' (singular retrieval) and other CRUD operations. Scope is explicitly defined as supporting 'filtering, sorting, and pagination'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides extensive filter documentation that implies usage for bulk/search operations, but lacks explicit contrast with sibling 'get_test_plan' for single-item retrieval or guidance on when to prefer filtering vs. direct ID lookup. No 'when not to use' guidance provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TCSoftInc/testcollab-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server