Skip to content

Commit 17f7c6a

Browse files
committed
feat: add issue creation tools and fix markdown heading processing
- Add new MCP tools: jira_get_create_meta and jira_create_issue - Implement comprehensive issue creation with metadata-first workflow - Support all field types: standard, custom, markdown descriptions - Add ADF conversion for rich text descriptions - Fix markdown heading parsing to process inline formatting - Resolve double-formatting issue where ## **text** showed literal ** - Add proper TypeScript types and validation schemas - Include comprehensive error handling and user-friendly messages BREAKING CHANGE: None - these are new additive features Tools added: - jira_get_create_meta: Discover project-specific requirements - jira_create_issue: Create issues with full field support Bug fixes: - Markdown headings now properly process bold, italic, code, links - Eliminates literal ** characters in Jira heading display
1 parent 7e1ee84 commit 17f7c6a

8 files changed

Lines changed: 844 additions & 6 deletions
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { Logger } from '../utils/logger.util.js';
2+
import atlassianIssuesService from '../services/vendor.atlassian.issues.service.js';
3+
import {
4+
GetCreateMetaToolArgsType,
5+
CreateIssueToolArgsType,
6+
} from '../tools/atlassian.issues.create.types.js';
7+
import {
8+
formatCreateMeta,
9+
formatCreateIssueResponse,
10+
} from './atlassian.issues.create.formatter.js';
11+
import { CreateIssueParams } from '../services/vendor.atlassian.issues.types.js';
12+
import { markdownToAdf } from '../utils/adf-from-markdown.util.js';
13+
14+
// Create a contextualized logger for this file
15+
const controllerLogger = Logger.forContext(
16+
'controllers/atlassian.issues.create.controller.ts',
17+
);
18+
19+
// Log controller initialization
20+
controllerLogger.debug('Jira issues create controller initialized');
21+
22+
/**
23+
* Get create metadata for a project and its issue types
24+
* @param args Arguments containing project identifier and optional filters
25+
* @returns Formatted create metadata response
26+
*/
27+
async function getCreateMeta(args: GetCreateMetaToolArgsType) {
28+
const methodLogger = Logger.forContext(
29+
'controllers/atlassian.issues.create.controller.ts',
30+
'getCreateMeta',
31+
);
32+
33+
methodLogger.debug(
34+
`Getting create metadata for project: ${args.projectKeyOrId}`,
35+
args,
36+
);
37+
38+
const response = await atlassianIssuesService.getCreateMeta(
39+
args.projectKeyOrId,
40+
args.issueTypeId,
41+
{
42+
...(args.issuetypeNames && { issuetypeNames: args.issuetypeNames }),
43+
},
44+
);
45+
46+
methodLogger.debug('Retrieved create metadata successfully');
47+
48+
return {
49+
content: formatCreateMeta(response, args.projectKeyOrId),
50+
};
51+
}
52+
53+
/**
54+
* Create a new Jira issue
55+
* @param args Arguments containing issue creation data
56+
* @returns Formatted create issue response
57+
*/
58+
async function createIssue(args: CreateIssueToolArgsType) {
59+
const methodLogger = Logger.forContext(
60+
'controllers/atlassian.issues.create.controller.ts',
61+
'createIssue',
62+
);
63+
64+
methodLogger.debug('Creating new issue:', args);
65+
66+
// Build the fields object for issue creation
67+
const fields: Record<string, unknown> = {
68+
project: {
69+
key: args.projectKeyOrId,
70+
},
71+
issuetype: {
72+
id: args.issueTypeId,
73+
},
74+
summary: args.summary,
75+
};
76+
77+
// Add description as ADF if provided
78+
if (args.description) {
79+
fields.description = markdownToAdf(args.description);
80+
}
81+
82+
// Add optional fields
83+
if (args.priority) {
84+
// Try as ID first, then as name
85+
if (/^\d+$/.test(args.priority)) {
86+
fields.priority = { id: args.priority };
87+
} else {
88+
fields.priority = { name: args.priority };
89+
}
90+
}
91+
92+
if (args.assignee) {
93+
// Assume account ID format
94+
fields.assignee = { accountId: args.assignee };
95+
}
96+
97+
if (args.labels?.length) {
98+
fields.labels = args.labels;
99+
}
100+
101+
if (args.components?.length) {
102+
fields.components = args.components.map((comp) => {
103+
// Try as ID first, then as name
104+
if (/^\d+$/.test(comp)) {
105+
return { id: comp };
106+
} else {
107+
return { name: comp };
108+
}
109+
});
110+
}
111+
112+
if (args.fixVersions?.length) {
113+
fields.fixVersions = args.fixVersions.map((version) => {
114+
// Try as ID first, then as name
115+
if (/^\d+$/.test(version)) {
116+
return { id: version };
117+
} else {
118+
return { name: version };
119+
}
120+
});
121+
}
122+
123+
// Add custom fields if provided
124+
if (args.customFields) {
125+
Object.assign(fields, args.customFields);
126+
}
127+
128+
const createParams: CreateIssueParams = {
129+
fields,
130+
};
131+
132+
methodLogger.debug('Calling service to create issue with fields:', fields);
133+
134+
const response = await atlassianIssuesService.createIssue(createParams);
135+
136+
methodLogger.debug('Created issue successfully:', response);
137+
138+
return {
139+
content: formatCreateIssueResponse(response),
140+
};
141+
}
142+
143+
export default {
144+
getCreateMeta,
145+
createIssue,
146+
};
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/**
2+
* Formatter for Jira issue creation responses
3+
*/
4+
5+
import {
6+
formatHeading,
7+
formatBulletList,
8+
formatUrl,
9+
formatDate,
10+
formatSeparator,
11+
} from '../utils/formatter.util.js';
12+
import {
13+
CreateMetaResponse,
14+
CreateMetaField,
15+
CreateIssueResponse,
16+
} from '../services/vendor.atlassian.issues.types.js';
17+
18+
/**
19+
* Format create metadata response for display
20+
* @param metadata - Create metadata response from Jira API
21+
* @param projectKey - Project key for context
22+
* @returns Formatted string with create metadata in markdown format
23+
*/
24+
export function formatCreateMeta(
25+
metadata: CreateMetaResponse,
26+
projectKey: string,
27+
): string {
28+
const lines: string[] = [];
29+
30+
lines.push(formatHeading('Issue Creation Metadata', 1));
31+
lines.push(`Project: **${projectKey}**`);
32+
lines.push('');
33+
34+
// Handle single issue type response (new API endpoint)
35+
if (metadata.fields && metadata.name) {
36+
lines.push(formatHeading(`Issue Type: ${metadata.name}`, 2));
37+
if (metadata.description) {
38+
lines.push(`*${metadata.description}*`);
39+
lines.push('');
40+
}
41+
42+
lines.push(formatHeading('Required Fields', 3));
43+
const requiredFields = Object.entries(metadata.fields).filter(
44+
([, field]) => field.required,
45+
);
46+
lines.push(formatFieldList(requiredFields));
47+
48+
lines.push('');
49+
lines.push(formatHeading('Optional Fields', 3));
50+
const optionalFields = Object.entries(metadata.fields).filter(
51+
([, field]) => !field.required,
52+
);
53+
lines.push(formatFieldList(optionalFields));
54+
}
55+
// Handle multiple issue types response (legacy API endpoint)
56+
else if (metadata.projects?.length) {
57+
const project = metadata.projects[0];
58+
lines.push(`**${project.name}** (${project.key})`);
59+
lines.push('');
60+
61+
project.issuetypes.forEach((issueType, index) => {
62+
if (index > 0) {
63+
lines.push('');
64+
lines.push(formatSeparator());
65+
lines.push('');
66+
}
67+
68+
lines.push(formatHeading(`${issueType.name}`, 2));
69+
lines.push(`**ID**: ${issueType.id}`);
70+
if (issueType.description) {
71+
lines.push(`**Description**: ${issueType.description}`);
72+
}
73+
lines.push(`**Subtask**: ${issueType.subtask ? 'Yes' : 'No'}`);
74+
lines.push('');
75+
76+
lines.push(formatHeading('Required Fields', 3));
77+
const requiredFields = Object.entries(issueType.fields).filter(
78+
([, field]) => field.required,
79+
);
80+
lines.push(formatFieldList(requiredFields));
81+
82+
lines.push('');
83+
lines.push(formatHeading('Optional Fields', 3));
84+
const optionalFields = Object.entries(issueType.fields).filter(
85+
([, field]) => !field.required,
86+
);
87+
if (optionalFields.length > 0) {
88+
lines.push(formatFieldList(optionalFields, true));
89+
} else {
90+
lines.push('*No optional fields available.*');
91+
}
92+
});
93+
} else {
94+
lines.push('*No issue types available for this project.*');
95+
}
96+
97+
lines.push('');
98+
lines.push(formatSeparator());
99+
lines.push(`*Retrieved at: ${formatDate(new Date())}*`);
100+
101+
return lines.join('\n');
102+
}
103+
104+
/**
105+
* Format field list for display
106+
* @param fields - Array of field entries [fieldId, field]
107+
* @param limitOptional - Whether to limit optional fields display
108+
* @returns Formatted field list
109+
*/
110+
function formatFieldList(
111+
fields: [string, CreateMetaField][],
112+
limitOptional = false,
113+
): string {
114+
if (fields.length === 0) {
115+
return '*None*';
116+
}
117+
118+
const lines: string[] = [];
119+
120+
// Limit optional fields to prevent overwhelming output
121+
const fieldsToShow = limitOptional ? fields.slice(0, 10) : fields;
122+
123+
fieldsToShow.forEach(([fieldId, field]) => {
124+
const displayId = field.key || field.fieldId || fieldId;
125+
lines.push(`- **${field.name}** (\`${displayId}\`)`);
126+
127+
const details: string[] = [];
128+
details.push(`Type: ${field.schema.type}`);
129+
130+
if (field.schema.system) {
131+
details.push(`System: ${field.schema.system}`);
132+
}
133+
if (field.schema.custom) {
134+
details.push(`Custom: ${field.schema.custom}`);
135+
}
136+
137+
if (field.allowedValues && Array.isArray(field.allowedValues)) {
138+
const values = field.allowedValues
139+
.slice(0, 5)
140+
.map((val: unknown) => {
141+
if (
142+
typeof val === 'object' &&
143+
val !== null &&
144+
'name' in val
145+
) {
146+
return String((val as { name: unknown }).name);
147+
}
148+
return String(val);
149+
});
150+
details.push(
151+
`Values: ${values.join(', ')}${field.allowedValues.length > 5 ? '...' : ''}`,
152+
);
153+
}
154+
155+
if (field.defaultValue !== undefined && field.defaultValue !== null) {
156+
details.push(`Default: ${String(field.defaultValue)}`);
157+
}
158+
159+
lines.push(` ${details.join(' | ')}`);
160+
});
161+
162+
if (limitOptional && fields.length > 10) {
163+
lines.push(`*... and ${fields.length - 10} more optional fields*`);
164+
}
165+
166+
return lines.join('\n');
167+
}
168+
169+
/**
170+
* Format create issue response for display
171+
* @param response - Create issue response from Jira API
172+
* @returns Formatted string with creation result in markdown format
173+
*/
174+
export function formatCreateIssueResponse(
175+
response: CreateIssueResponse,
176+
): string {
177+
const lines: string[] = [];
178+
179+
lines.push(formatHeading('✅ Issue Created Successfully', 1));
180+
lines.push('');
181+
182+
const issueInfo = {
183+
'Issue Key': response.key,
184+
'Issue ID': response.id,
185+
'Jira URL': formatUrl(response.self, 'Open in Jira'),
186+
'Browse URL': formatUrl(
187+
response.self.replace('/rest/api/3/issue/', '/browse/'),
188+
'View in Browser',
189+
),
190+
};
191+
192+
lines.push(formatBulletList(issueInfo));
193+
194+
// Handle transition status if present
195+
if (response.transition) {
196+
lines.push('');
197+
lines.push(formatHeading('Creation Status', 2));
198+
lines.push(`Status Code: ${response.transition.status}`);
199+
200+
if (response.transition.errorCollection) {
201+
const errors = response.transition.errorCollection;
202+
if (errors.errorMessages?.length) {
203+
lines.push('');
204+
lines.push('**Warnings:**');
205+
errors.errorMessages.forEach((msg) => {
206+
lines.push(`- ${msg}`);
207+
});
208+
}
209+
if (errors.errors && Object.keys(errors.errors).length > 0) {
210+
lines.push('');
211+
lines.push('**Field Errors:**');
212+
Object.entries(errors.errors).forEach(([field, error]) => {
213+
lines.push(`- **${field}**: ${error}`);
214+
});
215+
}
216+
}
217+
}
218+
219+
lines.push('');
220+
lines.push(formatSeparator());
221+
lines.push(`*Issue created at: ${formatDate(new Date())}*`);
222+
223+
return lines.join('\n');
224+
}

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import cors from 'cors';
1313
// Import Jira-specific tools
1414
import atlassianProjectsTools from './tools/atlassian.projects.tool.js';
1515
import atlassianIssuesTools from './tools/atlassian.issues.tool.js';
16+
import atlassianIssuesCreateTools from './tools/atlassian.issues.create.tool.js';
1617
import atlassianStatusesTools from './tools/atlassian.statuses.tool.js';
1718
import atlassianCommentsTools from './tools/atlassian.comments.tool.js';
1819
import atlassianWorklogsTools from './tools/atlassian.worklogs.tool.js';
@@ -73,6 +74,9 @@ export async function startServer(
7374
atlassianIssuesTools.registerTools(serverInstance);
7475
serverLogger.debug('Registered Issues tools');
7576

77+
atlassianIssuesCreateTools.registerTools(serverInstance);
78+
serverLogger.debug('Registered Issues Create tools');
79+
7680
atlassianStatusesTools.registerTools(serverInstance);
7781
serverLogger.debug('Registered Statuses tools');
7882

0 commit comments

Comments
 (0)