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
104 changes: 88 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,32 @@ Retrieves an Aha! feature or requirement by reference number.
**Response:**
```json
{
"reference_num": "DEVELOP-123",
"id": "123456",
"name": "Feature name",
"description": "Feature description",
"workflow_status": {
"name": "In development",
"id": "123456"
}
"referenceNum": "DEVELOP-123",
"path": "/features/DEVELOP-123",
"description": {
"markdownBody": "Feature description in markdown"
},
"workflowStatus": {
"name": "In development"
},
"assignedToUser": {
"name": "John Developer",
"email": "developer@company.com"
},
"release": {
"name": "Q1 Release",
"referenceNum": "PRJ-R-1"
},
"project": {
"name": "Project Name",
"referencePrefix": "PRJ"
},
"comments": [],
"commentsCount": 0,
"requirements": [],
"requirementsCount": 0
}
```

Expand All @@ -186,12 +205,19 @@ Gets an Aha! page by reference number.
**Response:**
```json
{
"reference_num": "ABC-N-213",
"name": "Page title",
"body": "Page content",
"description": {
"markdownBody": "Page content in markdown"
},
"children": [
{
"name": "Child page",
"referenceNum": "ABC-N-214"
}
],
"parent": {
"reference_num": "ABC-N-200",
"name": "Parent page"
"name": "Parent page",
"referenceNum": "ABC-N-200"
}
}
```
Expand All @@ -215,19 +241,63 @@ Searches for Aha! documents.
**Response:**
```json
{
"results": [
"nodes": [
{
"reference_num": "ABC-N-123",
"name": "Product Roadmap 2025",
"type": "Page",
"url": "https://company.aha.io/pages/ABC-N-123"
"url": "/pages/ABC-N-123",
"searchableId": "123456789",
"searchableType": "Page"
}
],
"currentPage": 1,
"totalCount": 1,
"totalPages": 1,
"isLastPage": true
}
```

### 4. search_features

Searches for Aha! features by name, assignee, tag, or update date.

**Parameters:**
- `q` (optional): Search term to match against feature name
- `product_id` (optional): Filter by product/project ID
- `assigned_to_user` (optional): Filter by assignee (user ID or email)
- `tag` (optional): Filter by tag
- `updated_since` (optional): Only features updated after this timestamp (ISO8601)

**Example:**
```json
{
"q": "authentication",
"assigned_to_user": "developer@company.com"
}
```

**Response:**
```json
{
"features": [
{
"id": "12345",
"reference_num": "DEVELOP-123",
"name": "User Authentication",
"created_at": "2024-01-15T10:30:00.000Z",
"url": "https://company.aha.io/features/DEVELOP-123",
"resource": "https://company.aha.io/api/v1/features/DEVELOP-123",
"product_id": "67890"
}
],
"total_results": 1
"pagination": {
"total_records": 1,
"total_pages": 1,
"current_page": 1
}
}
```

### 4. create_feature
### 5. create_feature

Creates a new feature in Aha!

Expand Down Expand Up @@ -295,6 +365,8 @@ Creates a new feature in Aha!
- "Search for pages about launch planning"
- "Get requirement ADT-123-1"
- "Find all pages mentioning Q2 goals"
- "Search for features assigned to me"
- "Find features with the tag 'backend'"
- "Create a new feature called 'User Authentication' in release PRJ1-R-1"
- "Create a feature for mobile push notifications in release MOBILE-R-2"

Expand Down
69 changes: 69 additions & 0 deletions src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
SearchResponse,
CreateFeatureRequest,
CreateFeatureResponse,
SearchFeaturesRequest,
SearchFeaturesResponse,
} from "./types.js";
import {
getFeatureQuery,
Expand Down Expand Up @@ -325,4 +327,71 @@ export class Handlers {
);
}
}

async handleSearchFeatures(request: any) {
const { q, product_id, assigned_to_user, tag, updated_since } =
request.params.arguments as SearchFeaturesRequest;

try {
const ahaApiToken = process.env.AHA_API_TOKEN;
const ahaDomain = process.env.AHA_DOMAIN;

if (!ahaApiToken || !ahaDomain) {
throw new McpError(
ErrorCode.InternalError,
"Missing AHA_API_TOKEN or AHA_DOMAIN environment variables"
);
}

// Build query string
const params = new URLSearchParams();
if (q) params.append("q", q);
if (product_id) params.append("product_id", product_id);
if (assigned_to_user) params.append("assigned_to_user", assigned_to_user);
if (tag) params.append("tag", tag);
if (updated_since) params.append("updated_since", updated_since);

const response = await fetch(
`https://${ahaDomain}.aha.io/api/v1/features?${params.toString()}`,
{
method: "GET",
headers: {
"Authorization": `Bearer ${ahaApiToken}`,
"Accept": 'application/json',
},
}
);

if (!response.ok) {
const errorText = await response.text();
throw new McpError(
ErrorCode.InternalError,
`Aha! API error (${response.status}): ${errorText}`
);
}

const data: SearchFeaturesResponse = await response.json();

return {
content: [
{
type: "text",
text: JSON.stringify(data, 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 search features: ${errorMessage}`
);
}
}
}
33 changes: 33 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,37 @@ class AhaMcp {
required: ["query"],
},
},
{
name: "search_features",
description:
"Search for Aha! features by name, assignee, tag, or update date",
inputSchema: {
type: "object",
properties: {
q: {
type: "string",
description: "Search term to match against feature name",
},
product_id: {
type: "string",
description: "Filter by product/project ID",
},
assigned_to_user: {
type: "string",
description: "Filter by assignee (user ID or email)",
},
tag: {
type: "string",
description: "Filter by tag",
},
updated_since: {
type: "string",
description:
"Only features updated after this timestamp (ISO8601)",
},
},
},
},
{
name: "create_feature",
description: "Create a new feature in Aha!",
Expand Down Expand Up @@ -222,6 +253,8 @@ class AhaMcp {
return this.handlers.handleGetPage(request);
} else if (request.params.name === "search_documents") {
return this.handlers.handleSearchDocuments(request);
} else if (request.params.name === "search_features") {
return this.handlers.handleSearchFeatures(request);
} else if (request.params.name === "create_feature") {
return this.handlers.handleCreateFeature(request);
}
Expand Down
26 changes: 23 additions & 3 deletions src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,30 @@ export const getPageQuery = `
export const getFeatureQuery = `
query GetFeature($id: ID!) {
feature(id: $id) {
id
name
description {
markdownBody
}
referenceNum
path
description { markdownBody }
workflowStatus { name }
assignedToUser { name email }
createdByUser { name }
createdAt
updatedAt
startDate
dueDate

epic { name referenceNum }
initiative { name referenceNum description { markdownBody } }
release { name referenceNum }
project { name referencePrefix }
goals { name }
tagList

comments { body createdAt user { name } }
commentsCount
requirements { name description { markdownBody } workflowStatus { name } }
requirementsCount
}
}
`;
Expand Down
76 changes: 74 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,92 @@
export interface Description {
htmlBody: string;
htmlBody?: string;
markdownBody?: string;
}

export interface User {
name: string;
email?: string;
}

export interface WorkflowStatus {
name: string;
}

export interface Comment {
body: string;
createdAt: string;
user: User;
}

export interface Requirement {
name: string;
description: Description;
workflowStatus: WorkflowStatus;
}

export interface Record {
name: string;
description: Description;
}

export interface Feature {
id: string;
name: string;
referenceNum: string;
path: string;
description: Description;
workflowStatus: WorkflowStatus;
assignedToUser?: User;
createdByUser?: User;
createdAt: string;
updatedAt: string;
startDate?: string;
dueDate?: string;
epic?: { name: string; referenceNum: string };
initiative?: { name: string; referenceNum: string; description: Description };
release: { name: string; referenceNum: string };
project: { name: string; referencePrefix: string };
goals: Array<{ name: string }>;
tagList: string;
comments: Comment[];
commentsCount: number;
requirements: Requirement[];
requirementsCount: number;
}

export interface FeatureResponse {
feature: Record;
feature: Feature;
}

export interface RequirementResponse {
requirement: Record;
}

// Search features (REST API)
export interface SearchFeaturesRequest {
q?: string;
product_id?: string;
assigned_to_user?: string;
tag?: string;
updated_since?: string;
}

export interface SearchFeaturesResponse {
features: Array<{
id: string;
reference_num: string;
name: string;
created_at: string;
url: string;
product_id: string;
}>;
pagination: {
total_records: number;
total_pages: number;
current_page: number;
};
}

export interface PageResponse {
page: {
name: string;
Expand Down