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",

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