create-work-item
Create new work items in Azure DevOps, including tasks, bugs, and user stories, with options for assignment, tagging, and sprint planning.
Instructions
Create a new work item in Azure DevOps
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| type | Yes | Work item type (e.g., Task, Bug, User Story) | |
| title | Yes | Work item title | |
| description | No | Work item description | |
| assignedTo | No | Email of the person to assign the work item to | |
| tags | No | Semicolon-separated tags | |
| parent | No | Parent work item ID for establishing hierarchy during creation | |
| iterationPath | No | Iteration path for sprint assignment (e.g., ProjectName\Sprint 1) | |
| state | No | Initial work item state (e.g., New, Active) |
Implementation Reference
- src/handlers/tool-handlers.ts:441-661 (handler)Full implementation of the create-work-item handler. Creates work items via Azure DevOps WIT API, supports parent hierarchy, iteration path normalization/validation, state validation, generic fields, and detailed response with relations.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:123-164 (registration)Tool registration in MCP server. Defines the 'create-work-item' tool name, description, and input schema in the list returned by ListToolsRequest.{ 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/index.ts:126-162 (schema)Input schema for create-work-item tool, defining properties like type, title (required), description, assignedTo, tags, parent, iterationPath, state.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 (handler)Dispatch case in handleToolCall method that routes 'create-work-item' calls to the createWorkItem implementation.case 'create-work-item': return await this.createWorkItem(args || {});
- Helper function for normalizing iteration paths, crucial for create-work-item to handle sprint assignments correctly and avoid TF401347 errors.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; }