From b7cbcf47315493d5d3d44f183a29b95d6be75a56 Mon Sep 17 00:00:00 2001 From: jmynes <15176546+jmynes@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:54:19 -0500 Subject: [PATCH 1/2] fix(mcp): prioritize exact match in label name resolution (PUNT-405) Extract findLabelByName() helper that tries exact match first, then falls back to case-insensitive substring matching. Fixes ambiguous matches when label names overlap (e.g., "ux" incorrectly matching "UI/UX" instead of the exact "ux" label). Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp/src/tools/labels.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/mcp/src/tools/labels.ts b/mcp/src/tools/labels.ts index 51aac496..f6ae5b2d 100644 --- a/mcp/src/tools/labels.ts +++ b/mcp/src/tools/labels.ts @@ -10,6 +10,19 @@ import { } from '../api-client.js' import { errorResponse, escapeMarkdown, safeTableCell, textResponse } from '../utils.js' +/** + * Find a label by name using tiered matching: exact match first, then substring fallback. + */ +function findLabelByName( + labels: T[], + labelName: string, +): T | undefined { + const nameLower = labelName.toLowerCase() + const exactMatch = labels.find((l) => l.name.toLowerCase() === nameLower) + const substringMatch = labels.find((l) => l.name.toLowerCase().includes(nameLower)) + return exactMatch ?? substringMatch +} + /** * Format a list of labels for display */ @@ -94,9 +107,7 @@ export function registerLabelTools(server: McpServer) { return errorResponse(listResult.error) } - const label = listResult.data?.find((l) => - l.name.toLowerCase().includes(labelName.toLowerCase()), - ) + const label = listResult.data ? findLabelByName(listResult.data, labelName) : undefined if (!label) { return errorResponse(`Label not found: ${labelName}`) @@ -145,9 +156,7 @@ export function registerLabelTools(server: McpServer) { return errorResponse(listResult.error) } - const label = listResult.data?.find((l) => - l.name.toLowerCase().includes(labelName.toLowerCase()), - ) + const label = listResult.data ? findLabelByName(listResult.data, labelName) : undefined if (!label) { return errorResponse(`Label not found: ${labelName}`) @@ -188,9 +197,7 @@ export function registerLabelTools(server: McpServer) { return errorResponse(labelsResult.error) } - const label = labelsResult.data?.find((l) => - l.name.toLowerCase().includes(labelName.toLowerCase()), - ) + const label = labelsResult.data ? findLabelByName(labelsResult.data, labelName) : undefined if (!label) { return errorResponse(`Label not found: ${labelName}`) @@ -257,9 +264,7 @@ export function registerLabelTools(server: McpServer) { } // Find the label on the ticket - const label = ticket.labels.find((l) => - l.name.toLowerCase().includes(labelName.toLowerCase()), - ) + const label = findLabelByName(ticket.labels, labelName) if (!label) { return errorResponse( From 4a8b69639790e5d47bd877d419515a5440dfbee0 Mon Sep 17 00:00:00 2001 From: jmynes <15176546+jmynes@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:42:12 -0500 Subject: [PATCH 2/2] fix(mcp): add empty-string guard and short-circuit exact match in label lookup Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp/src/tools/labels.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mcp/src/tools/labels.ts b/mcp/src/tools/labels.ts index f6ae5b2d..92275f30 100644 --- a/mcp/src/tools/labels.ts +++ b/mcp/src/tools/labels.ts @@ -17,10 +17,11 @@ function findLabelByName( labels: T[], labelName: string, ): T | undefined { + if (!labelName.trim()) return undefined const nameLower = labelName.toLowerCase() const exactMatch = labels.find((l) => l.name.toLowerCase() === nameLower) - const substringMatch = labels.find((l) => l.name.toLowerCase().includes(nameLower)) - return exactMatch ?? substringMatch + if (exactMatch) return exactMatch + return labels.find((l) => l.name.toLowerCase().includes(nameLower)) } /**