From 2eeb0ecffe1334776e8be7f5c94dc177359dab63 Mon Sep 17 00:00:00 2001 From: Jack Willis Date: Wed, 17 Dec 2025 14:08:46 -0600 Subject: [PATCH] Add search_features tool and enhance get_record with rich context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add search_features tool using REST API for text-based feature search - Supports filters: q, product_id, assigned_to_user, tag, updated_since - Enhance get_record to return comprehensive feature data: - Comments with author and timestamp - Requirements with descriptions and status - Epic, initiative, release, project context - Goals, tags, workflow status, assignee - Created/updated timestamps and dates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 104 ++++++++++++++++++++++++++++++++++++++++-------- src/handlers.ts | 69 ++++++++++++++++++++++++++++++++ src/index.ts | 33 +++++++++++++++ src/queries.ts | 26 ++++++++++-- src/types.ts | 76 ++++++++++++++++++++++++++++++++++- 5 files changed, 287 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 131c7f5..2385d88 100644 --- a/README.md +++ b/README.md @@ -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 } ``` @@ -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" } } ``` @@ -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! @@ -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" diff --git a/src/handlers.ts b/src/handlers.ts index ec2bccc..c7011b5 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -11,6 +11,8 @@ import { SearchResponse, CreateFeatureRequest, CreateFeatureResponse, + SearchFeaturesRequest, + SearchFeaturesResponse, } from "./types.js"; import { getFeatureQuery, @@ -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}` + ); + } + } } diff --git a/src/index.ts b/src/index.ts index 52130e4..24ebf33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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!", @@ -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); } diff --git a/src/queries.ts b/src/queries.ts index 974fc0d..a417208 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -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 } } `; diff --git a/src/types.ts b/src/types.ts index 0db52b3..f30ea6e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,27 @@ 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 { @@ -7,14 +29,64 @@ export interface Record { 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;