create-work-item
Generate and assign work items in Azure DevOps, specifying type, title, description, assignee, tags, parent ID, iteration path, and state for streamlined project tracking.
Instructions
Create a new work item in Azure DevOps
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| assignedTo | No | Email of the person to assign the work item to | |
| description | No | Work item description | |
| iterationPath | No | Iteration path for sprint assignment (e.g., ProjectName\Sprint 1) | |
| parent | No | Parent work item ID for establishing hierarchy during creation | |
| state | No | Initial work item state (e.g., New, Active) | |
| tags | No | Semicolon-separated tags | |
| title | Yes | Work item title | |
| type | Yes | Work item type (e.g., Task, Bug, User Story) |
Implementation Reference
- src/handlers/tool-handlers.ts:441-661 (handler)The primary handler function executing the 'create-work-item' tool. Constructs PATCH operations for Azure DevOps WIT API, handles iteration path normalization/validation, parent-child relationships, state validation, generic fields, and returns formatted response with work item details.private async createWorkItem(args: any): Promise<any> { if (!args.type || !args.title) { throw new Error('Work item type and title are required'); } try { const operations = [ { op: 'add', path: '/fields/System.Title', value: args.title } ]; if (args.description) { operations.push({ op: 'add', path: '/fields/System.Description', value: args.description }); } if (args.assignedTo) { operations.push({ op: 'add', path: '/fields/System.AssignedTo', value: args.assignedTo }); } if (args.tags) { operations.push({ op: 'add', path: '/fields/System.Tags', value: args.tags }); } // Support parent relationship during creation using relations API if (args.parent) { // Validate parent ID is a number const parentId = parseInt(args.parent, 10); if (isNaN(parentId) || parentId <= 0) { throw new Error(`Invalid parent work item ID: ${args.parent}. Must be a positive integer.`); } const parentUrl = `${this.currentConfig!.organizationUrl}/${this.currentConfig!.project}/_apis/wit/workItems/${parentId}`; console.log(`[DEBUG] Setting parent relationship to work item ${parentId} using URL: ${parentUrl}`); operations.push({ op: 'add', path: '/relations/-', value: { rel: 'System.LinkTypes.Hierarchy-Reverse', url: parentUrl, attributes: { comment: `Parent relationship set via MCP create-work-item command` } } }); } // Enhanced iteration path handling with normalization and validation let iterationPathHandled = false; let iterationPathError = null; let finalIterationPath = null; if (args.iterationPath) { try { // Validate and normalize the iteration path finalIterationPath = await this.validateIterationPath(args.iterationPath); // Add normalized path to the creation operations operations.push({ op: 'add', path: '/fields/System.IterationPath', value: finalIterationPath }); iterationPathHandled = true; console.log(`[DEBUG] Iteration path normalized to '${finalIterationPath}' and will be set during creation`); } catch (validationError) { iterationPathError = validationError; finalIterationPath = this.normalizeIterationPath(args.iterationPath); console.log(`[DEBUG] Iteration path validation failed: ${validationError instanceof Error ? validationError.message : 'Unknown error'}`); console.log(`[DEBUG] Will attempt to set normalized path '${finalIterationPath}' after work item creation`); } } // Support state during creation with validation if (args.state) { // Validate state for work item type to prevent TF401347-like errors const validatedState = await this.validateWorkItemState(args.type, args.state); operations.push({ op: 'add', path: '/fields/System.State', value: validatedState }); } // Handle generic field creation with intelligent field name resolution if (args.fields && typeof args.fields === 'object') { Object.entries(args.fields).forEach(([fieldName, fieldValue]) => { // CRITICAL FIX: Implement proper field name resolution as specified in GitHub issue #53 let normalizedFieldName = fieldName; // CRITICAL: Microsoft.VSTS.* fields must NEVER be prefixed with System. // Azure DevOps field categories: // - System fields: Always prefixed with "System." (e.g., System.Title, System.State) // - Microsoft fields: Never prefixed, use full name (e.g., Microsoft.VSTS.Common.Priority) // - Custom fields: May have organization-specific prefixes // Apply System. prefix ONLY to fields that don't already have System. or Microsoft. prefixes if (!fieldName.startsWith('System.') && !fieldName.startsWith('Microsoft.')) { // Only add System. prefix for known system fields without namespaces const knownSystemFields = ['Title', 'Description', 'State', 'AssignedTo', 'Tags', 'IterationPath', 'AreaPath']; if (knownSystemFields.includes(fieldName)) { normalizedFieldName = `System.${fieldName}`; } // All other fields (including BusinessValue, Priority, Effort) remain unchanged // This preserves custom fields and Microsoft.VSTS.* fields correctly } // System.* and Microsoft.* fields are preserved exactly as-is console.log(`[DEBUG] Field resolution: "${fieldName}" → "${normalizedFieldName}"`); operations.push({ op: 'add', path: `/fields/${normalizedFieldName}`, value: fieldValue }); }); } // Debug logging to validate the endpoint construction const endpoint = `/wit/workitems/$${args.type}?api-version=7.1`; console.log(`[DEBUG] Creating work item with endpoint: ${endpoint}`); console.log(`[DEBUG] Full URL will be: ${this.currentConfig!.organizationUrl}/${this.currentConfig!.project}/_apis${endpoint}`); // Create the work item const result = await this.makeApiRequest( endpoint, 'PATCH', operations ); // Handle iteration path post-creation if it wasn't set during creation if (args.iterationPath && !iterationPathHandled && finalIterationPath) { try { console.log(`[DEBUG] Attempting to set normalized iteration path '${finalIterationPath}' post-creation for work item ${result.id}`); await this.updateWorkItemIterationPath(result.id, finalIterationPath); // Refresh the work item to get updated fields const updatedResult = await this.makeApiRequest(`/wit/workitems/${result.id}?api-version=7.1`); Object.assign(result, updatedResult); console.log(`[DEBUG] Successfully set iteration path post-creation`); } catch (postCreationError) { console.error(`[WARNING] Failed to set iteration path post-creation: ${postCreationError instanceof Error ? postCreationError.message : 'Unknown error'}`); // Don't fail the entire operation, just log the warning } } // Extract parent information from relations let parentInfo = null; if (result.relations && result.relations.length > 0) { const parentRelation = result.relations.find((rel: any) => rel.rel === 'System.LinkTypes.Hierarchy-Reverse' ); if (parentRelation) { // Extract parent ID from URL (e.g., .../workItems/1562 -> 1562) const match = parentRelation.url.match(/workItems\/(\d+)$/); parentInfo = { id: match ? parseInt(match[1], 10) : null, url: parentRelation.url, comment: parentRelation.attributes?.comment }; } } // Prepare response with enhanced error reporting const response: any = { success: true, workItem: { id: result.id, title: result.fields['System.Title'], type: result.fields['System.WorkItemType'], state: result.fields['System.State'], parent: result.fields['System.Parent'] || parentInfo?.id || null, parentRelation: parentInfo, iterationPath: result.fields['System.IterationPath'], assignedTo: result.fields['System.AssignedTo']?.displayName || result.fields['System.AssignedTo'], url: result._links.html.href, relations: result.relations?.length || 0 }, message: args.parent ? `Work item created with parent relationship to work item ${args.parent}` : 'Work item created successfully' }; // Add iteration path handling details to response if (args.iterationPath) { response.iterationPathHandling = { requested: args.iterationPath, normalized: finalIterationPath, setDuringCreation: iterationPathHandled, finalValue: result.fields['System.IterationPath'] }; if (iterationPathError) { response.iterationPathHandling.validationError = iterationPathError instanceof Error ? iterationPathError.message : 'Unknown validation error'; } } return { content: [{ type: 'text', text: JSON.stringify(response, null, 2), }], }; } catch (error) { throw new Error(`Failed to create work item: ${error instanceof Error ? error.message : 'Unknown error'}`); } }
- src/index.ts:124-163 (schema)Input schema definition for the 'create-work-item' tool, specifying parameters, types, descriptions, and required fields for MCP tool registration.name: 'create-work-item', description: 'Create a new work item in Azure DevOps', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Work item type (e.g., Task, Bug, User Story)', }, title: { type: 'string', description: 'Work item title', }, description: { type: 'string', description: 'Work item description', }, assignedTo: { type: 'string', description: 'Email of the person to assign the work item to', }, tags: { type: 'string', description: 'Semicolon-separated tags', }, parent: { type: 'number', description: 'Parent work item ID for establishing hierarchy during creation', }, iterationPath: { type: 'string', description: 'Iteration path for sprint assignment (e.g., ProjectName\\Sprint 1)', }, state: { type: 'string', description: 'Initial work item state (e.g., New, Active)', }, }, required: ['type', 'title'], },
- src/handlers/tool-handlers.ts:35-36 (registration)Registration of the 'create-work-item' tool in the central handleToolCall switch statement, dispatching to the createWorkItem handler method.case 'create-work-item': return await this.createWorkItem(args || {});
- Helper function to normalize iteration paths for Azure DevOps API compatibility, crucial for create-work-item functionality.private normalizeIterationPath(iterationPath: string): string { // Remove leading/trailing whitespace let normalized = iterationPath.trim(); // Convert forward slashes to backslashes for consistency with Azure DevOps normalized = normalized.replace(/\//g, '\\'); // Remove leading backslash if present if (normalized.startsWith('\\')) { normalized = normalized.substring(1); } // Handle different input scenarios const projectName = this.currentConfig!.project; // Case 1: Path starts with project name and has proper Iteration prefix if (normalized.startsWith(`${projectName}\\Iteration\\`)) { console.log(`[DEBUG] Path already in correct format with Iteration prefix: ${normalized}`); return normalized; } // Case 2: Path starts with project but missing Iteration component if (normalized.startsWith(`${projectName}\\`) && !normalized.includes('\\Iteration\\')) { // Insert Iteration component after project name const pathParts = normalized.split('\\'); if (pathParts.length >= 2) { pathParts.splice(1, 0, 'Iteration'); normalized = pathParts.join('\\'); console.log(`[DEBUG] Added Iteration component to path: ${normalized}`); return normalized; } } // Case 3: Has Iteration prefix but missing project name (Iteration\SprintName) if (normalized.startsWith('Iteration\\')) { normalized = `${projectName}\\${normalized}`; console.log(`[DEBUG] Added project name prefix to Iteration path: ${normalized}`); return normalized; } // Case 4: Just the sprint name (SprintName or Sprint 3) if (!normalized.includes('\\')) { normalized = `${projectName}\\Iteration\\${normalized}`; console.log(`[DEBUG] Added full project and Iteration prefix to sprint: ${normalized}`); return normalized; } // Case 5: Starts with something else - ensure proper format if (!normalized.startsWith(projectName)) { // Check if it already has an Iteration component if (normalized.includes('\\Iteration\\')) { normalized = `${projectName}\\${normalized}`; } else { // Add both project name and Iteration component normalized = `${projectName}\\Iteration\\${normalized}`; } console.log(`[DEBUG] Added project name prefix with Iteration: ${normalized}`); } console.log(`[DEBUG] Normalized iteration path from '${iterationPath}' to '${normalized}'`); return normalized; }
- Helper function to validate existence of iteration paths before work item creation, preventing TF401347 errors.private async validateIterationPath(iterationPath: string): Promise<string> { try { const normalizedPath = this.normalizeIterationPath(iterationPath); // Approach 1: Get project classification nodes with deep traversal try { const classificationNodes = await this.makeApiRequest('/wit/classificationnodes/iterations?api-version=7.1&$depth=10'); const findInNodes = (node: any, targetPath: string): boolean => { // Check current node path if (node.path === targetPath) { console.log(`[DEBUG] Found exact path match: ${node.path}`); return true; } // Check alternative path formats (direct hierarchy without Iteration component) const alternativePaths = [ node.path, node.name, `${this.currentConfig!.project}\\${node.name}`, node.structureType === 'iteration' ? node.path : null ].filter(Boolean); for (const altPath of alternativePaths) { if (altPath === targetPath || altPath?.replace(/\\/g, '/') === targetPath.replace(/\\/g, '/')) { console.log(`[DEBUG] Found alternative path match: ${altPath} -> ${targetPath}`); return true; } } // Recursively check children if (node.children && node.children.length > 0) { for (const child of node.children) { if (findInNodes(child, targetPath)) { return true; } } } return false; }; if (classificationNodes && findInNodes(classificationNodes, normalizedPath)) { console.log(`[DEBUG] Iteration path '${normalizedPath}' validated successfully`); return normalizedPath; } // Also try with original path format if (normalizedPath !== iterationPath && findInNodes(classificationNodes, iterationPath)) { console.log(`[DEBUG] Original iteration path '${iterationPath}' validated successfully`); return iterationPath; } } catch (classificationError) { console.log(`[DEBUG] Classification nodes query failed: ${classificationError instanceof Error ? classificationError.message : 'Unknown error'}`); } // Approach 2: Get team iterations (fallback) try { const iterations = await this.makeApiRequest('/work/teamsettings/iterations?api-version=7.1'); const pathExists = iterations.value.some((iteration: any) => { const possiblePaths = [ iteration.path, iteration.name, `${this.currentConfig!.project}\\${iteration.name}`, `${this.currentConfig!.project}/${iteration.name}` ].filter(Boolean); return possiblePaths.some(path => path === normalizedPath || path === iterationPath || path?.replace(/\\/g, '/') === normalizedPath.replace(/\\/g, '/') || path?.replace(/\\/g, '/') === iterationPath.replace(/\\/g, '/') ); }); if (pathExists) { console.log(`[DEBUG] Iteration path validated via team iterations`); return normalizedPath; } } catch (teamError) { console.log(`[DEBUG] Team iterations query failed: ${teamError instanceof Error ? teamError.message : 'Unknown error'}`); } // If both validation attempts failed to find the path, it doesn't exist console.log(`[DEBUG] Could not validate iteration path '${iterationPath}', normalized format '${normalizedPath}' does not exist`); console.log(`[DEBUG] SUGGESTION: Ensure the iteration '${normalizedPath}' exists in Azure DevOps project settings`); console.log(`[DEBUG] Expected format: ProjectName\\SprintName (e.g., '${this.currentConfig!.project}\\Sprint 1')`); throw new Error(`Iteration path '${iterationPath}' does not exist in project '${this.currentConfig!.project}'`); } catch (error) { // If there was an error that's not related to path validation (e.g., auth, network), // check if it's our custom "doesn't exist" error, if so re-throw it if (error instanceof Error && error.message.includes('does not exist in project')) { throw error; } // For other errors (network, auth, etc.), return normalized path with warning const normalizedPath = this.normalizeIterationPath(iterationPath); console.log(`[DEBUG] Validation error for path '${iterationPath}': ${error instanceof Error ? error.message : 'Unknown error'}`); console.log(`[DEBUG] Using normalized path due to validation service unavailability`); return normalizedPath; } }