@yoonion/mimi-seed-mcp 0.3.27 → 0.3.28

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.
package/LICENSE CHANGED
@@ -1,75 +1,75 @@
1
- # PolyForm Noncommercial License 1.0.0
2
-
3
- <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
-
5
- Required Notice: Copyright 2026 Pryzm GG (https://mimi-seed.pryzm.gg)
6
-
7
- ## Acceptance
8
-
9
- In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
-
11
- ## Copyright License
12
-
13
- The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
-
15
- ## Distribution License
16
-
17
- The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
-
19
- ## Notices
20
-
21
- You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
-
23
- > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
-
25
- ## Changes and New Works License
26
-
27
- The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
-
29
- ## Patent License
30
-
31
- The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
-
33
- ## Noncommercial Purposes
34
-
35
- Any noncommercial purpose is a permitted purpose.
36
-
37
- ## Personal Uses
38
-
39
- Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
-
41
- ## Noncommercial Organizations
42
-
43
- Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
-
45
- ## Fair Use
46
-
47
- You may have "fair use" rights for the software under the law. These terms do not limit them.
48
-
49
- ## No Other Rights
50
-
51
- These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
-
53
- ## Patent Defense
54
-
55
- If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
-
57
- ## Violations
58
-
59
- The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
-
61
- ## No Liability
62
-
63
- ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
-
65
- ## Definitions
66
-
67
- The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
-
69
- **You** refers to the individual or entity agreeing to these terms.
70
-
71
- **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
-
73
- **Your licenses** are all the licenses granted to you for the software under these terms.
74
-
75
- **Use** means anything you do with the software requiring one of your licenses.
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ Required Notice: Copyright 2026 Pryzm GG (https://mimi-seed.pryzm.gg)
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
package/README.md CHANGED
@@ -1,192 +1,235 @@
1
- # @yoonion/mimi-seed-mcp
2
-
3
- **Mimi Seed** — Firebase · AdMob · Google Play · App Store Connect를 AI 콘솔에서 관리. Claude Code / Cursor / 기타 MCP 클라이언트에서 한 줄 등록으로 사용.
4
-
5
- > 이 패키지는 Mimi Seed의 **로컬 MCP 서버**만 포함합니다. 웹 콘솔(Next.js 앱)은 <https://mimi-seed.pryzm.gg>.
6
-
7
- ## 설치
8
-
1
+ # @yoonion/mimi-seed-mcp
2
+
3
+ **Mimi Seed** — Firebase · AdMob · Google Play · App Store Connect를 AI 콘솔에서 관리. Claude Code / Codex / Cursor / 기타 MCP 클라이언트에서 한 줄 등록으로 사용.
4
+
5
+ > 이 패키지는 Mimi Seed의 **로컬 MCP 서버**만 포함합니다. 웹 콘솔(Next.js 앱)은 <https://mimi-seed.pryzm.gg/tool>.
6
+
7
+ ## 설치
8
+
9
9
  Claude Code:
10
10
 
11
11
  ```bash
12
12
  claude mcp add mimi-seed -- npx -y @yoonion/mimi-seed-mcp
13
13
  ```
14
14
 
15
- Claude Desktop (`claude_desktop_config.json`):
16
-
17
- ```json
18
- {
19
- "mcpServers": {
20
- "mimi-seed": {
21
- "command": "npx",
22
- "args": ["-y", "@yoonion/mimi-seed-mcp"]
23
- }
24
- }
25
- }
26
- ```
27
-
28
- ## 첫 사용 전 인증
15
+ Codex (`~/.codex/config.toml`):
29
16
 
30
- ```bash
31
- npx -y @yoonion/mimi-seed-mcp mimi-seed-auth
17
+ ```toml
18
+ [mcp_servers.mimi-seed]
19
+ command = "npx"
20
+ args = ["-y", "@yoonion/mimi-seed-mcp"]
21
+ enabled = true
32
22
  ```
33
-
34
- 브라우저가 열리면 Google 계정으로 로그인. 토큰은 `~/.mimi-seed/tokens.json`에 저장되고 자동 갱신됨.
35
-
36
- App Store Connect까지 쓰려면 별도로:
37
-
38
- ```bash
39
- npx -y @yoonion/mimi-seed-mcp mimi-seed-appstore-auth
40
- ```
41
-
42
- (App Store Connect → Users and Access → Keys에서 API Key 생성 후 Issuer ID / Key ID / .p8 경로 입력)
43
-
44
- AI 기능(릴리즈 노트 생성, 리뷰 답변)을 쓰려면:
45
-
46
- ```bash
47
- export ANTHROPIC_API_KEY=sk-ant-...
48
- ```
49
-
50
- ---
51
-
52
- ## 제공 도구 (65+개)
53
-
54
- | 영역 | 주요 도구 |
55
- |------|---------|
56
- | Firebase | `firebase_list_projects` / `firebase_create_android_app` / `firebase_get_android_config` / `firebase_enable_service` |
57
- | AdMob | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
58
- | Google Play | `playstore_list_tracks` / `playstore_update_listing` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
59
- | 인앱 결제 | `playstore_create_onetime_product` / `playstore_create_subscription` / `appstore_create_inapp_purchase` / `appstore_create_subscription` |
60
- | Google Cloud IAM | `iam_list_service_accounts` / `iam_create_service_account` / `iam_list_keys` / `iam_create_key` / `iam_add_iam_policy_binding` |
61
- | App Store Connect | `appstore_list_apps` / `appstore_list_builds` / `appstore_upload_screenshot` / `appstore_update_whats_new` |
62
- | 제출 위험 점검 | `playstore_check_submission_risks` / `appstore_check_submission_risks` |
63
- | 스크린샷 검증 | `screenshot_validate` |
64
- | AI (Claude) | `generate_release_notes_from_commits` / `generate_review_reply` |
65
- | 인증 | `mimi_seed_auth_status` / `mimi_seed_auth_start` |
66
-
67
- ---
68
-
69
- ## 주요 기능
70
-
71
- ### 제출 위험 점검
72
-
73
- 출시 전 블로커와 경고를 자동으로 점검합니다.
74
-
75
- ```
76
- "내 앱 출시 전 위험 요소 확인해줘"
77
- playstore_check_submission_risks("com.example.myapp")
78
- → appstore_check_submission_risks("1234567890")
79
- ```
80
-
81
- **점검 항목:**
82
- - Google Play: 리스팅 완성도(제목/설명/짧은설명), 스크린샷 수, 아이콘, 내부 빌드 존재, 연락처
83
- - App Store: What's New, 설명/키워드, 스크린샷 커버리지, TestFlight 빌드, 개인정보처리방침 URL
84
-
85
- ---
86
-
87
- ### 스크린샷 해상도 검증
88
-
89
- 업로드 로컬 파일을 스토어 규격과 비교합니다.
90
-
91
- ```
92
- "이 스크린샷들이 App Store 규격에 맞는지 확인해줘"
93
- → screenshot_validate(["/path/to/screen1.png", "/path/to/screen2.png"], platform="ios", displayType="APP_IPHONE_69")
94
- ```
95
-
96
- iOS displayType 예시: `APP_IPHONE_69`, `APP_IPHONE_67`, `APP_IPHONE_65`, `APP_IPAD_PRO_3GEN_129`
97
- Android imageType 예시: `phoneScreenshots`, `sevenInchScreenshots`, `featureGraphic`
98
-
99
- ---
100
-
101
- ### AI 릴리즈 노트 생성 (ANTHROPIC_API_KEY 필요)
102
-
103
- git 커밋 내역을 Claude가 사용자 친화적인 릴리즈 노트로 변환합니다.
104
-
105
- ```
106
- "최근 커밋으로 릴리즈 노트 만들어줘"
107
- → generate_release_notes_from_commits(
108
- commits=[{message: "feat: 다크모드 추가"}, ...],
109
- appName="MyApp",
110
- locales=["ko", "en-US", "ja"]
111
- )
112
- ```
113
-
114
- 3가지 톤(간결/상세/마케팅) + 다국어를 한 번에 생성. 이후 `playstore_update_release_notes` 또는 `appstore_update_whats_new`로 바로 적용.
115
-
116
- ---
117
-
118
- ### AI 리뷰 답변 생성 (ANTHROPIC_API_KEY 필요)
119
-
120
- 스토어 리뷰에 대한 AI 답변 초안을 생성합니다.
121
-
122
- ```
123
- "이 리뷰에 답변 작성해줘"
124
- generate_review_reply(
125
- reviewText="앱이 자꾸 튕겨요",
126
- rating=2,
127
- appName="MyApp",
128
- tone="empathetic",
129
- language="ko"
130
- )
131
- ```
132
-
133
- tone 옵션: `friendly`(친근) / `professional`(정중) / `empathetic`(공감) / `brief`(간결)
134
-
135
- > AI 생성 답변은 초안입니다. 게시 전 반드시 검토하세요.
136
- > 답변 게시는 `playstore_reply_to_review` 도구를 사용하세요.
137
-
138
- ---
139
-
140
- ## End-to-end: 서비스 계정 → JSON 키 → Play Console 권한
141
-
142
- 서버(예: [onesub](https://github.com/jeonghwanko/onesub))가 Google Play 영수증을 백그라운드로 검증하려면 서비스 계정 JSON이 필요합니다. Claude에게 한 번에 시킬 수 있어요:
143
-
144
- > 1. `my-project`에 `onesub-play-verifier` 서비스 계정 만들고
145
- > 2. JSON 키 발급받아서
146
- > 3. `com.yourapp.id`에 대해 검증
147
-
148
- Claude가 연쇄 호출:
149
-
150
- - `iam_create_service_account("my-project", "onesub-play-verifier", "onesub Play verifier")`
151
- - `iam_create_key("onesub-play-verifier@my-project.iam.gserviceaccount.com")` → JSON 반환
152
- - `playstore_verify_service_account(<json>, "com.yourapp.id")` 아직 Play Console 권한이 없어서 403 반환 기대
153
-
154
- 다음 **Play Console에서 수동으로** (또는 별도 androidpublisher.users API 호출):
155
-
156
- - Users and permissions → 서비스 계정 이메일 초대
157
- - **View financial data, orders, and cancellation survey responses** 체크
158
- - **~5분 대기** 후 `playstore_verify_service_account` 재실행 → ✓
159
-
160
- 마지막으로 JSON을 `GOOGLE_SERVICE_ACCOUNT_KEY` 서버 env에 넣으면 Play 영수증 검증 가능.
161
-
162
- > Cloud IAM 역할과 Play Console 권한은 다릅니다. `iam_add_iam_policy_binding`은 Cloud IAM 역할(예: `roles/iam.serviceAccountTokenCreator`)만 부여 — Play Console의 "View financial data"는 별도.
163
-
164
- ---
165
-
166
- ## 환경변수
167
-
168
- | 변수 | 설명 |
169
- |------|------|
170
- | `ANTHROPIC_API_KEY` | AI 릴리즈 노트 생성 / 리뷰 답변 활성화 (선택) |
171
-
172
- ---
173
-
174
- ## 레거시 호환성
175
-
176
- Preseed 시절(`~/.preseed/`) 데이터는 자동으로 이어받음:
177
-
178
- - `~/.preseed/tokens.json` 있으면 읽음 (재인증 불필요)
179
- - `~/.preseed/appstore.json`도 동일
180
- - 환경변수 `PRESEED_GOOGLE_CLIENT_ID` / `PRESEED_GOOGLE_CLIENT_SECRET` 계속 인식
181
-
182
- 새로 쓰는 `~/.mimi-seed/`.
183
-
184
- ---
185
-
186
- ## Links
187
-
188
- - CLI 패키지: [`mimi-seed`](https://www.npmjs.com/package/mimi-seed)
189
- - 웹 콘솔: https://mimi-seed.pryzm.gg
190
- - 저장소: https://github.com/jeonghwanko/mimi-seed
191
-
192
- MIT © jeonghwanko
23
+
24
+ Claude Desktop (`claude_desktop_config.json`):
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "mimi-seed": {
30
+ "command": "npx",
31
+ "args": ["-y", "@yoonion/mimi-seed-mcp"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ ## 첫 사용 전 인증
38
+
39
+ ```bash
40
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-auth
41
+ ```
42
+
43
+ 브라우저가 열리면 Google 계정으로 로그인. 토큰은 `~/.mimi-seed/tokens.json`에 저장되고 자동 갱신됨.
44
+
45
+ 플랫폼별 추가 인증 (필요한 것만):
46
+
47
+ ```bash
48
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-appstore-auth # App Store Connect (API Key)
49
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-playstore-auth # Google Play 서비스 계정 JSON
50
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-bigquery-auth # BigQuery (Crashlytics export 등)
51
+ ```
52
+
53
+ - **App Store Connect**: Users and Access → Keys에서 API Key 생성 후 Issuer ID / Key ID / .p8 경로 입력 → `~/.mimi-seed/appstore.json`
54
+ - **Google Play**: Play Console 서비스 계정 JSON 경로 입력 (패키지별 등록)
55
+ - **BigQuery**: Google OAuth 또는 서비스 계정으로 데이터셋 조회 권한 부여
56
+
57
+ AI 기능(릴리즈 노트 생성, 리뷰 답변)을 쓰려면:
58
+
59
+ ```bash
60
+ export ANTHROPIC_API_KEY=sk-ant-...
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 제공 도구 (110+개)
66
+
67
+ | 영역 | 도구 수 | 주요 도구 |
68
+ |------|---------|-----------|
69
+ | App Store Connect | 30 | `appstore_submit_for_review` / `appstore_upload_screenshot` / `appstore_update_whats_new` / `appstore_create_version` |
70
+ | Google Play | 26 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
71
+ | Firebase | 17 | `firebase_create_android_app` / `firebase_get_android_config` / `firebase_create_ios_app` / `firebase_enable_service` |
72
+ | AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
73
+ | CI/CD (GitHub Actions · GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
74
+ | Facebook | 6 | `facebook_post_photo` / `facebook_post_multi_photo` / `facebook_list_pages` |
75
+ | Google Cloud IAM | 5 | `iam_create_service_account` / `iam_create_key` / `iam_add_iam_policy_binding` |
76
+ | BigQuery | 5 | `bigquery_run_query` / `bigquery_list_datasets` / `bigquery_get_table_schema` |
77
+ | 점검 / 위험 | 4 | `playstore_check_submission_risks` / `appstore_check_submission_risks` / `screenshot_validate` / `release_status` |
78
+ | Instagram | 4 | `instagram_post_image` / `instagram_post_carousel` / `instagram_save_config` |
79
+ | AI (Claude) | 2 | `generate_release_notes_from_commits` / `generate_review_reply` |
80
+ | 인증 | 2 | `mimi_seed_auth_start` / `mimi_seed_auth_status` |
81
+
82
+ > 인앱 결제(IAP·구독) 도구는 위 Play Store·App Store 카운트에 포함됩니다 — `playstore_create_subscription` · `appstore_create_inapp_purchase` 등 (`@onesub/providers` 위임).
83
+
84
+ ---
85
+
86
+ ## 슬래시 커맨드 (MCP Prompts)
87
+
88
+ MCP 클라이언트(Claude Code, Codex 등)에서 슬래시 커맨드로 바로 노출됩니다.
89
+
90
+ | 커맨드 | 설명 |
91
+ |--------|------|
92
+ | `/mimi-seed:deploy` | 블로커 점검 → 릴리즈 노트 생성 → 스토어 적용을 한 번에 |
93
+ | `/mimi-seed:health` | 인증 상태 + 출시 준비도 빠른 요약 |
94
+ | `/mimi-seed:review-inbox` | 미답변 리뷰 조회 → AI 답변 초안 생성 |
95
+
96
+ ## MCP Resources
97
+
98
+ | URI | 설명 |
99
+ |-----|------|
100
+ | `mimi-seed://auth/status` | Google OAuth 토큰 상태 (JSON) |
101
+ | `mimi-seed://agent/guide` | 에이전트 역할 정의 — 출시 워크플로우·주의사항 (Markdown) |
102
+
103
+ ---
104
+
105
+ ## 주요 기능
106
+
107
+ ### 제출 위험 점검
108
+
109
+ 출시 전 블로커와 경고를 자동으로 점검합니다.
110
+
111
+ ```
112
+ "내 앱 출시 전 위험 요소 확인해줘"
113
+ playstore_check_submission_risks("com.example.myapp")
114
+ appstore_check_submission_risks("1234567890")
115
+ ```
116
+
117
+ **점검 항목:**
118
+ - Google Play: 리스팅 완성도(제목/설명/짧은설명), 스크린샷 수, 아이콘, 내부 빌드 존재, 연락처
119
+ - App Store: What's New, 설명/키워드, 스크린샷 커버리지, TestFlight 빌드, 개인정보처리방침 URL
120
+
121
+ ---
122
+
123
+ ### 스크린샷 해상도 검증
124
+
125
+ 업로드 로컬 파일을 스토어 규격과 비교합니다.
126
+
127
+ ```
128
+ "이 스크린샷들이 App Store 규격에 맞는지 확인해줘"
129
+ → screenshot_validate(["/path/to/screen1.png", "/path/to/screen2.png"], platform="ios", displayType="APP_IPHONE_69")
130
+ ```
131
+
132
+ iOS displayType 예시: `APP_IPHONE_69`, `APP_IPHONE_67`, `APP_IPHONE_65`, `APP_IPAD_PRO_3GEN_129`
133
+ Android imageType 예시: `phoneScreenshots`, `sevenInchScreenshots`, `featureGraphic`
134
+
135
+ ---
136
+
137
+ ### AI 릴리즈 노트 생성 (ANTHROPIC_API_KEY 필요)
138
+
139
+ git 커밋 내역을 Claude가 사용자 친화적인 릴리즈 노트로 변환합니다.
140
+
141
+ ```
142
+ "최근 커밋으로 릴리즈 노트 만들어줘"
143
+ → generate_release_notes_from_commits(
144
+ commits=[{message: "feat: 다크모드 추가"}, ...],
145
+ appName="MyApp",
146
+ locales=["ko", "en-US", "ja"]
147
+ )
148
+ ```
149
+
150
+ 3가지 톤(간결/상세/마케팅) + 다국어를 번에 생성. 이후 `playstore_update_release_notes` 또는 `appstore_update_whats_new`로 바로 적용.
151
+
152
+ ---
153
+
154
+ ### AI 리뷰 답변 생성 (ANTHROPIC_API_KEY 필요)
155
+
156
+ 스토어 리뷰에 대한 AI 답변 초안을 생성합니다.
157
+
158
+ ```
159
+ "이 리뷰에 답변 작성해줘"
160
+ generate_review_reply(
161
+ reviewText="앱이 자꾸 튕겨요",
162
+ rating=2,
163
+ appName="MyApp",
164
+ tone="empathetic",
165
+ language="ko"
166
+ )
167
+ ```
168
+
169
+ tone 옵션: `friendly`(친근) / `professional`(정중) / `empathetic`(공감) / `brief`(간결)
170
+
171
+ > ⚠ AI 생성 답변은 초안입니다. 게시 전 반드시 검토하세요.
172
+ > 답변 게시는 `playstore_reply_to_review` 도구를 사용하세요.
173
+
174
+ ---
175
+
176
+ ## End-to-end: 서비스 계정 → JSON 키 → Play Console 권한
177
+
178
+ 서버(예: [onesub](https://github.com/jeonghwanko/onesub))가 Google Play 영수증을 백그라운드로 검증하려면 서비스 계정 JSON이 필요합니다. Claude에게 한 번에 시킬 수 있어요:
179
+
180
+ > 1. `my-project`에 `onesub-play-verifier` 서비스 계정 만들고
181
+ > 2. JSON 키 발급받아서
182
+ > 3. `com.yourapp.id`에 대해 검증
183
+
184
+ Claude가 연쇄 호출:
185
+
186
+ - `iam_create_service_account("my-project", "onesub-play-verifier", "onesub Play verifier")`
187
+ - `iam_create_key("onesub-play-verifier@my-project.iam.gserviceaccount.com")` → JSON 반환
188
+ - `playstore_verify_service_account(<json>, "com.yourapp.id")` → 아직 Play Console 권한이 없어서 403 반환 기대
189
+
190
+ 그 다음 **Play Console에서 수동으로** (또는 별도 androidpublisher.users API 호출):
191
+
192
+ - Users and permissions → 서비스 계정 이메일 초대
193
+ - **View financial data, orders, and cancellation survey responses** 체크
194
+ - **~5분 대기** 후 `playstore_verify_service_account` 재실행 → ✓
195
+
196
+ 마지막으로 JSON을 `GOOGLE_SERVICE_ACCOUNT_KEY` 서버 env에 넣으면 Play 영수증 검증 가능.
197
+
198
+ > Cloud IAM 역할과 Play Console 권한은 다릅니다. `iam_add_iam_policy_binding`은 Cloud IAM 역할(예: `roles/iam.serviceAccountTokenCreator`)만 부여 — Play Console의 "View financial data"는 별도.
199
+
200
+ ---
201
+
202
+ ## 환경변수
203
+
204
+ | 변수 | 설명 |
205
+ |------|------|
206
+ | `ANTHROPIC_API_KEY` | AI 릴리즈 노트 생성 / 리뷰 답변 활성화 (선택) |
207
+
208
+ ---
209
+
210
+ ## 레거시 호환성
211
+
212
+ Preseed 시절(`~/.preseed/`) 데이터는 자동으로 이어받음:
213
+
214
+ - `~/.preseed/tokens.json` 있으면 읽음 (재인증 불필요)
215
+ - `~/.preseed/appstore.json`도 동일
216
+ - 환경변수 `PRESEED_GOOGLE_CLIENT_ID` / `PRESEED_GOOGLE_CLIENT_SECRET` 계속 인식
217
+
218
+ 새로 쓰는 건 `~/.mimi-seed/`.
219
+
220
+ ---
221
+
222
+ ## Links
223
+
224
+ - CLI 패키지: [`mimi-seed`](https://www.npmjs.com/package/mimi-seed)
225
+ - 웹 콘솔: <https://mimi-seed.pryzm.gg/tool>
226
+ - 저장소: <https://github.com/jeonghwanko/mimi-seed>
227
+
228
+ ---
229
+
230
+ ## License
231
+
232
+ [PolyForm Noncommercial License 1.0.0](../../LICENSE) — 비상업적 사용만 허용.
233
+ 상업적 이용 문의: turbo08@gmail.com
234
+
235
+ **Required Notice:** Copyright 2026 Pryzm GG (https://mimi-seed.pryzm.gg)
@@ -42,7 +42,7 @@ async function main() {
42
42
  console.log('');
43
43
  console.log(' ✅ 연결 완료!');
44
44
  console.log('');
45
- console.log(' 이제 Claude Code에서:');
45
+ console.log(' 이제 Claude Code 또는 Codex에서:');
46
46
  console.log(' "내 앱스토어 앱 목록 보여줘"');
47
47
  console.log(' "TestFlight 빌드 목록 보여줘"');
48
48
  console.log('');
@@ -83,7 +83,7 @@ async function main() {
83
83
  }
84
84
  }
85
85
  console.log('');
86
- console.log(' 이제 Claude Code 에서:');
86
+ console.log(' 이제 Claude Code 또는 Codex에서:');
87
87
  console.log(' "BigQuery 로 GA4 트래픽 분석해줘"');
88
88
  console.log('');
89
89
  rl.close();
package/dist/auth/cli.js CHANGED
@@ -216,7 +216,7 @@ async function cmdLogin() {
216
216
  err('');
217
217
  err(' ✅ 연결 완료!');
218
218
  err('');
219
- err(' 이제 Claude Code에서 이렇게 쓸 수 있어:');
219
+ err(' 이제 Claude Code 또는 Codex에서 이렇게 쓸 수 있어:');
220
220
  err(' "내 Firebase 프로젝트 보여줘"');
221
221
  err(' "새 Android 앱 등록해줘"');
222
222
  err(' "google-services.json 다운로드해줘"');
@@ -214,7 +214,7 @@ export function startAuth(clientId, clientSecret, options = {}) {
214
214
  <html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
215
215
  <div style="text-align:center">
216
216
  <h1>✅ Mimi Seed 인증 완료!</h1>
217
- <p>이 창을 닫고 Claude Code로 돌아가세요.</p>
217
+ <p>이 창을 닫고 Claude Code 또는 Codex로 돌아가세요.</p>
218
218
  </div>
219
219
  </body></html>
220
220
  `);
@@ -55,7 +55,7 @@ async function main() {
55
55
  console.log('');
56
56
  console.log(` ✅ 저장 완료! (${parsed.client_email})`);
57
57
  console.log('');
58
- console.log(' 이제 Claude Code에서:');
58
+ console.log(' 이제 Claude Code 또는 Codex에서:');
59
59
  console.log(' "내 Play 스토어 앱 리스팅 보여줘"');
60
60
  console.log(' "Play 구독 상품 목록 보여줘"');
61
61
  console.log('');
@@ -0,0 +1,10 @@
1
+ export interface GoogleAdsConfig {
2
+ developerToken: string;
3
+ customerId: string;
4
+ loginCustomerId?: string;
5
+ }
6
+ /** 하이픈 제거 (API는 숫자만 허용) */
7
+ export declare function normalizeCustomerId(id: string): string;
8
+ export declare function saveConfig(cfg: GoogleAdsConfig): void;
9
+ export declare function loadConfig(): GoogleAdsConfig | null;
10
+ export declare function requireConfig(): GoogleAdsConfig;
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
5
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'google-ads.json');
6
+ function ensureDir() {
7
+ if (!fs.existsSync(CONFIG_DIR))
8
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
9
+ }
10
+ /** 하이픈 제거 (API는 숫자만 허용) */
11
+ export function normalizeCustomerId(id) {
12
+ return id.replace(/-/g, '');
13
+ }
14
+ export function saveConfig(cfg) {
15
+ ensureDir();
16
+ const normalized = {
17
+ ...cfg,
18
+ customerId: normalizeCustomerId(cfg.customerId),
19
+ loginCustomerId: cfg.loginCustomerId ? normalizeCustomerId(cfg.loginCustomerId) : undefined,
20
+ };
21
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2), { mode: 0o600 });
22
+ }
23
+ export function loadConfig() {
24
+ if (!fs.existsSync(CONFIG_PATH))
25
+ return null;
26
+ try {
27
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ export function requireConfig() {
34
+ const cfg = loadConfig();
35
+ if (!cfg) {
36
+ throw new Error([
37
+ '❌ Google Ads 설정이 없어.',
38
+ '',
39
+ 'googleads_save_config 도구로 먼저 설정해줘:',
40
+ ' - developerToken: Google Ads 콘솔 → 관리자 → API 센터에서 발급',
41
+ ' - customerId: Google Ads 계정 ID (예: 123-456-7890)',
42
+ ].join('\n'));
43
+ }
44
+ return cfg;
45
+ }
@@ -0,0 +1,48 @@
1
+ import type { OAuth2Client } from 'google-auth-library';
2
+ import type { GoogleAdsConfig } from './config.js';
3
+ export interface DateRange {
4
+ startDate: string;
5
+ endDate: string;
6
+ }
7
+ export declare function listAccessibleCustomers(auth: OAuth2Client, cfg: GoogleAdsConfig): Promise<any>;
8
+ export declare function listCampaigns(auth: OAuth2Client, cfg: GoogleAdsConfig): Promise<{
9
+ id: any;
10
+ name: any;
11
+ status: any;
12
+ channelType: any;
13
+ channelSubType: any;
14
+ startDate: any;
15
+ endDate: any;
16
+ dailyBudget: number;
17
+ }[]>;
18
+ export declare function getCampaignReport(auth: OAuth2Client, cfg: GoogleAdsConfig, range: DateRange): Promise<{
19
+ id: any;
20
+ name: any;
21
+ status: any;
22
+ channelType: any;
23
+ channelSubType: any;
24
+ clicks: number;
25
+ impressions: number;
26
+ cost: number;
27
+ conversions: number;
28
+ conversionsValue: number;
29
+ cpi: number;
30
+ ctr: number;
31
+ avgCpc: number;
32
+ }[]>;
33
+ export declare function getUacReport(auth: OAuth2Client, cfg: GoogleAdsConfig, range: DateRange): Promise<{
34
+ id: string;
35
+ name: string;
36
+ status: string;
37
+ subType: string;
38
+ clicks: number;
39
+ impressions: number;
40
+ cost: number;
41
+ installs: number;
42
+ installsValue: number;
43
+ cpi: number;
44
+ dateRange: {
45
+ from: string;
46
+ to: string;
47
+ };
48
+ }[]>;
@@ -0,0 +1,221 @@
1
+ const API_VERSION = 'v17';
2
+ const BASE = `https://googleads.googleapis.com/${API_VERSION}`;
3
+ // ─── 내부 헬퍼 ───────────────────────────────────────────────
4
+ async function getAccessToken(auth) {
5
+ const token = await auth.getAccessToken();
6
+ if (!token.token)
7
+ throw new Error('OAuth 토큰을 가져올 수 없음. 재인증 필요.');
8
+ return token.token;
9
+ }
10
+ function buildHeaders(accessToken, cfg) {
11
+ const headers = {
12
+ Authorization: `Bearer ${accessToken}`,
13
+ 'developer-token': cfg.developerToken,
14
+ 'Content-Type': 'application/json',
15
+ };
16
+ if (cfg.loginCustomerId) {
17
+ headers['login-customer-id'] = cfg.loginCustomerId;
18
+ }
19
+ return headers;
20
+ }
21
+ /** search 엔드포인트 (paged JSON, searchStream보다 안정적) */
22
+ async function search(auth, cfg, query) {
23
+ const accessToken = await getAccessToken(auth);
24
+ const url = `${BASE}/customers/${cfg.customerId}/googleAds:search`;
25
+ const all = [];
26
+ let pageToken;
27
+ do {
28
+ const body = { query, pageSize: 1000 };
29
+ if (pageToken)
30
+ body.pageToken = pageToken;
31
+ const res = await fetch(url, {
32
+ method: 'POST',
33
+ headers: buildHeaders(accessToken, cfg),
34
+ body: JSON.stringify(body),
35
+ });
36
+ const text = await res.text();
37
+ if (!res.ok) {
38
+ let msg = `Google Ads API ${res.status}`;
39
+ try {
40
+ const err = JSON.parse(text);
41
+ const detail = err?.error?.message ?? err?.[0]?.error?.message ?? text;
42
+ msg += `: ${detail}`;
43
+ }
44
+ catch {
45
+ msg += `: ${text.slice(0, 300)}`;
46
+ }
47
+ throw new Error(msg);
48
+ }
49
+ const json = JSON.parse(text);
50
+ all.push(...(json.results ?? []));
51
+ pageToken = json.nextPageToken ?? undefined;
52
+ } while (pageToken);
53
+ return all;
54
+ }
55
+ function microsToCurrency(micros) {
56
+ if (micros == null)
57
+ return 0;
58
+ return Number(micros) / 1_000_000;
59
+ }
60
+ // ─── 접근 가능한 고객 목록 (API 연결 확인용) ─────────────────────
61
+ export async function listAccessibleCustomers(auth, cfg) {
62
+ const accessToken = await getAccessToken(auth);
63
+ const url = `${BASE}/customers:listAccessibleCustomers`;
64
+ const res = await fetch(url, {
65
+ method: 'GET',
66
+ headers: buildHeaders(accessToken, cfg),
67
+ });
68
+ const text = await res.text();
69
+ if (!res.ok) {
70
+ let msg = `Google Ads API ${res.status}`;
71
+ try {
72
+ const err = JSON.parse(text);
73
+ msg += `: ${err?.error?.message ?? text.slice(0, 300)}`;
74
+ }
75
+ catch {
76
+ msg += `: ${text.slice(0, 300)}`;
77
+ }
78
+ throw new Error(msg);
79
+ }
80
+ return JSON.parse(text);
81
+ }
82
+ // ─── 캠페인 목록 ─────────────────────────────────────────────
83
+ export async function listCampaigns(auth, cfg) {
84
+ const query = `
85
+ SELECT
86
+ campaign.id,
87
+ campaign.name,
88
+ campaign.status,
89
+ campaign.advertising_channel_type,
90
+ campaign.advertising_channel_sub_type,
91
+ campaign.start_date,
92
+ campaign.end_date,
93
+ campaign_budget.amount_micros
94
+ FROM campaign
95
+ WHERE campaign.status != 'REMOVED'
96
+ ORDER BY campaign.name ASC
97
+ LIMIT 200
98
+ `;
99
+ const rows = await search(auth, cfg, query);
100
+ return rows.map((r) => ({
101
+ id: r.campaign?.id,
102
+ name: r.campaign?.name,
103
+ status: r.campaign?.status,
104
+ channelType: r.campaign?.advertisingChannelType,
105
+ channelSubType: r.campaign?.advertisingChannelSubType,
106
+ startDate: r.campaign?.startDate,
107
+ endDate: r.campaign?.endDate,
108
+ dailyBudget: microsToCurrency(r.campaignBudget?.amountMicros),
109
+ }));
110
+ }
111
+ // ─── 캠페인 성과 리포트 ────────────────────────────────────────
112
+ export async function getCampaignReport(auth, cfg, range) {
113
+ const query = `
114
+ SELECT
115
+ campaign.id,
116
+ campaign.name,
117
+ campaign.status,
118
+ campaign.advertising_channel_type,
119
+ campaign.advertising_channel_sub_type,
120
+ metrics.clicks,
121
+ metrics.impressions,
122
+ metrics.cost_micros,
123
+ metrics.conversions,
124
+ metrics.conversions_value,
125
+ metrics.cost_per_conversion,
126
+ metrics.ctr,
127
+ metrics.average_cpc
128
+ FROM campaign
129
+ WHERE campaign.status != 'REMOVED'
130
+ AND segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'
131
+ ORDER BY metrics.cost_micros DESC
132
+ LIMIT 500
133
+ `;
134
+ const rows = await search(auth, cfg, query);
135
+ return rows.map((r) => ({
136
+ id: r.campaign?.id,
137
+ name: r.campaign?.name,
138
+ status: r.campaign?.status,
139
+ channelType: r.campaign?.advertisingChannelType,
140
+ channelSubType: r.campaign?.advertisingChannelSubType,
141
+ clicks: Number(r.metrics?.clicks ?? 0),
142
+ impressions: Number(r.metrics?.impressions ?? 0),
143
+ cost: microsToCurrency(r.metrics?.costMicros),
144
+ conversions: Number(r.metrics?.conversions ?? 0),
145
+ conversionsValue: Number(r.metrics?.conversionsValue ?? 0),
146
+ cpi: microsToCurrency(r.metrics?.costPerConversion),
147
+ ctr: Number(r.metrics?.ctr ?? 0),
148
+ avgCpc: microsToCurrency(r.metrics?.averageCpc),
149
+ }));
150
+ }
151
+ // ─── UAC(앱 캠페인) 리포트 ─────────────────────────────────────
152
+ export async function getUacReport(auth, cfg, range) {
153
+ const query = `
154
+ SELECT
155
+ campaign.id,
156
+ campaign.name,
157
+ campaign.status,
158
+ campaign.advertising_channel_sub_type,
159
+ metrics.clicks,
160
+ metrics.impressions,
161
+ metrics.cost_micros,
162
+ metrics.conversions,
163
+ metrics.conversions_value,
164
+ metrics.cost_per_conversion,
165
+ metrics.ctr,
166
+ segments.date
167
+ FROM campaign
168
+ WHERE campaign.advertising_channel_type = 'MULTI_CHANNEL'
169
+ AND campaign.advertising_channel_sub_type IN ('APP_CAMPAIGN', 'APP_CAMPAIGN_FOR_ENGAGEMENT', 'APP_CAMPAIGN_FOR_PRE_REGISTRATION')
170
+ AND segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'
171
+ ORDER BY segments.date DESC, metrics.cost_micros DESC
172
+ LIMIT 500
173
+ `;
174
+ const rows = await search(auth, cfg, query);
175
+ const byId = new Map();
176
+ for (const r of rows) {
177
+ const id = String(r.campaign?.id ?? '');
178
+ const existing = byId.get(id);
179
+ const cost = microsToCurrency(r.metrics?.costMicros);
180
+ const installs = Number(r.metrics?.conversions ?? 0);
181
+ if (existing) {
182
+ existing.clicks += Number(r.metrics?.clicks ?? 0);
183
+ existing.impressions += Number(r.metrics?.impressions ?? 0);
184
+ existing.cost += cost;
185
+ existing.installs += installs;
186
+ existing.installsValue += Number(r.metrics?.conversionsValue ?? 0);
187
+ if (r.segments?.date)
188
+ existing.dates.push(r.segments.date);
189
+ }
190
+ else {
191
+ byId.set(id, {
192
+ id,
193
+ name: r.campaign?.name ?? '',
194
+ status: r.campaign?.status ?? '',
195
+ subType: r.campaign?.advertisingChannelSubType ?? '',
196
+ clicks: Number(r.metrics?.clicks ?? 0),
197
+ impressions: Number(r.metrics?.impressions ?? 0),
198
+ cost,
199
+ installs,
200
+ installsValue: Number(r.metrics?.conversionsValue ?? 0),
201
+ dates: r.segments?.date ? [r.segments.date] : [],
202
+ });
203
+ }
204
+ }
205
+ return Array.from(byId.values()).map((c) => ({
206
+ id: c.id,
207
+ name: c.name,
208
+ status: c.status,
209
+ subType: c.subType,
210
+ clicks: c.clicks,
211
+ impressions: c.impressions,
212
+ cost: c.cost,
213
+ installs: c.installs,
214
+ installsValue: c.installsValue,
215
+ cpi: c.installs > 0 ? c.cost / c.installs : 0,
216
+ dateRange: {
217
+ from: c.dates.length ? [...c.dates].sort()[0] : range.startDate,
218
+ to: c.dates.length ? [...c.dates].sort().reverse()[0] : range.endDate,
219
+ },
220
+ }));
221
+ }
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { registerAuthTools } from './registers/auth.js';
13
13
  import { registerCiTools } from './registers/ci.js';
14
14
  import { registerInstagramTools } from './registers/instagram.js';
15
15
  import { registerFacebookTools } from './registers/facebook.js';
16
+ import { registerGoogleAdsTools } from './registers/googleads.js';
16
17
  import { registerPrompts } from './prompts.js';
17
18
  import { registerResources } from './resources.js';
18
19
  const server = new McpServer({
@@ -31,6 +32,7 @@ registerAuthTools(server);
31
32
  registerCiTools(server);
32
33
  registerInstagramTools(server);
33
34
  registerFacebookTools(server);
35
+ registerGoogleAdsTools(server);
34
36
  registerPrompts(server);
35
37
  registerResources(server);
36
38
  // `npx -y @yoonion/mimi-seed-mcp <subcommand>` 처리.
@@ -8,7 +8,22 @@ export type PlayImageType = 'featureGraphic' | 'icon' | 'phoneScreenshots' | 'pr
8
8
  * 여기서는 기존 앱의 메타데이터, 빌드, 출시를 관리.
9
9
  */
10
10
  export declare const publisher: () => import("googleapis").androidpublisher_v3.Androidpublisher;
11
+ export type PlayVitalsMetricSet = 'anrRate' | 'crashRate' | 'errorCount';
12
+ export interface PlayStatisticsQuery {
13
+ metricSet?: PlayVitalsMetricSet;
14
+ startDate: string;
15
+ endDate: string;
16
+ aggregationPeriod?: 'DAILY' | 'HOURLY';
17
+ dimensions?: string[];
18
+ metrics?: string[];
19
+ filter?: string;
20
+ pageSize?: number;
21
+ pageToken?: string;
22
+ userCohort?: 'OS_PUBLIC' | 'APP_TESTERS' | 'OS_BETA';
23
+ timeZone?: string;
24
+ }
11
25
  export declare function withEdit<T>(auth: OAuth2Client | JWT, packageName: string, fn: (editId: string) => Promise<T>, commit?: boolean): Promise<T>;
26
+ export declare function getStatistics(auth: OAuth2Client | JWT, packageName: string, query: PlayStatisticsQuery): Promise<unknown>;
12
27
  export declare function getAppDetails(auth: OAuth2Client | JWT, packageName: string): Promise<import("googleapis").androidpublisher_v3.Schema$AppDetails>;
13
28
  export declare function getListing(auth: OAuth2Client | JWT, packageName: string, language?: string): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
14
29
  export declare function updateListing(auth: OAuth2Client | JWT, packageName: string, language: string, data: {
@@ -18,6 +18,28 @@ function mimeTypeFor(filePath) {
18
18
  * 여기서는 기존 앱의 메타데이터, 빌드, 출시를 관리.
19
19
  */
20
20
  export const publisher = () => google.androidpublisher('v3');
21
+ const REPORTING_API_BASE = 'https://playdeveloperreporting.googleapis.com/v1beta1';
22
+ const METRIC_SET_RESOURCE = {
23
+ anrRate: 'anrRateMetricSet',
24
+ crashRate: 'crashRateMetricSet',
25
+ errorCount: 'errorCountMetricSet',
26
+ };
27
+ const DEFAULT_METRICS = {
28
+ anrRate: ['anrRate', 'userPerceivedAnrRate', 'distinctUsers'],
29
+ crashRate: ['crashRate', 'userPerceivedCrashRate', 'distinctUsers'],
30
+ errorCount: ['errorReportCount', 'distinctUsers'],
31
+ };
32
+ function dateToReportingDateTime(date, timeZone) {
33
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
34
+ if (!match)
35
+ throw new Error(`날짜는 YYYY-MM-DD 형식이어야 해: ${date}`);
36
+ return {
37
+ year: Number(match[1]),
38
+ month: Number(match[2]),
39
+ day: Number(match[3]),
40
+ timeZone: { id: timeZone },
41
+ };
42
+ }
21
43
  export async function withEdit(auth, packageName, fn, commit = false) {
22
44
  const res = await publisher().edits.insert({ auth, packageName });
23
45
  const editId = res.data.id;
@@ -36,6 +58,31 @@ export async function withEdit(auth, packageName, fn, commit = false) {
36
58
  }
37
59
  }
38
60
  }
61
+ // ─── Play Developer Reporting API / Android vitals ───
62
+ export async function getStatistics(auth, packageName, query) {
63
+ const metricSet = query.metricSet ?? 'anrRate';
64
+ const resource = METRIC_SET_RESOURCE[metricSet];
65
+ const timeZone = query.timeZone ?? 'America/Los_Angeles';
66
+ const url = `${REPORTING_API_BASE}/apps/${encodeURIComponent(packageName)}/${resource}:query`;
67
+ const res = await auth.request({
68
+ url,
69
+ method: 'POST',
70
+ data: {
71
+ timelineSpec: {
72
+ aggregationPeriod: query.aggregationPeriod ?? 'DAILY',
73
+ startTime: dateToReportingDateTime(query.startDate, timeZone),
74
+ endTime: dateToReportingDateTime(query.endDate, timeZone),
75
+ },
76
+ dimensions: query.dimensions ?? ['versionCode'],
77
+ metrics: query.metrics ?? DEFAULT_METRICS[metricSet],
78
+ ...(query.filter ? { filter: query.filter } : {}),
79
+ ...(query.pageSize ? { pageSize: query.pageSize } : {}),
80
+ ...(query.pageToken ? { pageToken: query.pageToken } : {}),
81
+ ...(query.userCohort ? { userCohort: query.userCohort } : {}),
82
+ },
83
+ });
84
+ return res.data;
85
+ }
39
86
  // ─── 앱 목록 ───
40
87
  export async function getAppDetails(auth, packageName) {
41
88
  return withEdit(auth, packageName, async (editId) => {
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerGoogleAdsTools(server: McpServer): void;
@@ -0,0 +1,120 @@
1
+ import { z } from 'zod';
2
+ import { requireAuth } from '../helpers.js';
3
+ import { saveConfig, loadConfig, requireConfig } from '../googleads/config.js';
4
+ import * as googleads from '../googleads/tools.js';
5
+ export function registerGoogleAdsTools(server) {
6
+ server.tool('googleads_save_config', 'Google Ads API 설정 저장 (Developer Token + 계정 ID). 최초 1회만 필요.', {
7
+ developerToken: z.string().describe('Google Ads 콘솔 → 관리자 → API 센터에서 발급한 Developer Token'),
8
+ customerId: z.string().describe('Google Ads 계정 ID (예: 123-456-7890)'),
9
+ loginCustomerId: z.string().optional().describe('MCC(관리자) 계정 ID — 하위 계정 접근 시 필요'),
10
+ }, async ({ developerToken, customerId, loginCustomerId }) => {
11
+ saveConfig({ developerToken, customerId, loginCustomerId });
12
+ return {
13
+ content: [{
14
+ type: 'text',
15
+ text: [
16
+ '✅ Google Ads 설정 저장 완료.',
17
+ ` customerId: ${customerId}`,
18
+ loginCustomerId ? ` loginCustomerId: ${loginCustomerId}` : '',
19
+ '',
20
+ '이제 googleads_list_campaigns 나 googleads_get_uac_report 를 사용할 수 있어.',
21
+ '',
22
+ '⚠️ Google Ads API는 adwords OAuth 스코프가 필요해.',
23
+ '기존 토큰에 스코프가 없으면 npx -y @yoonion/mimi-seed-mcp mimi-seed-auth 로 재인증해줘.',
24
+ ].filter(Boolean).join('\n'),
25
+ }],
26
+ };
27
+ });
28
+ server.tool('googleads_list_campaigns', 'Google Ads 캠페인 목록 조회 (상태, 채널 타입, 일일 예산 포함)', {}, async () => {
29
+ const auth = requireAuth();
30
+ const cfg = requireConfig();
31
+ const campaigns = await googleads.listCampaigns(auth, cfg);
32
+ return {
33
+ content: [{
34
+ type: 'text',
35
+ text: JSON.stringify(campaigns, null, 2),
36
+ }],
37
+ };
38
+ });
39
+ server.tool('googleads_get_campaign_report', '기간별 캠페인 성과 리포트 (클릭, 노출, 비용, 전환수, CPI, CTR)', {
40
+ startDate: z.string().describe('시작일 (YYYY-MM-DD)'),
41
+ endDate: z.string().describe('종료일 (YYYY-MM-DD)'),
42
+ }, async ({ startDate, endDate }) => {
43
+ const auth = requireAuth();
44
+ const cfg = requireConfig();
45
+ const report = await googleads.getCampaignReport(auth, cfg, { startDate, endDate });
46
+ const totalCost = report.reduce((s, r) => s + r.cost, 0);
47
+ const totalClicks = report.reduce((s, r) => s + r.clicks, 0);
48
+ const totalImpressions = report.reduce((s, r) => s + r.impressions, 0);
49
+ const totalConversions = report.reduce((s, r) => s + r.conversions, 0);
50
+ return {
51
+ content: [{
52
+ type: 'text',
53
+ text: JSON.stringify({
54
+ period: { startDate, endDate },
55
+ summary: {
56
+ totalCost: Math.round(totalCost * 100) / 100,
57
+ totalClicks,
58
+ totalImpressions,
59
+ totalConversions,
60
+ avgCpi: totalConversions > 0 ? Math.round(totalCost / totalConversions * 100) / 100 : null,
61
+ },
62
+ campaigns: report,
63
+ }, null, 2),
64
+ }],
65
+ };
66
+ });
67
+ server.tool('googleads_get_uac_report', '앱 캠페인(UAC) 리포트 — 앱 설치 캠페인별 설치수, CPI, ROAS 집계', {
68
+ startDate: z.string().describe('시작일 (YYYY-MM-DD)'),
69
+ endDate: z.string().describe('종료일 (YYYY-MM-DD)'),
70
+ }, async ({ startDate, endDate }) => {
71
+ const auth = requireAuth();
72
+ const cfg = requireConfig();
73
+ const report = await googleads.getUacReport(auth, cfg, { startDate, endDate });
74
+ const totalCost = report.reduce((s, r) => s + r.cost, 0);
75
+ const totalInstalls = report.reduce((s, r) => s + r.installs, 0);
76
+ return {
77
+ content: [{
78
+ type: 'text',
79
+ text: JSON.stringify({
80
+ period: { startDate, endDate },
81
+ summary: {
82
+ totalCost: Math.round(totalCost * 100) / 100,
83
+ totalInstalls,
84
+ avgCpi: totalInstalls > 0 ? Math.round(totalCost / totalInstalls * 100) / 100 : null,
85
+ campaignCount: report.length,
86
+ },
87
+ campaigns: report,
88
+ }, null, 2),
89
+ }],
90
+ };
91
+ });
92
+ server.tool('googleads_list_accessible_customers', 'OAuth 토큰으로 접근 가능한 Google Ads 계정 목록 (API 연결 확인용)', {}, async () => {
93
+ const auth = requireAuth();
94
+ const cfg = requireConfig();
95
+ const result = await googleads.listAccessibleCustomers(auth, cfg);
96
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
97
+ });
98
+ server.tool('googleads_config_status', 'Google Ads 연동 설정 현황 확인', {}, async () => {
99
+ const cfg = loadConfig();
100
+ if (!cfg) {
101
+ return {
102
+ content: [{
103
+ type: 'text',
104
+ text: '❌ Google Ads 설정 없음. googleads_save_config 로 먼저 설정해.',
105
+ }],
106
+ };
107
+ }
108
+ return {
109
+ content: [{
110
+ type: 'text',
111
+ text: JSON.stringify({
112
+ status: 'configured',
113
+ customerId: cfg.customerId,
114
+ loginCustomerId: cfg.loginCustomerId ?? null,
115
+ hasDeveloperToken: !!cfg.developerToken,
116
+ }, null, 2),
117
+ }],
118
+ };
119
+ });
120
+ }
@@ -37,6 +37,45 @@ export function registerPlaystoreTools(server) {
37
37
  const tracks = await playstore.listTracks(auth, packageName);
38
38
  return { content: [{ type: 'text', text: JSON.stringify(tracks, null, 2) }] };
39
39
  });
40
+ server.tool('playstore_get_statistics', 'Google Play Developer Reporting API / Android vitals 통계 조회. ANR/Crash/Error count를 기간·버전·기기·국가 등으로 분해해 확인', {
41
+ packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
42
+ metricSet: z.enum(['anrRate', 'crashRate', 'errorCount'])
43
+ .default('anrRate')
44
+ .describe('조회할 Vitals metric set. 기본: anrRate'),
45
+ startDate: z.string().describe('시작일 YYYY-MM-DD (포함)'),
46
+ endDate: z.string().describe('종료일 YYYY-MM-DD (보통 exclusive처럼 다음날 지정 권장)'),
47
+ aggregationPeriod: z.enum(['DAILY', 'HOURLY']).default('DAILY').describe('집계 단위'),
48
+ dimensions: z.array(z.enum([
49
+ 'apiLevel',
50
+ 'versionCode',
51
+ 'deviceModel',
52
+ 'deviceBrand',
53
+ 'deviceType',
54
+ 'countryCode',
55
+ 'deviceRamBucket',
56
+ 'deviceSocMake',
57
+ 'deviceSocModel',
58
+ 'deviceCpuMake',
59
+ 'deviceCpuModel',
60
+ 'deviceGpuMake',
61
+ 'deviceGpuModel',
62
+ 'deviceGpuVersion',
63
+ 'deviceVulkanVersion',
64
+ 'deviceGlEsVersion',
65
+ 'deviceScreenSize',
66
+ 'deviceScreenDpi',
67
+ ])).optional().describe('분해 차원. 기본: ["versionCode"]'),
68
+ metrics: z.array(z.string()).optional().describe('조회 metric. 기본은 metricSet별 핵심 metric + distinctUsers'),
69
+ filter: z.string().optional().describe('AIP-160 필터. 예: versionCode = 42'),
70
+ pageSize: z.number().int().min(1).max(100000).optional().describe('최대 행 수'),
71
+ pageToken: z.string().optional().describe('다음 페이지 토큰'),
72
+ userCohort: z.enum(['OS_PUBLIC', 'APP_TESTERS', 'OS_BETA']).optional().describe('사용자 cohort'),
73
+ timeZone: z.string().optional().describe('날짜 기준 timezone. 기본: America/Los_Angeles (Play Reporting API 샘플/지원 기준)'),
74
+ }, async (args) => {
75
+ const auth = requirePlayStoreAuth(args.packageName);
76
+ const result = await playstore.getStatistics(auth, args.packageName, args);
77
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
78
+ });
40
79
  server.tool('playstore_list_images', 'Google Play 리스팅 이미지 목록 조회 (imageType별)', {
41
80
  packageName: z.string().describe('패키지명'),
42
81
  language: z.string().describe('언어 코드 (예: ko-KR)'),
package/dist/resources.js CHANGED
@@ -25,7 +25,7 @@ export function registerResources(server) {
25
25
  '# Mimi Seed — 앱 출시·운영 Agent',
26
26
  '',
27
27
  '당신은 Mimi Seed MCP를 통해 인디 개발자의 앱 출시와 운영을 돕는 에이전트입니다.',
28
- 'Google Play · App Store · Firebase · AdMob 직접 제어하는 65+ 도구를 사용할 수 있습니다.',
28
+ 'Google Play · App Store · Firebase · AdMob · CI/CD · BigQuery를 직접 제어하는 110+ 도구를 사용할 수 있습니다.',
29
29
  '',
30
30
  '## 출시 요청 처리 순서',
31
31
  '',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.27",
4
- "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Cursor / any MCP client.",
3
+ "version": "0.3.28",
4
+ "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mimi-seed-mcp": "dist/index.js",
@@ -27,6 +27,7 @@
27
27
  "mcp-server",
28
28
  "model-context-protocol",
29
29
  "claude",
30
+ "codex",
30
31
  "firebase",
31
32
  "admob",
32
33
  "google-play",