Skip to main content
Glama

app-store-connect-mcp-server

submit_app.py13.6 kB
#!/usr/bin/env python3 """ Unified App Submission Tool Combines IPA building and App Store Connect submission into a single tool. Based on Codemagic CLI Tools implementation. """ import argparse import pathlib import subprocess import sys import json import time from typing import Optional, Dict, Any, List class SubmitAppException(Exception): """Custom exception for app submission errors""" pass class SubmitApp: """ Unified tool for building IPA and submitting to App Store Connect """ def __init__(self): self.verbose = False def log(self, message: str, level: str = "INFO"): """Simple logging function""" if self.verbose or level == "ERROR": print(f"[{level}] {message}") def run_command(self, command: List[str], capture_output: bool = True) -> subprocess.CompletedProcess: """Run a shell command and return the result""" self.log(f"Running command: {' '.join(command)}") try: result = subprocess.run( command, capture_output=capture_output, text=True, check=True ) return result except subprocess.CalledProcessError as e: self.log(f"Command failed: {e}", "ERROR") self.log(f"stdout: {e.stdout}", "ERROR") self.log(f"stderr: {e.stderr}", "ERROR") raise SubmitAppException(f"Command failed: {' '.join(command)}") def build_ipa(self, args: argparse.Namespace) -> pathlib.Path: """Build IPA using xcode-project build-ipa""" self.log("Building IPA...") # Build the xcode-project command cmd = ["python3", "-m", "codemagic.tools.xcode_project", "build-ipa"] # Add project/workspace path if args.xcode_project: cmd.extend(["--project", str(args.xcode_project)]) elif args.xcode_workspace: cmd.extend(["--workspace", str(args.xcode_workspace)]) else: raise SubmitAppException("Either --xcode-project or --xcode-workspace must be specified") # Add optional parameters if args.scheme: cmd.extend(["--scheme", args.scheme]) if args.target: cmd.extend(["--target", args.target]) if args.configuration: cmd.extend(["--configuration", args.configuration]) if args.clean: cmd.append("--clean") if args.archive_directory: cmd.extend(["--archive-directory", str(args.archive_directory)]) if args.ipa_directory: cmd.extend(["--ipa-directory", str(args.ipa_directory)]) if args.export_options_plist: cmd.extend(["--export-options-plist", str(args.export_options_plist)]) if args.remove_xcarchive: cmd.append("--remove-xcarchive") # Set PYTHONPATH to include the cli-tools src directory env = { "PYTHONPATH": str(pathlib.Path(__file__).parent / "cli-tools" / "src") } try: # Change to cli-tools directory and run the command result = subprocess.run( cmd, cwd=pathlib.Path(__file__).parent / "cli-tools", env={**subprocess.os.environ, **env}, capture_output=True, text=True, check=True ) # Parse the output to find the IPA path # The build-ipa command typically outputs the path to the built IPA output_lines = result.stdout.strip().split('\n') ipa_path = None # Look for IPA file in the output or use default location for line in output_lines: if line.endswith('.ipa') and pathlib.Path(line).exists(): ipa_path = pathlib.Path(line) break if not ipa_path: # Default IPA location ipa_dir = args.ipa_directory or pathlib.Path("build/ios/ipa") ipa_files = list(ipa_dir.glob("*.ipa")) if ipa_files: ipa_path = ipa_files[0] # Take the first IPA found else: raise SubmitAppException("Could not find built IPA file") self.log(f"Successfully built IPA: {ipa_path}") return ipa_path except subprocess.CalledProcessError as e: self.log(f"IPA build failed: {e}", "ERROR") self.log(f"stdout: {e.stdout}", "ERROR") self.log(f"stderr: {e.stderr}", "ERROR") raise SubmitAppException("IPA build failed") def submit_to_app_store(self, ipa_path: pathlib.Path, args: argparse.Namespace) -> None: """Submit IPA to App Store Connect using the publish action""" self.log(f"Submitting IPA to App Store Connect: {ipa_path}") # Build the app-store-connect publish command cmd = ["python3", "-m", "codemagic.tools.app_store_connect", "publish", str(ipa_path)] # Add authentication parameters if args.key_identifier: cmd.extend(["--key-id", args.key_identifier]) if args.issuer_id: cmd.extend(["--issuer-id", args.issuer_id]) if args.private_key: cmd.extend(["--private-key", args.private_key]) elif args.private_key_path: cmd.extend(["--private-key", f"@file:{args.private_key_path}"]) # Alternative authentication (username/password) if args.apple_id: cmd.extend(["--apple-id", args.apple_id]) if args.app_specific_password: cmd.extend(["--app-specific-password", args.app_specific_password]) # App Store submission parameters if args.submit_to_app_store: cmd.append("--submit-to-app-store") if args.version_string: cmd.extend(["--version-string", args.version_string]) if args.whats_new: cmd.extend(["--whats-new", args.whats_new]) if args.description: cmd.extend(["--description", args.description]) if args.keywords: cmd.extend(["--keywords", args.keywords]) if args.marketing_url: cmd.extend(["--marketing-url", args.marketing_url]) if args.support_url: cmd.extend(["--support-url", args.support_url]) if args.copyright: cmd.extend(["--copyright", args.copyright]) if args.earliest_release_date: cmd.extend(["--earliest-release-date", args.earliest_release_date]) if args.phased_release: cmd.append("--enable-phased-release") if args.cancel_previous_submissions: cmd.append("--cancel-previous-submissions") # Processing wait parameters if args.max_build_processing_wait: cmd.extend(["--max-build-processing-wait", str(args.max_build_processing_wait)]) # Set PYTHONPATH to include the cli-tools src directory env = { "PYTHONPATH": str(pathlib.Path(__file__).parent / "cli-tools" / "src") } try: # Change to cli-tools directory and run the command result = subprocess.run( cmd, cwd=pathlib.Path(__file__).parent / "cli-tools", env={**subprocess.os.environ, **env}, capture_output=True, text=True, check=True ) self.log("Successfully submitted to App Store Connect") if self.verbose: self.log(f"Output: {result.stdout}") except subprocess.CalledProcessError as e: self.log(f"App Store Connect submission failed: {e}", "ERROR") self.log(f"stdout: {e.stdout}", "ERROR") self.log(f"stderr: {e.stderr}", "ERROR") raise SubmitAppException("App Store Connect submission failed") def submit_app(self, args: argparse.Namespace) -> None: """Main method that builds IPA and submits to App Store Connect""" try: # Step 1: Build IPA ipa_path = self.build_ipa(args) # Step 2: Submit to App Store Connect self.submit_to_app_store(ipa_path, args) self.log("App submission completed successfully!") except SubmitAppException as e: self.log(f"App submission failed: {e}", "ERROR") sys.exit(1) except Exception as e: self.log(f"Unexpected error: {e}", "ERROR") sys.exit(1) def create_argument_parser() -> argparse.ArgumentParser: """Create and configure the argument parser""" parser = argparse.ArgumentParser( description="Unified tool for building IPA and submitting to App Store Connect", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Build and submit with API key authentication python submit_app.py --xcode-workspace MyApp.xcworkspace --scheme MyApp \\ --key-identifier ABC123 --issuer-id def456 --private-key-path AuthKey_ABC123.p8 \\ --submit-to-app-store --version-string "1.0.0" --whats-new "Initial release" # Build and submit with Apple ID authentication python submit_app.py --xcode-project MyApp.xcodeproj --scheme MyApp \\ --apple-id user@example.com --app-specific-password "abcd-efgh-ijkl-mnop" \\ --submit-to-app-store --version-string "1.0.0" """ ) # Xcode project parameters project_group = parser.add_mutually_exclusive_group(required=True) project_group.add_argument( "--xcode-project", type=pathlib.Path, help="Path to Xcode project (.xcodeproj)" ) project_group.add_argument( "--xcode-workspace", type=pathlib.Path, help="Path to Xcode workspace (.xcworkspace)" ) parser.add_argument("--scheme", help="Xcode scheme name") parser.add_argument("--target", help="Xcode target name") parser.add_argument("--configuration", help="Build configuration (Debug, Release)") parser.add_argument("--clean", action="store_true", help="Clean before building") # Build directories parser.add_argument( "--archive-directory", type=pathlib.Path, help="Directory to store xcarchive" ) parser.add_argument( "--ipa-directory", type=pathlib.Path, help="Directory to store built IPA" ) parser.add_argument( "--export-options-plist", type=pathlib.Path, help="Path to export options plist" ) parser.add_argument( "--remove-xcarchive", action="store_true", help="Remove xcarchive after IPA export" ) # App Store Connect API authentication auth_group = parser.add_argument_group("App Store Connect API Authentication") auth_group.add_argument("--key-identifier", help="App Store Connect API key identifier") auth_group.add_argument("--issuer-id", help="App Store Connect API issuer ID") auth_group.add_argument("--private-key", help="App Store Connect API private key content") auth_group.add_argument( "--private-key-path", type=pathlib.Path, help="Path to App Store Connect API private key file (.p8)" ) # Alternative authentication alt_auth_group = parser.add_argument_group("Alternative Authentication") alt_auth_group.add_argument("--apple-id", help="Apple ID for authentication") alt_auth_group.add_argument("--app-specific-password", help="App-specific password") # App Store submission parameters submission_group = parser.add_argument_group("App Store Submission") submission_group.add_argument( "--submit-to-app-store", action="store_true", help="Submit to App Store (not just TestFlight)" ) submission_group.add_argument("--version-string", help="App version string") submission_group.add_argument("--whats-new", help="What's new in this version") submission_group.add_argument("--description", help="App description") submission_group.add_argument("--keywords", help="App keywords") submission_group.add_argument("--marketing-url", help="Marketing URL") submission_group.add_argument("--support-url", help="Support URL") submission_group.add_argument("--copyright", help="Copyright information") submission_group.add_argument("--earliest-release-date", help="Earliest release date") submission_group.add_argument( "--phased-release", action="store_true", help="Enable phased release" ) submission_group.add_argument( "--cancel-previous-submissions", action="store_true", help="Cancel previous submissions" ) # Processing options parser.add_argument( "--max-build-processing-wait", type=int, default=600, help="Maximum time to wait for build processing (seconds)" ) # General options parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") return parser def main(): """Main entry point""" parser = create_argument_parser() args = parser.parse_args() submit_app = SubmitApp() submit_app.verbose = args.verbose submit_app.submit_app(args) if __name__ == "__main__": main()

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/JoshuaRileyDev/app-store-connect-mcp-server'

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