@aaronsb/jira-cloud-mcp 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/client/field-discovery.js +113 -6
- package/build/client/jira-client.js +22 -0
- package/build/handlers/analysis-handler.js +9 -1
- package/build/handlers/project-handlers.js +12 -0
- package/build/index.js +54 -1
- package/build/mcp/markdown-renderer.js +11 -0
- package/build/schemas/tool-schemas.js +3 -3
- package/build/utils/next-steps.js +1 -1
- package/package.json +1 -1
|
@@ -14,10 +14,18 @@ const SCREEN_WEIGHT = 10;
|
|
|
14
14
|
const RECENCY_WEIGHT = 5;
|
|
15
15
|
const RECENCY_HALF_LIFE_DAYS = 30;
|
|
16
16
|
// ── Field Discovery ────────────────────────────────────────────────────
|
|
17
|
+
/** Well-known locked fields identified by schema custom type */
|
|
18
|
+
const WELL_KNOWN_FIELDS = {
|
|
19
|
+
'com.pyxis.greenhopper.jira:gh-sprint': 'sprint',
|
|
20
|
+
'com.pyxis.greenhopper.jira:jsw-story-points': 'storyPoints',
|
|
21
|
+
'com.atlassian.jpo:jpo-custom-field-baseline-start': 'startDate',
|
|
22
|
+
'com.atlassian.jpo:jpo-custom-field-baseline-end': 'targetDate',
|
|
23
|
+
};
|
|
17
24
|
export class FieldDiscovery {
|
|
18
25
|
catalog = [];
|
|
19
26
|
nameToId = new Map();
|
|
20
27
|
idToField = new Map();
|
|
28
|
+
wellKnown = new Map(); // logical name → field ID
|
|
21
29
|
stats = null;
|
|
22
30
|
ready = false;
|
|
23
31
|
error = null;
|
|
@@ -37,6 +45,14 @@ export class FieldDiscovery {
|
|
|
37
45
|
getStats() {
|
|
38
46
|
return this.stats;
|
|
39
47
|
}
|
|
48
|
+
/** Get a well-known field ID by logical name (e.g., 'sprint', 'storyPoints') */
|
|
49
|
+
getWellKnownFieldId(logicalName) {
|
|
50
|
+
return this.wellKnown.get(logicalName) ?? null;
|
|
51
|
+
}
|
|
52
|
+
/** All discovered well-known field mappings */
|
|
53
|
+
getWellKnownFields() {
|
|
54
|
+
return Object.fromEntries(this.wellKnown);
|
|
55
|
+
}
|
|
40
56
|
/** Resolve a human-readable field name to its Jira field ID */
|
|
41
57
|
resolveNameToId(name) {
|
|
42
58
|
return this.nameToId.get(name.toLowerCase()) ?? null;
|
|
@@ -55,12 +71,9 @@ export class FieldDiscovery {
|
|
|
55
71
|
return [];
|
|
56
72
|
}
|
|
57
73
|
try {
|
|
58
|
-
// Step 1: Get issue types
|
|
59
|
-
const issueTypes = await
|
|
60
|
-
|
|
61
|
-
});
|
|
62
|
-
const matchingType = (issueTypes.issueTypes || issueTypes.createMetaIssueType || [])
|
|
63
|
-
.find(t => t.name?.toLowerCase() === issueTypeName.toLowerCase());
|
|
74
|
+
// Step 1: Get issue types (uses shared cache)
|
|
75
|
+
const issueTypes = await this.getIssueTypes(client, projectKey);
|
|
76
|
+
const matchingType = issueTypes.find(t => t.name.toLowerCase() === issueTypeName.toLowerCase());
|
|
64
77
|
if (!matchingType?.id) {
|
|
65
78
|
console.error(`[field-discovery] Issue type "${issueTypeName}" not found in project ${projectKey}`);
|
|
66
79
|
return this.catalog.filter(f => f.writable);
|
|
@@ -97,6 +110,92 @@ export class FieldDiscovery {
|
|
|
97
110
|
return this.catalog.filter(f => f.writable);
|
|
98
111
|
}
|
|
99
112
|
}
|
|
113
|
+
// ── Required Fields Cache ────────────────────────────────────────────
|
|
114
|
+
requiredFieldsCache = new Map();
|
|
115
|
+
issueTypesCache = new Map();
|
|
116
|
+
/** Get issue types available for a project (lazy cached). */
|
|
117
|
+
async getIssueTypes(client, projectKey) {
|
|
118
|
+
const cacheKey = projectKey.toUpperCase();
|
|
119
|
+
const cached = this.issueTypesCache.get(cacheKey);
|
|
120
|
+
if (cached)
|
|
121
|
+
return cached;
|
|
122
|
+
try {
|
|
123
|
+
const result = await client.issues.getCreateIssueMetaIssueTypes({
|
|
124
|
+
projectIdOrKey: projectKey,
|
|
125
|
+
});
|
|
126
|
+
const types = (result.issueTypes || result.createMetaIssueType || [])
|
|
127
|
+
.filter((t) => t.id && t.name)
|
|
128
|
+
.map((t) => ({
|
|
129
|
+
id: t.id,
|
|
130
|
+
name: t.name,
|
|
131
|
+
subtask: t.subtask ?? false,
|
|
132
|
+
}));
|
|
133
|
+
this.issueTypesCache.set(cacheKey, types);
|
|
134
|
+
return types;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error(`[field-discovery] Issue type fetch failed for ${projectKey}: ${err instanceof Error ? err.message : err}`);
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Get required fields for a project + issue type combination (lazy cached). */
|
|
142
|
+
async getRequiredFields(client, projectKey, issueTypeName) {
|
|
143
|
+
const cacheKey = `${projectKey}:${issueTypeName}`.toLowerCase();
|
|
144
|
+
const cached = this.requiredFieldsCache.get(cacheKey);
|
|
145
|
+
if (cached)
|
|
146
|
+
return cached;
|
|
147
|
+
try {
|
|
148
|
+
const issueTypes = await this.getIssueTypes(client, projectKey);
|
|
149
|
+
const matchingType = issueTypes.find(t => t.name.toLowerCase() === issueTypeName.toLowerCase());
|
|
150
|
+
if (!matchingType)
|
|
151
|
+
return [];
|
|
152
|
+
const required = [];
|
|
153
|
+
let startAt = 0;
|
|
154
|
+
const maxResults = 50;
|
|
155
|
+
let hasMore = true;
|
|
156
|
+
while (hasMore) {
|
|
157
|
+
const fieldMeta = await client.issues.getCreateIssueMetaIssueTypeId({
|
|
158
|
+
projectIdOrKey: projectKey,
|
|
159
|
+
issueTypeId: matchingType.id,
|
|
160
|
+
startAt,
|
|
161
|
+
maxResults,
|
|
162
|
+
});
|
|
163
|
+
const fields = (fieldMeta.fields || fieldMeta.results || []);
|
|
164
|
+
for (const f of fields) {
|
|
165
|
+
if (f.required && !f.hasDefaultValue) {
|
|
166
|
+
const info = {
|
|
167
|
+
fieldId: f.fieldId,
|
|
168
|
+
name: f.name,
|
|
169
|
+
schemaType: f.schema?.type ?? 'unknown',
|
|
170
|
+
};
|
|
171
|
+
if (f.allowedValues && Array.isArray(f.allowedValues)) {
|
|
172
|
+
info.allowedValues = f.allowedValues
|
|
173
|
+
.slice(0, 20)
|
|
174
|
+
.map((v) => v.name ?? v.value ?? String(v));
|
|
175
|
+
}
|
|
176
|
+
required.push(info);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (fields.length < maxResults)
|
|
180
|
+
hasMore = false;
|
|
181
|
+
startAt += fields.length;
|
|
182
|
+
}
|
|
183
|
+
this.requiredFieldsCache.set(cacheKey, required);
|
|
184
|
+
return required;
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
console.error(`[field-discovery] Required fields fetch failed for ${projectKey}/${issueTypeName}: ${err instanceof Error ? err.message : err}`);
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Clear required fields cache for a project (on 400 errors, cache may be stale). */
|
|
192
|
+
invalidateRequiredFields(projectKey) {
|
|
193
|
+
for (const key of this.requiredFieldsCache.keys()) {
|
|
194
|
+
if (key.startsWith(projectKey.toLowerCase() + ':')) {
|
|
195
|
+
this.requiredFieldsCache.delete(key);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
100
199
|
/**
|
|
101
200
|
* Start discovery in the background. Does not block.
|
|
102
201
|
* Returns a promise that resolves when done (for testing).
|
|
@@ -115,6 +214,14 @@ export class FieldDiscovery {
|
|
|
115
214
|
console.error('[field-discovery] Starting custom field discovery...');
|
|
116
215
|
const rawFields = await this.fetchAllCustomFields(client);
|
|
117
216
|
console.error(`[field-discovery] Fetched ${rawFields.length} custom fields`);
|
|
217
|
+
// Detect well-known locked fields by schema type (before filtering)
|
|
218
|
+
for (const field of rawFields) {
|
|
219
|
+
const logicalName = WELL_KNOWN_FIELDS[field.schemaCustom];
|
|
220
|
+
if (logicalName) {
|
|
221
|
+
this.wellKnown.set(logicalName, field.id);
|
|
222
|
+
console.error(`[field-discovery] Well-known: ${logicalName} → ${field.id} (${field.name})`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
118
225
|
const { qualified, stats } = this.filterAndClassify(rawFields);
|
|
119
226
|
console.error(`[field-discovery] ${qualified.length} fields passed filters`);
|
|
120
227
|
const scored = this.scoreFields(qualified);
|
|
@@ -28,8 +28,13 @@ export class JiraClient {
|
|
|
28
28
|
this.customFields = {
|
|
29
29
|
startDate: config.customFields?.startDate ?? 'customfield_10015',
|
|
30
30
|
storyPoints: config.customFields?.storyPoints ?? 'customfield_10016',
|
|
31
|
+
sprint: config.customFields?.sprint ?? null, // Discovered at runtime via field-discovery
|
|
31
32
|
};
|
|
32
33
|
}
|
|
34
|
+
/** Update a custom field ID after runtime discovery */
|
|
35
|
+
setCustomFieldId(logicalName, fieldId) {
|
|
36
|
+
this.customFields[logicalName] = fieldId;
|
|
37
|
+
}
|
|
33
38
|
/** Standard Jira fields that require ADF format in v3 API */
|
|
34
39
|
static ADF_FIELDS = new Set(['environment']);
|
|
35
40
|
/** Convert any ADF-type fields in customFields from markdown to ADF */
|
|
@@ -89,9 +94,24 @@ export class JiraClient {
|
|
|
89
94
|
this.customFields.startDate,
|
|
90
95
|
this.customFields.storyPoints,
|
|
91
96
|
'timeestimate',
|
|
97
|
+
...(this.customFields.sprint ? [this.customFields.sprint] : []),
|
|
92
98
|
'issuelinks',
|
|
93
99
|
];
|
|
94
100
|
}
|
|
101
|
+
/** Extract the most relevant sprint name from the sprint field array */
|
|
102
|
+
extractSprintName(sprints) {
|
|
103
|
+
if (!Array.isArray(sprints) || sprints.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
// Prefer active sprint, then future, then most recent closed
|
|
106
|
+
const active = sprints.find((s) => s.state === 'active');
|
|
107
|
+
if (active)
|
|
108
|
+
return active.name ?? null;
|
|
109
|
+
const future = sprints.find((s) => s.state === 'future');
|
|
110
|
+
if (future)
|
|
111
|
+
return future.name ?? null;
|
|
112
|
+
// Fall back to last sprint in array (most recent)
|
|
113
|
+
return sprints[sprints.length - 1]?.name ?? null;
|
|
114
|
+
}
|
|
95
115
|
/** Maps a raw Jira API issue to our JiraIssueDetails shape */
|
|
96
116
|
mapIssueFields(issue) {
|
|
97
117
|
const fields = issue.fields ?? issue.fields;
|
|
@@ -127,6 +147,7 @@ export class JiraClient {
|
|
|
127
147
|
startDate: fields?.[this.customFields.startDate] || null,
|
|
128
148
|
storyPoints: fields?.[this.customFields.storyPoints] ?? null,
|
|
129
149
|
timeEstimate: fields?.timeestimate ?? null,
|
|
150
|
+
sprint: this.customFields.sprint ? this.extractSprintName(fields?.[this.customFields.sprint]) : null,
|
|
130
151
|
issueLinks: (fields?.issuelinks || []).map((link) => ({
|
|
131
152
|
type: link.type?.name || '',
|
|
132
153
|
outward: link.outwardIssue?.key || null,
|
|
@@ -517,6 +538,7 @@ export class JiraClient {
|
|
|
517
538
|
'status', 'resolution', 'labels', 'created', 'updated',
|
|
518
539
|
'resolutiondate', 'statuscategorychangedate', 'duedate', 'timeestimate',
|
|
519
540
|
this.customFields.startDate, this.customFields.storyPoints,
|
|
541
|
+
...(this.customFields.sprint ? [this.customFields.sprint] : []),
|
|
520
542
|
];
|
|
521
543
|
const params = {
|
|
522
544
|
jql: cleanJql,
|
|
@@ -6,7 +6,7 @@ import { analysisNextSteps } from '../utils/next-steps.js';
|
|
|
6
6
|
import { normalizeArgs } from '../utils/normalize-args.js';
|
|
7
7
|
const ALL_METRICS = ['points', 'time', 'schedule', 'cycle', 'distribution'];
|
|
8
8
|
// flow and hierarchy are opt-in only
|
|
9
|
-
const VALID_GROUP_BY = ['project', 'assignee', 'priority', 'issuetype', 'parent'];
|
|
9
|
+
const VALID_GROUP_BY = ['project', 'assignee', 'priority', 'issuetype', 'parent', 'sprint'];
|
|
10
10
|
const MAX_ISSUES_HARD = 500; // absolute ceiling for detail metrics — beyond this, context explodes
|
|
11
11
|
const MAX_ISSUES_DEFAULT = 100;
|
|
12
12
|
const CUBE_SAMPLE_PCT = 0.2; // 20% of total issues
|
|
@@ -272,6 +272,10 @@ export function renderDistribution(issues, groupLimit = DEFAULT_GROUP_LIMIT) {
|
|
|
272
272
|
lines.push(`**By priority:** ${mapToString(byPriority, ' | ', groupLimit)}`);
|
|
273
273
|
const byType = countBy(issues, i => i.issueType || 'Unknown');
|
|
274
274
|
lines.push(`**By type:** ${mapToString(byType, ' | ', groupLimit)}`);
|
|
275
|
+
const bySprint = countBy(issues, i => i.sprint || '(no sprint)');
|
|
276
|
+
if (bySprint.size > 1 || !bySprint.has('(no sprint)')) {
|
|
277
|
+
lines.push(`**By sprint:** ${mapToString(bySprint, ' | ', groupLimit)}`);
|
|
278
|
+
}
|
|
275
279
|
return lines.join('\n');
|
|
276
280
|
}
|
|
277
281
|
export async function renderFlow(jiraClient, issues) {
|
|
@@ -437,6 +441,8 @@ export function groupByJqlClause(dimension, values) {
|
|
|
437
441
|
return values.map(v => `issuetype = "${v}"`);
|
|
438
442
|
case 'parent':
|
|
439
443
|
return values.map(v => v === '(no parent)' ? 'issue not in childIssuesOf("")' : `parent = ${v}`);
|
|
444
|
+
case 'sprint':
|
|
445
|
+
return values.map(v => v === '(no sprint)' ? 'sprint is EMPTY' : `sprint = "${v}"`);
|
|
440
446
|
}
|
|
441
447
|
}
|
|
442
448
|
async function buildCountRow(jiraClient, label, baseJql, implicitMeasureNames, implicitMeasureDefs) {
|
|
@@ -597,6 +603,7 @@ export function extractDimensions(issues, groupLimit = DEFAULT_GROUP_LIMIT) {
|
|
|
597
603
|
{ name: 'priority', extractor: i => i.priority || 'None' },
|
|
598
604
|
{ name: 'issuetype', extractor: i => i.issueType || 'Unknown' },
|
|
599
605
|
{ name: 'parent', extractor: i => i.parent || '(no parent)' },
|
|
606
|
+
{ name: 'sprint', extractor: i => i.sprint || '(no sprint)' },
|
|
600
607
|
];
|
|
601
608
|
return dims.map(({ name, extractor }) => {
|
|
602
609
|
const counts = new Map();
|
|
@@ -770,6 +777,7 @@ function graphIssueToDetails(issue) {
|
|
|
770
777
|
dueDate: issue.dueDate,
|
|
771
778
|
startDate: issue.startDate,
|
|
772
779
|
storyPoints: issue.storyPoints,
|
|
780
|
+
sprint: null,
|
|
773
781
|
timeEstimate: null,
|
|
774
782
|
issueLinks: [],
|
|
775
783
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { fieldDiscovery } from '../client/field-discovery.js';
|
|
2
3
|
import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
|
|
3
4
|
import { projectNextSteps } from '../utils/next-steps.js';
|
|
4
5
|
import { normalizeArgs } from '../utils/normalize-args.js';
|
|
@@ -70,6 +71,16 @@ async function handleGetProject(jiraClient, args) {
|
|
|
70
71
|
description: project.description || undefined,
|
|
71
72
|
lead: project.lead || undefined,
|
|
72
73
|
};
|
|
74
|
+
// Fetch issue types (always — essential context for creating issues)
|
|
75
|
+
try {
|
|
76
|
+
const issueTypes = await fieldDiscovery.getIssueTypes(jiraClient.v3Client, projectKey);
|
|
77
|
+
if (issueTypes.length > 0) {
|
|
78
|
+
projectData.issueTypes = issueTypes.map(t => ({ name: t.name, subtask: t.subtask }));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Non-fatal — continue without issue types
|
|
83
|
+
}
|
|
73
84
|
// If status counts are requested, get them
|
|
74
85
|
if (includeStatusCounts) {
|
|
75
86
|
try {
|
|
@@ -125,6 +136,7 @@ async function handleGetProject(jiraClient, args) {
|
|
|
125
136
|
name: projectData.name,
|
|
126
137
|
description: projectData.description,
|
|
127
138
|
lead: projectData.lead,
|
|
139
|
+
issueTypes: projectData.issueTypes,
|
|
128
140
|
statusCounts: projectData.statusCounts,
|
|
129
141
|
boards: projectData.boards,
|
|
130
142
|
recentIssues: projectData.recentIssues,
|
package/build/index.js
CHANGED
|
@@ -83,7 +83,16 @@ class JiraServer {
|
|
|
83
83
|
});
|
|
84
84
|
this.setupHandlers();
|
|
85
85
|
// Start async field discovery (non-blocking)
|
|
86
|
-
fieldDiscovery.startAsync(this.jiraClient.v3Client)
|
|
86
|
+
fieldDiscovery.startAsync(this.jiraClient.v3Client).then(() => {
|
|
87
|
+
const wirable = ['sprint', 'storyPoints', 'startDate'];
|
|
88
|
+
for (const name of wirable) {
|
|
89
|
+
const fieldId = fieldDiscovery.getWellKnownFieldId(name);
|
|
90
|
+
if (fieldId) {
|
|
91
|
+
this.jiraClient.setCustomFieldId(name, fieldId);
|
|
92
|
+
console.error(`[jira-cloud] ${name} field: ${fieldId}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}).catch(() => { });
|
|
87
96
|
// CloudId discovery happens in run() before server connects — must complete
|
|
88
97
|
// before ListTools so analyze_jira_plan is registered if available.
|
|
89
98
|
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
@@ -214,6 +223,50 @@ class JiraServer {
|
|
|
214
223
|
isError: true,
|
|
215
224
|
};
|
|
216
225
|
}
|
|
226
|
+
if (status === 400) {
|
|
227
|
+
const fieldErrors = error?.response?.data?.errors;
|
|
228
|
+
const errorMessages = error?.response?.data?.errorMessages;
|
|
229
|
+
const lines = ['**Jira rejected this request:**'];
|
|
230
|
+
if (errorMessages?.length > 0) {
|
|
231
|
+
lines.push(...errorMessages.map((m) => `- ${m}`));
|
|
232
|
+
}
|
|
233
|
+
if (fieldErrors && Object.keys(fieldErrors).length > 0) {
|
|
234
|
+
lines.push('', '**Field errors:**');
|
|
235
|
+
for (const [field, msg] of Object.entries(fieldErrors)) {
|
|
236
|
+
lines.push(`- \`${field}\`: ${msg}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// On create failures, invalidate cache and append required fields guidance
|
|
240
|
+
const reqArgs = request.params.arguments;
|
|
241
|
+
if (reqArgs?.operation === 'create' && reqArgs?.projectKey) {
|
|
242
|
+
const pKey = reqArgs.projectKey;
|
|
243
|
+
fieldDiscovery.invalidateRequiredFields(pKey);
|
|
244
|
+
const iType = reqArgs.issueType;
|
|
245
|
+
try {
|
|
246
|
+
// Show valid issue types
|
|
247
|
+
const issueTypes = await fieldDiscovery.getIssueTypes(this.jiraClient.v3Client, pKey);
|
|
248
|
+
if (issueTypes.length > 0) {
|
|
249
|
+
lines.push('', `**Valid issue types for ${pKey}:** ${issueTypes.map(t => t.name).join(', ')}`);
|
|
250
|
+
}
|
|
251
|
+
// Show required fields for the requested type
|
|
252
|
+
if (iType) {
|
|
253
|
+
const required = await fieldDiscovery.getRequiredFields(this.jiraClient.v3Client, pKey, iType);
|
|
254
|
+
if (required.length > 0) {
|
|
255
|
+
lines.push(`**Required fields for ${pKey}/${iType}:** ${required.map(f => {
|
|
256
|
+
const vals = f.allowedValues ? ` (${f.schemaType}: ${f.allowedValues.slice(0, 5).join(', ')}${f.allowedValues.length > 5 ? '...' : ''})` : '';
|
|
257
|
+
return f.name + vals;
|
|
258
|
+
}).join(', ')}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch { /* best-effort */ }
|
|
263
|
+
lines.push('', '*Tip: Use `manage_jira_project get` to see valid issue types before creating.*');
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
267
|
+
isError: true,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
217
270
|
if (status === 404) {
|
|
218
271
|
return {
|
|
219
272
|
content: [{
|
|
@@ -272,6 +272,17 @@ export function renderProject(project) {
|
|
|
272
272
|
lines.push('');
|
|
273
273
|
lines.push(truncate(stripHtml(project.description), 200));
|
|
274
274
|
}
|
|
275
|
+
// Issue types
|
|
276
|
+
if (project.issueTypes && project.issueTypes.length > 0) {
|
|
277
|
+
const regular = project.issueTypes.filter(t => !t.subtask).map(t => t.name);
|
|
278
|
+
const subtasks = project.issueTypes.filter(t => t.subtask).map(t => t.name);
|
|
279
|
+
const parts = [...regular];
|
|
280
|
+
if (subtasks.length > 0) {
|
|
281
|
+
parts.push(...subtasks.map(n => `${n} (subtask)`));
|
|
282
|
+
}
|
|
283
|
+
lines.push('');
|
|
284
|
+
lines.push(`**Issue Types:** ${parts.join(', ')}`);
|
|
285
|
+
}
|
|
275
286
|
// Status counts
|
|
276
287
|
if (project.statusCounts && Object.keys(project.statusCounts).length > 0) {
|
|
277
288
|
lines.push('');
|
|
@@ -169,7 +169,7 @@ export const toolSchemas = {
|
|
|
169
169
|
},
|
|
170
170
|
issueType: {
|
|
171
171
|
type: 'string',
|
|
172
|
-
description: 'Issue type (e.g., Story, Bug, Task). Required for create.',
|
|
172
|
+
description: 'Issue type name — must match project configuration (e.g., Story, Bug, Task, Feature). Use manage_jira_project get to discover valid types. Required for create.',
|
|
173
173
|
},
|
|
174
174
|
priority: {
|
|
175
175
|
type: 'string',
|
|
@@ -352,8 +352,8 @@ export const toolSchemas = {
|
|
|
352
352
|
},
|
|
353
353
|
groupBy: {
|
|
354
354
|
type: 'string',
|
|
355
|
-
enum: ['project', 'assignee', 'priority', 'issuetype', 'parent'],
|
|
356
|
-
description: 'Split counts by this dimension — produces a breakdown table. Use with metrics: ["summary"] for exact counts. This is the correct approach for "how many issues per assignee/priority/type" questions. "project" produces a per-project comparison. "parent" groups by parent issue
|
|
355
|
+
enum: ['project', 'assignee', 'priority', 'issuetype', 'parent', 'sprint'],
|
|
356
|
+
description: 'Split counts by this dimension — produces a breakdown table. Use with metrics: ["summary"] for exact counts. This is the correct approach for "how many issues per assignee/priority/type" questions. "project" produces a per-project comparison. "parent" groups by parent issue. "sprint" groups by sprint name.',
|
|
357
357
|
},
|
|
358
358
|
compute: {
|
|
359
359
|
type: 'array',
|
|
@@ -108,7 +108,7 @@ export function projectNextSteps(operation, projectKey) {
|
|
|
108
108
|
steps.push({ description: 'Get project details', tool: 'manage_jira_project', example: { operation: 'get', projectKey: '<key>' } }, { description: 'Search issues in a project', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: `project = <key>` } });
|
|
109
109
|
break;
|
|
110
110
|
case 'get':
|
|
111
|
-
steps.push({ description: 'Search issues in this project', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: `project = ${projectKey}` } }, { description: 'View project boards', tool: 'manage_jira_board', example: { operation: 'list' } }
|
|
111
|
+
steps.push({ description: 'Create an issue in this project (use issue types shown above)', tool: 'manage_jira_issue', example: { operation: 'create', projectKey, summary: '<title>', issueType: '<type from list above>' } }, { description: 'Search issues in this project', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: `project = ${projectKey}` } }, { description: 'View project boards', tool: 'manage_jira_board', example: { operation: 'list' } });
|
|
112
112
|
break;
|
|
113
113
|
}
|
|
114
114
|
return steps.length > 0 ? formatSteps(steps) : '';
|
package/package.json
CHANGED