Skip to main content
Glama

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
NameRequiredDescriptionDefault
assignedToNoEmail of the person to assign the work item to
descriptionNoWork item description
iterationPathNoIteration path for sprint assignment (e.g., ProjectName\Sprint 1)
parentNoParent work item ID for establishing hierarchy during creation
stateNoInitial work item state (e.g., New, Active)
tagsNoSemicolon-separated tags
titleYesWork item title
typeYesWork item type (e.g., Task, Bug, User Story)

Implementation Reference

  • 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'}`); } }
  • 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'], },
  • 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; } }

Other Tools

Related Tools

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/wangkanai/devops-enhanced-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server