app-store-connect-mcp-server

import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import jwt from 'jsonwebtoken'; import fs from 'fs/promises'; import axios from 'axios'; // Load environment variables const config = { keyId: process.env.APP_STORE_CONNECT_KEY_ID, issuerId: process.env.APP_STORE_CONNECT_ISSUER_ID, privateKeyPath: process.env.APP_STORE_CONNECT_P8_PATH, }; // Validate required environment variables if (!config.keyId || !config.issuerId || !config.privateKeyPath) { throw new Error("Missing required environment variables. Please set: " + "APP_STORE_CONNECT_KEY_ID, APP_STORE_CONNECT_ISSUER_ID, APP_STORE_CONNECT_P8_PATH"); } class AppStoreConnectServer { server; axiosInstance; constructor() { this.server = new Server({ name: "appstore-connect-server", version: "1.0.0" }, { capabilities: { tools: {} } }); this.axiosInstance = axios.create({ baseURL: 'https://api.appstoreconnect.apple.com/v1', }); this.setupHandlers(); } async generateToken() { const privateKey = await fs.readFile(config.privateKeyPath, 'utf-8'); const token = jwt.sign({}, privateKey, { algorithm: 'ES256', expiresIn: '20m', // App Store Connect tokens can be valid for up to 20 minutes audience: 'appstoreconnect-v1', keyid: config.keyId, issuer: config.issuerId, }); return token; } setupHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: "list_apps", description: "Get a list of all apps in App Store Connect", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of apps to return (default: 100)", minimum: 1, maximum: 200 } } } }, { name: "list_beta_groups", description: "Get a list of all beta groups (internal and external)", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of groups to return (default: 100)", minimum: 1, maximum: 200 } } } }, { name: "list_group_testers", description: "Get a list of all testers in a specific beta group", inputSchema: { type: "object", properties: { groupId: { type: "string", description: "The ID of the beta group" }, limit: { type: "number", description: "Maximum number of testers to return (default: 100)", minimum: 1, maximum: 200 } }, required: ["groupId"] } }, { name: "add_tester_to_group", description: "Add a new tester to a beta group", inputSchema: { type: "object", properties: { groupId: { type: "string", description: "The ID of the beta group" }, email: { type: "string", description: "Email address of the tester" }, firstName: { type: "string", description: "First name of the tester" }, lastName: { type: "string", description: "Last name of the tester" } }, required: ["groupId", "email", "firstName", "lastName"] } }, { name: "remove_tester_from_group", description: "Remove a tester from a beta group", inputSchema: { type: "object", properties: { groupId: { type: "string", description: "The ID of the beta group" }, testerId: { type: "string", description: "The ID of the beta tester" } }, required: ["groupId", "testerId"] } }, { name: "get_app_info", description: "Get detailed information about a specific app", inputSchema: { type: "object", properties: { appId: { type: "string", description: "The ID of the app to get information for" }, include: { type: "array", items: { type: "string", enum: [ "appClips", "appInfos", "appStoreVersions", "availableTerritories", "betaAppReviewDetail", "betaGroups", "betaLicenseAgreement", "builds", "endUserLicenseAgreement", "gameCenterEnabledVersions", "inAppPurchases", "preOrder", "prices", "reviewSubmissions" ] }, description: "Optional relationships to include in the response" } }, required: ["appId"] } }, { name: "create_bundle_id", description: "Register a new bundle ID for app development", inputSchema: { type: "object", properties: { identifier: { type: "string", description: "The bundle ID string (e.g., 'com.example.app')" }, name: { type: "string", description: "A name for the bundle ID" }, platform: { type: "string", enum: ["IOS", "MAC_OS", "UNIVERSAL"], description: "The platform for this bundle ID" }, seedId: { type: "string", description: "Your team's seed ID (optional)", required: false } }, required: ["identifier", "name", "platform"] } }, { name: "list_bundle_ids", description: "Find and list bundle IDs that are registered to your team", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of bundle IDs to return (default: 100, max: 200)", minimum: 1, maximum: 200 }, sort: { type: "string", description: "Sort order for the results", enum: [ "name", "-name", "platform", "-platform", "identifier", "-identifier", "seedId", "-seedId", "id", "-id" ] }, filter: { type: "object", properties: { identifier: { type: "string", description: "Filter by bundle identifier" }, name: { type: "string", description: "Filter by name" }, platform: { type: "string", description: "Filter by platform", enum: ["IOS", "MAC_OS", "UNIVERSAL"] }, seedId: { type: "string", description: "Filter by seed ID" } } }, include: { type: "array", items: { type: "string", enum: ["profiles", "bundleIdCapabilities", "app"] }, description: "Related resources to include in the response" } } } }, { name: "get_bundle_id_info", description: "Get detailed information about a specific bundle ID", inputSchema: { type: "object", properties: { bundleIdId: { type: "string", description: "The ID of the bundle ID to get information for" }, include: { type: "array", items: { type: "string", enum: ["profiles", "bundleIdCapabilities", "app"], description: "Related resources to include in the response" }, description: "Optional relationships to include in the response" }, fields: { type: "object", properties: { bundleIds: { type: "array", items: { type: "string", enum: ["name", "platform", "identifier", "seedId"] }, description: "Fields to include for the bundle ID" } }, description: "Specific fields to include in the response" } }, required: ["bundleIdId"] } }, { name: "list_devices", description: "Get a list of all devices registered to your team", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of devices to return (default: 100, max: 200)", minimum: 1, maximum: 200 }, sort: { type: "string", description: "Sort order for the results", enum: [ "name", "-name", "platform", "-platform", "status", "-status", "udid", "-udid", "deviceClass", "-deviceClass", "model", "-model", "addedDate", "-addedDate" ] }, filter: { type: "object", properties: { name: { type: "string", description: "Filter by device name" }, platform: { type: "string", description: "Filter by platform", enum: ["IOS", "MAC_OS"] }, status: { type: "string", description: "Filter by status", enum: ["ENABLED", "DISABLED"] }, udid: { type: "string", description: "Filter by device UDID" }, deviceClass: { type: "string", description: "Filter by device class", enum: ["APPLE_WATCH", "IPAD", "IPHONE", "IPOD", "APPLE_TV", "MAC"] } } }, fields: { type: "object", properties: { devices: { type: "array", items: { type: "string", enum: ["name", "platform", "udid", "deviceClass", "status", "model", "addedDate"] }, description: "Fields to include for each device" } } } } } }, { name: "enable_bundle_capability", description: "Enable a capability for a bundle ID", inputSchema: { type: "object", properties: { bundleIdId: { type: "string", description: "The ID of the bundle ID" }, capabilityType: { type: "string", description: "The type of capability to enable", enum: [ "ICLOUD", "IN_APP_PURCHASE", "GAME_CENTER", "PUSH_NOTIFICATIONS", "WALLET", "INTER_APP_AUDIO", "MAPS", "ASSOCIATED_DOMAINS", "PERSONAL_VPN", "APP_GROUPS", "HEALTHKIT", "HOMEKIT", "WIRELESS_ACCESSORY_CONFIGURATION", "APPLE_PAY", "DATA_PROTECTION", "SIRIKIT", "NETWORK_EXTENSIONS", "MULTIPATH", "HOT_SPOT", "NFC_TAG_READING", "CLASSKIT", "AUTOFILL_CREDENTIAL_PROVIDER", "ACCESS_WIFI_INFORMATION", "NETWORK_CUSTOM_PROTOCOL", "COREMEDIA_HLS_LOW_LATENCY", "SYSTEM_EXTENSION_INSTALL", "USER_MANAGEMENT", "APPLE_ID_AUTH" ] }, settings: { type: "array", description: "Optional capability settings", items: { type: "object", properties: { key: { type: "string", description: "The setting key" }, options: { type: "array", items: { type: "object", properties: { key: { type: "string" }, enabled: { type: "boolean" } } } } } } } }, required: ["bundleIdId", "capabilityType"] } }, { name: "disable_bundle_capability", description: "Disable a capability for a bundle ID", inputSchema: { type: "object", properties: { capabilityId: { type: "string", description: "The ID of the capability to disable" } }, required: ["capabilityId"] } }, { name: "list_users", description: "Get a list of all users registered on your App Store Connect team", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of users to return (default: 100, max: 200)", minimum: 1, maximum: 200 }, sort: { type: "string", description: "Sort order for the results", enum: [ "username", "-username", "firstName", "-firstName", "lastName", "-lastName", "roles", "-roles" ] }, filter: { type: "object", properties: { username: { type: "string", description: "Filter by username" }, roles: { type: "array", items: { type: "string", enum: [ "ADMIN", "FINANCE", "TECHNICAL", "SALES", "MARKETING", "DEVELOPER", "ACCOUNT_HOLDER", "READ_ONLY", "APP_MANAGER", "ACCESS_TO_REPORTS", "CUSTOMER_SUPPORT" ] }, description: "Filter by user roles" }, visibleApps: { type: "array", items: { type: "string" }, description: "Filter by apps the user can see (app IDs)" } } }, include: { type: "array", items: { type: "string", enum: ["visibleApps"], description: "Related resources to include in the response" } } } } }] })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const token = await this.generateToken(); switch (request.params.name) { case "list_apps": const response = await this.axiosInstance.get('/apps', { headers: { 'Authorization': `Bearer ${token}` }, params: { limit: request.params.arguments?.limit || 100 } }); return { toolResult: response.data }; case "list_beta_groups": { const response = await this.axiosInstance.get('/betaGroups', { headers: { 'Authorization': `Bearer ${token}` }, params: { limit: request.params.arguments?.limit || 100, // Include relationships to get more details include: 'app,betaTesters' } }); return { toolResult: response.data }; } case "list_group_testers": { const { groupId, limit = 100 } = request.params.arguments || {}; if (!groupId) { throw new McpError(ErrorCode.InvalidParams, "groupId is required"); } const response = await this.axiosInstance.get(`/betaGroups/${groupId}/betaTesters`, { headers: { 'Authorization': `Bearer ${token}` }, params: { limit } }); return { toolResult: response.data }; } case "add_tester_to_group": { const { groupId, email, firstName, lastName } = request.params.arguments; if (!groupId || !email || !firstName || !lastName) { throw new McpError(ErrorCode.InvalidParams, "groupId, email, firstName, and lastName are required"); } const requestBody = { data: { type: "betaTesters", attributes: { email, firstName, lastName }, relationships: { betaGroups: { data: [{ id: groupId, type: "betaGroups" }] } } } }; const response = await this.axiosInstance.post('/betaTesters', requestBody, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); return { toolResult: response.data }; } case "remove_tester_from_group": { const { groupId, testerId } = request.params.arguments; if (!groupId || !testerId) { throw new McpError(ErrorCode.InvalidParams, "groupId and testerId are required"); } await this.axiosInstance.delete(`/betaGroups/${groupId}/relationships/betaTesters`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, data: { data: [{ id: testerId, type: "betaTesters" }] } }); return { toolResult: { success: true, message: "Tester removed from group successfully" } }; } case "get_app_info": { const { appId, include } = request.params.arguments; if (!appId) { throw new McpError(ErrorCode.InvalidParams, "appId is required"); } const response = await this.axiosInstance.get(`/apps/${appId}`, { headers: { 'Authorization': `Bearer ${token}` }, params: { include: include?.join(',') } }); return { toolResult: response.data }; } case "create_bundle_id": { const { identifier, name, platform, seedId } = request.params.arguments; if (!identifier || !name || !platform) { throw new McpError(ErrorCode.InvalidParams, "identifier, name, and platform are required"); } const requestBody = { data: { type: "bundleIds", attributes: { identifier, name, platform, seedId } } }; const response = await this.axiosInstance.post('/bundleIds', requestBody, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); return { toolResult: response.data }; } case "list_bundle_ids": { const { limit = 100, sort, filter, include } = request.params.arguments || {}; const params = { limit: Math.min(Number(limit), 200) }; // Add sort parameter if provided if (sort) { params.sort = sort; } // Add filters if provided if (filter) { if (filter.identifier) params['filter[identifier]'] = filter.identifier; if (filter.name) params['filter[name]'] = filter.name; if (filter.platform) params['filter[platform]'] = filter.platform; if (filter.seedId) params['filter[seedId]'] = filter.seedId; } // Add includes if provided if (Array.isArray(include) && include.length > 0) { params.include = include.join(','); } const response = await this.axiosInstance.get('/bundleIds', { headers: { 'Authorization': `Bearer ${token}` }, params }); return { toolResult: response.data }; } case "get_bundle_id_info": { const { bundleIdId, include, fields } = request.params.arguments; if (!bundleIdId) { throw new McpError(ErrorCode.InvalidParams, "bundleIdId is required"); } const params = {}; // Add fields if provided if (fields?.bundleIds?.length) { params['fields[bundleIds]'] = fields.bundleIds.join(','); } // Add includes if provided if (include?.length) { params.include = include.join(','); } const response = await this.axiosInstance.get(`/bundleIds/${bundleIdId}`, { headers: { 'Authorization': `Bearer ${token}` }, params }); return { toolResult: response.data }; } case "list_devices": { const { limit = 100, sort, filter, fields } = request.params.arguments || {}; const params = { limit: Math.min(Number(limit), 200) }; // Add sort parameter if provided if (sort) { params.sort = sort; } // Add filters if provided if (filter) { if (filter.name) params['filter[name]'] = filter.name; if (filter.platform) params['filter[platform]'] = filter.platform; if (filter.status) params['filter[status]'] = filter.status; if (filter.udid) params['filter[udid]'] = filter.udid; if (filter.deviceClass) params['filter[deviceClass]'] = filter.deviceClass; } // Add fields if provided if (fields?.devices?.length) { params['fields[devices]'] = fields.devices.join(','); } const response = await this.axiosInstance.get('/devices', { headers: { 'Authorization': `Bearer ${token}` }, params }); return { toolResult: response.data }; } case "enable_bundle_capability": { const { bundleIdId, capabilityType, settings } = request.params.arguments; if (!bundleIdId || !capabilityType) { throw new McpError(ErrorCode.InvalidParams, "bundleIdId and capabilityType are required"); } const requestBody = { data: { type: "bundleIdCapabilities", attributes: { capabilityType, settings }, relationships: { bundleId: { data: { id: bundleIdId, type: "bundleIds" } } } } }; const response = await this.axiosInstance.post('/bundleIdCapabilities', requestBody, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); return { toolResult: response.data }; } case "disable_bundle_capability": { const { capabilityId } = request.params.arguments; if (!capabilityId) { throw new McpError(ErrorCode.InvalidParams, "capabilityId is required"); } await this.axiosInstance.delete(`/bundleIdCapabilities/${capabilityId}`, { headers: { 'Authorization': `Bearer ${token}` } }); return { toolResult: { success: true, message: "Capability disabled successfully" } }; } case "list_users": { const { limit = 100, sort, filter, include } = request.params.arguments || {}; const params = { limit: Math.min(Number(limit), 200) }; // Add sort parameter if provided if (sort) { params.sort = sort; } // Add filters if provided if (filter) { if (filter.username) params['filter[username]'] = filter.username; if (filter.roles?.length) params['filter[roles]'] = filter.roles.join(','); if (filter.visibleApps?.length) params['filter[visibleApps]'] = filter.visibleApps.join(','); } // Add includes if provided if (Array.isArray(include) && include.length > 0) { params.include = include.join(','); } const response = await this.axiosInstance.get('/users', { headers: { 'Authorization': `Bearer ${token}` }, params }); return { toolResult: response.data }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } } catch (error) { if (axios.isAxiosError(error)) { throw new McpError(ErrorCode.InternalError, `App Store Connect API error: ${error.response?.data?.errors?.[0]?.detail ?? error.message}`); } throw error; } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("App Store Connect MCP server running on stdio"); } } // Start the server const server = new AppStoreConnectServer(); server.run().catch(console.error);