@yoonion/mimi-seed-mcp 0.3.16 → 0.3.18
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 +75 -0
- package/dist/appstore/tools.d.ts +7 -0
- package/dist/appstore/tools.js +72 -25
- package/dist/auth/cli.js +3 -2
- package/dist/auth/constants.d.ts +4 -2
- package/dist/auth/constants.js +17 -11
- package/dist/auth/google-auth.js +5 -4
- package/dist/auth/playstore-auth.d.ts +35 -2
- package/dist/auth/playstore-auth.js +95 -11
- package/dist/index.js +156 -34
- package/package.json +4 -3
package/LICENSE
ADDED
|
@@ -0,0 +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.
|
package/dist/appstore/tools.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare function updateVersionWhatsNew(versionId: string, locale: string,
|
|
|
44
44
|
export declare function updateReviewNotes(versionId: string, notes: string): Promise<{
|
|
45
45
|
reviewDetailId: string;
|
|
46
46
|
notes: string;
|
|
47
|
+
created: boolean;
|
|
47
48
|
}>;
|
|
48
49
|
export declare function getReviewNotes(versionId: string): Promise<{
|
|
49
50
|
reviewDetailId: string | null;
|
|
@@ -88,6 +89,12 @@ export declare function submitVersionForReview(versionId: string): Promise<{
|
|
|
88
89
|
itemAttached: boolean;
|
|
89
90
|
state: any;
|
|
90
91
|
}>;
|
|
92
|
+
export declare function cancelVersionReview(versionId: string): Promise<{
|
|
93
|
+
submissionId: string;
|
|
94
|
+
previousState: string;
|
|
95
|
+
newState: string;
|
|
96
|
+
versionId: string;
|
|
97
|
+
}>;
|
|
91
98
|
export type AppleIapType = 'CONSUMABLE' | 'NON_CONSUMABLE' | 'NON_RENEWING_SUBSCRIPTION';
|
|
92
99
|
export interface CreateInAppPurchaseInput {
|
|
93
100
|
appId: string;
|
package/dist/appstore/tools.js
CHANGED
|
@@ -186,8 +186,16 @@ export async function updateVersionWhatsNew(versionId, locale, fields) {
|
|
|
186
186
|
return updateVersionLocalization(target.id, fields);
|
|
187
187
|
}
|
|
188
188
|
// ─── 리뷰어 노트 (appStoreReviewDetail.notes) ───
|
|
189
|
+
/**
|
|
190
|
+
* apiGet이 throw한 에러가 404(리소스 없음)인지 판별.
|
|
191
|
+
* apiGet은 `App Store API ${status}: ${body}` 형식으로 throw하므로 prefix로 판별.
|
|
192
|
+
* 404 외(401/403/500 등)는 마스킹하지 않고 그대로 throw해야 디버깅 가능.
|
|
193
|
+
*/
|
|
194
|
+
function isNotFoundError(err) {
|
|
195
|
+
return err instanceof Error && /^App Store API 404:/.test(err.message);
|
|
196
|
+
}
|
|
189
197
|
export async function updateReviewNotes(versionId, notes) {
|
|
190
|
-
// 1. 기존 reviewDetail 조회
|
|
198
|
+
// 1. 기존 reviewDetail 조회 — 404면 신규 생성, 그 외 에러는 throw
|
|
191
199
|
let reviewDetailId = null;
|
|
192
200
|
try {
|
|
193
201
|
const existing = await apiGet(`/appStoreVersions/${versionId}/appStoreReviewDetail`, {
|
|
@@ -195,34 +203,36 @@ export async function updateReviewNotes(versionId, notes) {
|
|
|
195
203
|
});
|
|
196
204
|
reviewDetailId = existing?.data?.id ?? null;
|
|
197
205
|
}
|
|
198
|
-
catch {
|
|
199
|
-
|
|
206
|
+
catch (err) {
|
|
207
|
+
if (!isNotFoundError(err))
|
|
208
|
+
throw err;
|
|
200
209
|
}
|
|
201
210
|
if (reviewDetailId) {
|
|
202
|
-
// 2a. 있으면 PATCH
|
|
203
211
|
const updated = await apiPatch(`/appStoreReviewDetails/${reviewDetailId}`, {
|
|
204
|
-
data: {
|
|
205
|
-
type: 'appStoreReviewDetails',
|
|
206
|
-
id: reviewDetailId,
|
|
207
|
-
attributes: { notes },
|
|
208
|
-
},
|
|
212
|
+
data: { type: 'appStoreReviewDetails', id: reviewDetailId, attributes: { notes } },
|
|
209
213
|
});
|
|
210
|
-
return {
|
|
214
|
+
return {
|
|
215
|
+
reviewDetailId,
|
|
216
|
+
notes: updated?.data?.attributes?.notes ?? notes,
|
|
217
|
+
created: false,
|
|
218
|
+
};
|
|
211
219
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
|
|
220
|
-
},
|
|
220
|
+
// 신규 생성
|
|
221
|
+
const created = await apiPost('/appStoreReviewDetails', {
|
|
222
|
+
data: {
|
|
223
|
+
type: 'appStoreReviewDetails',
|
|
224
|
+
attributes: { notes },
|
|
225
|
+
relationships: {
|
|
226
|
+
appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
|
|
221
227
|
},
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const newId = created?.data?.id ?? '';
|
|
231
|
+
return {
|
|
232
|
+
reviewDetailId: newId,
|
|
233
|
+
notes: created?.data?.attributes?.notes ?? notes,
|
|
234
|
+
created: true,
|
|
235
|
+
};
|
|
226
236
|
}
|
|
227
237
|
export async function getReviewNotes(versionId) {
|
|
228
238
|
try {
|
|
@@ -235,8 +245,11 @@ export async function getReviewNotes(versionId) {
|
|
|
235
245
|
contactEmail: data?.data?.attributes?.contactEmail ?? null,
|
|
236
246
|
};
|
|
237
247
|
}
|
|
238
|
-
catch {
|
|
239
|
-
|
|
248
|
+
catch (err) {
|
|
249
|
+
if (isNotFoundError(err)) {
|
|
250
|
+
return { reviewDetailId: null, notes: null, contactEmail: null };
|
|
251
|
+
}
|
|
252
|
+
throw err;
|
|
240
253
|
}
|
|
241
254
|
}
|
|
242
255
|
// ─── 빌드 ───
|
|
@@ -464,6 +477,40 @@ export async function submitVersionForReview(versionId) {
|
|
|
464
477
|
state: submitted?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW',
|
|
465
478
|
};
|
|
466
479
|
}
|
|
480
|
+
// ─── 심사 철회 (Cancel Review) ───
|
|
481
|
+
// WAITING_FOR_REVIEW 상태의 reviewSubmission에만 적용 가능.
|
|
482
|
+
// IN_REVIEW 진입 후에는 Apple API가 거부함 (409).
|
|
483
|
+
// submitted=false PATCH → version이 PREPARE_FOR_SUBMISSION으로 복귀.
|
|
484
|
+
export async function cancelVersionReview(versionId) {
|
|
485
|
+
const { appId, platform } = await getVersionAppAndPlatform(versionId);
|
|
486
|
+
// 취소 가능한 상태(WAITING_FOR_REVIEW)의 submission 검색
|
|
487
|
+
const data = await apiGet('/reviewSubmissions', {
|
|
488
|
+
'filter[app]': appId,
|
|
489
|
+
'filter[platform]': platform,
|
|
490
|
+
'filter[state]': 'WAITING_FOR_REVIEW',
|
|
491
|
+
'limit': '1',
|
|
492
|
+
});
|
|
493
|
+
const submission = data?.data?.[0];
|
|
494
|
+
if (!submission) {
|
|
495
|
+
throw new Error([
|
|
496
|
+
`취소 가능한 심사 제출이 없어 (WAITING_FOR_REVIEW 상태 없음).`,
|
|
497
|
+
`IN_REVIEW 이상은 API로 취소 불가 — App Store Connect 웹에서 직접 처리하거나`,
|
|
498
|
+
`Apple 심사 결과(APPROVED/REJECTED)를 기다려야 해.`,
|
|
499
|
+
].join('\n'));
|
|
500
|
+
}
|
|
501
|
+
const submissionId = submission.id;
|
|
502
|
+
const previousState = submission.attributes?.state ?? 'WAITING_FOR_REVIEW';
|
|
503
|
+
// submitted=false → WAITING_FOR_REVIEW → READY_FOR_REVIEW (version은 PREPARE_FOR_SUBMISSION으로 복귀)
|
|
504
|
+
const patched = await apiPatch(`/reviewSubmissions/${submissionId}`, {
|
|
505
|
+
data: {
|
|
506
|
+
type: 'reviewSubmissions',
|
|
507
|
+
id: submissionId,
|
|
508
|
+
attributes: { submitted: false },
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
const newState = patched?.data?.attributes?.state ?? 'READY_FOR_REVIEW';
|
|
512
|
+
return { submissionId, previousState, newState, versionId };
|
|
513
|
+
}
|
|
467
514
|
// ─── 인앱 구매 (IAP) 생성 ───
|
|
468
515
|
// 2023년 출시된 IAP v2 API (POST /v2/inAppPurchases) 사용.
|
|
469
516
|
// 흐름: (1) IAP draft 생성 → (2) 로컬라이제이션 → (3) priceSchedule → (4) submission(선택).
|
package/dist/auth/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import readline from 'node:readline';
|
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import { startAuth, getStoredTokens, ensureFreshAccessToken, } from './google-auth.js';
|
|
5
5
|
import { AuthError, classifyError } from './errors.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getMcpOAuthClient } from './constants.js';
|
|
7
7
|
const args = process.argv.slice(2);
|
|
8
8
|
const hasFlag = (name) => args.includes(`--${name}`);
|
|
9
9
|
const flagValue = (name) => {
|
|
@@ -158,7 +158,8 @@ async function cmdLogin() {
|
|
|
158
158
|
let url;
|
|
159
159
|
let wait;
|
|
160
160
|
try {
|
|
161
|
-
const
|
|
161
|
+
const { clientId, clientSecret } = await getMcpOAuthClient();
|
|
162
|
+
const r = startAuth(clientId, clientSecret, {
|
|
162
163
|
timeoutMs: timeoutSec * 1000,
|
|
163
164
|
});
|
|
164
165
|
url = r.url;
|
package/dist/auth/constants.d.ts
CHANGED
package/dist/auth/constants.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
const WEB_BASE = process.env.MIMI_SEED_WEB_BASE ?? 'https://mimi-seed.pryzm.gg';
|
|
2
|
+
let _cached = null;
|
|
3
|
+
export async function getMcpOAuthClient() {
|
|
4
|
+
const id = process.env.MIMI_SEED_GOOGLE_CLIENT_ID ??
|
|
5
|
+
process.env.PRESEED_GOOGLE_CLIENT_ID;
|
|
6
|
+
const secret = process.env.MIMI_SEED_GOOGLE_CLIENT_SECRET ??
|
|
7
|
+
process.env.PRESEED_GOOGLE_CLIENT_SECRET;
|
|
8
|
+
if (id && secret)
|
|
9
|
+
return { clientId: id, clientSecret: secret };
|
|
10
|
+
if (_cached)
|
|
11
|
+
return _cached;
|
|
12
|
+
const res = await fetch(`${WEB_BASE}/api/mcp-auth-config`);
|
|
13
|
+
if (!res.ok)
|
|
14
|
+
throw new Error(`mcp-auth-config fetch failed (${res.status})`);
|
|
15
|
+
_cached = (await res.json());
|
|
16
|
+
return _cached;
|
|
17
|
+
}
|
package/dist/auth/google-auth.js
CHANGED
|
@@ -4,7 +4,7 @@ import open from 'open';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
|
-
import {
|
|
7
|
+
import { getMcpOAuthClient } from './constants.js';
|
|
8
8
|
import { AuthError, classifyError } from './errors.js';
|
|
9
9
|
const SCOPES = [
|
|
10
10
|
'https://www.googleapis.com/auth/firebase',
|
|
@@ -285,10 +285,11 @@ export async function ensureFreshAccessToken(marginMs = 60_000) {
|
|
|
285
285
|
},
|
|
286
286
|
};
|
|
287
287
|
}
|
|
288
|
-
// refresh 시도 — credentials.json 우선, 없으면
|
|
288
|
+
// refresh 시도 — credentials.json 우선, 없으면 원격에서 클라이언트 조회
|
|
289
289
|
const stored = getStoredCredentials();
|
|
290
|
-
const clientId =
|
|
291
|
-
const
|
|
290
|
+
const { clientId: defaultId, clientSecret: defaultSecret } = await getMcpOAuthClient();
|
|
291
|
+
const clientId = stored?.clientId ?? defaultId;
|
|
292
|
+
const clientSecret = stored?.clientSecret ?? defaultSecret;
|
|
292
293
|
const client = createOAuth2Client(clientId, clientSecret);
|
|
293
294
|
client.setCredentials({ refresh_token: tokens.refresh_token });
|
|
294
295
|
try {
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
import { JWT } from 'google-auth-library';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* 패키지별 → 레거시(default) 순으로 SA JSON을 로드.
|
|
4
|
+
* 여러 앱이 다른 GCP 프로젝트의 SA를 쓰는 환경 지원.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getServiceAccountJson(packageName?: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* 레거시 호환 — 단일 SA 저장 (default).
|
|
9
|
+
*/
|
|
3
10
|
export declare function saveServiceAccountJson(json: string): void;
|
|
4
|
-
|
|
11
|
+
/**
|
|
12
|
+
* 패키지명에 묶여 SA JSON을 저장. 여러 앱이 다른 GCP 프로젝트일 때 사용.
|
|
13
|
+
* ~/.mimi-seed/play-service-accounts/{packageName}.json
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveServiceAccountJsonForPackage(packageName: string, json: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* 등록된 패키지별 SA + default(레거시) 정보 요약.
|
|
18
|
+
*/
|
|
19
|
+
export declare function listRegisteredServiceAccounts(): {
|
|
20
|
+
perPackage: {
|
|
21
|
+
packageName: string;
|
|
22
|
+
clientEmail: string | null;
|
|
23
|
+
projectId: string | null;
|
|
24
|
+
}[];
|
|
25
|
+
default: {
|
|
26
|
+
clientEmail: string | null;
|
|
27
|
+
projectId: string | null;
|
|
28
|
+
} | null;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 패키지별 SA 삭제. 없으면 false.
|
|
32
|
+
*/
|
|
33
|
+
export declare function deleteServiceAccountJsonForPackage(packageName: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* SA JWT 클라이언트 생성. packageName이 주어지면 패키지별 → default 순으로 탐색.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getServiceAccountClient(packageName?: string): JWT | null;
|
|
@@ -2,25 +2,109 @@ import { JWT } from 'google-auth-library';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
|
|
6
|
+
const SA_DIR = path.join(CONFIG_DIR, 'play-service-accounts');
|
|
7
|
+
const LEGACY_SA_PATH = path.join(CONFIG_DIR, 'play-service-account.json');
|
|
8
|
+
function safeReadFile(p) {
|
|
9
9
|
try {
|
|
10
|
-
return fs.readFileSync(
|
|
10
|
+
return fs.readFileSync(p, 'utf-8');
|
|
11
11
|
}
|
|
12
12
|
catch {
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* 패키지별 → 레거시(default) 순으로 SA JSON을 로드.
|
|
18
|
+
* 여러 앱이 다른 GCP 프로젝트의 SA를 쓰는 환경 지원.
|
|
19
|
+
*/
|
|
20
|
+
export function getServiceAccountJson(packageName) {
|
|
21
|
+
if (packageName) {
|
|
22
|
+
const perPkg = path.join(SA_DIR, `${packageName}.json`);
|
|
23
|
+
if (fs.existsSync(perPkg)) {
|
|
24
|
+
const json = safeReadFile(perPkg);
|
|
25
|
+
if (json)
|
|
26
|
+
return json;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (fs.existsSync(LEGACY_SA_PATH)) {
|
|
30
|
+
const json = safeReadFile(LEGACY_SA_PATH);
|
|
31
|
+
if (json)
|
|
32
|
+
return json;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 레거시 호환 — 단일 SA 저장 (default).
|
|
38
|
+
*/
|
|
16
39
|
export function saveServiceAccountJson(json) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
41
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
42
|
+
fs.writeFileSync(LEGACY_SA_PATH, json, { mode: 0o600 });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 패키지명에 묶여 SA JSON을 저장. 여러 앱이 다른 GCP 프로젝트일 때 사용.
|
|
46
|
+
* ~/.mimi-seed/play-service-accounts/{packageName}.json
|
|
47
|
+
*/
|
|
48
|
+
export function saveServiceAccountJsonForPackage(packageName, json) {
|
|
49
|
+
if (!fs.existsSync(SA_DIR))
|
|
50
|
+
fs.mkdirSync(SA_DIR, { recursive: true });
|
|
51
|
+
const filePath = path.join(SA_DIR, `${packageName}.json`);
|
|
52
|
+
fs.writeFileSync(filePath, json, { mode: 0o600 });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 등록된 패키지별 SA + default(레거시) 정보 요약.
|
|
56
|
+
*/
|
|
57
|
+
export function listRegisteredServiceAccounts() {
|
|
58
|
+
const perPackage = [];
|
|
59
|
+
if (fs.existsSync(SA_DIR)) {
|
|
60
|
+
for (const f of fs.readdirSync(SA_DIR)) {
|
|
61
|
+
if (!f.endsWith('.json'))
|
|
62
|
+
continue;
|
|
63
|
+
const packageName = f.replace(/\.json$/, '');
|
|
64
|
+
const json = safeReadFile(path.join(SA_DIR, f));
|
|
65
|
+
let clientEmail = null;
|
|
66
|
+
let projectId = null;
|
|
67
|
+
if (json) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(json);
|
|
70
|
+
clientEmail = parsed.client_email ?? null;
|
|
71
|
+
projectId = parsed.project_id ?? null;
|
|
72
|
+
}
|
|
73
|
+
catch { /* ignore parse errors */ }
|
|
74
|
+
}
|
|
75
|
+
perPackage.push({ packageName, clientEmail, projectId });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let defaultInfo = null;
|
|
79
|
+
if (fs.existsSync(LEGACY_SA_PATH)) {
|
|
80
|
+
const json = safeReadFile(LEGACY_SA_PATH);
|
|
81
|
+
if (json) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(json);
|
|
84
|
+
defaultInfo = { clientEmail: parsed.client_email ?? null, projectId: parsed.project_id ?? null };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
defaultInfo = { clientEmail: null, projectId: null };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { perPackage, default: defaultInfo };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 패키지별 SA 삭제. 없으면 false.
|
|
95
|
+
*/
|
|
96
|
+
export function deleteServiceAccountJsonForPackage(packageName) {
|
|
97
|
+
const filePath = path.join(SA_DIR, `${packageName}.json`);
|
|
98
|
+
if (!fs.existsSync(filePath))
|
|
99
|
+
return false;
|
|
100
|
+
fs.unlinkSync(filePath);
|
|
101
|
+
return true;
|
|
21
102
|
}
|
|
22
|
-
|
|
23
|
-
|
|
103
|
+
/**
|
|
104
|
+
* SA JWT 클라이언트 생성. packageName이 주어지면 패키지별 → default 순으로 탐색.
|
|
105
|
+
*/
|
|
106
|
+
export function getServiceAccountClient(packageName) {
|
|
107
|
+
const json = getServiceAccountJson(packageName);
|
|
24
108
|
if (!json)
|
|
25
109
|
return null;
|
|
26
110
|
try {
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { getAuthenticatedClient, startAuth, ensureFreshAccessToken } from './auth/google-auth.js';
|
|
6
|
-
import { getServiceAccountClient, getServiceAccountJson } from './auth/playstore-auth.js';
|
|
6
|
+
import { getServiceAccountClient, getServiceAccountJson, saveServiceAccountJsonForPackage, listRegisteredServiceAccounts, deleteServiceAccountJsonForPackage, } from './auth/playstore-auth.js';
|
|
7
7
|
import { getAppStoreCredentials } from './appstore/auth.js';
|
|
8
8
|
import { createGoogleOneTimePurchase, createGoogleSubscription, updateGoogleProduct, deleteGoogleProduct, listGoogleProducts, createAppleOneTimePurchase, createAppleSubscription, updateAppleProduct, deleteAppleProduct, listAppleProducts, } from '@onesub/providers';
|
|
9
|
-
import {
|
|
9
|
+
import { getMcpOAuthClient } from './auth/constants.js';
|
|
10
10
|
import * as firebase from './firebase/tools.js';
|
|
11
11
|
import * as admob from './admob/tools.js';
|
|
12
12
|
import * as playstore from './playstore/tools.js';
|
|
@@ -60,14 +60,14 @@ const APPSTORE_AUTH_HINT = [
|
|
|
60
60
|
'Issuer ID, Key ID, .p8 파일 경로를 입력하면 저장 완료.',
|
|
61
61
|
'그 다음에 다시 물어봐줘.',
|
|
62
62
|
].join('\n');
|
|
63
|
-
function requirePlayStoreAuth() {
|
|
64
|
-
const client = getServiceAccountClient();
|
|
63
|
+
function requirePlayStoreAuth(packageName) {
|
|
64
|
+
const client = getServiceAccountClient(packageName);
|
|
65
65
|
if (!client)
|
|
66
66
|
throw new Error(PLAY_AUTH_HINT);
|
|
67
67
|
return client;
|
|
68
68
|
}
|
|
69
|
-
function requireServiceAccountJson() {
|
|
70
|
-
const json = getServiceAccountJson();
|
|
69
|
+
function requireServiceAccountJson(packageName) {
|
|
70
|
+
const json = getServiceAccountJson(packageName);
|
|
71
71
|
if (!json)
|
|
72
72
|
throw new Error(PLAY_AUTH_HINT);
|
|
73
73
|
return json;
|
|
@@ -301,7 +301,7 @@ server.tool('admob_create_ad_unit', 'AdMob 광고 단위 생성 (v1beta — Limi
|
|
|
301
301
|
// Google Play Store Tools
|
|
302
302
|
// ══════════════════════════════════════════════════
|
|
303
303
|
server.tool('playstore_get_app', 'Google Play 앱 상세 정보 조회', { packageName: z.string().describe('패키지명 (예: com.findthem.app)') }, async ({ packageName }) => {
|
|
304
|
-
const auth = requirePlayStoreAuth();
|
|
304
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
305
305
|
const details = await playstore.getAppDetails(auth, packageName);
|
|
306
306
|
return { content: [{ type: 'text', text: JSON.stringify(details, null, 2) }] };
|
|
307
307
|
});
|
|
@@ -309,7 +309,7 @@ server.tool('playstore_get_listing', 'Google Play 스토어 리스팅 조회 (
|
|
|
309
309
|
packageName: z.string().describe('패키지명'),
|
|
310
310
|
language: z.string().default('ko-KR').describe('언어 코드 (기본: ko-KR)'),
|
|
311
311
|
}, async ({ packageName, language }) => {
|
|
312
|
-
const auth = requirePlayStoreAuth();
|
|
312
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
313
313
|
const listing = await playstore.getListing(auth, packageName, language);
|
|
314
314
|
return { content: [{ type: 'text', text: JSON.stringify(listing, null, 2) }] };
|
|
315
315
|
});
|
|
@@ -320,14 +320,14 @@ server.tool('playstore_update_listing', 'Google Play 스토어 리스팅 수정
|
|
|
320
320
|
shortDescription: z.string().optional().describe('짧은 설명 (80자 이내)'),
|
|
321
321
|
fullDescription: z.string().optional().describe('전체 설명 (4000자 이내)'),
|
|
322
322
|
}, async ({ packageName, language, title, shortDescription, fullDescription }) => {
|
|
323
|
-
const auth = requirePlayStoreAuth();
|
|
323
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
324
324
|
const result = await playstore.updateListing(auth, packageName, language, {
|
|
325
325
|
title, shortDescription, fullDescription,
|
|
326
326
|
});
|
|
327
327
|
return { content: [{ type: 'text', text: `수정 완료:\n${JSON.stringify(result, null, 2)}` }] };
|
|
328
328
|
});
|
|
329
329
|
server.tool('playstore_list_tracks', 'Google Play 릴리스 트랙 현황 (프로덕션/베타/알파/내부)', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
330
|
-
const auth = requirePlayStoreAuth();
|
|
330
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
331
331
|
const tracks = await playstore.listTracks(auth, packageName);
|
|
332
332
|
return { content: [{ type: 'text', text: JSON.stringify(tracks, null, 2) }] };
|
|
333
333
|
});
|
|
@@ -336,7 +336,7 @@ server.tool('playstore_list_images', 'Google Play 리스팅 이미지 목록 조
|
|
|
336
336
|
language: z.string().describe('언어 코드 (예: ko-KR)'),
|
|
337
337
|
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']).describe('이미지 타입'),
|
|
338
338
|
}, async ({ packageName, language, imageType }) => {
|
|
339
|
-
const auth = requirePlayStoreAuth();
|
|
339
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
340
340
|
const images = await playstore.listImages(auth, packageName, language, imageType);
|
|
341
341
|
return { content: [{ type: 'text', text: JSON.stringify(images, null, 2) }] };
|
|
342
342
|
});
|
|
@@ -346,7 +346,7 @@ server.tool('playstore_upload_image', 'Google Play 리스팅 이미지 단일
|
|
|
346
346
|
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
347
347
|
filePath: z.string().describe('업로드할 이미지 절대 경로'),
|
|
348
348
|
}, async ({ packageName, language, imageType, filePath }) => {
|
|
349
|
-
const auth = requirePlayStoreAuth();
|
|
349
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
350
350
|
const result = await playstore.uploadImage(auth, packageName, language, imageType, filePath);
|
|
351
351
|
return { content: [{ type: 'text', text: `✅ 업로드 완료\n${JSON.stringify(result, null, 2)}` }] };
|
|
352
352
|
});
|
|
@@ -355,7 +355,7 @@ server.tool('playstore_delete_all_images', 'Google Play 리스팅 특정 imageTy
|
|
|
355
355
|
language: z.string().describe('언어 코드'),
|
|
356
356
|
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
357
357
|
}, async ({ packageName, language, imageType }) => {
|
|
358
|
-
const auth = requirePlayStoreAuth();
|
|
358
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
359
359
|
const result = await playstore.deleteAllImages(auth, packageName, language, imageType);
|
|
360
360
|
return { content: [{ type: 'text', text: `✅ 전체 삭제\n${JSON.stringify(result, null, 2)}` }] };
|
|
361
361
|
});
|
|
@@ -365,7 +365,7 @@ server.tool('playstore_replace_images', 'Google Play 리스팅 이미지 일괄
|
|
|
365
365
|
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
366
366
|
filePaths: z.array(z.string()).describe('업로드할 이미지 절대 경로 배열 (순서 = 노출 순서)'),
|
|
367
367
|
}, async ({ packageName, language, imageType, filePaths }) => {
|
|
368
|
-
const auth = requirePlayStoreAuth();
|
|
368
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
369
369
|
const result = await playstore.replaceImages(auth, packageName, language, imageType, filePaths);
|
|
370
370
|
return { content: [{ type: 'text', text: `✅ ${result.count}장 교체 완료\n${JSON.stringify(result, null, 2)}` }] };
|
|
371
371
|
});
|
|
@@ -376,7 +376,7 @@ server.tool('playstore_update_release_notes', "Google Play 트랙 릴리스의 '
|
|
|
376
376
|
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
377
377
|
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
378
378
|
}, async ({ packageName, track, versionCode, language, text }) => {
|
|
379
|
-
const auth = requirePlayStoreAuth();
|
|
379
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
380
380
|
const result = await playstore.updateReleaseNotes(auth, packageName, track, versionCode, language, text);
|
|
381
381
|
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode} ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
382
382
|
});
|
|
@@ -386,12 +386,12 @@ server.tool('playstore_update_latest_release_notes', "Google Play 트랙의 최
|
|
|
386
386
|
language: z.string().describe('언어 코드 (예: ko-KR)'),
|
|
387
387
|
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
388
388
|
}, async ({ packageName, track, language, text }) => {
|
|
389
|
-
const auth = requirePlayStoreAuth();
|
|
389
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
390
390
|
const result = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
|
|
391
391
|
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(result.updatedVersionCodes)}) ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
392
392
|
});
|
|
393
393
|
server.tool('playstore_list_reviews', 'Google Play 리뷰 목록 조회', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
394
|
-
const auth = requirePlayStoreAuth();
|
|
394
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
395
395
|
const reviews = await playstore.listReviews(auth, packageName);
|
|
396
396
|
return { content: [{ type: 'text', text: JSON.stringify(reviews, null, 2) }] };
|
|
397
397
|
});
|
|
@@ -400,17 +400,17 @@ server.tool('playstore_reply_review', 'Google Play 리뷰에 답변', {
|
|
|
400
400
|
reviewId: z.string().describe('리뷰 ID'),
|
|
401
401
|
replyText: z.string().describe('답변 내용'),
|
|
402
402
|
}, async ({ packageName, reviewId, replyText }) => {
|
|
403
|
-
const auth = requirePlayStoreAuth();
|
|
403
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
404
404
|
const result = await playstore.replyToReview(auth, packageName, reviewId, replyText);
|
|
405
405
|
return { content: [{ type: 'text', text: `답변 완료:\n${JSON.stringify(result, null, 2)}` }] };
|
|
406
406
|
});
|
|
407
407
|
server.tool('playstore_list_inapp_products', 'Google Play 인앱 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
408
|
-
const auth = requirePlayStoreAuth();
|
|
408
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
409
409
|
const products = await playstore.listInAppProducts(auth, packageName);
|
|
410
410
|
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
|
|
411
411
|
});
|
|
412
412
|
server.tool('playstore_list_subscriptions', 'Google Play 구독 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
413
|
-
const auth = requirePlayStoreAuth();
|
|
413
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
414
414
|
const subs = await playstore.listSubscriptions(auth, packageName);
|
|
415
415
|
return { content: [{ type: 'text', text: JSON.stringify(subs, null, 2) }] };
|
|
416
416
|
});
|
|
@@ -437,7 +437,7 @@ server.tool('playstore_create_onetime_product', [
|
|
|
437
437
|
.optional()
|
|
438
438
|
.describe('추가 지역별 명시 가격. 자동 환산이 부정확한 KRW/JPY 등에 직접 지정.'),
|
|
439
439
|
}, async (args) => {
|
|
440
|
-
const json = requireServiceAccountJson();
|
|
440
|
+
const json = requireServiceAccountJson(args.packageName);
|
|
441
441
|
const result = await createGoogleOneTimePurchase({
|
|
442
442
|
packageName: args.packageName,
|
|
443
443
|
productId: args.productId,
|
|
@@ -488,7 +488,7 @@ server.tool('playstore_create_subscription', [
|
|
|
488
488
|
.optional()
|
|
489
489
|
.describe('추가 지역별 명시 가격'),
|
|
490
490
|
}, async (args) => {
|
|
491
|
-
const json = requireServiceAccountJson();
|
|
491
|
+
const json = requireServiceAccountJson(args.packageName);
|
|
492
492
|
const result = await createGoogleSubscription({
|
|
493
493
|
packageName: args.packageName,
|
|
494
494
|
productId: args.productId,
|
|
@@ -584,6 +584,96 @@ server.tool('playstore_verify_service_account', [
|
|
|
584
584
|
}
|
|
585
585
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
586
586
|
});
|
|
587
|
+
server.tool('playstore_register_service_account', [
|
|
588
|
+
'Google Play 서비스 계정 JSON을 패키지 단위로 등록 (~/.mimi-seed/play-service-accounts/{packageName}.json, 0600 mode).',
|
|
589
|
+
'여러 앱이 서로 다른 GCP 프로젝트의 SA를 쓰는 환경 지원. 등록 후 해당 packageName으로 호출하는 모든 playstore_* 도구가 이 SA를 사용 (등록 안 된 패키지는 ~/.mimi-seed/play-service-account.json 의 default SA로 폴백).',
|
|
590
|
+
'먼저 playstore_verify_service_account 로 권한 확인 후 등록을 권장.',
|
|
591
|
+
].join(' '), {
|
|
592
|
+
packageName: z.string().describe('Android 패키지명 (예: gg.pryzm.weather)'),
|
|
593
|
+
serviceAccountJson: z.string().describe('서비스 계정 JSON 전체 내용 (문자열)'),
|
|
594
|
+
skipVerify: z.boolean().optional().describe('true면 사전 검증 건너뜀 (기본 false: 등록 전 verifyServiceAccountJson 실행)'),
|
|
595
|
+
}, async ({ packageName, serviceAccountJson, skipVerify }) => {
|
|
596
|
+
if (!skipVerify) {
|
|
597
|
+
const verify = await playstore.verifyServiceAccountJson(serviceAccountJson, packageName);
|
|
598
|
+
if (!verify.ok) {
|
|
599
|
+
return {
|
|
600
|
+
content: [{
|
|
601
|
+
type: 'text',
|
|
602
|
+
text: [
|
|
603
|
+
`❌ 검증 실패 (stage: ${verify.stage})로 등록 중단.`,
|
|
604
|
+
verify.message,
|
|
605
|
+
'',
|
|
606
|
+
`검증을 건너뛰고 강제 등록하려면 skipVerify=true 옵션 추가.`,
|
|
607
|
+
].join('\n'),
|
|
608
|
+
}],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
let clientEmail = '';
|
|
613
|
+
let projectId = '';
|
|
614
|
+
try {
|
|
615
|
+
const parsed = JSON.parse(serviceAccountJson);
|
|
616
|
+
clientEmail = parsed.client_email ?? '';
|
|
617
|
+
projectId = parsed.project_id ?? '';
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
return { content: [{ type: 'text', text: '❌ JSON 파싱 실패 — 서비스 계정 JSON 형식이 올바르지 않음.' }] };
|
|
621
|
+
}
|
|
622
|
+
saveServiceAccountJsonForPackage(packageName, serviceAccountJson);
|
|
623
|
+
return {
|
|
624
|
+
content: [{
|
|
625
|
+
type: 'text',
|
|
626
|
+
text: [
|
|
627
|
+
`✓ ${packageName} 서비스 계정 등록 완료`,
|
|
628
|
+
'',
|
|
629
|
+
`**clientEmail**: \`${clientEmail}\``,
|
|
630
|
+
`**projectId**: \`${projectId}\``,
|
|
631
|
+
`**저장 경로**: \`~/.mimi-seed/play-service-accounts/${packageName}.json\` (0600)`,
|
|
632
|
+
'',
|
|
633
|
+
'이후 이 packageName으로 호출하는 모든 playstore_* 도구가 자동으로 이 SA 사용.',
|
|
634
|
+
].join('\n'),
|
|
635
|
+
}],
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
server.tool('playstore_list_service_accounts', '등록된 패키지별 서비스 계정 + default(레거시) SA 정보 요약. clientEmail / projectId 만 노출 (private_key 미노출).', {}, async () => {
|
|
639
|
+
const info = listRegisteredServiceAccounts();
|
|
640
|
+
const lines = [];
|
|
641
|
+
if (info.default) {
|
|
642
|
+
lines.push('**Default (legacy)**: `~/.mimi-seed/play-service-account.json`');
|
|
643
|
+
lines.push(` - clientEmail: \`${info.default.clientEmail ?? '(parse error)'}\``);
|
|
644
|
+
lines.push(` - projectId: \`${info.default.projectId ?? '(parse error)'}\``);
|
|
645
|
+
lines.push('');
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
lines.push('**Default (legacy)**: 미등록');
|
|
649
|
+
lines.push('');
|
|
650
|
+
}
|
|
651
|
+
if (info.perPackage.length === 0) {
|
|
652
|
+
lines.push('**Per-package**: 없음');
|
|
653
|
+
lines.push('');
|
|
654
|
+
lines.push('등록 방법: `playstore_register_service_account(packageName, serviceAccountJson)`');
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
lines.push(`**Per-package** (${info.perPackage.length}개):`);
|
|
658
|
+
for (const item of info.perPackage) {
|
|
659
|
+
lines.push(`- \`${item.packageName}\` → \`${item.clientEmail ?? '(parse error)'}\` (project: \`${item.projectId ?? 'unknown'}\`)`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
663
|
+
});
|
|
664
|
+
server.tool('playstore_delete_service_account', '등록된 패키지별 서비스 계정 삭제. default(레거시) SA는 영향 없음.', {
|
|
665
|
+
packageName: z.string().describe('삭제할 패키지명'),
|
|
666
|
+
}, async ({ packageName }) => {
|
|
667
|
+
const deleted = deleteServiceAccountJsonForPackage(packageName);
|
|
668
|
+
return {
|
|
669
|
+
content: [{
|
|
670
|
+
type: 'text',
|
|
671
|
+
text: deleted
|
|
672
|
+
? `✓ ${packageName} 서비스 계정 파일 삭제 완료. 이후 이 패키지는 default SA로 폴백.`
|
|
673
|
+
: `(skip) ${packageName} 등록된 패키지별 SA 없음.`,
|
|
674
|
+
}],
|
|
675
|
+
};
|
|
676
|
+
});
|
|
587
677
|
// ══════════════════════════════════════════════════
|
|
588
678
|
// Google Cloud IAM Tools
|
|
589
679
|
// ══════════════════════════════════════════════════
|
|
@@ -831,15 +921,17 @@ server.tool('appstore_update_whats_new', "App Store '이 버전의 새로운 기
|
|
|
831
921
|
const result = await appstore.updateVersionWhatsNew(versionId, locale, { whatsNew });
|
|
832
922
|
return { content: [{ type: 'text', text: `✅ ${locale} 로캘의 What's New가 업데이트됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
833
923
|
});
|
|
834
|
-
server.tool('appstore_update_review_notes', "App Store 심사 리뷰어 노트(Notes for App Review) 등록/수정. versionId 버전에 appStoreReviewDetail.notes를 PATCH하거나 없으면 POST로 생성. 심사 시 리뷰어에게 전달되는 테스트 계정·기능 안내 텍스트 작성에 사용.", {
|
|
924
|
+
server.tool('appstore_update_review_notes', "App Store 심사 리뷰어 노트(Notes for App Review) 등록/수정. versionId 버전에 appStoreReviewDetail.notes를 PATCH하거나 없으면 POST로 생성. 심사 시 리뷰어에게 전달되는 테스트 계정·기능 안내 텍스트 작성에 사용. 4000자 권장 한도.", {
|
|
835
925
|
versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
|
|
836
|
-
notes: z.string().describe('리뷰어에게 전달할 메모 (테스트 계정, 주요 변경사항, 접근 방법 등). 4000자
|
|
926
|
+
notes: z.string().min(1).max(4000).describe('리뷰어에게 전달할 메모 (테스트 계정, 주요 변경사항, 접근 방법 등). 4000자 이내.'),
|
|
837
927
|
}, async ({ versionId, notes }) => {
|
|
838
928
|
const result = await appstore.updateReviewNotes(versionId, notes);
|
|
929
|
+
const action = result.created ? 'created' : 'updated';
|
|
930
|
+
const summary = `✅ 리뷰어 노트 ${result.created ? '신규 등록' : '수정'} 완료 (reviewDetailId: ${result.reviewDetailId})`;
|
|
839
931
|
return {
|
|
840
932
|
content: [{
|
|
841
933
|
type: 'text',
|
|
842
|
-
text:
|
|
934
|
+
text: `${summary}\n\n${JSON.stringify({ ok: true, action, ...result }, null, 2)}`,
|
|
843
935
|
}],
|
|
844
936
|
};
|
|
845
937
|
});
|
|
@@ -848,12 +940,18 @@ server.tool('appstore_get_review_notes', "App Store 심사 리뷰어 노트(Note
|
|
|
848
940
|
}, async ({ versionId }) => {
|
|
849
941
|
const result = await appstore.getReviewNotes(versionId);
|
|
850
942
|
if (!result.reviewDetailId) {
|
|
851
|
-
return {
|
|
943
|
+
return {
|
|
944
|
+
content: [{
|
|
945
|
+
type: 'text',
|
|
946
|
+
text: `이 버전에는 아직 리뷰어 노트가 없어. appstore_update_review_notes로 등록해줘.\n\n${JSON.stringify({ ok: true, exists: false }, null, 2)}`,
|
|
947
|
+
}],
|
|
948
|
+
};
|
|
852
949
|
}
|
|
950
|
+
const summary = `reviewDetailId: ${result.reviewDetailId}\ncontactEmail: ${result.contactEmail ?? '(없음)'}\n\n노트:\n${result.notes ?? '(비어있음)'}`;
|
|
853
951
|
return {
|
|
854
952
|
content: [{
|
|
855
953
|
type: 'text',
|
|
856
|
-
text:
|
|
954
|
+
text: `${summary}\n\n${JSON.stringify({ ok: true, exists: true, ...result }, null, 2)}`,
|
|
857
955
|
}],
|
|
858
956
|
};
|
|
859
957
|
});
|
|
@@ -1049,7 +1147,7 @@ server.tool('appstore_create_subscription', [
|
|
|
1049
1147
|
server.tool('playstore_list_products', 'Google Play의 모든 IAP 상품(구독 + 일회성) 통합 조회. productId / name / status / type / price / currency 반환.', {
|
|
1050
1148
|
packageName: z.string().describe('패키지명'),
|
|
1051
1149
|
}, async ({ packageName }) => {
|
|
1052
|
-
const json = requireServiceAccountJson();
|
|
1150
|
+
const json = requireServiceAccountJson(packageName);
|
|
1053
1151
|
const products = await listGoogleProducts({ packageName, serviceAccountKey: json });
|
|
1054
1152
|
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
|
|
1055
1153
|
});
|
|
@@ -1059,7 +1157,7 @@ server.tool('playstore_update_product', 'Google Play IAP 상품의 표시 이름
|
|
|
1059
1157
|
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
1060
1158
|
name: z.string().describe('새 표시 이름'),
|
|
1061
1159
|
}, async ({ packageName, productId, productType, name }) => {
|
|
1062
|
-
const json = requireServiceAccountJson();
|
|
1160
|
+
const json = requireServiceAccountJson(packageName);
|
|
1063
1161
|
const result = await updateGoogleProduct({
|
|
1064
1162
|
packageName, productId, productType, name, serviceAccountKey: json,
|
|
1065
1163
|
});
|
|
@@ -1073,7 +1171,7 @@ server.tool('playstore_delete_product', '⚠️ 비가역. Google Play IAP 상
|
|
|
1073
1171
|
productId: z.string().describe('상품 ID'),
|
|
1074
1172
|
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
1075
1173
|
}, async ({ packageName, productId, productType }) => {
|
|
1076
|
-
const json = requireServiceAccountJson();
|
|
1174
|
+
const json = requireServiceAccountJson(packageName);
|
|
1077
1175
|
const result = await deleteGoogleProduct({
|
|
1078
1176
|
packageName, productId, productType, serviceAccountKey: json,
|
|
1079
1177
|
});
|
|
@@ -1146,7 +1244,7 @@ server.tool('playstore_plan_release', [
|
|
|
1146
1244
|
track: z.enum(['production', 'beta', 'alpha', 'internal']).optional().describe('대상 트랙 (기본: production)'),
|
|
1147
1245
|
language: z.string().optional().describe('점검할 리스팅 언어 (기본: ko-KR)'),
|
|
1148
1246
|
}, async ({ packageName, versionCode, track, language }) => {
|
|
1149
|
-
const auth = requirePlayStoreAuth();
|
|
1247
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
1150
1248
|
const text = await buildPlayStoreReleasePlan({
|
|
1151
1249
|
auth,
|
|
1152
1250
|
packageName,
|
|
@@ -1183,6 +1281,29 @@ server.tool('appstore_submit_for_review', [
|
|
|
1183
1281
|
const result = await appstore.submitVersionForReview(versionId);
|
|
1184
1282
|
return { content: [{ type: 'text', text: `✅ 버전 ${versionId} 심사 제출 완료 (state: ${result.state}). App Store Connect에서 진행 상태 확인 가능.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1185
1283
|
});
|
|
1284
|
+
// --- App Store 심사 철회 (Cancel Review) ---
|
|
1285
|
+
server.tool('appstore_cancel_review', [
|
|
1286
|
+
'App Store 심사 제출을 철회 — WAITING_FOR_REVIEW 상태에서만 가능.',
|
|
1287
|
+
'submitted=false PATCH → reviewSubmission이 READY_FOR_REVIEW로 복귀, 버전은 PREPARE_FOR_SUBMISSION 상태로 돌아가 메타데이터/빌드 수정 가능.',
|
|
1288
|
+
'⚠️ IN_REVIEW 이상이면 Apple API가 409로 거부함 — 이 경우 App Store Connect 웹에서 직접 처리하거나 심사 결과를 기다려야 함.',
|
|
1289
|
+
'철회 후 수정 완료 시 appstore_submit_for_review로 재제출 가능.',
|
|
1290
|
+
].join(' '), {
|
|
1291
|
+
versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
|
|
1292
|
+
}, async ({ versionId }) => {
|
|
1293
|
+
const result = await appstore.cancelVersionReview(versionId);
|
|
1294
|
+
return {
|
|
1295
|
+
content: [{
|
|
1296
|
+
type: 'text',
|
|
1297
|
+
text: [
|
|
1298
|
+
`✅ 심사 철회 완료`,
|
|
1299
|
+
` submissionId: ${result.submissionId}`,
|
|
1300
|
+
` ${result.previousState} → ${result.newState}`,
|
|
1301
|
+
` 버전 ${result.versionId}이(가) PREPARE_FOR_SUBMISSION 상태로 복귀됨.`,
|
|
1302
|
+
` 메타데이터/빌드 수정 후 appstore_submit_for_review로 재제출 가능.`,
|
|
1303
|
+
].join('\n'),
|
|
1304
|
+
}],
|
|
1305
|
+
};
|
|
1306
|
+
});
|
|
1186
1307
|
// --- Play Store 심사 제출 / release status 변경 ---
|
|
1187
1308
|
server.tool('playstore_submit_release', [
|
|
1188
1309
|
'Google Play 트랙의 release status를 변경 — 일반적으로 draft → completed로 바꿔 검토/배포 큐에 진입시킬 때 사용.',
|
|
@@ -1195,7 +1316,7 @@ server.tool('playstore_submit_release', [
|
|
|
1195
1316
|
versionCode: z.string().describe('대상 versionCode (문자열)'),
|
|
1196
1317
|
status: z.enum(['draft', 'inProgress', 'completed', 'halted']).optional().describe('새 status (기본: completed)'),
|
|
1197
1318
|
}, async ({ packageName, track, versionCode, status }) => {
|
|
1198
|
-
const auth = requirePlayStoreAuth();
|
|
1319
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
1199
1320
|
const result = await playstore.submitRelease(auth, packageName, track, versionCode, status);
|
|
1200
1321
|
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode}: ${result.previousStatus} → ${result.newStatus}\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1201
1322
|
});
|
|
@@ -1221,7 +1342,7 @@ server.tool('playstore_promote_release', [
|
|
|
1221
1342
|
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
1222
1343
|
})).optional().describe('대상 트랙용 릴리스 노트 (미지정 + copyReleaseNotes=true면 source 그대로)'),
|
|
1223
1344
|
}, async ({ packageName, fromTrack, toTrack, versionCode, status, userFraction, releaseName, copyReleaseNotes, releaseNotes }) => {
|
|
1224
|
-
const auth = requirePlayStoreAuth();
|
|
1345
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
1225
1346
|
const result = await playstore.promoteRelease(auth, packageName, fromTrack, toTrack, versionCode, {
|
|
1226
1347
|
status,
|
|
1227
1348
|
userFraction,
|
|
@@ -1239,7 +1360,8 @@ server.tool('mimi_seed_auth_start', [
|
|
|
1239
1360
|
'이후 playstore_*, firebase_*, admob_* 등 다른 MCP 도구 바로 호출 가능.',
|
|
1240
1361
|
'토큰 만료(invalid_rapt) / 재인증 필요 시 사용. 10분 내 완료해야 함.',
|
|
1241
1362
|
].join(' '), {}, async () => {
|
|
1242
|
-
const {
|
|
1363
|
+
const { clientId, clientSecret } = await getMcpOAuthClient();
|
|
1364
|
+
const { url, wait } = startAuth(clientId, clientSecret);
|
|
1243
1365
|
// fire-and-forget — 토큰은 콜백 서버가 자동 저장
|
|
1244
1366
|
wait.then(() => { }, (err) => { console.error('[mimi-seed auth]', err.message); });
|
|
1245
1367
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoonion/mimi-seed-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Cursor / any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"mimi-seed-playstore-auth": "dist/auth/playstore-setup-cli.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"dist"
|
|
13
|
+
"dist",
|
|
14
|
+
"LICENSE"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"build": "tsc",
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"publishConfig": {
|
|
39
40
|
"access": "public"
|
|
40
41
|
},
|
|
41
|
-
"license": "
|
|
42
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
42
43
|
"engines": {
|
|
43
44
|
"node": ">=20"
|
|
44
45
|
},
|