From 826db961e7bed45a5f157f5a5ed438906628b9ff Mon Sep 17 00:00:00 2001 From: Rory Scott Date: Wed, 4 Mar 2026 09:17:29 -0500 Subject: [PATCH] Add release support: get_release, list_releases, and enriched queries - Add `get_release` tool to fetch a release by reference (e.g., IDP-R-23) with dates, status, progress, owner, and features - Add `list_releases` tool to list releases for a product, with optional active-only filter - Route release references (PREFIX-R-###) through `get_record` so it handles features, requirements, and releases uniformly - Enrich feature query to return referenceNum, workflowStatus, startDate, dueDate, and parent release - Enrich requirement query to return referenceNum, workflowStatus, and parent release - Update search_documents description to mention Release as a searchable type - Bump version to 1.2.0 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/handlers.ts | 122 +++++++++++++++++++++++++++++++++++++++++++++++- src/index.ts | 49 +++++++++++++++++-- src/queries.ts | 78 +++++++++++++++++++++++++++++++ src/types.ts | 13 ++++++ 5 files changed, 257 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 9fa9e62..cbedbdb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aha-mcp", - "version": "1.1.0", + "version": "1.2.0", "description": "MCP server for accessing Aha! GraphQL API", "type": "module", "main": "build/index.js", diff --git a/src/handlers.ts b/src/handlers.ts index dd0f088..b6cf70d 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -4,9 +4,12 @@ import { FEATURE_REF_REGEX, REQUIREMENT_REF_REGEX, NOTE_REF_REGEX, + RELEASE_REF_REGEX, Record, FeatureResponse, RequirementResponse, + ReleaseResponse, + ListReleasesResponse, PageResponse, SearchResponse, } from "./types.js"; @@ -14,6 +17,8 @@ import { getFeatureQuery, getRequirementQuery, getPageQuery, + getReleaseQuery, + listReleasesQuery, searchDocumentsQuery, } from "./queries.js"; @@ -33,7 +38,15 @@ export class Handlers { try { let result: Record | undefined; - if (FEATURE_REF_REGEX.test(reference)) { + if (RELEASE_REF_REGEX.test(reference)) { + const data = await this.client.request( + getReleaseQuery, + { + id: reference, + } + ); + result = data.release; + } else if (FEATURE_REF_REGEX.test(reference)) { const data = await this.client.request( getFeatureQuery, { @@ -50,7 +63,7 @@ export class Handlers { } else { throw new McpError( ErrorCode.InvalidParams, - "Invalid reference number format. Expected DEVELOP-123 or ADT-123-1" + "Invalid reference number format. Expected DEVELOP-123, ADT-123-1, or DEVELOP-R-123" ); } @@ -88,6 +101,111 @@ export class Handlers { } } + async handleGetRelease(request: any) { + const { reference } = request.params.arguments as { reference: string }; + + if (!reference) { + throw new McpError( + ErrorCode.InvalidParams, + "Release reference number is required" + ); + } + + if (!RELEASE_REF_REGEX.test(reference)) { + throw new McpError( + ErrorCode.InvalidParams, + "Invalid release reference format. Expected PREFIX-R-123 (e.g., IDP-R-23)" + ); + } + + try { + const data = await this.client.request( + getReleaseQuery, + { + id: reference, + } + ); + + if (!data.release) { + return { + content: [ + { + type: "text", + text: `No release found for reference ${reference}`, + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(data.release, null, 2), + }, + ], + }; + } catch (error) { + if (error instanceof McpError) { + throw error; + } + + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error("API Error:", errorMessage); + throw new McpError( + ErrorCode.InternalError, + `Failed to fetch release: ${errorMessage}` + ); + } + } + + async handleListReleases(request: any) { + const { projectId, active } = request.params.arguments as { + projectId: string; + active?: boolean; + }; + + if (!projectId) { + throw new McpError(ErrorCode.InvalidParams, "projectId is required"); + } + + try { + const filters: { projectId: string; active?: boolean } = { projectId }; + if (active !== undefined) { + filters.active = active; + } + + const data = await this.client.request( + listReleasesQuery, + { + filters, + } + ); + + return { + content: [ + { + type: "text", + text: JSON.stringify(data.releases, null, 2), + }, + ], + }; + } catch (error) { + if (error instanceof McpError) { + throw error; + } + + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error("API Error:", errorMessage); + throw new McpError( + ErrorCode.InternalError, + `Failed to list releases: ${errorMessage}` + ); + } + } + async handleGetPage(request: any) { const { reference, includeParent = false } = request.params.arguments as { reference: string; diff --git a/src/index.ts b/src/index.ts index 0a4795e..c442352 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,7 +38,7 @@ class AhaMcp { this.server = new Server( { name: "aha-mcp", - version: "1.1.0", + version: "1.2.0", }, { capabilities: { @@ -62,19 +62,55 @@ class AhaMcp { tools: [ { name: "get_record", - description: "Get an Aha! feature or requirement by reference number", + description: + "Get an Aha! feature, requirement, or release by reference number", + inputSchema: { + type: "object", + properties: { + reference: { + type: "string", + description: + "Reference number (e.g., DEVELOP-123, ADT-123-1, or DEVELOP-R-123 for releases)", + }, + }, + required: ["reference"], + }, + }, + { + name: "get_release", + description: + "Get an Aha! release by reference number with dates, status, progress, and features", inputSchema: { type: "object", properties: { reference: { type: "string", description: - "Reference number (e.g., DEVELOP-123 or ADT-123-1)", + "Release reference number (e.g., IDP-R-23, DUM-R-6)", }, }, required: ["reference"], }, }, + { + name: "list_releases", + description: "List releases for an Aha! product by project ID", + inputSchema: { + type: "object", + properties: { + projectId: { + type: "string", + description: "Aha! product/project ID", + }, + active: { + type: "boolean", + description: + "Filter to only active (non-shipped) releases", + }, + }, + required: ["projectId"], + }, + }, { name: "get_page", description: @@ -107,7 +143,8 @@ class AhaMcp { }, searchableType: { type: "string", - description: "Type of document to search for (e.g., Page)", + description: + "Type of document to search for (e.g., Page, Feature, Release)", default: "Page", }, }, @@ -120,6 +157,10 @@ class AhaMcp { this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "get_record") { return this.handlers.handleGetRecord(request); + } else if (request.params.name === "get_release") { + return this.handlers.handleGetRelease(request); + } else if (request.params.name === "list_releases") { + return this.handlers.handleListReleases(request); } else if (request.params.name === "get_page") { return this.handlers.handleGetPage(request); } else if (request.params.name === "search_documents") { diff --git a/src/queries.ts b/src/queries.ts index 974fc0d..85fcec6 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -21,6 +21,16 @@ export const getFeatureQuery = ` query GetFeature($id: ID!) { feature(id: $id) { name + referenceNum + workflowStatus { + name + } + startDate + dueDate + release { + name + referenceNum + } description { markdownBody } @@ -32,6 +42,14 @@ export const getRequirementQuery = ` query GetRequirement($id: ID!) { requirement(id: $id) { name + referenceNum + workflowStatus { + name + } + release { + name + referenceNum + } description { markdownBody } @@ -39,6 +57,66 @@ export const getRequirementQuery = ` } `; +export const getReleaseQuery = ` + query GetRelease($id: ID!) { + release(id: $id) { + name + referenceNum + releaseDate + startOn + endOn + developmentStartedOn + releasedOn + progress + parkingLot + daysToRelease + workflowStatus { + name + } + owner { + name + } + project { + name + } + featuresCount + features { + name + referenceNum + workflowStatus { + name + } + } + } + } +`; + +export const listReleasesQuery = ` + query ListReleases($filters: ReleaseFilters) { + releases(filters: $filters, per: 50) { + nodes { + name + referenceNum + releaseDate + startOn + endOn + progress + parkingLot + daysToRelease + workflowStatus { + name + } + owner { + name + } + featuresCount + } + totalCount + isLastPage + } + } +`; + export const searchDocumentsQuery = ` query SearchDocuments($query: String!, $searchableType: [String!]!) { searchDocuments(filters: {query: $query, searchableType: $searchableType}) { diff --git a/src/types.ts b/src/types.ts index c7dca9c..126b166 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,6 +34,7 @@ export interface PageResponse { export const FEATURE_REF_REGEX = /^([A-Z][A-Z0-9]*)-(\d+)$/; export const REQUIREMENT_REF_REGEX = /^([A-Z][A-Z0-9]*)-(\d+)-(\d+)$/; export const NOTE_REF_REGEX = /^([A-Z][A-Z0-9]*)-N-(\d+)$/; +export const RELEASE_REF_REGEX = /^([A-Z][A-Z0-9]*)-R-(\d+)$/; export interface SearchNode { name: string | null; @@ -42,6 +43,18 @@ export interface SearchNode { searchableType: string; } +export interface ReleaseResponse { + release: Record; +} + +export interface ListReleasesResponse { + releases: { + nodes: Record[]; + totalCount: number; + isLastPage: boolean; + }; +} + export interface SearchResponse { searchDocuments: { nodes: SearchNode[];