aipm-mcp
Version:
Complete AIPM integration for Cursor IDE - Get tasks, manage features, track time, and build features with AI. Supports both MCP stdio mode and HTTP server mode.
415 lines • 18.4 kB
JavaScript
import axios, { AxiosError } from 'axios';
import { TicketSchema, FeatureSchema, ProductSchema, UserSchema, TimeLogSchema } from './types.js';
// Enhanced error handling
class AIPMError extends Error {
statusCode;
errorCode;
details;
requestContext;
constructor(message, statusCode, errorCode, details, requestContext) {
super(message);
this.name = 'AIPMError';
this.statusCode = statusCode;
this.errorCode = errorCode;
this.details = details;
this.requestContext = requestContext;
}
}
// Error extraction helper
function extractErrorDetails(error, context) {
if (error instanceof AxiosError) {
const statusCode = error.response?.status;
const responseData = error.response?.data;
// Handle different HTTP status codes
switch (statusCode) {
case 400:
if (responseData?.errors && Array.isArray(responseData.errors)) {
const validationErrors = responseData.errors.map((err) => `${err.path?.join('.') || 'field'}: ${err.msg || err.message}`).join(', ');
return new AIPMError(`Validation failed: ${validationErrors}`, statusCode, 'VALIDATION_ERROR', responseData.errors, context);
}
return new AIPMError(responseData?.error || 'Bad request', statusCode, 'BAD_REQUEST', responseData, context);
case 401:
return new AIPMError('Authentication failed. Please check your credentials.', statusCode, 'UNAUTHORIZED', responseData, context);
case 403:
if (responseData?.error?.includes('pending approval')) {
return new AIPMError('Access denied. Your account may be pending approval.', statusCode, 'PENDING_APPROVAL', responseData, context);
}
return new AIPMError(responseData?.error || 'Access forbidden', statusCode, 'FORBIDDEN', responseData, context);
case 404:
return new AIPMError(responseData?.error || 'Resource not found', statusCode, 'NOT_FOUND', responseData, context);
case 422:
return new AIPMError('Request validation failed', statusCode, 'VALIDATION_ERROR', responseData, context);
case 500:
return new AIPMError('Internal server error. Please try again later.', statusCode, 'SERVER_ERROR', responseData, context);
default:
return new AIPMError(responseData?.error || error.message || 'Unknown error occurred', statusCode, 'UNKNOWN_ERROR', responseData, context);
}
}
// Handle non-HTTP errors
if (error instanceof Error) {
return new AIPMError(error.message, undefined, 'NETWORK_ERROR', error, context);
}
return new AIPMError('An unexpected error occurred', undefined, 'UNKNOWN_ERROR', error, context);
}
export class AIPMClient {
client;
currentUser = null;
constructor(apiUrl, token) {
this.client = axios.create({
baseURL: apiUrl,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
timeout: 30000
});
// Add response interceptor for error handling
this.client.interceptors.response.use((response) => response, (error) => {
if (error.response?.status === 401) {
throw new Error('Authentication failed. Please check your AIPM token.');
}
if (error.response?.status === 403) {
throw new Error('Access denied. Your account may be pending approval.');
}
throw new Error(error.response?.data?.message || error.message || 'API request failed');
});
}
async getCurrentUser() {
if (this.currentUser) {
return this.currentUser;
}
try {
const response = await this.client.get('/api/auth/me');
this.currentUser = UserSchema.parse(response.data.user);
return this.currentUser;
}
catch (error) {
throw extractErrorDetails(error, 'getCurrentUser()');
}
}
async getTasks(params = {}) {
try {
const queryParams = new URLSearchParams();
if (params.status)
queryParams.append('status', params.status);
if (params.priority)
queryParams.append('priority', params.priority);
if (params.productId)
queryParams.append('productId', params.productId);
if (params.featureId)
queryParams.append('featureId', params.featureId);
if (params.limit)
queryParams.append('limit', params.limit.toString());
const response = await this.client.get(`/api/tickets?${queryParams.toString()}`);
if (!response.data.tickets) {
throw new Error('Invalid response format from AIPM API');
}
return response.data.tickets.map((ticket) => TicketSchema.parse(ticket));
}
catch (error) {
throw extractErrorDetails(error, `getTasks(${JSON.stringify(params)})`);
}
}
async getTaskById(taskId) {
try {
const response = await this.client.get(`/api/tickets/${taskId}`);
return TicketSchema.parse(response.data.ticket);
}
catch (error) {
throw new Error(`Failed to get task ${taskId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async startTask(taskId, notes) {
try {
const response = await this.client.put(`/api/tickets/${taskId}`, {
status: 'IN_PROGRESS',
startedAt: new Date().toISOString(),
notes
});
// Also create a time log entry
const timeLogResponse = await this.client.post('/api/time-logs/start', {
ticketId: taskId,
description: notes || `Started working on task: ${taskId}`
});
return {
success: true,
timeLogId: timeLogResponse.data?.id
};
}
catch (error) {
throw new Error(`Failed to start task ${taskId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async completeTask(taskId, options = {}) {
try {
// Update task status
await this.client.put(`/api/tickets/${taskId}`, {
status: 'COMPLETED',
completedAt: new Date().toISOString(),
generatedCode: options.codeChanges,
implementationDetails: options.implementationDetails,
testResults: options.testResults,
...(options.completionNotes && { description: options.completionNotes })
});
// Stop any active time logs
const activeTimeLogs = await this.client.get(`/api/time-logs?ticketId=${taskId}&isActive=true`);
if (activeTimeLogs.data.timeLogs?.length > 0) {
for (const timeLog of activeTimeLogs.data.timeLogs) {
await this.client.put(`/api/time-logs/${timeLog.id}`, {
isActive: false,
endedAt: new Date().toISOString(),
hours: options.timeSpent || timeLog.hours,
description: options.completionNotes || timeLog.description
});
}
}
return { success: true };
}
catch (error) {
throw new Error(`Failed to complete task ${taskId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async updateTaskImplementation(taskId, options) {
try {
const response = await this.client.put(`/api/tickets/${taskId}`, {
implementationDetails: options.implementationDetails,
testResults: options.testResults,
codeChanges: options.codeChanges,
});
return { success: true };
}
catch (error) {
throw new Error(`Failed to update task implementation ${taskId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async createTask(taskData) {
try {
const user = await this.getCurrentUser();
const response = await this.client.post('/api/tickets', {
...taskData,
createdById: user.id,
organizationId: user.organizationId,
assignedToId: taskData.assignedToId || user.id
});
return TicketSchema.parse(response.data.ticket);
}
catch (error) {
throw extractErrorDetails(error, `createTask(${taskData.title})`);
}
}
async getFeatures(params = {}) {
try {
const queryParams = new URLSearchParams();
if (params.productId)
queryParams.append('productId', params.productId);
if (params.status)
queryParams.append('status', params.status);
if (params.includeTickets)
queryParams.append('includeTickets', 'true');
const response = await this.client.get(`/api/features?${queryParams.toString()}`);
if (!response.data.features) {
throw new Error('Invalid response format from AIPM API');
}
return response.data.features.map((feature) => FeatureSchema.parse(feature));
}
catch (error) {
throw new Error(`Failed to get features: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async breakdownFeature(featureId, additionalContext) {
try {
const response = await this.client.post('/api/ai/breakdown', {
featureId,
additionalContext
});
return {
tasks: response.data.tickets?.map((ticket) => TicketSchema.parse(ticket)) || [],
breakdown: response.data.breakdown || 'Feature breakdown completed'
};
}
catch (error) {
throw new Error(`Failed to breakdown feature ${featureId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getProducts() {
try {
const response = await this.client.get('/api/products');
if (!response.data.products) {
throw new Error('Invalid response format from AIPM API');
}
return response.data.products.map((product) => ProductSchema.parse(product));
}
catch (error) {
throw new Error(`Failed to get products: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async logTime(timeData) {
try {
const response = await this.client.post('/api/time-logs', {
...timeData,
date: timeData.date || new Date().toISOString().split('T')[0]
});
return TimeLogSchema.parse(response.data);
}
catch (error) {
throw new Error(`Failed to log time: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getTimeLogs(params = {}) {
try {
const queryParams = new URLSearchParams();
if (params.ticketId)
queryParams.append('ticketId', params.ticketId);
if (params.featureId)
queryParams.append('featureId', params.featureId);
if (params.startDate)
queryParams.append('startDate', params.startDate);
if (params.endDate)
queryParams.append('endDate', params.endDate);
const response = await this.client.get(`/api/time-logs?${queryParams.toString()}`);
if (!response.data.timeLogs) {
throw new Error('Invalid response format from AIPM API');
}
return response.data.timeLogs.map((timeLog) => TimeLogSchema.parse(timeLog));
}
catch (error) {
throw new Error(`Failed to get time logs: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async searchTasks(query, limit = 10) {
try {
const response = await this.client.get(`/api/search?q=${encodeURIComponent(query)}&type=tickets&limit=${limit}`);
if (!response.data.results?.tickets) {
return [];
}
return response.data.results.tickets.map((ticket) => TicketSchema.parse(ticket));
}
catch (error) {
throw new Error(`Failed to search tasks: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Health check method
async healthCheck() {
try {
// Health endpoint doesn't require authentication, so we make a direct request
const axios = await import('axios');
// Ensure baseURL doesn't have trailing slash to avoid double slashes
const baseURL = this.client.defaults.baseURL?.replace(/\/$/, '') || '';
const response = await axios.default.get(`${baseURL}/health`);
return response.status === 200;
}
catch (error) {
return false;
}
}
// Update feature with build results
async updateFeature(featureId, options) {
try {
const response = await this.client.patch(`/api/features/${featureId}`, options);
return { success: true };
}
catch (error) {
throw new Error(`Failed to update feature ${featureId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Create feature build record
async createFeatureBuild(buildData) {
try {
// Use the existing build endpoint with required fields
const response = await this.client.post('/api/features/build', {
featureId: buildData.featureId,
requirements: buildData.metadata?.message || 'Feature implementation completed',
acceptanceCriteria: buildData.metadata?.acceptanceCriteria || '',
openCursor: false, // Already handled by MCP
showProgress: false
});
return { success: true };
}
catch (error) {
throw new Error(`Failed to create feature build: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Get pending build requests for a user
async getPendingBuildRequests(userId, lastPolledAt) {
try {
const params = new URLSearchParams({ userId });
if (lastPolledAt) {
params.append('lastPolledAt', lastPolledAt);
}
const response = await this.client.get(`/api/features/build-requests/pending?${params.toString()}`);
// Handle nested response structure
return response.data?.pendingRequests || response.data || [];
}
catch (error) {
// If endpoint doesn't exist yet, return empty array
if (error instanceof Error && error.message.includes('404')) {
return [];
}
throw new Error(`Failed to get pending build requests: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Mark a build request as processed
async markBuildRequestProcessed(requestId, result) {
try {
const response = await this.client.post(`/api/features/build-requests/${requestId}/processed`, {
result,
processedAt: new Date().toISOString()
});
// Check if the response indicates success
if (response.data && response.data.success) {
console.log(`✅ Successfully marked build request ${requestId} as processed`);
return { success: true };
}
else {
console.warn(`⚠️ Backend returned non-success for build request ${requestId}:`, response.data);
return { success: false };
}
}
catch (error) {
// If endpoint doesn't exist yet, just log and continue
if (error instanceof AxiosError && error.response?.status === 404) {
console.warn(`⚠️ Build request processing endpoint not found (404) - this may be expected in development`);
return { success: false };
}
console.error(`❌ Failed to mark build request ${requestId} as processed:`, error instanceof Error ? error.message : 'Unknown error');
return { success: false };
}
}
// Get build request status
async getBuildRequestStatus(requestId) {
try {
const response = await this.client.get(`/api/features/build-requests/${requestId}/status`);
// Handle the new response format
if (response.data && response.data.success) {
return {
status: response.data.status,
processedAt: response.data.processedAt
};
}
return null;
}
catch (error) {
// If endpoint doesn't exist or request not found, return null
if (error instanceof AxiosError && (error.response?.status === 404 || error.response?.status === 500)) {
return null;
}
throw new Error(`Failed to get build request status: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Update build status with detailed information
async updateBuildStatus(featureId, status, detailedStatus, message, metadata) {
try {
const response = await this.client.patch(`/api/features/${featureId}/build-status`, {
status,
detailedStatus,
message,
metadata
});
return { success: true };
}
catch (error) {
throw new Error(`Failed to update build status: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
//# sourceMappingURL=aipm-client.js.map