Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
122 changes: 120 additions & 2 deletions src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ import {
FEATURE_REF_REGEX,
REQUIREMENT_REF_REGEX,
NOTE_REF_REGEX,
RELEASE_REF_REGEX,
Record,
FeatureResponse,
RequirementResponse,
ReleaseResponse,
ListReleasesResponse,
PageResponse,
SearchResponse,
} from "./types.js";
import {
getFeatureQuery,
getRequirementQuery,
getPageQuery,
getReleaseQuery,
listReleasesQuery,
searchDocumentsQuery,
} from "./queries.js";

Expand All @@ -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<ReleaseResponse>(
getReleaseQuery,
{
id: reference,
}
);
result = data.release;
} else if (FEATURE_REF_REGEX.test(reference)) {
const data = await this.client.request<FeatureResponse>(
getFeatureQuery,
{
Expand All @@ -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"
);
}

Expand Down Expand Up @@ -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<ReleaseResponse>(
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<ListReleasesResponse>(
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;
Expand Down
49 changes: 45 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AhaMcp {
this.server = new Server(
{
name: "aha-mcp",
version: "1.1.0",
version: "1.2.0",
},
{
capabilities: {
Expand All @@ -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:
Expand Down Expand Up @@ -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",
},
},
Expand All @@ -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") {
Expand Down
78 changes: 78 additions & 0 deletions src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -32,13 +42,81 @@ export const getRequirementQuery = `
query GetRequirement($id: ID!) {
requirement(id: $id) {
name
referenceNum
workflowStatus {
name
}
release {
name
referenceNum
}
description {
markdownBody
}
}
}
`;

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}) {
Expand Down
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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[];
Expand Down