google-play-mcp 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google_play_mcp-0.2.0/.env.example +4 -0
- google_play_mcp-0.2.0/.gitignore +27 -0
- google_play_mcp-0.2.0/PKG-INFO +204 -0
- google_play_mcp-0.2.0/README.md +190 -0
- google_play_mcp-0.2.0/pyproject.toml +25 -0
- google_play_mcp-0.2.0/server.json +20 -0
- google_play_mcp-0.2.0/smithery.yaml +18 -0
- google_play_mcp-0.2.0/src/google_play_mcp/__init__.py +1 -0
- google_play_mcp-0.2.0/src/google_play_mcp/client.py +253 -0
- google_play_mcp-0.2.0/src/google_play_mcp/server.py +373 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Environment & secrets
|
|
2
|
+
.env
|
|
3
|
+
*.json
|
|
4
|
+
!*example*.json
|
|
5
|
+
!server.json
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*.pyo
|
|
11
|
+
*.pyd
|
|
12
|
+
.Python
|
|
13
|
+
*.egg-info/
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
.eggs/
|
|
17
|
+
*.egg
|
|
18
|
+
|
|
19
|
+
# Virtual envs
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
env/
|
|
23
|
+
|
|
24
|
+
# IDE
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
*.swp
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: google-play-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Google Play Console MCP Server — manage production releases from your AI assistant.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: android,google-play,mcp,play-console
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: google-api-python-client>=2.120.0
|
|
9
|
+
Requires-Dist: google-auth-httplib2>=0.2.0
|
|
10
|
+
Requires-Dist: google-auth>=2.28.0
|
|
11
|
+
Requires-Dist: mcp[cli]>=1.3.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Google Play Console MCP
|
|
16
|
+
|
|
17
|
+
A Python [Model Context Protocol](https://modelcontextprotocol.io/) server that
|
|
18
|
+
lets AI assistants (Claude, etc.) manage your Google Play Store production
|
|
19
|
+
releases directly.
|
|
20
|
+
|
|
21
|
+
<!-- mcp-name: io.github.agimaulana/google-play-mcp -->
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
**stdio — one command, no install:**
|
|
28
|
+
```bash
|
|
29
|
+
claude mcp add google-play-mcp \
|
|
30
|
+
-e GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json \
|
|
31
|
+
-- uvx google-play-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**HTTP — run as a local server:**
|
|
35
|
+
```bash
|
|
36
|
+
# Terminal 1 — start the server
|
|
37
|
+
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json \
|
|
38
|
+
uvx google-play-mcp --transport http --port 8080
|
|
39
|
+
|
|
40
|
+
# Terminal 2 — register with Claude
|
|
41
|
+
claude mcp add --transport http google-play-mcp http://localhost:8080
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> Requires `uv` — install with `brew install uv` or `curl -Lsf https://astral.sh/uv/install.sh | sh`
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
| Tool | Description |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `promote_to_production` | Promote a build from internal track to production with a custom rollout % |
|
|
53
|
+
| `check_review_status` | Check production release status (draft / inProgress / halted / completed) |
|
|
54
|
+
| `update_rollout_percentage` | Increase or complete an existing staged rollout |
|
|
55
|
+
| `get_production_release_info` | Fetch current rollout %, version codes, and release notes |
|
|
56
|
+
| `get_crash_rate` | Fetch user-perceived crash rate via the Play Developer Reporting API |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Prerequisites
|
|
61
|
+
|
|
62
|
+
1. **`uv`** — [install guide](https://docs.astral.sh/uv/getting-started/installation/)
|
|
63
|
+
2. A **Google Cloud service account** with the JSON key downloaded.
|
|
64
|
+
3. The service account added to **Google Play Console** with the correct permissions (see below).
|
|
65
|
+
4. These APIs enabled in your Google Cloud project:
|
|
66
|
+
- [Google Play Android Developer API](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com)
|
|
67
|
+
- [Google Play Developer Reporting API](https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com)
|
|
68
|
+
|
|
69
|
+
### Required Play Console permissions
|
|
70
|
+
|
|
71
|
+
| Tool | Minimum permission required |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `promote_to_production` | **Release to production, exclude devices, and use app signing by Google Play** |
|
|
74
|
+
| `update_rollout_percentage` | **Release to production, exclude devices, and use app signing by Google Play** |
|
|
75
|
+
| `check_review_status` | **View app information and download bulk reports (read-only)** |
|
|
76
|
+
| `get_production_release_info` | **View app information and download bulk reports (read-only)** |
|
|
77
|
+
| `get_crash_rate` | **View app information and download bulk reports (read-only)** |
|
|
78
|
+
|
|
79
|
+
> **Important:** Release Manager does **not** grant Reporting API access. You must also enable
|
|
80
|
+
> **View app information and download bulk reports (read-only)** — both at account level and
|
|
81
|
+
> per-app level — for `get_crash_rate` to work.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Claude Desktop integration
|
|
86
|
+
|
|
87
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"google-play": {
|
|
93
|
+
"command": "uvx",
|
|
94
|
+
"args": ["google-play-mcp"],
|
|
95
|
+
"env": {
|
|
96
|
+
"GOOGLE_APPLICATION_CREDENTIALS": "/absolute/path/to/service-account.json"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Restart Claude Desktop after saving.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Service account setup
|
|
108
|
+
|
|
109
|
+
1. Go to [IAM & Admin → Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts) in your GCP project.
|
|
110
|
+
2. Create a service account (or use an existing one) and download a JSON key.
|
|
111
|
+
3. In [Google Play Console](https://play.google.com/console) → **Setup → API access**:
|
|
112
|
+
- Link your Google Cloud project.
|
|
113
|
+
- Find the service account → **Manage Play Console permissions**.
|
|
114
|
+
- Under **Account permissions**, enable **View app information and download bulk reports (read-only)**.
|
|
115
|
+
- Under **App permissions** for each app, enable:
|
|
116
|
+
- **View app information and download bulk reports (read-only)**
|
|
117
|
+
- **Release to production…** _(if you need write access)_
|
|
118
|
+
- Click **Apply** → **Invite user**.
|
|
119
|
+
|
|
120
|
+
> Permissions must be granted at **both** account level and per-app level.
|
|
121
|
+
> Account-level alone is not sufficient for the Reporting API.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Tool reference
|
|
126
|
+
|
|
127
|
+
### `promote_to_production`
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
package_name : str — e.g. "com.example.myapp"
|
|
131
|
+
version_codes : list[int] — e.g. [1042]
|
|
132
|
+
rollout_percentage : float — 1–100 (use 100 for immediate full release)
|
|
133
|
+
release_name : str — optional, e.g. "2.4.1"
|
|
134
|
+
release_notes_en : str — optional English release notes
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `check_review_status`
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
package_name : str
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Returns each production release with its status and rollout percentage.
|
|
144
|
+
|
|
145
|
+
### `update_rollout_percentage`
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
package_name : str
|
|
149
|
+
rollout_percentage : float — new %, 0 < value <= 100
|
|
150
|
+
version_codes : list[int] — optional; targets the first inProgress release if omitted
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `get_production_release_info`
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
package_name : str
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Returns the active release's rollout percentage, version codes, and release notes.
|
|
160
|
+
|
|
161
|
+
> **Note:** Raw install/update event counts are not available via the public API.
|
|
162
|
+
> Use **Play Console → Statistics**, [BigQuery exports](https://support.google.com/googleplay/android-developer/answer/10668107), or Firebase Analytics.
|
|
163
|
+
|
|
164
|
+
### `get_crash_rate`
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
package_name : str
|
|
168
|
+
days : int — look-back window, 1–30 (default 7)
|
|
169
|
+
version_code : str — optional single version code to filter
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Returns daily `crashRate`, `userPerceivedCrashRate`, and `distinctUsers` per version code.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Troubleshooting
|
|
177
|
+
|
|
178
|
+
### `403 Forbidden` on `get_crash_rate`
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
403 Client Error: Forbidden for url: https://playdeveloperreporting.googleapis.com/...
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The service account lacks per-app Reporting API access. Retrying will not help.
|
|
185
|
+
|
|
186
|
+
**Fix:**
|
|
187
|
+
1. Play Console → **Setup → API access** → find the service account → **Manage Play Console permissions**.
|
|
188
|
+
2. Under **App permissions**, select the app and enable **View app information and download bulk reports (read-only)**.
|
|
189
|
+
3. Save and wait a few minutes for the change to propagate.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Marketplaces
|
|
194
|
+
|
|
195
|
+
| Registry | Identifier |
|
|
196
|
+
|---|---|
|
|
197
|
+
| [Smithery](https://smithery.ai) | search `google-play-mcp` |
|
|
198
|
+
| [Official MCP Registry](https://registry.modelcontextprotocol.io) | `io.github.agimaulana/google-play-mcp` |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Google Play Console MCP
|
|
2
|
+
|
|
3
|
+
A Python [Model Context Protocol](https://modelcontextprotocol.io/) server that
|
|
4
|
+
lets AI assistants (Claude, etc.) manage your Google Play Store production
|
|
5
|
+
releases directly.
|
|
6
|
+
|
|
7
|
+
<!-- mcp-name: io.github.agimaulana/google-play-mcp -->
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
**stdio — one command, no install:**
|
|
14
|
+
```bash
|
|
15
|
+
claude mcp add google-play-mcp \
|
|
16
|
+
-e GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json \
|
|
17
|
+
-- uvx google-play-mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**HTTP — run as a local server:**
|
|
21
|
+
```bash
|
|
22
|
+
# Terminal 1 — start the server
|
|
23
|
+
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json \
|
|
24
|
+
uvx google-play-mcp --transport http --port 8080
|
|
25
|
+
|
|
26
|
+
# Terminal 2 — register with Claude
|
|
27
|
+
claude mcp add --transport http google-play-mcp http://localhost:8080
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> Requires `uv` — install with `brew install uv` or `curl -Lsf https://astral.sh/uv/install.sh | sh`
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `promote_to_production` | Promote a build from internal track to production with a custom rollout % |
|
|
39
|
+
| `check_review_status` | Check production release status (draft / inProgress / halted / completed) |
|
|
40
|
+
| `update_rollout_percentage` | Increase or complete an existing staged rollout |
|
|
41
|
+
| `get_production_release_info` | Fetch current rollout %, version codes, and release notes |
|
|
42
|
+
| `get_crash_rate` | Fetch user-perceived crash rate via the Play Developer Reporting API |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Prerequisites
|
|
47
|
+
|
|
48
|
+
1. **`uv`** — [install guide](https://docs.astral.sh/uv/getting-started/installation/)
|
|
49
|
+
2. A **Google Cloud service account** with the JSON key downloaded.
|
|
50
|
+
3. The service account added to **Google Play Console** with the correct permissions (see below).
|
|
51
|
+
4. These APIs enabled in your Google Cloud project:
|
|
52
|
+
- [Google Play Android Developer API](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com)
|
|
53
|
+
- [Google Play Developer Reporting API](https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com)
|
|
54
|
+
|
|
55
|
+
### Required Play Console permissions
|
|
56
|
+
|
|
57
|
+
| Tool | Minimum permission required |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `promote_to_production` | **Release to production, exclude devices, and use app signing by Google Play** |
|
|
60
|
+
| `update_rollout_percentage` | **Release to production, exclude devices, and use app signing by Google Play** |
|
|
61
|
+
| `check_review_status` | **View app information and download bulk reports (read-only)** |
|
|
62
|
+
| `get_production_release_info` | **View app information and download bulk reports (read-only)** |
|
|
63
|
+
| `get_crash_rate` | **View app information and download bulk reports (read-only)** |
|
|
64
|
+
|
|
65
|
+
> **Important:** Release Manager does **not** grant Reporting API access. You must also enable
|
|
66
|
+
> **View app information and download bulk reports (read-only)** — both at account level and
|
|
67
|
+
> per-app level — for `get_crash_rate` to work.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Claude Desktop integration
|
|
72
|
+
|
|
73
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"google-play": {
|
|
79
|
+
"command": "uvx",
|
|
80
|
+
"args": ["google-play-mcp"],
|
|
81
|
+
"env": {
|
|
82
|
+
"GOOGLE_APPLICATION_CREDENTIALS": "/absolute/path/to/service-account.json"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Restart Claude Desktop after saving.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Service account setup
|
|
94
|
+
|
|
95
|
+
1. Go to [IAM & Admin → Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts) in your GCP project.
|
|
96
|
+
2. Create a service account (or use an existing one) and download a JSON key.
|
|
97
|
+
3. In [Google Play Console](https://play.google.com/console) → **Setup → API access**:
|
|
98
|
+
- Link your Google Cloud project.
|
|
99
|
+
- Find the service account → **Manage Play Console permissions**.
|
|
100
|
+
- Under **Account permissions**, enable **View app information and download bulk reports (read-only)**.
|
|
101
|
+
- Under **App permissions** for each app, enable:
|
|
102
|
+
- **View app information and download bulk reports (read-only)**
|
|
103
|
+
- **Release to production…** _(if you need write access)_
|
|
104
|
+
- Click **Apply** → **Invite user**.
|
|
105
|
+
|
|
106
|
+
> Permissions must be granted at **both** account level and per-app level.
|
|
107
|
+
> Account-level alone is not sufficient for the Reporting API.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Tool reference
|
|
112
|
+
|
|
113
|
+
### `promote_to_production`
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
package_name : str — e.g. "com.example.myapp"
|
|
117
|
+
version_codes : list[int] — e.g. [1042]
|
|
118
|
+
rollout_percentage : float — 1–100 (use 100 for immediate full release)
|
|
119
|
+
release_name : str — optional, e.g. "2.4.1"
|
|
120
|
+
release_notes_en : str — optional English release notes
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `check_review_status`
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
package_name : str
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Returns each production release with its status and rollout percentage.
|
|
130
|
+
|
|
131
|
+
### `update_rollout_percentage`
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
package_name : str
|
|
135
|
+
rollout_percentage : float — new %, 0 < value <= 100
|
|
136
|
+
version_codes : list[int] — optional; targets the first inProgress release if omitted
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### `get_production_release_info`
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
package_name : str
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Returns the active release's rollout percentage, version codes, and release notes.
|
|
146
|
+
|
|
147
|
+
> **Note:** Raw install/update event counts are not available via the public API.
|
|
148
|
+
> Use **Play Console → Statistics**, [BigQuery exports](https://support.google.com/googleplay/android-developer/answer/10668107), or Firebase Analytics.
|
|
149
|
+
|
|
150
|
+
### `get_crash_rate`
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
package_name : str
|
|
154
|
+
days : int — look-back window, 1–30 (default 7)
|
|
155
|
+
version_code : str — optional single version code to filter
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Returns daily `crashRate`, `userPerceivedCrashRate`, and `distinctUsers` per version code.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
### `403 Forbidden` on `get_crash_rate`
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
403 Client Error: Forbidden for url: https://playdeveloperreporting.googleapis.com/...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The service account lacks per-app Reporting API access. Retrying will not help.
|
|
171
|
+
|
|
172
|
+
**Fix:**
|
|
173
|
+
1. Play Console → **Setup → API access** → find the service account → **Manage Play Console permissions**.
|
|
174
|
+
2. Under **App permissions**, select the app and enable **View app information and download bulk reports (read-only)**.
|
|
175
|
+
3. Save and wait a few minutes for the change to propagate.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Marketplaces
|
|
180
|
+
|
|
181
|
+
| Registry | Identifier |
|
|
182
|
+
|---|---|
|
|
183
|
+
| [Smithery](https://smithery.ai) | search `google-play-mcp` |
|
|
184
|
+
| [Official MCP Registry](https://registry.modelcontextprotocol.io) | `io.github.agimaulana/google-play-mcp` |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "google-play-mcp"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Google Play Console MCP Server — manage production releases from your AI assistant."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["mcp", "google-play", "android", "play-console"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"mcp[cli]>=1.3.0",
|
|
15
|
+
"google-auth>=2.28.0",
|
|
16
|
+
"google-auth-httplib2>=0.2.0",
|
|
17
|
+
"google-api-python-client>=2.120.0",
|
|
18
|
+
"python-dotenv>=1.0.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
google-play-mcp = "google_play_mcp.server:main"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/google_play_mcp"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.agimaulana/google-play-mcp",
|
|
4
|
+
"description": "Manage Google Play Store production releases from your AI assistant — promote builds, adjust rollout percentages, check review status, and fetch crash-rate vitals.",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"packages": [
|
|
7
|
+
{
|
|
8
|
+
"registryType": "pypi",
|
|
9
|
+
"name": "google-play-mcp",
|
|
10
|
+
"version": "0.2.0",
|
|
11
|
+
"runtimeArguments": ["uvx", "google-play-mcp"],
|
|
12
|
+
"environmentVariables": [
|
|
13
|
+
{
|
|
14
|
+
"name": "GOOGLE_APPLICATION_CREDENTIALS",
|
|
15
|
+
"description": "Absolute path to the Google Cloud service account JSON key file"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
startCommand:
|
|
2
|
+
type: stdio
|
|
3
|
+
configSchema:
|
|
4
|
+
type: object
|
|
5
|
+
required:
|
|
6
|
+
- googleApplicationCredentials
|
|
7
|
+
properties:
|
|
8
|
+
googleApplicationCredentials:
|
|
9
|
+
type: string
|
|
10
|
+
description: "Absolute path to your Google Cloud service account JSON key file"
|
|
11
|
+
commandFunction: |-
|
|
12
|
+
(config) => ({
|
|
13
|
+
command: "uvx",
|
|
14
|
+
args: ["google-play-mcp"],
|
|
15
|
+
env: {
|
|
16
|
+
GOOGLE_APPLICATION_CREDENTIALS: config.googleApplicationCredentials
|
|
17
|
+
}
|
|
18
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Google Play Console MCP Server."""
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Google Play API clients for the Android Publisher API and Reporting API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import google.auth
|
|
8
|
+
from google.auth.transport.requests import AuthorizedSession
|
|
9
|
+
from google.oauth2 import service_account
|
|
10
|
+
from googleapiclient.discovery import build
|
|
11
|
+
|
|
12
|
+
PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher"
|
|
13
|
+
REPORTING_SCOPE = "https://www.googleapis.com/auth/playdeveloperreporting"
|
|
14
|
+
REPORTING_BASE_URL = "https://playdeveloperreporting.googleapis.com/v1beta1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_credentials(
|
|
18
|
+
scopes: List[str], credentials_file: Optional[str] = None
|
|
19
|
+
):
|
|
20
|
+
"""Return Google credentials from a service account file or ADC."""
|
|
21
|
+
creds_path = credentials_file or os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
|
|
22
|
+
if creds_path:
|
|
23
|
+
return service_account.Credentials.from_service_account_file(
|
|
24
|
+
creds_path, scopes=scopes
|
|
25
|
+
)
|
|
26
|
+
creds, _ = google.auth.default(scopes=scopes)
|
|
27
|
+
return creds
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PublisherClient:
|
|
31
|
+
"""Wraps the Google Play Android Publisher API v3 (edits + tracks)."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, credentials_file: Optional[str] = None) -> None:
|
|
34
|
+
self.creds = _get_credentials([PUBLISHER_SCOPE], credentials_file)
|
|
35
|
+
self.service = build("androidpublisher", "v3", credentials=self.creds)
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
# Low-level edit helpers
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def _create_edit(self, package_name: str) -> str:
|
|
42
|
+
result = self.service.edits().insert(
|
|
43
|
+
packageName=package_name, body={}
|
|
44
|
+
).execute()
|
|
45
|
+
return result["id"]
|
|
46
|
+
|
|
47
|
+
def _commit_edit(self, package_name: str, edit_id: str) -> Dict[str, Any]:
|
|
48
|
+
return self.service.edits().commit(
|
|
49
|
+
packageName=package_name, editId=edit_id
|
|
50
|
+
).execute()
|
|
51
|
+
|
|
52
|
+
def _delete_edit(self, package_name: str, edit_id: str) -> None:
|
|
53
|
+
"""Best-effort cleanup for read-only edits."""
|
|
54
|
+
try:
|
|
55
|
+
self.service.edits().delete(
|
|
56
|
+
packageName=package_name, editId=edit_id
|
|
57
|
+
).execute()
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def _get_track(
|
|
62
|
+
self, package_name: str, edit_id: str, track: str
|
|
63
|
+
) -> Dict[str, Any]:
|
|
64
|
+
return self.service.edits().tracks().get(
|
|
65
|
+
packageName=package_name, editId=edit_id, track=track
|
|
66
|
+
).execute()
|
|
67
|
+
|
|
68
|
+
def _update_track(
|
|
69
|
+
self,
|
|
70
|
+
package_name: str,
|
|
71
|
+
edit_id: str,
|
|
72
|
+
track: str,
|
|
73
|
+
body: Dict[str, Any],
|
|
74
|
+
) -> Dict[str, Any]:
|
|
75
|
+
return self.service.edits().tracks().update(
|
|
76
|
+
packageName=package_name, editId=edit_id, track=track, body=body
|
|
77
|
+
).execute()
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
# Public API
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def get_track(self, package_name: str, track: str) -> Dict[str, Any]:
|
|
84
|
+
"""Read-only fetch of a track. Creates and deletes a temporary edit."""
|
|
85
|
+
edit_id = self._create_edit(package_name)
|
|
86
|
+
try:
|
|
87
|
+
return self._get_track(package_name, edit_id, track)
|
|
88
|
+
finally:
|
|
89
|
+
self._delete_edit(package_name, edit_id)
|
|
90
|
+
|
|
91
|
+
def promote_to_production(
|
|
92
|
+
self,
|
|
93
|
+
package_name: str,
|
|
94
|
+
version_codes: List[int],
|
|
95
|
+
rollout_percentage: float,
|
|
96
|
+
release_name: Optional[str] = None,
|
|
97
|
+
release_notes: Optional[List[Dict[str, str]]] = None,
|
|
98
|
+
) -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Promote the given version codes to the production track.
|
|
101
|
+
|
|
102
|
+
rollout_percentage: 0 < value <= 100
|
|
103
|
+
"""
|
|
104
|
+
if not (0 < rollout_percentage <= 100):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"rollout_percentage must be > 0 and <= 100."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
release: Dict[str, Any] = {
|
|
110
|
+
"versionCodes": [str(vc) for vc in version_codes],
|
|
111
|
+
"status": "completed" if rollout_percentage == 100 else "inProgress",
|
|
112
|
+
}
|
|
113
|
+
if rollout_percentage < 100:
|
|
114
|
+
release["userFraction"] = round(rollout_percentage / 100.0, 4)
|
|
115
|
+
if release_name:
|
|
116
|
+
release["name"] = release_name
|
|
117
|
+
if release_notes:
|
|
118
|
+
release["releaseNotes"] = release_notes
|
|
119
|
+
|
|
120
|
+
edit_id = self._create_edit(package_name)
|
|
121
|
+
try:
|
|
122
|
+
track_body = {"track": "production", "releases": [release]}
|
|
123
|
+
updated_track = self._update_track(
|
|
124
|
+
package_name, edit_id, "production", track_body
|
|
125
|
+
)
|
|
126
|
+
commit = self._commit_edit(package_name, edit_id)
|
|
127
|
+
return {"track": updated_track, "commit": commit}
|
|
128
|
+
except Exception:
|
|
129
|
+
self._delete_edit(package_name, edit_id)
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
def update_rollout_percentage(
|
|
133
|
+
self,
|
|
134
|
+
package_name: str,
|
|
135
|
+
rollout_percentage: float,
|
|
136
|
+
version_codes: Optional[List[int]] = None,
|
|
137
|
+
) -> Dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Update the rollout percentage of an existing production release.
|
|
140
|
+
|
|
141
|
+
Targets the first release whose status is inProgress or halted.
|
|
142
|
+
If version_codes is provided, only a release containing those codes
|
|
143
|
+
will be updated.
|
|
144
|
+
"""
|
|
145
|
+
if not (0 < rollout_percentage <= 100):
|
|
146
|
+
raise ValueError(
|
|
147
|
+
"rollout_percentage must be > 0 and <= 100."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
edit_id = self._create_edit(package_name)
|
|
151
|
+
try:
|
|
152
|
+
track_data = self._get_track(package_name, edit_id, "production")
|
|
153
|
+
releases: List[Dict[str, Any]] = track_data.get("releases", [])
|
|
154
|
+
|
|
155
|
+
target_vcs = (
|
|
156
|
+
{str(vc) for vc in version_codes} if version_codes else None
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
updated = False
|
|
160
|
+
for release in releases:
|
|
161
|
+
if release.get("status") not in ("inProgress", "halted", "completed"):
|
|
162
|
+
continue
|
|
163
|
+
if target_vcs:
|
|
164
|
+
existing_vcs = set(release.get("versionCodes", []))
|
|
165
|
+
if not existing_vcs.intersection(target_vcs):
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
if rollout_percentage >= 100:
|
|
169
|
+
release["status"] = "completed"
|
|
170
|
+
release.pop("userFraction", None)
|
|
171
|
+
else:
|
|
172
|
+
release["status"] = "inProgress"
|
|
173
|
+
release["userFraction"] = round(rollout_percentage / 100.0, 4)
|
|
174
|
+
updated = True
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
if not updated:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
"No matching inProgress/halted/completed release found "
|
|
180
|
+
"in the production track."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
track_body = {"track": "production", "releases": releases}
|
|
184
|
+
updated_track = self._update_track(
|
|
185
|
+
package_name, edit_id, "production", track_body
|
|
186
|
+
)
|
|
187
|
+
commit = self._commit_edit(package_name, edit_id)
|
|
188
|
+
return {"track": updated_track, "commit": commit}
|
|
189
|
+
except Exception:
|
|
190
|
+
self._delete_edit(package_name, edit_id)
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ReportingClient:
|
|
195
|
+
"""Wraps the Google Play Developer Reporting API v1beta1."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, credentials_file: Optional[str] = None) -> None:
|
|
198
|
+
self.creds = _get_credentials([REPORTING_SCOPE], credentials_file)
|
|
199
|
+
self.session = AuthorizedSession(self.creds)
|
|
200
|
+
|
|
201
|
+
def _app_path(self, package_name: str) -> str:
|
|
202
|
+
return f"apps/{package_name}"
|
|
203
|
+
|
|
204
|
+
def query_crash_rate(
|
|
205
|
+
self,
|
|
206
|
+
package_name: str,
|
|
207
|
+
days: int = 7,
|
|
208
|
+
version_code: Optional[str] = None,
|
|
209
|
+
) -> Dict[str, Any]:
|
|
210
|
+
"""
|
|
211
|
+
Query crash-rate metrics for the app.
|
|
212
|
+
|
|
213
|
+
Returns crashRate, userPerceivedCrashRate, and distinctUsers
|
|
214
|
+
aggregated daily, broken down by versionCode.
|
|
215
|
+
"""
|
|
216
|
+
app = self._app_path(package_name)
|
|
217
|
+
url = f"{REPORTING_BASE_URL}/{app}/crashRateMetricSet:query"
|
|
218
|
+
|
|
219
|
+
# API data lags by 1 day; using today as endTime returns 400
|
|
220
|
+
end = datetime.now(timezone.utc) - timedelta(days=1)
|
|
221
|
+
start = end - timedelta(days=days)
|
|
222
|
+
|
|
223
|
+
def _date(dt: datetime) -> Dict[str, int]:
|
|
224
|
+
return {"year": dt.year, "month": dt.month, "day": dt.day}
|
|
225
|
+
|
|
226
|
+
body: Dict[str, Any] = {
|
|
227
|
+
"timelineSpec": {
|
|
228
|
+
"aggregationPeriod": "DAILY",
|
|
229
|
+
"startTime": _date(start),
|
|
230
|
+
"endTime": _date(end),
|
|
231
|
+
},
|
|
232
|
+
"metrics": [
|
|
233
|
+
"crashRate",
|
|
234
|
+
"userPerceivedCrashRate",
|
|
235
|
+
"distinctUsers",
|
|
236
|
+
],
|
|
237
|
+
"dimensions": ["versionCode"],
|
|
238
|
+
"pageSize": days * 10,
|
|
239
|
+
}
|
|
240
|
+
if version_code:
|
|
241
|
+
body["filter"] = f'versionCode = "{version_code}"'
|
|
242
|
+
|
|
243
|
+
resp = self.session.post(url, json=body)
|
|
244
|
+
resp.raise_for_status()
|
|
245
|
+
return resp.json()
|
|
246
|
+
|
|
247
|
+
def list_available_metrics(self, package_name: str) -> Dict[str, Any]:
|
|
248
|
+
"""List all available metric sets for the app (for debugging)."""
|
|
249
|
+
app = self._app_path(package_name)
|
|
250
|
+
url = f"{REPORTING_BASE_URL}/{app}/crashRateMetricSet"
|
|
251
|
+
resp = self.session.get(url)
|
|
252
|
+
resp.raise_for_status()
|
|
253
|
+
return resp.json()
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Google Play Console MCP Server.
|
|
2
|
+
|
|
3
|
+
Exposes Google Play Console operations as MCP tools so AI assistants
|
|
4
|
+
(Claude, etc.) can manage production releases programmatically.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from dotenv import load_dotenv
|
|
12
|
+
from mcp.server.fastmcp import FastMCP
|
|
13
|
+
|
|
14
|
+
from .client import PublisherClient, ReportingClient
|
|
15
|
+
|
|
16
|
+
load_dotenv()
|
|
17
|
+
|
|
18
|
+
mcp = FastMCP(
|
|
19
|
+
"Google Play Console MCP",
|
|
20
|
+
instructions=(
|
|
21
|
+
"Tools for managing Google Play Store production releases: "
|
|
22
|
+
"promoting builds, adjusting rollout percentages, checking review "
|
|
23
|
+
"status, and fetching crash-rate vitals."
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_CREDS = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _publisher() -> PublisherClient:
|
|
31
|
+
return PublisherClient(_CREDS)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _reporting() -> ReportingClient:
|
|
35
|
+
return ReportingClient(_CREDS)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Helpers
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def _format_release(release: dict) -> dict:
|
|
43
|
+
"""Return a clean summary dict for a single release."""
|
|
44
|
+
fraction = release.get("userFraction")
|
|
45
|
+
rollout_pct = round(fraction * 100, 2) if fraction is not None else (
|
|
46
|
+
100.0 if release.get("status") == "completed" else None
|
|
47
|
+
)
|
|
48
|
+
return {
|
|
49
|
+
"name": release.get("name"),
|
|
50
|
+
"versionCodes": release.get("versionCodes", []),
|
|
51
|
+
"status": release.get("status"),
|
|
52
|
+
"rolloutPercentage": rollout_pct,
|
|
53
|
+
"releaseNotes": release.get("releaseNotes", []),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _format_track(track_data: dict) -> dict:
|
|
58
|
+
return {
|
|
59
|
+
"track": track_data.get("track"),
|
|
60
|
+
"releases": [_format_release(r) for r in track_data.get("releases", [])],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Tool: promote_to_production
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
def promote_to_production(
|
|
70
|
+
package_name: str,
|
|
71
|
+
version_codes: list[int],
|
|
72
|
+
rollout_percentage: float,
|
|
73
|
+
release_name: str = "",
|
|
74
|
+
release_notes_en: str = "",
|
|
75
|
+
) -> str:
|
|
76
|
+
"""Promote an app build from the internal track to production with a
|
|
77
|
+
staged rollout.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
package_name: App package name, e.g. com.example.myapp
|
|
81
|
+
version_codes: List of version codes to promote (e.g. [1234]).
|
|
82
|
+
rollout_percentage: Initial rollout percentage, 0 < value <= 100.
|
|
83
|
+
Use 100 for an immediate full release.
|
|
84
|
+
release_name: Optional human-readable release name.
|
|
85
|
+
release_notes_en: Optional English release notes (plain text).
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
notes = (
|
|
89
|
+
[{"language": "en-US", "text": release_notes_en}]
|
|
90
|
+
if release_notes_en
|
|
91
|
+
else None
|
|
92
|
+
)
|
|
93
|
+
result = _publisher().promote_to_production(
|
|
94
|
+
package_name=package_name,
|
|
95
|
+
version_codes=version_codes,
|
|
96
|
+
rollout_percentage=rollout_percentage,
|
|
97
|
+
release_name=release_name or None,
|
|
98
|
+
release_notes=notes,
|
|
99
|
+
)
|
|
100
|
+
track_summary = _format_track(result["track"])
|
|
101
|
+
commit = result.get("commit", {})
|
|
102
|
+
return json.dumps(
|
|
103
|
+
{
|
|
104
|
+
"success": True,
|
|
105
|
+
"message": (
|
|
106
|
+
f"Version codes {version_codes} promoted to production "
|
|
107
|
+
f"at {rollout_percentage}% rollout."
|
|
108
|
+
),
|
|
109
|
+
"track": track_summary,
|
|
110
|
+
"editId": commit.get("editId"),
|
|
111
|
+
},
|
|
112
|
+
indent=2,
|
|
113
|
+
)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
return json.dumps({"success": False, "error": str(exc)}, indent=2)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Tool: check_review_status
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
@mcp.tool()
|
|
123
|
+
def check_review_status(package_name: str) -> str:
|
|
124
|
+
"""Check the current production release status for an app.
|
|
125
|
+
|
|
126
|
+
Returns all production releases with their status (draft, inProgress,
|
|
127
|
+
halted, completed) and rollout percentages. A release in 'inProgress'
|
|
128
|
+
state indicates it is either under review or actively rolling out.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
package_name: App package name, e.g. com.example.myapp
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
track_data = _publisher().get_track(package_name, "production")
|
|
135
|
+
releases = track_data.get("releases", [])
|
|
136
|
+
|
|
137
|
+
if not releases:
|
|
138
|
+
return json.dumps(
|
|
139
|
+
{
|
|
140
|
+
"packageName": package_name,
|
|
141
|
+
"message": "No releases found in the production track.",
|
|
142
|
+
"releases": [],
|
|
143
|
+
},
|
|
144
|
+
indent=2,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
formatted = [_format_release(r) for r in releases]
|
|
148
|
+
|
|
149
|
+
# Derive a human-readable overall status
|
|
150
|
+
statuses = {r["status"] for r in formatted if r["status"]}
|
|
151
|
+
if "inProgress" in statuses:
|
|
152
|
+
summary = "Staged rollout in progress."
|
|
153
|
+
elif "draft" in statuses:
|
|
154
|
+
summary = "Release is in draft / under Google Play review."
|
|
155
|
+
elif "halted" in statuses:
|
|
156
|
+
summary = "Rollout is halted."
|
|
157
|
+
elif statuses == {"completed"}:
|
|
158
|
+
summary = "Release fully rolled out (100%)."
|
|
159
|
+
else:
|
|
160
|
+
summary = f"Status: {', '.join(statuses)}"
|
|
161
|
+
|
|
162
|
+
return json.dumps(
|
|
163
|
+
{
|
|
164
|
+
"packageName": package_name,
|
|
165
|
+
"summary": summary,
|
|
166
|
+
"releases": formatted,
|
|
167
|
+
},
|
|
168
|
+
indent=2,
|
|
169
|
+
)
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
return json.dumps({"success": False, "error": str(exc)}, indent=2)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Tool: update_rollout_percentage
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
@mcp.tool()
|
|
179
|
+
def update_rollout_percentage(
|
|
180
|
+
package_name: str,
|
|
181
|
+
rollout_percentage: float,
|
|
182
|
+
version_codes: list[int] | None = None,
|
|
183
|
+
) -> str:
|
|
184
|
+
"""Update the rollout percentage for the current production release.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
package_name: App package name, e.g. com.example.myapp
|
|
188
|
+
rollout_percentage: New rollout percentage, 0 < value <= 100.
|
|
189
|
+
Pass 100 to complete the rollout for all users.
|
|
190
|
+
version_codes: Optional list of version codes to target. If omitted,
|
|
191
|
+
the most recent inProgress or halted release is updated.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
result = _publisher().update_rollout_percentage(
|
|
195
|
+
package_name=package_name,
|
|
196
|
+
rollout_percentage=rollout_percentage,
|
|
197
|
+
version_codes=version_codes,
|
|
198
|
+
)
|
|
199
|
+
track_summary = _format_track(result["track"])
|
|
200
|
+
commit = result.get("commit", {})
|
|
201
|
+
return json.dumps(
|
|
202
|
+
{
|
|
203
|
+
"success": True,
|
|
204
|
+
"message": f"Rollout updated to {rollout_percentage}%.",
|
|
205
|
+
"track": track_summary,
|
|
206
|
+
"editId": commit.get("editId"),
|
|
207
|
+
},
|
|
208
|
+
indent=2,
|
|
209
|
+
)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
return json.dumps({"success": False, "error": str(exc)}, indent=2)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Tool: get_production_release_info
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
@mcp.tool()
|
|
219
|
+
def get_production_release_info(package_name: str) -> str:
|
|
220
|
+
"""Fetch the current production release info: rollout percentage,
|
|
221
|
+
version codes, status, and release notes.
|
|
222
|
+
|
|
223
|
+
Note: Detailed install / update event analytics (raw install counts,
|
|
224
|
+
update events) are not exposed by the public Google Play Developer API.
|
|
225
|
+
They are available in Google Play Console UI, BigQuery data exports, or
|
|
226
|
+
Firebase Analytics if integrated with your app.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
package_name: App package name, e.g. com.example.myapp
|
|
230
|
+
"""
|
|
231
|
+
try:
|
|
232
|
+
track_data = _publisher().get_track(package_name, "production")
|
|
233
|
+
releases = track_data.get("releases", [])
|
|
234
|
+
|
|
235
|
+
formatted = [_format_release(r) for r in releases]
|
|
236
|
+
|
|
237
|
+
# Find the active / latest release
|
|
238
|
+
active = next(
|
|
239
|
+
(r for r in formatted if r["status"] in ("inProgress", "completed")),
|
|
240
|
+
formatted[0] if formatted else None,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return json.dumps(
|
|
244
|
+
{
|
|
245
|
+
"packageName": package_name,
|
|
246
|
+
"activeRelease": active,
|
|
247
|
+
"allReleases": formatted,
|
|
248
|
+
"note": (
|
|
249
|
+
"Install & Update event counts are not available via the "
|
|
250
|
+
"public API. Use Play Console > Statistics or set up "
|
|
251
|
+
"BigQuery exports for detailed analytics."
|
|
252
|
+
),
|
|
253
|
+
},
|
|
254
|
+
indent=2,
|
|
255
|
+
)
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
return json.dumps({"success": False, "error": str(exc)}, indent=2)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Tool: get_crash_rate
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
@mcp.tool()
|
|
265
|
+
def get_crash_rate(
|
|
266
|
+
package_name: str,
|
|
267
|
+
days: int = 7,
|
|
268
|
+
version_code: str = "",
|
|
269
|
+
) -> str:
|
|
270
|
+
"""Fetch user-perceived crash rate for the production release.
|
|
271
|
+
|
|
272
|
+
Uses the Google Play Developer Reporting API v1beta1.
|
|
273
|
+
Returns daily crashRate, userPerceivedCrashRate, and distinctUsers
|
|
274
|
+
broken down by version code for the requested period.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
package_name: App package name, e.g. com.example.myapp
|
|
278
|
+
days: Number of past days to include (default 7, max 30).
|
|
279
|
+
version_code: Optional single version code string to filter results.
|
|
280
|
+
"""
|
|
281
|
+
days = max(1, min(days, 30))
|
|
282
|
+
try:
|
|
283
|
+
raw = _reporting().query_crash_rate(
|
|
284
|
+
package_name=package_name,
|
|
285
|
+
days=days,
|
|
286
|
+
version_code=version_code or None,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
rows = raw.get("rows", [])
|
|
290
|
+
if not rows:
|
|
291
|
+
return json.dumps(
|
|
292
|
+
{
|
|
293
|
+
"packageName": package_name,
|
|
294
|
+
"message": (
|
|
295
|
+
"No crash data found. Data may not be available yet "
|
|
296
|
+
"or the app has no crashes in this period."
|
|
297
|
+
),
|
|
298
|
+
"rows": [],
|
|
299
|
+
},
|
|
300
|
+
indent=2,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Parse rows into a readable format
|
|
304
|
+
parsed_rows = []
|
|
305
|
+
for row in rows:
|
|
306
|
+
dims = {d.get("dimension"): d.get("stringValue") or d.get("int64Value")
|
|
307
|
+
for d in row.get("dimensions", [])}
|
|
308
|
+
metrics = {}
|
|
309
|
+
for m in row.get("metrics", []):
|
|
310
|
+
name = m.get("metric")
|
|
311
|
+
val = m.get("decimalValue") or m.get("int64Value")
|
|
312
|
+
if val is not None:
|
|
313
|
+
# decimalValue comes as a string like "0.0012"
|
|
314
|
+
try:
|
|
315
|
+
val = float(val)
|
|
316
|
+
except (TypeError, ValueError):
|
|
317
|
+
pass
|
|
318
|
+
metrics[name] = val
|
|
319
|
+
|
|
320
|
+
date_info = row.get("startTime", {})
|
|
321
|
+
parsed_rows.append(
|
|
322
|
+
{
|
|
323
|
+
"date": date_info,
|
|
324
|
+
"versionCode": dims.get("versionCode"),
|
|
325
|
+
"crashRate": metrics.get("crashRate"),
|
|
326
|
+
"userPerceivedCrashRate": metrics.get("userPerceivedCrashRate"),
|
|
327
|
+
"distinctUsers": metrics.get("distinctUsers"),
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return json.dumps(
|
|
332
|
+
{
|
|
333
|
+
"packageName": package_name,
|
|
334
|
+
"periodDays": days,
|
|
335
|
+
"totalRows": len(parsed_rows),
|
|
336
|
+
"rows": parsed_rows,
|
|
337
|
+
},
|
|
338
|
+
indent=2,
|
|
339
|
+
)
|
|
340
|
+
except Exception as exc:
|
|
341
|
+
return json.dumps({"success": False, "error": str(exc)}, indent=2)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# Entry point
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
def main() -> None:
|
|
349
|
+
import argparse
|
|
350
|
+
|
|
351
|
+
parser = argparse.ArgumentParser(description="Google Play Console MCP Server")
|
|
352
|
+
parser.add_argument(
|
|
353
|
+
"--transport",
|
|
354
|
+
choices=["stdio", "http"],
|
|
355
|
+
default="stdio",
|
|
356
|
+
help="Transport mode (default: stdio)",
|
|
357
|
+
)
|
|
358
|
+
parser.add_argument(
|
|
359
|
+
"--port",
|
|
360
|
+
type=int,
|
|
361
|
+
default=8080,
|
|
362
|
+
help="Port for HTTP transport (default: 8080)",
|
|
363
|
+
)
|
|
364
|
+
args = parser.parse_args()
|
|
365
|
+
|
|
366
|
+
if args.transport == "http":
|
|
367
|
+
mcp.run(transport="streamable-http", host="0.0.0.0", port=args.port)
|
|
368
|
+
else:
|
|
369
|
+
mcp.run()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if __name__ == "__main__":
|
|
373
|
+
main()
|