@rui.branco/jira-mcp 1.6.16 → 1.6.18

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.
Files changed (2) hide show
  1. package/index.js +73 -37
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -208,22 +208,58 @@ async function fetchJiraTeams(endpoint, options = {}, instance = defaultInstance
208
208
  return text ? JSON.parse(text) : {};
209
209
  }
210
210
 
211
- async function resolveTeamId(teamName, instance) {
212
- const teams = await fetchJiraTeams(
213
- `/teams/find?query=${encodeURIComponent(teamName)}&excludeMembers=true`,
211
+ async function searchTeamsViaJql(query, instance) {
212
+ const data = await fetchJira(
213
+ `/jql/autocompletedata/suggestions?fieldName=cf[10001]&fieldValue=${encodeURIComponent(query)}`,
214
214
  {},
215
215
  instance,
216
216
  );
217
- const match = teams.find(
218
- (t) => t.title.toLowerCase() === teamName.toLowerCase(),
219
- );
220
- if (!match) {
221
- const available = teams.map((t) => t.title).join(", ");
222
- throw new Error(
223
- `Team "${teamName}" not found.${available ? ` Similar teams: ${available}` : ""}`,
217
+ return (data.results || []).map((r) => ({
218
+ title: r.displayName.replace(/<[^>]*>/g, "").replace(/&amp;/g, "&"),
219
+ id: r.value,
220
+ }));
221
+ }
222
+
223
+ async function listTeams(instance) {
224
+ // Try Teams API first, fall back to JQL autocomplete
225
+ try {
226
+ const teams = await fetchJiraTeams(
227
+ `/teams/find?query=&excludeMembers=true`, {}, instance,
224
228
  );
229
+ return teams.map((t) => ({ title: t.title, id: `${t.organizationId}-${t.id}` }));
230
+ } catch {
231
+ // Teams API not available, use JQL autocomplete with a broad search
232
+ return searchTeamsViaJql("", instance);
225
233
  }
226
- return `${match.organizationId}-${match.id}`;
234
+ }
235
+
236
+ async function resolveTeamId(teamName, instance) {
237
+ // Try Teams API first
238
+ try {
239
+ const teams = await fetchJiraTeams(
240
+ `/teams/find?query=${encodeURIComponent(teamName)}&excludeMembers=true`,
241
+ {},
242
+ instance,
243
+ );
244
+ const match = teams.find(
245
+ (t) => t.title.toLowerCase() === teamName.toLowerCase(),
246
+ );
247
+ if (match) return `${match.organizationId}-${match.id}`;
248
+ } catch {
249
+ // Teams API not available, fall through to JQL
250
+ }
251
+
252
+ // Fallback: JQL autocomplete
253
+ const jqlTeams = await searchTeamsViaJql(teamName, instance);
254
+ const match = jqlTeams.find(
255
+ (t) => t.title.toLowerCase() === teamName.toLowerCase(),
256
+ );
257
+ if (match) return match.id;
258
+
259
+ const available = jqlTeams.map((t) => t.title).join(", ");
260
+ throw new Error(
261
+ `Team "${teamName}" not found.${available ? ` Similar teams: ${available}` : ""}`,
262
+ );
227
263
  }
228
264
 
229
265
  async function downloadAttachment(url, filename, issueKey, instance) {
@@ -1795,7 +1831,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1795
1831
  },
1796
1832
  defaultTeam: {
1797
1833
  type: "string",
1798
- description: "Default team name to auto-assign when creating tickets (e.g., 'Site Surveys (MODS)'). Resolved and validated via Jira Teams API. Pass empty string to clear.",
1834
+ description: "Default team name to auto-assign when creating tickets (e.g., 'Site Surveys (MODS)'). Resolved and validated via Jira Teams API. Pass 'none' to explicitly disable team prompts. Pass empty string to reset.",
1799
1835
  },
1800
1836
  },
1801
1837
  required: ["name"],
@@ -2859,6 +2895,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2859
2895
  if (args.defaultTeam !== undefined) {
2860
2896
  if (args.defaultTeam === "") {
2861
2897
  defaultTeam = undefined;
2898
+ } else if (args.defaultTeam.toLowerCase() === "none") {
2899
+ defaultTeam = "none";
2862
2900
  } else {
2863
2901
  // Validate team exists via Jira Teams API
2864
2902
  const tempInst = { baseUrl, auth: authStr };
@@ -2868,11 +2906,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2868
2906
  } catch (e) {
2869
2907
  // Try to list available teams for a helpful error
2870
2908
  try {
2871
- const teams = await fetchJiraTeams(
2872
- `/teams/find?query=&excludeMembers=true`,
2873
- {},
2874
- tempInst,
2875
- );
2909
+ const teams = await listTeams(tempInst);
2876
2910
  const available = teams.map((t) => t.title).join(", ");
2877
2911
  return {
2878
2912
  content: [{ type: "text", text: `Team "${args.defaultTeam}" not found. Available teams: ${available}` }],
@@ -2936,18 +2970,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2936
2970
  let text = `${action} instance "${instName}" (${baseUrl}).`;
2937
2971
  if (projects.length > 0) text += ` Projects: ${projects.join(", ")}.`;
2938
2972
  if (args.setDefault) text += " Set as default.";
2939
- if (defaultTeam) {
2973
+ if (defaultTeam === "none") {
2974
+ text += " Default team: None (disabled).";
2975
+ } else if (defaultTeam) {
2940
2976
  text += ` Default team: ${defaultTeam.name}.`;
2941
2977
  } else {
2942
2978
  // No default team — fetch available teams and prompt
2943
2979
  try {
2944
2980
  const tempInst = { baseUrl, auth: authStr };
2945
- const teams = await fetchJiraTeams(
2946
- `/teams/find?query=&excludeMembers=true`, {}, tempInst,
2947
- );
2981
+ const teams = await listTeams(tempInst);
2948
2982
  if (teams && teams.length > 0) {
2949
2983
  const list = teams.map((t, i) => `${i + 1}. ${t.title}`).join("\n");
2950
- text += `\n\n⚠ No default team configured. Available teams:\n${list}\n0. None\n\nAsk the user which team to set as default. If they pick one, call jira_add_instance with name="${instName}" and defaultTeam="<team name>".`;
2984
+ text += `\n\n⚠ No default team configured. Available teams:\n${list}\n0. None\n\nAsk the user which team to set as default. If they pick one, call jira_add_instance with name="${instName}" and defaultTeam="<team name>". If "None" is selected, call jira_add_instance with defaultTeam="none" to stop future prompts.`;
2951
2985
  }
2952
2986
  } catch {
2953
2987
  // Teams API not available, skip
@@ -2994,11 +3028,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2994
3028
  }
2995
3029
  const currentDefault = rawConfig.defaultInstance || instances[0].name;
2996
3030
  let text = `# Configured Jira Instances (${instances.length})\n\n`;
3031
+ const missingTeam = [];
2997
3032
  for (const inst of instances) {
2998
3033
  const isDefault = inst.name === currentDefault ? " **(default)**" : "";
2999
3034
  const projs = inst.projects?.length > 0 ? `\n Projects: ${inst.projects.join(", ")}` : "";
3000
- const team = inst.defaultTeam ? `\n Default team: ${inst.defaultTeam.name}` : "";
3035
+ const team = inst.defaultTeam === "none" ? "\n Default team: None (disabled)" : inst.defaultTeam ? `\n Default team: ${inst.defaultTeam.name}` : "";
3001
3036
  text += `- **${inst.name}**${isDefault}: ${inst.baseUrl} (${inst.email})${projs}${team}\n`;
3037
+ if (!inst.defaultTeam) missingTeam.push(inst);
3038
+ }
3039
+ if (missingTeam.length > 0) {
3040
+ const names = missingTeam.map((i) => `"${i.name}"`).join(", ");
3041
+ text += `\n⚠ Instances without a default team: ${names}.\nAsk the user if they want to configure a default team. To set one, call jira_add_instance with name="<instance>" and defaultTeam="<team name>".`;
3002
3042
  }
3003
3043
  return { content: [{ type: "text", text }] };
3004
3044
 
@@ -3045,17 +3085,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3045
3085
  let teamPrompt = "";
3046
3086
  if (args.team) {
3047
3087
  fields.customfield_10001 = await resolveTeamId(args.team, inst);
3048
- } else if (inst.defaultTeam) {
3088
+ } else if (inst.defaultTeam && inst.defaultTeam !== "none") {
3049
3089
  fields.customfield_10001 = inst.defaultTeam.id;
3050
- } else {
3090
+ } else if (!inst.defaultTeam) {
3051
3091
  // No team param and no default — fetch available teams and prompt user
3052
3092
  try {
3053
- const teams = await fetchJiraTeams(
3054
- `/teams/find?query=&excludeMembers=true`, {}, inst,
3055
- );
3093
+ const teams = await listTeams(inst);
3056
3094
  if (teams && teams.length > 0) {
3057
3095
  const list = teams.map((t, i) => `${i + 1}. ${t.title}`).join("\n");
3058
- teamPrompt = `\n\n⚠ No team assigned and no default team configured for instance "${inst.name}". Available teams:\n${list}\n0. None\n\nTo assign a team to this ticket, call jira_update_ticket with issueKey and team parameter.\nIf a team is selected, ask the user if it should also be saved as the default team for instance "${inst.name}" (via jira_add_instance with defaultTeam).`;
3096
+ teamPrompt = `\n\n⚠ No team assigned and no default team configured for instance "${inst.name}". Available teams:\n${list}\n0. None\n\nTo assign a team to this ticket, call jira_update_ticket with issueKey and team parameter.\nIf a team is selected, ask the user if it should also be saved as the default team for instance "${inst.name}" (via jira_add_instance with defaultTeam). If "None" is selected, call jira_add_instance with defaultTeam="none" to stop future prompts.`;
3059
3097
  }
3060
3098
  } catch {
3061
3099
  // Teams API not available, proceed without team
@@ -3118,7 +3156,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3118
3156
  );
3119
3157
  if (parentIssue.fields?.customfield_10001) {
3120
3158
  fields.customfield_10001 = parentIssue.fields.customfield_10001;
3121
- } else if (inst.defaultTeam) {
3159
+ } else if (inst.defaultTeam && inst.defaultTeam !== "none") {
3122
3160
  teamWarning = `\n\nNote: Parent ${args.parentKey} has no team assigned. Instance default team is "${inst.defaultTeam.name}". Use team parameter to assign it.`;
3123
3161
  }
3124
3162
  }
@@ -3469,16 +3507,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3469
3507
  let teamPrompt = "";
3470
3508
  if (of.customfield_10001) {
3471
3509
  fields.customfield_10001 = of.customfield_10001;
3472
- } else if (inst.defaultTeam) {
3510
+ } else if (inst.defaultTeam && inst.defaultTeam !== "none") {
3473
3511
  fields.customfield_10001 = inst.defaultTeam.id;
3474
- } else {
3512
+ } else if (!inst.defaultTeam) {
3475
3513
  try {
3476
- const teams = await fetchJiraTeams(
3477
- `/teams/find?query=&excludeMembers=true`, {}, inst,
3478
- );
3514
+ const teams = await listTeams(inst);
3479
3515
  if (teams && teams.length > 0) {
3480
3516
  const list = teams.map((t, i) => `${i + 1}. ${t.title}`).join("\n");
3481
- teamPrompt = `\n\n⚠ No team assigned (original had none) and no default team configured for instance "${inst.name}". Available teams:\n${list}\n0. None\n\nTo assign a team to this ticket, call jira_update_ticket with issueKey and team parameter.\nIf a team is selected, ask the user if it should also be saved as the default team for instance "${inst.name}" (via jira_add_instance with defaultTeam).`;
3517
+ teamPrompt = `\n\n⚠ No team assigned (original had none) and no default team configured for instance "${inst.name}". Available teams:\n${list}\n0. None\n\nTo assign a team to this ticket, call jira_update_ticket with issueKey and team parameter.\nIf a team is selected, ask the user if it should also be saved as the default team for instance "${inst.name}" (via jira_add_instance with defaultTeam). If "None" is selected, call jira_add_instance with defaultTeam="none" to stop future prompts.`;
3482
3518
  }
3483
3519
  } catch {
3484
3520
  // Teams API not available, proceed without team
@@ -3694,5 +3730,5 @@ if (require.main === module) {
3694
3730
 
3695
3731
  // Export for testing
3696
3732
  if (typeof module !== "undefined") {
3697
- module.exports = { buildCommentADF, parseInlineFormatting, findJiraTicketKeys, resolveTeamId, fetchJiraTeams };
3733
+ module.exports = { buildCommentADF, parseInlineFormatting, findJiraTicketKeys, resolveTeamId, fetchJiraTeams, listTeams, searchTeamsViaJql };
3698
3734
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rui.branco/jira-mcp",
3
- "version": "1.6.16",
3
+ "version": "1.6.18",
4
4
  "description": "Jira MCP server for Claude Code - fetch tickets, search with JQL, update tickets, manage comments, change status, and get Figma designs",
5
5
  "main": "index.js",
6
6
  "bin": {