Skip to main content
Glama
TrialAndErrorAI

App Store Connect MCP Server

client.ts6.97 kB
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios'; import { JWTManager } from '../auth/jwt-manager.js'; import { PagedResponse, AppStoreError } from '../types/api.js'; export class AppStoreClient { private baseURL = 'https://api.appstoreconnect.apple.com/v1'; private auth: JWTManager; private axiosInstance: AxiosInstance; private requestCount = 0; private requestResetTime: Date; constructor(auth: JWTManager) { this.auth = auth; this.requestResetTime = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now // Create axios instance with defaults this.axiosInstance = axios.create({ baseURL: this.baseURL, timeout: 30000, // 30 second timeout headers: { 'Content-Type': 'application/json' } }); // Add request interceptor for auth this.axiosInstance.interceptors.request.use( async (config) => { const token = await this.auth.getToken(); config.headers.Authorization = `Bearer ${token}`; return config; }, (error) => Promise.reject(error) ); // Add response interceptor for error handling this.axiosInstance.interceptors.response.use( (response) => response, (error) => this.handleError(error) ); // App Store API Client initialized } /** * Make a GET request to the App Store Connect API */ async request<T = any>(endpoint: string, params?: any, options?: AxiosRequestConfig): Promise<T> { // Check rate limit await this.checkRateLimit(); try { // For reports endpoints, we need to handle binary/gzipped responses const isReportEndpoint = endpoint.includes('Reports'); const config: AxiosRequestConfig = { params, ...options }; // Set response type to arraybuffer for report endpoints to handle gzipped data if (isReportEndpoint) { config.responseType = 'arraybuffer'; } const response = await this.axiosInstance.get<T>(endpoint, config); this.requestCount++; // For report endpoints, convert arraybuffer to string if (isReportEndpoint && response.data instanceof ArrayBuffer) { // Convert ArrayBuffer to string for processing const buffer = Buffer.from(response.data); return buffer as any; } return response.data; } catch (error) { // Error is already handled by interceptor throw error; } } /** * Handle paginated endpoints with automatic page fetching */ async *paginate<T>(endpoint: string, params?: any): AsyncGenerator<T, void, unknown> { let nextUrl: string | null = endpoint; let currentParams = params; while (nextUrl) { const response: PagedResponse<T> = await this.request<PagedResponse<T>>(nextUrl, currentParams); // Yield each item for (const item of response.data) { yield item; } // Check for next page if (response.links?.next) { // Extract path from full URL nextUrl = response.links.next.replace(this.baseURL, ''); currentParams = undefined; // Params are included in next URL } else { nextUrl = null; } } } /** * Get all items from a paginated endpoint (use with caution for large datasets) */ async getAll<T>(endpoint: string, params?: any): Promise<T[]> { const items: T[] = []; for await (const item of this.paginate<T>(endpoint, params)) { items.push(item); } return items; } /** * Check and enforce rate limiting (3600 requests per hour) */ private async checkRateLimit(): Promise<void> { // Reset counter if hour has passed if (new Date() > this.requestResetTime) { this.requestCount = 0; this.requestResetTime = new Date(Date.now() + 60 * 60 * 1000); } // Check if we're approaching the limit if (this.requestCount >= 3500) { // Leave 100 request buffer const waitTime = this.requestResetTime.getTime() - Date.now(); if (waitTime > 0) { await this.sleep(waitTime); this.requestCount = 0; this.requestResetTime = new Date(Date.now() + 60 * 60 * 1000); } } } /** * Handle API errors with proper formatting */ private async handleError(error: AxiosError): Promise<never> { if (error.response) { const status = error.response.status; const data = error.response.data as AppStoreError | any; // Check if it's an App Store error response if (data?.errors && Array.isArray(data.errors)) { const firstError = data.errors[0]; const message = firstError.detail || firstError.title || 'Unknown error'; // Special handling for common errors switch (status) { case 401: throw new Error(`Authentication failed: ${message}. Check your credentials.`); case 403: throw new Error(`Permission denied: ${message}. Check your API key permissions.`); case 404: // Log more details for debugging process.stderr.write(`404 Debug - URL: ${error.request?.path || error.config?.url}\n`); process.stderr.write(`404 Debug - Params: ${JSON.stringify(error.config?.params)}\n`); throw new Error(`Resource not found: ${message}`); case 429: // Rate limited - wait and retry const retryAfter = error.response.headers['retry-after']; const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 60000; await this.sleep(waitTime); throw new Error('Rate limited - please retry'); default: throw new Error(`API Error (${status}): ${message}`); } } else { // Generic error response throw new Error(`API Error (${status}): ${error.message}`); } } else if (error.request) { // Request made but no response throw new Error(`Network error: No response from App Store Connect API`); } else { // Something else happened throw new Error(`Request error: ${error.message}`); } } /** * Helper function to sleep for specified milliseconds */ private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Test the connection to App Store Connect */ async testConnection(): Promise<boolean> { try { // Try to fetch apps (simplest endpoint) const response = await this.request('/apps', { limit: 1 }); return true; } catch (error) { return false; } } /** * Get request statistics */ getStats() { const resetIn = Math.max(0, this.requestResetTime.getTime() - Date.now()); return { requestCount: this.requestCount, requestLimit: 3600, resetInSeconds: Math.ceil(resetIn / 1000), resetAt: this.requestResetTime }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TrialAndErrorAI/appstore-connect-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server