UNPKG

@blocktopus/mcp-google-play

Version:

MCP server for Google Play Store command line tools

588 lines 23.1 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { google } from 'googleapis'; import { GoogleAuth } from 'google-auth-library'; import { z } from 'zod'; import * as fs from 'fs/promises'; // Get API key from command line arguments or environment const args = process.argv.slice(2); let apiKeyPath; // Parse command line arguments for (let i = 0; i < args.length; i++) { if (args[i] === '--api-key' && i + 1 < args.length) { apiKeyPath = args[i + 1]; break; } } // Fall back to environment variable if not provided via CLI if (!apiKeyPath) { apiKeyPath = process.env.GOOGLE_APPLICATION_CREDENTIALS; } // Allow test mode without API key const isTestMode = args.includes('--test'); if (!apiKeyPath && !isTestMode) { console.error('Error: No API key provided. Use --api-key <path> or set GOOGLE_APPLICATION_CREDENTIALS'); console.error('For testing without API key, use --test flag'); process.exit(1); } // Initialize Google Auth with the provided key let auth; let androidpublisher; // Initialize auth and API in an async function async function initializeAuth() { if (isTestMode) { console.error('Running in test mode - API calls will not work without credentials'); return; } try { // Verify the file exists await fs.access(apiKeyPath); auth = new GoogleAuth({ keyFile: apiKeyPath, scopes: ['https://www.googleapis.com/auth/androidpublisher'] }); // Initialize Play Developer API with auth const authClient = await auth.getClient(); androidpublisher = google.androidpublisher({ version: 'v3', auth: authClient }); } catch (error) { console.error(`Error: Cannot access API key file at ${apiKeyPath}`); process.exit(1); } } // Tool schemas const ListAppsSchema = z.object({}); const GetAppInfoSchema = z.object({ packageName: z.string().describe('The package name of the app (e.g., com.example.app)') }); const ListReleasesSchema = z.object({ packageName: z.string().describe('The package name of the app'), track: z.enum(['internal', 'alpha', 'beta', 'production']).describe('The release track') }); const GetReviewsSchema = z.object({ packageName: z.string().describe('The package name of the app'), maxResults: z.number().optional().default(10).describe('Maximum number of reviews to return') }); const ReplyToReviewSchema = z.object({ packageName: z.string().describe('The package name of the app'), reviewId: z.string().describe('The ID of the review to reply to'), replyText: z.string().describe('The reply text to post') }); const UpdateListingSchema = z.object({ packageName: z.string().describe('The package name of the app'), language: z.string().describe('Language code (e.g., en-US)'), title: z.string().optional().describe('App title'), shortDescription: z.string().optional().describe('Short description'), fullDescription: z.string().optional().describe('Full description'), video: z.string().optional().describe('YouTube video URL') }); const GetListingSchema = z.object({ packageName: z.string().describe('The package name of the app'), language: z.string().describe('Language code (e.g., en-US)') }); const CreateReleaseSchema = z.object({ packageName: z.string().describe('The package name of the app'), track: z.enum(['internal', 'alpha', 'beta', 'production']).describe('The release track'), versionCode: z.number().describe('Version code of the APK/AAB to release'), releaseNotes: z.string().optional().describe('Release notes for this version'), userFraction: z.number().optional().describe('Fraction of users to get update (0.0-1.0)') }); const GetStatisticsSchema = z.object({ packageName: z.string().describe('The package name of the app'), metric: z.enum(['installs', 'ratings', 'crashes']).describe('Type of statistics to retrieve') }); // Create MCP server const server = new Server({ name: 'google-play', version: '0.1.2', }, { capabilities: { tools: {}, }, }); // Handle list tools request server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'list_apps', description: 'List all apps in your Google Play Console', inputSchema: { type: 'object', properties: {}, required: [] }, }, { name: 'get_app_info', description: 'Get detailed information about a specific app', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'The package name of the app (e.g., com.example.app)' } }, required: ['packageName'] }, }, { name: 'list_releases', description: 'List releases for an app in a specific track', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'The package name of the app' }, track: { type: 'string', enum: ['internal', 'alpha', 'beta', 'production'], description: 'The release track' } }, required: ['packageName', 'track'] }, }, { name: 'get_reviews', description: 'Get recent reviews for an app', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'The package name of the app' }, maxResults: { type: 'number', description: 'Maximum number of reviews to return', default: 10 } }, required: ['packageName'] }, }, { name: 'reply_to_review', description: 'Reply to a user review', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'The package name of the app' }, reviewId: { type: 'string', description: 'The ID of the review to reply to' }, replyText: { type: 'string', description: 'The reply text to post' } }, required: ['packageName', 'reviewId', 'replyText'] }, }, { name: 'get_listing', description: 'Get store listing information for an app', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'The package name of the app' }, language: { type: 'string', description: 'Language code (e.g., en-US)' } }, required: ['packageName', 'language'] }, }, { name: 'update_listing', description: 'Update store listing information', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'The package name of the app' }, language: { type: 'string', description: 'Language code (e.g., en-US)' }, title: { type: 'string', description: 'App title' }, shortDescription: { type: 'string', description: 'Short description' }, fullDescription: { type: 'string', description: 'Full description' }, video: { type: 'string', description: 'YouTube video URL' } }, required: ['packageName', 'language'] }, }, { name: 'create_release', description: 'Create a new release for an app', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'The package name of the app' }, track: { type: 'string', enum: ['internal', 'alpha', 'beta', 'production'], description: 'The release track' }, versionCode: { type: 'number', description: 'Version code of the APK/AAB to release' }, releaseNotes: { type: 'string', description: 'Release notes for this version' }, userFraction: { type: 'number', description: 'Fraction of users to get update (0.0-1.0)' } }, required: ['packageName', 'track', 'versionCode'] }, }, { name: 'get_statistics', description: 'Get app statistics (installs, ratings, crashes)', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'The package name of the app' }, metric: { type: 'string', enum: ['installs', 'ratings', 'crashes'], description: 'Type of statistics to retrieve' } }, required: ['packageName', 'metric'] }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { // Check if we're in test mode if (isTestMode) { return { content: [ { type: 'text', text: 'Server is running in test mode. Please provide API credentials to use this tool.', }, ], }; } // Auth is already set up in androidpublisher instance switch (name) { case 'list_apps': { // Note: The Play Developer API doesn't have a direct "list all apps" endpoint // This would typically require knowing package names beforehand return { content: [ { type: 'text', text: 'To list apps, you need to provide package names. The Play Developer API requires knowing package names in advance.', }, ], }; } case 'get_app_info': { const { packageName } = GetAppInfoSchema.parse(args); const response = await androidpublisher.edits.insert({ packageName, }); const editId = response.data.id; // Get app details const appDetails = await androidpublisher.edits.details.get({ packageName, editId: editId, }); // Delete the edit (we're just reading) await androidpublisher.edits.delete({ packageName, editId: editId, }); return { content: [ { type: 'text', text: JSON.stringify(appDetails.data, null, 2), }, ], }; } case 'list_releases': { const { packageName, track } = ListReleasesSchema.parse(args); const response = await androidpublisher.edits.insert({ packageName, }); const editId = response.data.id; // Get track information const trackInfo = await androidpublisher.edits.tracks.get({ packageName, editId: editId, track, }); // Delete the edit await androidpublisher.edits.delete({ packageName, editId: editId, }); return { content: [ { type: 'text', text: JSON.stringify(trackInfo.data, null, 2), }, ], }; } case 'get_reviews': { const { packageName, maxResults } = GetReviewsSchema.parse(args); const reviews = await androidpublisher.reviews.list({ packageName, maxResults, }); return { content: [ { type: 'text', text: JSON.stringify(reviews.data, null, 2), }, ], }; } case 'reply_to_review': { const { packageName, reviewId, replyText } = ReplyToReviewSchema.parse(args); const result = await androidpublisher.reviews.reply({ packageName, reviewId, requestBody: { replyText, }, }); return { content: [ { type: 'text', text: `Successfully replied to review ${reviewId}`, }, ], }; } case 'get_listing': { const { packageName, language } = GetListingSchema.parse(args); const response = await androidpublisher.edits.insert({ packageName, }); const editId = response.data.id; // Get listing details const listing = await androidpublisher.edits.listings.get({ packageName, editId: editId, language, }); // Delete the edit await androidpublisher.edits.delete({ packageName, editId: editId, }); return { content: [ { type: 'text', text: JSON.stringify(listing.data, null, 2), }, ], }; } case 'update_listing': { const { packageName, language, title, shortDescription, fullDescription, video } = UpdateListingSchema.parse(args); const response = await androidpublisher.edits.insert({ packageName, }); const editId = response.data.id; // Update listing const updateData = {}; if (title) updateData.title = title; if (shortDescription) updateData.shortDescription = shortDescription; if (fullDescription) updateData.fullDescription = fullDescription; if (video) updateData.video = video; await androidpublisher.edits.listings.update({ packageName, editId: editId, language, requestBody: updateData, }); // Commit the edit const commitResult = await androidpublisher.edits.commit({ packageName, editId: editId, }); return { content: [ { type: 'text', text: `Successfully updated ${language} listing for ${packageName}`, }, ], }; } case 'create_release': { const { packageName, track, versionCode, releaseNotes, userFraction } = CreateReleaseSchema.parse(args); const response = await androidpublisher.edits.insert({ packageName, }); const editId = response.data.id; // Create release const releaseData = { versionCodes: [versionCode], status: userFraction && userFraction < 1 ? 'inProgress' : 'completed', }; if (releaseNotes) { releaseData.releaseNotes = [{ language: 'en-US', text: releaseNotes, }]; } if (userFraction) { releaseData.userFraction = userFraction; } await androidpublisher.edits.tracks.update({ packageName, editId: editId, track, requestBody: { track, releases: [releaseData], }, }); // Commit the edit const commitResult = await androidpublisher.edits.commit({ packageName, editId: editId, }); return { content: [ { type: 'text', text: `Successfully created release for version ${versionCode} on ${track} track`, }, ], }; } case 'get_statistics': { const { packageName, metric } = GetStatisticsSchema.parse(args); // Note: Statistics require different API endpoints based on metric // This is a simplified example let result; switch (metric) { case 'installs': // This would typically use the Reports API result = 'Install statistics would be retrieved from Play Console Reports API'; break; case 'ratings': // Get app details which includes ratings const response = await androidpublisher.edits.insert({ packageName, }); const editId = response.data.id; const appDetails = await androidpublisher.edits.details.get({ packageName, editId: editId, }); await androidpublisher.edits.delete({ packageName, editId: editId, }); result = JSON.stringify({ appDetails: appDetails.data, note: 'For detailed statistics, use Play Console Reports API', }, null, 2); break; case 'crashes': result = 'Crash statistics would be retrieved from Play Console Vitals API'; break; default: result = 'Unknown metric'; } return { content: [ { type: 'text', text: result, }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`, }, ], }; } }); // Start the server async function main() { await initializeAuth(); const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Google Play server started'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); }); //# sourceMappingURL=index.js.map