Skip to main content
Glama
TCSoftInc

TestCollab MCP Server

by TCSoftInc

create_test_plan

Create structured test plans in TestCollab with optional test cases, configurations, and team assignments to organize testing workflows.

Instructions

Create a test plan in TestCollab using a single MCP tool call.

Before calling this tool:

  • Ask follow-up questions for missing required information.

  • Do not infer or auto-generate required values like project_id.

Execution flow:

  1. POST /testplans

  2. POST /testplantestcases/bulkAdd (optional)

  3. POST /testplanconfigurations (optional)

  4. POST /testplans/assign (optional)

Optional:

  • project_id

  • title (defaults to "Test Plan DD Month YYYY HH:mm:ss" if omitted)

  • description

  • priority (0=Low, 1=Normal, 2=High)

  • test_plan_folder (ID or title)

  • release (ID or title)

  • start_date, end_date

  • custom_fields

  • test_cases (test_case_ids/selector/assignee; assignee supports user ID/"me"/name)

  • configurations

  • assignment (supports user IDs/"me"/names; if user says "assign to me", use "me")

Example: { "project_id": 16, "title": "Release 2.9 Regression", "priority": 1, "test_cases": { "test_case_ids": [101, 102, 103] }, "configurations": [ [{ "field": "Browser", "value": "Chrome" }, { "field": "OS", "value": "Windows" }] ], "assignment": { "executor": "team", "assignment_criteria": "testCase", "assignment_method": "automatic", "user_ids": [27, 31] } }

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idNoProject ID (optional if TC_DEFAULT_PROJECT is set)
titleNoTest plan title (optional; defaults to "Test Plan DD Month YYYY HH:mm:ss")
descriptionNoTest plan description (HTML supported)
priorityNoPriority: 0=Low, 1=Normal, 2=High
test_plan_folderNoTest plan folder ID or title (null to place at root)
releaseNoRelease ID or title
start_dateNoPlanned start date (YYYY-MM-DD)
end_dateNoPlanned end date (YYYY-MM-DD)
custom_fieldsNoArray of test plan custom field values
test_casesNoTest cases to bulk-add immediately after plan creation
configurationsNoConfiguration matrix to attach to the test plan
assignmentNoAssignment payload to execute after creation

Implementation Reference

  • The handler function that executes the "create_test_plan" tool logic.
    export async function handleCreateTestPlan(args: unknown): Promise<ToolResponse> {
      const parsed = createTestPlanSchema.safeParse(args);
      if (!parsed.success) {
        const missingFields = extractMissingRequiredFields(parsed.error.errors);
        if (missingFields.length > 0) {
          return toMissingInfoError(
            "MISSING_REQUIRED_INFO",
            "Missing required information to create a test plan.",
            missingFields
          );
        }
        return toError("VALIDATION_ERROR", "Invalid input parameters", parsed.error.errors);
      }
    
      const {
        project_id,
        title,
        description,
        priority,
        test_plan_folder,
        release,
        start_date,
        end_date,
        custom_fields,
        test_cases,
        configurations,
        assignment,
      } = parsed.data;
    
      const normalizedTitle = normalizeString(title) ?? buildDefaultTestPlanTitle();
    
      const requestContext = getRequestContext();
      const envConfig = requestContext ? null : getConfig();
      const resolvedProjectId =
        project_id ?? requestContext?.defaultProjectId ?? envConfig?.defaultProjectId;
    
      if (!resolvedProjectId) {
        return toMissingInfoError(
          "MISSING_PROJECT_ID",
          "project_id is required. Either provide it in the request or set TC_DEFAULT_PROJECT.",
          ["project_id"]
        );
      }
    
      const assignmentUsers = normalizeAssignmentUsers(assignment?.user_ids);
      if (assignmentUsers.invalidValues.length > 0) {
        return toError(
          "INVALID_ASSIGNMENT_USERS",
          "assignment.user_ids contains invalid values.",
          {
            invalid_values: assignmentUsers.invalidValues,
          }
        );
      }
      if (
        assignmentUsers.hasMe &&
        (assignmentUsers.userIds.length > 0 || assignmentUsers.userLookups.length > 0)
      ) {
        return toError(
          "INVALID_ASSIGNMENT_USERS",
          'assignment.user_ids cannot mix "me" with user IDs or user names.'
        );
      }
    
      const assignmentUserIds = assignmentUsers.userIds;
      const assignmentUserLookups = assignmentUsers.userLookups;
      const assignmentTargetsMe = assignmentUsers.hasMe || assignment?.executor === "me";
      const resolvedAssignmentExecutor: "me" | "team" = assignmentTargetsMe
        ? "me"
        : (assignment?.executor ?? "team");
      const assignmentTestCaseIds = normalizeNumberIds(assignment?.test_case_ids);
      const assignmentConfigurationIds = normalizeNumberIds(
        assignment?.configuration_ids
      );
      const assignmentSelector = assignment?.selector ?? [];
    
      if (
        assignment?.assignment_method === "manual" &&
        !assignmentTargetsMe &&
        assignmentUserIds.length === 0 &&
        assignmentUserLookups.length === 0
      ) {
        return toMissingInfoError(
          "MISSING_ASSIGNMENT_USERS",
          "Manual assignment requires at least one user_id in assignment.user_ids.",
          ["assignment.user_ids"]
        );
      }
    
      if (
        assignment?.assignment_method === "manual" &&
        assignment.assignment_criteria === "testCase" &&
        assignmentTestCaseIds.length === 0 &&
        assignmentSelector.length === 0
      ) {
        return toMissingInfoError(
          "MISSING_ASSIGNMENT_TEST_CASES",
          "Manual testCase assignment requires assignment.test_case_ids or assignment.selector.",
          ["assignment.test_case_ids", "assignment.selector"]
        );
      }
    
      if (
        assignment?.assignment_method === "manual" &&
        assignment.assignment_criteria === "configuration" &&
        assignmentConfigurationIds.length === 0 &&
        (!configurations || configurations.length === 0)
      ) {
        return toMissingInfoError(
          "MISSING_ASSIGNMENT_CONFIGURATIONS",
          "Manual configuration assignment requires assignment.configuration_ids or configurations.",
          ["assignment.configuration_ids", "configurations"]
        );
      }
    
      const testCaseIds = normalizeNumberIds(test_cases?.test_case_ids);
      const testCaseSelector = test_cases?.selector;
      const hasTestCaseAssignee = test_cases?.assignee !== undefined;
      const testCaseAssigneeValue = normalizeAssignee(test_cases?.assignee);
    
      if (hasTestCaseAssignee && testCaseAssigneeValue === undefined) {
        return toError(
          "INVALID_TEST_CASE_ASSIGNEE",
          'test_cases.assignee must be a user ID, "me", name, username, or email.'
        );
      }
    
      const shouldAddTestCases =
        test_cases !== undefined &&
        (testCaseIds.length > 0 || (testCaseSelector?.length ?? 0) > 0);
      const shouldCreateConfigurations =
        configurations !== undefined && configurations.length > 0;
      const shouldAssignFromTestCaseAssignee =
        assignment === undefined &&
        hasTestCaseAssignee &&
        shouldAddTestCases;
      const shouldAssign = assignment !== undefined || shouldAssignFromTestCaseAssignee;
    
      const steps: Record<string, { endpoint: string; status: string; detail?: string }> = {
        create_test_plan: { endpoint: "/testplans", status: "pending" },
        add_test_cases: {
          endpoint: "/testplantestcases/bulkAdd",
          status: shouldAddTestCases ? "pending" : "skipped",
        },
        add_configurations: {
          endpoint: "/testplanconfigurations",
          status: shouldCreateConfigurations ? "pending" : "skipped",
        },
        assign_test_plan: {
          endpoint: "/testplans/assign",
          status: shouldAssign ? "pending" : "skipped",
        },
      };
    
      let createdPlanId: number | undefined;
      let createdPlanTitle: string | undefined;
    
      try {
        const client = getApiClient();
    
        // Resolve folder (supports ID or title)
        let resolvedFolderId: number | null | undefined;
        if (test_plan_folder !== undefined) {
          if (test_plan_folder === null) {
            resolvedFolderId = null;
          } else {
            const numericFolderId = toNumberId(test_plan_folder);
            if (numericFolderId !== undefined) {
              resolvedFolderId = numericFolderId;
            } else {
              const folderTitle = normalizeString(test_plan_folder);
              if (!folderTitle) {
                return toError(
                  "INVALID_TEST_PLAN_FOLDER",
                  "test_plan_folder must be a numeric ID, non-empty title, or null."
                );
              }
    
              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 toError(
                  "TEST_PLAN_FOLDER_NOT_FOUND",
                  `Test plan folder not found with title "${folderTitle}" in that project.`
                );
              }
    
              if (matchedFolders.length > 1) {
                const matchingIds = matchedFolders.map((folder) => folder.id);
                return toError(
                  "AMBIGUOUS_TEST_PLAN_FOLDER",
                  `Multiple folders matched "${folderTitle}". Provide folder ID instead.`,
                  { matching_ids: matchingIds }
                );
              }
    
              resolvedFolderId = matchedFolders[0].id;
            }
          }
        }
    
        // Resolve release (supports ID or title)
        let resolvedReleaseId: number | undefined;
        if (release !== undefined) {
          const numericReleaseId = toNumberId(release);
          if (numericReleaseId !== undefined) {
            resolvedReleaseId = numericReleaseId;
          } else {
            const releaseTitle = normalizeString(release);
            if (!releaseTitle) {
              return toError(
                "INVALID_RELEASE",
                "release must be a numeric ID or non-empty 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 toError(
                "RELEASE_NOT_FOUND",
                `Release not found with title "${releaseTitle}" in that project.`
              );
            }
    
            if (matchedReleases.length > 1) {
              const matchingIds = matchedReleases.map((item) => item.id);
              return toError(
                "AMBIGUOUS_RELEASE",
                `Multiple releases matched "${releaseTitle}". Provide release ID instead.`,
                { matching_ids: matchingIds }
              );
            }
    
            resolvedReleaseId = matchedReleases[0].id;
          }
        }
    
        let resolvedAssignmentUserIds = assignmentUserIds;
        let resolvedTestCaseAssignee: ResolvedAssigneeValue | undefined =
          typeof testCaseAssigneeValue === "number" || testCaseAssigneeValue === "me"
            ? testCaseAssigneeValue
            : undefined;
        const testCaseAssigneeLookup =
          typeof testCaseAssigneeValue === "string" && testCaseAssigneeValue !== "me"
            ? testCaseAssigneeValue
            : undefined;
    
        if (assignmentUserLookups.length > 0 || testCaseAssigneeLookup !== undefined) {
          const projectUsersRaw = await client.listProjectUsers(resolvedProjectId);
          const projectUsers = mapProjectUsersForLookup(
            Array.isArray(projectUsersRaw) ? projectUsersRaw : []
          );
    
          const toUserMatchPayload = (user: ProjectUserLookup) => ({
            id: user.id,
            name: user.name,
            ...(user.username ? { username: user.username } : {}),
            ...(user.email ? { email: user.email } : {}),
          });
    
          if (assignmentUserLookups.length > 0) {
            const resolvedLookupIds: number[] = [];
    
            for (const lookup of assignmentUserLookups) {
              const matches = findProjectUserMatches(projectUsers, lookup);
              if (matches.length === 0) {
                return toError(
                  "ASSIGNEE_NOT_FOUND",
                  `No project user matched "${lookup}" for assignment.user_ids.`,
                  {
                    field: "assignment.user_ids",
                    lookup,
                  }
                );
              }
              if (matches.length > 1) {
                return toError(
                  "AMBIGUOUS_ASSIGNEE",
                  `Multiple project users matched "${lookup}" for assignment.user_ids. Use a numeric user ID.`,
                  {
                    field: "assignment.user_ids",
                    lookup,
                    matches: matches.map(toUserMatchPayload),
                  }
                );
              }
              resolvedLookupIds.push(matches[0].id);
            }
    
            resolvedAssignmentUserIds = dedupeNumbers([
              ...assignmentUserIds,
              ...resolvedLookupIds,
            ]);
          }
    
          if (testCaseAssigneeLookup) {
            const matches = findProjectUserMatches(projectUsers, testCaseAssigneeLookup);
            if (matches.length === 0) {
              return toError(
                "ASSIGNEE_NOT_FOUND",
                `No project user matched "${testCaseAssigneeLookup}" for test_cases.assignee.`,
                {
                  field: "test_cases.assignee",
                  lookup: testCaseAssigneeLookup,
                }
              );
            }
            if (matches.length > 1) {
              return toError(
                "AMBIGUOUS_ASSIGNEE",
                `Multiple project users matched "${testCaseAssigneeLookup}" for test_cases.assignee. Use a numeric user ID.`,
                {
                  field: "test_cases.assignee",
                  lookup: testCaseAssigneeLookup,
                  matches: matches.map(toUserMatchPayload),
                }
              );
            }
            resolvedTestCaseAssignee = matches[0].id;
          }
        }
    
        // Resolve custom fields for TestPlan entity
        let resolvedCustomFields:
          | Array<{
              id: number;
              name: string;
              label?: string;
              value: CustomFieldResolvedValue;
              valueLabel?: string | string[];
              color?: string;
            }>
          | undefined;
    
        if (custom_fields && custom_fields.length > 0) {
          const project = await client.getProject(resolvedProjectId);
          const companyId = getCompanyIdFromProject(project);
          const customFieldList = await client.listProjectCustomFields(
            resolvedProjectId,
            companyId,
            "TestPlan"
          );
    
          const definitionsByName = new Map<string, CustomFieldDefinition>();
          const definitionsById = new Map<number, CustomFieldDefinition>();
    
          customFieldList.forEach((field) => {
            const id = toNumberId(getField(field, "id"));
            const name = normalizeString(getField<string>(field, "name"));
            if (id === undefined || !name) {
              return;
            }
    
            const fieldType =
              getField<string>(field, "field_type") ?? getField<string>(field, "type");
            const directOptions = getField<unknown[]>(field, "options");
            const extra = getField<Record<string, unknown>>(field, "extra");
            const extraOptions = extra ? getField<unknown[]>(extra, "options") : undefined;
            const options = buildOptionLookup(directOptions ?? extraOptions ?? []);
    
            const definition: CustomFieldDefinition = {
              id,
              name,
              label: normalizeString(getField<string>(field, "label")),
              fieldType: normalizeString(fieldType),
              options,
            };
            definitionsByName.set(name, definition);
            definitionsById.set(id, definition);
          });
    
          const missingFields: string[] = [];
          resolvedCustomFields = custom_fields
            .map((field) => {
              const fieldId = toNumberId(field.id);
              const byId = fieldId !== undefined ? definitionsById.get(fieldId) : undefined;
              const byName = definitionsByName.get(field.name);
              const definition = byId ?? byName;
    
              if (!definition && fieldId === undefined) {
                missingFields.push(field.name);
                return undefined;
              }
    
              const resolvedId = definition?.id ?? fieldId;
              if (resolvedId === undefined) {
                missingFields.push(field.name);
                return undefined;
              }
    
              let resolvedValue: CustomFieldResolvedValue = field.value;
              let resolvedValueLabel: string | string[] | undefined = field.valueLabel;
    
              if (definition && isDropdownFieldType(definition.fieldType)) {
                if (Array.isArray(field.value)) {
                  throw new Error(
                    `Custom field "${definition.name}" expects a single value, not an array.`
                  );
                }
    
                if (isNonNumericString(field.value)) {
                  const matchedOption = findOptionByLabel(definition.options, field.value);
                  if (!matchedOption) {
                    throw new Error(
                      `Custom field option "${field.value}" not found for "${definition.name}".`
                    );
                  }
                  resolvedValue = matchedOption.id;
                  resolvedValueLabel =
                    typeof field.valueLabel === "string" && field.valueLabel.trim().length > 0
                      ? field.valueLabel
                      : matchedOption.label;
                }
              }
    
              if (definition && isMultiSelectFieldType(definition.fieldType)) {
                const inputValues = Array.isArray(field.value) ? field.value : [field.value];
                const outputValues: Array<string | number> = [];
                const outputLabels: string[] = [];
    
                inputValues.forEach((inputValue) => {
                  const numeric = toNumberId(inputValue);
                  if (numeric !== undefined) {
                    outputValues.push(numeric);
                    return;
                  }
                  if (isNonNumericString(inputValue)) {
                    const matchedOption = findOptionByLabel(
                      definition.options,
                      inputValue
                    );
                    if (!matchedOption) {
                      throw new Error(
                        `Custom field option "${inputValue}" not found for "${definition.name}".`
                      );
                    }
                    outputValues.push(matchedOption.id);
                    outputLabels.push(matchedOption.label);
                  }
                });
    
                resolvedValue = outputValues;
                if (Array.isArray(field.valueLabel)) {
                  resolvedValueLabel = field.valueLabel;
                } else if (outputLabels.length > 0) {
                  resolvedValueLabel = outputLabels;
                }
              }
    
              return {
                id: resolvedId,
                name: definition?.name ?? field.name,
                ...(field.label !== undefined
                  ? { label: field.label }
                  : definition?.label
                    ? { label: definition.label }
                    : {}),
                value: resolvedValue,
                ...(resolvedValueLabel !== undefined
                  ? { valueLabel: resolvedValueLabel }
                  : {}),
                ...(field.color !== undefined ? { color: field.color } : {}),
              };
            })
            .filter(
              (
                field
              ): field is {
                id: number;
                name: string;
                label?: string;
                value: CustomFieldResolvedValue;
                valueLabel?: string | string[];
                color?: string;
              } => field !== undefined
            );
    
          if (missingFields.length > 0) {
            return toError(
              "CUSTOM_FIELD_NOT_FOUND",
              `Custom field(s) not found: ${Array.from(new Set(missingFields)).join(", ")}`
            );
          }
        }
    
        // Step 1: Create test plan
        steps.create_test_plan.status = "in_progress";
        const createResult = await client.createTestPlan({
          projectId: resolvedProjectId,
          title: normalizedTitle,
          description,
          priority,
          testPlanFolderId: resolvedFolderId,
          release: resolvedReleaseId,
          startDate: start_date,
          endDate: end_date,
          customFields: resolvedCustomFields,
        });
    
        const createFailure = apiFailureMessage(createResult);
        if (createFailure) {
          steps.create_test_plan.status = "failed";
          steps.create_test_plan.detail = createFailure;
          return toToolResponse(
            {
              error: {
                code: "CREATE_TEST_PLAN_FAILED",
                message: createFailure,
                step: "create_test_plan",
              },
              steps,
            },
            true
          );
        }
    
        createdPlanId = extractId(createResult);
        createdPlanTitle =
          normalizeString(getField<string>(createResult, "title")) ?? normalizedTitle;
    
        if (createdPlanId === undefined) {
          steps.create_test_plan.status = "failed";
          steps.create_test_plan.detail = "Create response did not include test plan ID.";
          return toToolResponse(
            {
              error: {
                code: "INVALID_CREATE_TEST_PLAN_RESPONSE",
                message: "Create response did not include test plan ID.",
                step: "create_test_plan",
              },
              steps,
            },
            true
          );
        }
    
        steps.create_test_plan.status = "completed";
    
        // Step 2: Bulk add test cases
        let bulkAddResult: Record<string, unknown> | undefined;
        if (shouldAddTestCases) {
          steps.add_test_cases.status = "in_progress";
    
          bulkAddResult = await client.bulkAddTestPlanTestCases({
            testplan: createdPlanId,
            testCaseCollection: toSelectorCollection(testCaseIds, testCaseSelector),
            ...(resolvedTestCaseAssignee !== undefined
              ? { assignee: resolvedTestCaseAssignee }
              : {}),
          });
    
          const bulkAddFailure = apiFailureMessage(bulkAddResult);
          if (bulkAddFailure) {
            steps.add_test_cases.status = "failed";
            steps.add_test_cases.detail = bulkAddFailure;
            return toToolResponse(
              {
                error: {
                  code: "ADD_TEST_CASES_FAILED",
                  message: bulkAddFailure,
                  step: "add_test_cases",
                },
                testPlan: {
                  id: createdPlanId,
                  title: createdPlanTitle,
                  project_id: resolvedProjectId,
                },
                steps,
              },
              true
            );
          }
    
          steps.add_test_cases.status = "completed";
        }
    
        // Step 3: Create configurations
        let createConfigurationsResult:
          | Array<Record<string, unknown>>
          | Record<string, unknown>
          | undefined;
        let createdConfigurationIds: number[] = [];
    
        if (shouldCreateConfigurations) {
          steps.add_configurations.status = "in_progress";
    
          createConfigurationsResult = await client.createTestPlanConfigurations({
            projectId: resolvedProjectId,
            testplan: createdPlanId,
            parameters: (configurations ?? []).map((row) =>
              row.map((entry) => ({
                ...(entry.id !== undefined ? { id: entry.id } : {}),
                field: entry.field,
                value: entry.value,
              }))
            ),
          });
    
          const createConfigurationsFailure = apiFailureMessage(createConfigurationsResult);
          if (createConfigurationsFailure) {
            steps.add_configurations.status = "failed";
            steps.add_configurations.detail = createConfigurationsFailure;
            return toToolResponse(
              {
                error: {
                  code: "ADD_CONFIGURATIONS_FAILED",
                  message: createConfigurationsFailure,
                  step: "add_configurations",
                },
                testPlan: {
                  id: createdPlanId,
                  title: createdPlanTitle,
                  project_id: resolvedProjectId,
                },
                steps,
              },
              true
            );
          }
    
          createdConfigurationIds = extractIds(createConfigurationsResult);
          steps.add_configurations.status = "completed";
        }
    
        // Step 4: Assignment
        let assignResult: Record<string, unknown> | undefined;
        if (shouldAssign) {
          steps.assign_test_plan.status = "in_progress";
    
          const assignFromTestCaseAssignee =
            assignment === undefined &&
            resolvedTestCaseAssignee !== undefined &&
            shouldAddTestCases;
          const assignmentExecutorForPayload: "me" | "team" = assignFromTestCaseAssignee
            ? resolvedTestCaseAssignee === "me"
              ? "me"
              : "team"
            : resolvedAssignmentExecutor;
          const assignmentCriteriaForPayload: "testCase" | "configuration" =
            assignment?.assignment_criteria ?? "testCase";
          const assignmentMethodForPayload: "automatic" | "manual" =
            assignment?.assignment_method ?? "automatic";
    
          const resolvedAssignmentConfigIds =
            assignmentCriteriaForPayload === "configuration"
              ? assignmentConfigurationIds.length > 0
                ? assignmentConfigurationIds
                : createdConfigurationIds
              : [];
    
          if (
            assignmentMethodForPayload === "manual" &&
            assignmentCriteriaForPayload === "configuration" &&
            resolvedAssignmentConfigIds.length === 0
          ) {
            steps.assign_test_plan.status = "failed";
            steps.assign_test_plan.detail =
              "No configuration IDs available for manual configuration assignment.";
            return toToolResponse(
              {
                error: {
                  code: "MISSING_ASSIGNMENT_CONFIGURATIONS",
                  message:
                    "No configuration IDs available for manual configuration assignment.",
                  step: "assign_test_plan",
                  details: {
                    missing_fields: ["assignment.configuration_ids", "configurations"],
                    follow_up_questions: [
                      missingInfoQuestionByField["assignment.configuration_ids"],
                      missingInfoQuestionByField["configurations"],
                    ],
                  },
                },
                testPlan: {
                  id: createdPlanId,
                  title: createdPlanTitle,
                  project_id: resolvedProjectId,
                },
                steps,
              },
              true
            );
          }
    
          const assignmentUsersForPayload: Array<number | "me"> =
            assignFromTestCaseAssignee && resolvedTestCaseAssignee !== undefined
              ? [resolvedTestCaseAssignee]
              : assignmentExecutorForPayload === "me"
                ? ["me"]
                : resolvedAssignmentUserIds;
    
          assignResult = await client.assignTestPlan({
            projectId: resolvedProjectId,
            testplan: createdPlanId,
            executor: assignmentExecutorForPayload,
            assignmentCriteria: assignmentCriteriaForPayload,
            assignmentMethod: assignmentMethodForPayload,
            assignment: {
              user: assignmentUsersForPayload,
              testCases: assignFromTestCaseAssignee
                ? toSelectorCollection(testCaseIds, testCaseSelector)
                : toSelectorCollection(assignmentTestCaseIds, assignmentSelector),
              configuration:
                assignmentCriteriaForPayload === "configuration"
                  ? resolvedAssignmentConfigIds
                  : null,
            },
          });
    
          const assignFailure = apiFailureMessage(assignResult);
          if (assignFailure) {
            steps.assign_test_plan.status = "failed";
            steps.assign_test_plan.detail = assignFailure;
            return toToolResponse(
              {
                error: {
                  code: "ASSIGN_TEST_PLAN_FAILED",
                  message: assignFailure,
                  step: "assign_test_plan",
                },
                testPlan: {
                  id: createdPlanId,
                  title: createdPlanTitle,
                  project_id: resolvedProjectId,
                },
                steps,
              },
              true
            );
          }
    
          steps.assign_test_plan.status = "completed";
        }
    
        return toToolResponse(
          {
            success: true,
            message: "Test plan created successfully",
            testPlan: {
              id: createdPlanId,
              title: createdPlanTitle,
              project_id: resolvedProjectId,
            },
            steps,
            results: {
              ...(bulkAddResult !== undefined ? { add_test_cases: bulkAddResult } : {}),
              ...(createConfigurationsResult !== undefined
                ? { add_configurations: createConfigurationsResult }
                : {}),
              ...(assignResult !== undefined ? { assign_test_plan: assignResult } : {}),
            },
          },
          true
        );
      } catch (error) {
        const message = error instanceof Error ? error.message : "Unknown error";
        return toToolResponse(
          {
            error: {
              code: "API_ERROR",
              message,
            },
            ...(createdPlanId !== undefined
              ? {
                  testPlan: {
                    id: createdPlanId,
                    title: createdPlanTitle ?? normalizedTitle,
                    project_id: resolvedProjectId,
                  },
                }
              : {}),
            steps,
          },
          true
        );
      }
    }
  • The Zod schema definition for input validation of the "create_test_plan" tool.
    export const createTestPlanSchema = z.object({
      project_id: z
        .number()
        .optional()
        .describe("Project ID (optional if TC_DEFAULT_PROJECT is set)"),
      title: z
        .string()
        .optional()
        .describe(
          'Test plan title (optional; defaults to "Test Plan DD Month YYYY HH:mm:ss")'
        ),
      description: z.string().optional().describe("Test plan description (HTML supported)"),
      priority: z
        .number()
        .min(0)
        .max(2)
        .optional()
        .describe("Priority: 0=Low, 1=Normal, 2=High"),
      test_plan_folder: z
        .union([z.number(), z.string(), z.null()])
        .optional()
        .describe("Test plan folder ID or title (null to place at root)"),
      release: z
        .union([z.number(), z.string()])
        .optional()
        .describe("Release ID or title"),
      start_date: z
        .string()
        .optional()
        .describe("Planned start date (YYYY-MM-DD)"),
      end_date: z
        .string()
        .optional()
        .describe("Planned end date (YYYY-MM-DD)"),
      custom_fields: z
        .array(customFieldSchema)
        .optional()
        .describe("Array of test plan custom field values"),
      test_cases: testCasesSchema
        .optional()
        .describe("Test cases to bulk-add immediately after plan creation"),
      configurations: z
        .array(z.array(configurationPairSchema))
        .optional()
        .describe("Configuration matrix to attach to the test plan"),
      assignment: assignmentSchema
        .optional()
        .describe("Assignment payload to execute after creation"),
    });
  • Tool registration where "create_test_plan" is linked to its handler.
    createTestPlanTool.name,
    createTestPlanTool.description,
    createTestPlanSchema.shape,
    async (args) => {
      return handleCreateTestPlan(args);

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