update_test_plan
Modify existing test plans by updating fields like title, priority, status, dates, assignments, and custom fields to reflect changes in testing requirements.
Instructions
Update an existing test plan in TestCollab.
Required:
id (test plan ID)
All other fields are optional and only provided fields are updated.
Fields:
title
description (null to clear)
priority: 0/1/2 or low/normal/high
status: 0/1/2/3 or draft/ready/finished/finished_with_failures
test_plan_folder: ID/title/null
release: ID/title/null
start_date, end_date (null to clear)
archived
custom_fields (null/[] to clear)
assignee (single-user convenience)
assignment (advanced assignment payload)
Example: { "id": 812, "title": "Release 3.0 Regression", "status": "ready", "test_plan_folder": "Mobile", "assignee": "me" }
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | Test plan ID to update (required) | |
| project_id | No | Project ID (optional if TC_DEFAULT_PROJECT is set) | |
| title | No | New test plan title | |
| description | No | New test plan description (HTML supported, null to clear) | |
| priority | No | Priority: 0/1/2 or "low"/"normal"/"high" | |
| status | No | Status: 0/1/2/3 or "draft"/"ready"/"finished"/"finished_with_failures" | |
| test_plan_folder | No | Test plan folder ID or title (null to place at root) | |
| release | No | Release ID or title (null to clear) | |
| start_date | No | Planned start date (YYYY-MM-DD, null to clear) | |
| end_date | No | Planned end date (YYYY-MM-DD, null to clear) | |
| archived | No | Archive/unarchive this test plan | |
| custom_fields | No | Array of test plan custom field values (null/[] to clear) | |
| assignee | No | Convenience field to assign plan to one user (user ID, "me", name, username, or email) | |
| assignment | No | Assignment payload to execute after update |
Implementation Reference
- src/tools/test-plans/update.ts:906-1700 (handler)The `handleUpdateTestPlan` function serves as the primary handler for the `update_test_plan` tool. It validates the input against `updateTestPlanSchema`, processes both metadata and assignment updates, and interfaces with the TestCollab API client to perform the updates.
export async function handleUpdateTestPlan(args: unknown): Promise<ToolResponse> { const parsed = updateTestPlanSchema.safeParse(args); if (!parsed.success) { return toError("VALIDATION_ERROR", "Invalid input parameters", parsed.error.errors); } const { id, project_id, title, description, priority, status, test_plan_folder, release, start_date, end_date, archived, custom_fields, assignee, assignment, } = parsed.data; const rawArgs = args && typeof args === "object" ? (args as Record<string, unknown>) : {}; const hasField = (key: string) => Object.prototype.hasOwnProperty.call(rawArgs, key); const metadataFieldNames = [ "title", "description", "priority", "status", "test_plan_folder", "release", "start_date", "end_date", "archived", "custom_fields", ] as const; const assignmentFieldNames = ["assignee", "assignment"] as const; const hasMetadataUpdate = metadataFieldNames.some((field) => hasField(field)); const hasAssignmentUpdate = assignmentFieldNames.some((field) => hasField(field)); const userSuppliedUpdatableFields = [ ...metadataFieldNames, ...assignmentFieldNames, ].filter((field) => hasField(field)); if (hasField("assignee") && hasField("assignment")) { return toError( "INVALID_INPUT", "Provide either assignee or assignment, not both." ); } if (!hasMetadataUpdate && !hasAssignmentUpdate) { return toError( "INVALID_INPUT", "No updatable fields provided. Supply at least one metadata or assignment field to update." ); } const requestContext = getRequestContext(); const envConfig = requestContext ? null : getConfig(); const resolvedProjectId = project_id ?? requestContext?.defaultProjectId ?? envConfig?.defaultProjectId; if (!resolvedProjectId) { return toError( "MISSING_PROJECT_ID", "project_id is required. Either provide it in the request or set TC_DEFAULT_PROJECT." ); } try { const client = getApiClient(); let updatedId = id; let updatedTitle: string | undefined; if (hasMetadataUpdate) { const existingRaw = await client.getTestPlanRaw(id); const existing = unwrapApiEntity(existingRaw); if (!existing) { return toError( "INVALID_TEST_PLAN", `Unable to load test plan ${id} for update.` ); } const existingTitle = normalizeString(getField<string>(existing, "title")); const existingPriority = toNumberId(getField(existing, "priority")); const existingStatus = toNumberId(getField(existing, "status")); const existingFolderRaw = getField(existing, "test_plan_folder") ?? getField(existing, "testPlanFolder"); const existingFolderId = existingFolderRaw === null ? null : extractId(existingFolderRaw) ?? toNumberId(existingFolderRaw); const existingReleaseRaw = getField(existing, "release"); const existingReleaseId = existingReleaseRaw === null ? null : extractId(existingReleaseRaw) ?? toNumberId(existingReleaseRaw); const resolvedTitle = title ?? existingTitle; const resolvedPriority = toPriorityCode(priority) ?? existingPriority; const resolvedStatus = toStatusCode(status) ?? existingStatus; if (!resolvedTitle || resolvedPriority === undefined || resolvedStatus === undefined) { return toError( "INVALID_EXISTING_TEST_PLAN", "Unable to resolve required test plan fields (title, priority, status) for update.", { missing: [ ...(!resolvedTitle ? ["title"] : []), ...(resolvedPriority === undefined ? ["priority"] : []), ...(resolvedStatus === undefined ? ["status"] : []), ], } ); } let resolvedFolderId: number | null = existingFolderId ?? null; if (hasField("test_plan_folder")) { 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; } } } let resolvedReleaseId: number | null = existingReleaseId ?? null; if (hasField("release")) { if (release === null) { resolvedReleaseId = null; } else { 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, non-empty title, or null." ); } 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 resolvedCustomFields: | Array<{ id: number; name: string; label?: string; value: CustomFieldResolvedValue; valueLabel?: string | string[]; color?: string; }> | undefined; if (hasField("custom_fields")) { const customFieldsInput = custom_fields; if (!customFieldsInput || customFieldsInput.length === 0) { resolvedCustomFields = []; } else { 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 fieldId = toNumberId(getField(field, "id")); const name = normalizeString(getField<string>(field, "name")); if (fieldId === 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: fieldId, name, label: normalizeString(getField<string>(field, "label")), fieldType: normalizeString(fieldType), options, }; definitionsByName.set(name, definition); definitionsById.set(fieldId, definition); }); const missingFields: string[] = []; resolvedCustomFields = customFieldsInput .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(", ")}` ); } } } const updateResult = await client.updateTestPlan(id, { projectId: resolvedProjectId, title: resolvedTitle, priority: resolvedPriority, status: resolvedStatus, testPlanFolderId: resolvedFolderId, release: resolvedReleaseId, ...(hasField("description") ? { description: description ?? null } : {}), ...(hasField("start_date") ? { startDate: start_date ?? null } : {}), ...(hasField("end_date") ? { endDate: end_date ?? null } : {}), ...(hasField("archived") ? { archived } : {}), ...(hasField("custom_fields") ? { customFields: resolvedCustomFields ?? [] } : {}), }); const updateFailure = apiFailureMessage(updateResult); if (updateFailure) { return toToolResponse( { error: { code: "UPDATE_TEST_PLAN_FAILED", message: updateFailure, }, }, true ); } updatedId = extractId(updateResult) ?? id; updatedTitle = normalizeString(getField<string>(updateResult, "title")) ?? resolvedTitle; } let assignResult: Record<string, unknown> | undefined; if (hasAssignmentUpdate) { const assigneeInputProvided = hasField("assignee"); const assignmentInputProvided = hasField("assignment"); if (assignmentInputProvided && !assignment) { return toError( "INVALID_INPUT", "assignment must be an object when provided." ); } const normalizedAssignee = assigneeInputProvided ? normalizeAssignee(assignee) : undefined; if (assigneeInputProvided && normalizedAssignee === undefined) { return toError( "INVALID_ASSIGNEE", 'assignee must be a user ID, "me", name, username, or email.' ); } const assignmentInput = assignmentInputProvided ? assignment : { executor: normalizedAssignee === "me" ? ("me" as const) : ("team" as const), assignment_criteria: "testCase" as const, assignment_method: "automatic" as const, user_ids: normalizedAssignee !== undefined ? [normalizedAssignee] : undefined, test_case_ids: [] as Array<number | string>, selector: [] as TestCaseSelectorQuery[], configuration_ids: [] as Array<number | string>, }; const assignmentUsers = normalizeAssignmentUsers(assignmentInput?.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 assignmentTargetsMe = assignmentUsers.hasMe || assignmentInput?.executor === "me"; const resolvedAssignmentExecutor: "me" | "team" = assignmentTargetsMe ? "me" : (assignmentInput?.executor ?? "team"); const assignmentCriteria: "testCase" | "configuration" = assignmentInput?.assignment_criteria ?? "testCase"; const assignmentMethod: "automatic" | "manual" = assignmentInput?.assignment_method ?? "automatic"; const assignmentTestCaseIds = normalizeNumberIds( assignmentInput?.test_case_ids ); const assignmentConfigurationIds = normalizeNumberIds( assignmentInput?.configuration_ids ); const assignmentSelector = assignmentInput?.selector ?? []; if ( assignmentMethod === "manual" && !assignmentTargetsMe && assignmentUsers.userIds.length === 0 && assignmentUsers.userLookups.length === 0 ) { return toError( "MISSING_ASSIGNMENT_USERS", "Manual assignment requires at least one user_id in assignment.user_ids." ); } if ( assignmentMethod === "manual" && assignmentCriteria === "testCase" && assignmentTestCaseIds.length === 0 && assignmentSelector.length === 0 ) { return toError( "MISSING_ASSIGNMENT_TEST_CASES", "Manual testCase assignment requires assignment.test_case_ids or assignment.selector." ); } if ( assignmentMethod === "manual" && assignmentCriteria === "configuration" && assignmentConfigurationIds.length === 0 ) { return toError( "MISSING_ASSIGNMENT_CONFIGURATIONS", "Manual configuration assignment requires assignment.configuration_ids." ); } let resolvedAssignmentUserIds = assignmentUsers.userIds; if (assignmentUsers.userLookups.length > 0) { 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 } : {}), }); const resolvedLookupIds: number[] = []; for (const lookup of assignmentUsers.userLookups) { 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([ ...resolvedAssignmentUserIds, ...resolvedLookupIds, ]); } const assignmentUsersForPayload: Array<number | "me"> = resolvedAssignmentExecutor === "me" ? ["me"] : resolvedAssignmentUserIds; let assignUsedFallback = false; try { assignResult = await client.assignTestPlan({ projectId: resolvedProjectId, testplan: updatedId, executor: resolvedAssignmentExecutor, assignmentCriteria, assignmentMethod, assignment: { user: assignmentUsersForPayload, testCases: toSelectorCollection(assignmentTestCaseIds, assignmentSelector), configuration: assignmentCriteria === "configuration" ? assignmentConfigurationIds : null, }, }); } catch (assignError) { const assignErrorMessage = toErrorMessage(assignError); const shouldFallbackToPlanAssignees = assignmentMethod === "automatic" && resolvedAssignmentExecutor === "team" && isNoAssignableItemsError(assignErrorMessage); if (!shouldFallbackToPlanAssignees) { throw assignError; } let fallbackAssigneeIds = assignmentUsersForPayload.filter( (user): user is number => typeof user === "number" ); if (fallbackAssigneeIds.length === 0) { const fallbackUsersRaw = await client.listProjectUsers(resolvedProjectId); const fallbackUsers = mapProjectUsersForLookup( Array.isArray(fallbackUsersRaw) ? fallbackUsersRaw : [] ); fallbackAssigneeIds = fallbackUsers.map((user) => user.id); } fallbackAssigneeIds = dedupeNumbers(fallbackAssigneeIds); if (fallbackAssigneeIds.length === 0) { return toToolResponse( { error: { code: "ASSIGN_TEST_PLAN_FAILED", message: assignErrorMessage, }, ...(hasMetadataUpdate ? { testPlan: { id: updatedId, ...(updatedTitle ? { title: updatedTitle } : {}), project_id: resolvedProjectId, }, } : {}), }, true ); } const fallbackExistingRaw = await client.getTestPlanRaw(updatedId); const fallbackExisting = unwrapApiEntity(fallbackExistingRaw); if (!fallbackExisting) { return toError( "INVALID_TEST_PLAN", `Unable to load test plan ${updatedId} for fallback assignment update.` ); } const fallbackTitle = normalizeString(getField<string>(fallbackExisting, "title")); const fallbackPriority = toNumberId(getField(fallbackExisting, "priority")); const fallbackStatus = toNumberId(getField(fallbackExisting, "status")); const fallbackFolderRaw = getField(fallbackExisting, "test_plan_folder") ?? getField(fallbackExisting, "testPlanFolder"); const fallbackFolderId = fallbackFolderRaw === null ? null : extractId(fallbackFolderRaw) ?? toNumberId(fallbackFolderRaw); const fallbackReleaseRaw = getField(fallbackExisting, "release"); const fallbackReleaseId = fallbackReleaseRaw === null ? null : extractId(fallbackReleaseRaw) ?? toNumberId(fallbackReleaseRaw); if (!fallbackTitle || fallbackPriority === undefined || fallbackStatus === undefined) { return toError( "INVALID_EXISTING_TEST_PLAN", "Unable to resolve required test plan fields (title, priority, status) for fallback assignment update.", { missing: [ ...(!fallbackTitle ? ["title"] : []), ...(fallbackPriority === undefined ? ["priority"] : []), ...(fallbackStatus === undefined ? ["status"] : []), ], } ); } const fallbackUpdateResult = await client.updateTestPlan(updatedId, { projectId: resolvedProjectId, title: fallbackTitle, priority: fallbackPriority, status: fallbackStatus, testPlanFolderId: fallbackFolderId ?? null, ...(fallbackReleaseId !== undefined ? { release: fallbackReleaseId } : {}), assignmentMethod, assignmentCriteria, assignedTo: fallbackAssigneeIds, }); const fallbackUpdateFailure = apiFailureMessage(fallbackUpdateResult); if (fallbackUpdateFailure) { return toToolResponse( { error: { code: "ASSIGN_TEST_PLAN_FAILED", message: fallbackUpdateFailure, }, ...(hasMetadataUpdate ? { testPlan: { id: updatedId, ...(updatedTitle ? { title: updatedTitle } : {}), project_id: resolvedProjectId, }, } : {}), }, true ); } assignUsedFallback = true; assignResult = { status: true, fallback_assignment: true, assign_error: assignErrorMessage, assigned_to: fallbackAssigneeIds, }; } if (!assignUsedFallback) { const assignFailure = apiFailureMessage(assignResult); if (assignFailure) { return toToolResponse( { error: { code: "ASSIGN_TEST_PLAN_FAILED", message: assignFailure, }, ...(hasMetadataUpdate ? { testPlan: { id: updatedId, ...(updatedTitle ? { title: updatedTitle } : {}), project_id: resolvedProjectId, }, } : {}), }, true ); } } } const message = hasMetadataUpdate ? hasAssignmentUpdate ? "Test plan updated successfully and assignment applied." : "Test plan updated successfully" : "Test plan assignment updated successfully"; return toToolResponse( { success: true, message, testPlan: { id: updatedId, ...(updatedTitle ? { title: updatedTitle } : {}), project_id: resolvedProjectId, }, updatedFields: userSuppliedUpdatableFields, ...(assignResult !== undefined ? { results: { assign_test_plan: assignResult } } : {}), }, true ); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return toToolResponse( { error: { code: "API_ERROR", message, }, }, true ); } } - `updateTestPlanSchema` defines the input structure and validation for the `update_test_plan` tool using Zod. It enforces required fields (like `id`) and optional fields for various test plan attributes.
export const updateTestPlanSchema = z.object({ id: z.number().describe("Test plan ID to update (required)"), project_id: z .number() .optional() .describe("Project ID (optional if TC_DEFAULT_PROJECT is set)"), title: z.string().min(1).optional().describe("New test plan title"), description: z .string() .nullable() .optional() .describe("New test plan description (HTML supported, null to clear)"), priority: priorityInputSchema .optional() .describe('Priority: 0/1/2 or "low"/"normal"/"high"'), status: statusInputSchema .optional() .describe( 'Status: 0/1/2/3 or "draft"/"ready"/"finished"/"finished_with_failures"' ), 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(), z.null()]) .optional() .describe("Release ID or title (null to clear)"), start_date: z .string() .nullable() .optional() .describe("Planned start date (YYYY-MM-DD, null to clear)"), end_date: z .string() .nullable() .optional() .describe("Planned end date (YYYY-MM-DD, null to clear)"), archived: z.boolean().optional().describe("Archive/unarchive this test plan"), custom_fields: z .array(customFieldSchema) .nullable() .optional() .describe("Array of test plan custom field values (null/[] to clear)"), assignee: z .union([z.number(), z.string()]) .optional() .describe( 'Convenience field to assign plan to one user (user ID, "me", name, username, or email)' ), assignment: assignmentSchema .optional() .describe("Assignment payload to execute after update"), }); - src/tools/test-plans/update.ts:155-186 (registration)`updateTestPlanTool` provides the MCP registration object for `update_test_plan`, including its name, description, and the JSON schema definition for its inputs.
export const updateTestPlanTool = { name: "update_test_plan", description: `Update an existing test plan in TestCollab. Required: - id (test plan ID) All other fields are optional and only provided fields are updated. Fields: - title - description (null to clear) - priority: 0/1/2 or low/normal/high - status: 0/1/2/3 or draft/ready/finished/finished_with_failures - test_plan_folder: ID/title/null - release: ID/title/null - start_date, end_date (null to clear) - archived - custom_fields (null/[] to clear) - assignee (single-user convenience) - assignment (advanced assignment payload) Example: { "id": 812, "title": "Release 3.0 Regression", "status": "ready", "test_plan_folder": "Mobile", "assignee": "me" }`, inputSchema: {