@yoonion/mimi-seed-mcp 0.3.5 → 0.3.7

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/README.md CHANGED
@@ -56,6 +56,7 @@ export ANTHROPIC_API_KEY=sk-ant-...
56
56
  | Firebase | `firebase_list_projects` / `firebase_create_android_app` / `firebase_get_android_config` / `firebase_enable_service` |
57
57
  | AdMob | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
58
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` |
59
60
  | Google Cloud IAM | `iam_list_service_accounts` / `iam_create_service_account` / `iam_list_keys` / `iam_create_key` / `iam_add_iam_policy_binding` |
60
61
  | App Store Connect | `appstore_list_apps` / `appstore_list_builds` / `appstore_upload_screenshot` / `appstore_update_whats_new` |
61
62
  | 제출 위험 점검 | `playstore_check_submission_risks` / `appstore_check_submission_risks` |
@@ -55,3 +55,69 @@ export declare function submitVersionForReview(versionId: string): Promise<{
55
55
  itemAttached: boolean;
56
56
  state: any;
57
57
  }>;
58
+ export type AppleIapType = 'CONSUMABLE' | 'NON_CONSUMABLE' | 'NON_RENEWING_SUBSCRIPTION';
59
+ export interface CreateInAppPurchaseInput {
60
+ appId: string;
61
+ productId: string;
62
+ referenceName: string;
63
+ inAppPurchaseType: AppleIapType;
64
+ reviewNote?: string;
65
+ familySharable?: boolean;
66
+ locale?: string;
67
+ displayName: string;
68
+ description: string;
69
+ priceUsd: number;
70
+ baseTerritory?: string;
71
+ autoSubmit?: boolean;
72
+ }
73
+ export type FailedStep = 'localization' | 'priceSchedule' | 'submission';
74
+ interface CreatedIapSummary {
75
+ iapId: string;
76
+ productId: string;
77
+ state?: string;
78
+ localizationId?: string;
79
+ priceScheduleId?: string;
80
+ submissionId?: string;
81
+ submissionState?: string;
82
+ failedStep?: FailedStep;
83
+ error?: string;
84
+ priceMatchWarning?: string;
85
+ consoleUrl: string;
86
+ }
87
+ export declare function createInAppPurchase(input: CreateInAppPurchaseInput): Promise<CreatedIapSummary>;
88
+ export interface CreateSubscriptionGroupInput {
89
+ appId: string;
90
+ referenceName: string;
91
+ }
92
+ export interface CreateSubscriptionInput {
93
+ appId: string;
94
+ groupReferenceName: string;
95
+ productId: string;
96
+ referenceName: string;
97
+ subscriptionPeriod: 'ONE_WEEK' | 'ONE_MONTH' | 'TWO_MONTHS' | 'THREE_MONTHS' | 'SIX_MONTHS' | 'ONE_YEAR';
98
+ reviewNote?: string;
99
+ familySharable?: boolean;
100
+ groupLevel?: number;
101
+ locale?: string;
102
+ displayName: string;
103
+ description: string;
104
+ priceUsd: number;
105
+ baseTerritory?: string;
106
+ autoSubmit?: boolean;
107
+ }
108
+ interface CreatedSubscriptionSummary {
109
+ subscriptionId: string;
110
+ groupId: string;
111
+ productId: string;
112
+ state?: string;
113
+ localizationId?: string;
114
+ pricesCreated?: boolean;
115
+ submissionId?: string;
116
+ submissionState?: string;
117
+ failedStep?: FailedStep;
118
+ error?: string;
119
+ priceMatchWarning?: string;
120
+ consoleUrl: string;
121
+ }
122
+ export declare function createAutoRenewableSubscription(input: CreateSubscriptionInput): Promise<CreatedSubscriptionSummary>;
123
+ export {};
@@ -374,3 +374,302 @@ export async function submitVersionForReview(versionId) {
374
374
  state: submitted?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW',
375
375
  };
376
376
  }
377
+ // ─── 인앱 구매 (IAP) 생성 ───
378
+ // 2023년 출시된 IAP v2 API (POST /v2/inAppPurchases) 사용.
379
+ // 흐름: (1) IAP draft 생성 → (2) 로컬라이제이션 → (3) priceSchedule → (4) submission(선택).
380
+ // 가격은 territory별 pricePoint ID 기반 — IAP를 만든 뒤 GET /pricePoints로 조회해 매칭.
381
+ // 자동 제출(submission)은 권장 가이드라인 (스크린샷·리뷰 노트 등)이 충족돼야 통과.
382
+ const IAP_V2_BASE = 'https://api.appstoreconnect.apple.com/v2';
383
+ async function apiPostV2(path, body) {
384
+ const headers = getAuthHeaders();
385
+ if (!headers) {
386
+ throw new Error('App Store Connect 인증 필요 — npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth');
387
+ }
388
+ const res = await fetch(`${IAP_V2_BASE}${path}`, {
389
+ method: 'POST',
390
+ headers: { ...headers, 'Content-Type': 'application/json' },
391
+ body: JSON.stringify(body),
392
+ });
393
+ if (!res.ok) {
394
+ const text = await res.text();
395
+ throw new Error(`App Store v2 ${res.status}: ${text}`);
396
+ }
397
+ const text = await res.text();
398
+ return text ? JSON.parse(text) : { ok: true };
399
+ }
400
+ async function findClosestPricePoint(resourceUrl, territory, targetPrice) {
401
+ // resourceUrl 예: '/inAppPurchases/{id}/pricePoints' 또는 '/subscriptions/{id}/pricePoints'
402
+ const data = await apiGet(resourceUrl, {
403
+ 'filter[territory]': territory,
404
+ 'limit': '200',
405
+ });
406
+ const points = (data?.data ?? []);
407
+ let best = null;
408
+ let bestDiff = Infinity;
409
+ for (const p of points) {
410
+ const priceStr = p.attributes?.customerPrice;
411
+ if (!priceStr)
412
+ continue;
413
+ const price = parseFloat(priceStr);
414
+ if (Number.isNaN(price))
415
+ continue;
416
+ const diff = Math.abs(price - targetPrice);
417
+ if (diff < bestDiff) {
418
+ bestDiff = diff;
419
+ best = { id: p.id, customerPrice: price };
420
+ }
421
+ }
422
+ return best;
423
+ }
424
+ const PRICE_MATCH_WARN_THRESHOLD = 0.10;
425
+ function priceMatchWarning(targetPrice, matched) {
426
+ const diff = Math.abs(matched - targetPrice);
427
+ if (diff < PRICE_MATCH_WARN_THRESHOLD)
428
+ return undefined;
429
+ return `요청 $${targetPrice} → 매칭 $${matched.toFixed(2)} (Apple tier 제약). 의도와 다르면 Console에서 수정.`;
430
+ }
431
+ export async function createInAppPurchase(input) {
432
+ const locale = input.locale ?? 'en-US';
433
+ const baseTerritory = input.baseTerritory ?? 'USA';
434
+ // 1) IAP draft 생성 (v2 endpoint)
435
+ const created = await apiPostV2('/inAppPurchases', {
436
+ data: {
437
+ type: 'inAppPurchases',
438
+ attributes: {
439
+ name: input.referenceName,
440
+ productId: input.productId,
441
+ inAppPurchaseType: input.inAppPurchaseType,
442
+ reviewNote: input.reviewNote,
443
+ familySharable: input.familySharable ?? false,
444
+ },
445
+ relationships: {
446
+ app: { data: { type: 'apps', id: input.appId } },
447
+ },
448
+ },
449
+ });
450
+ const iapId = created?.data?.id;
451
+ if (!iapId)
452
+ throw new Error(`IAP 생성 응답에 id 없음: ${JSON.stringify(created)}`);
453
+ const consoleUrl = `https://appstoreconnect.apple.com/apps/${input.appId}/distribution/iaps/${iapId}`;
454
+ const summary = {
455
+ iapId,
456
+ productId: input.productId,
457
+ state: created?.data?.attributes?.state,
458
+ consoleUrl,
459
+ };
460
+ // 2) 로컬라이제이션
461
+ try {
462
+ const loc = await apiPost('/inAppPurchaseLocalizations', {
463
+ data: {
464
+ type: 'inAppPurchaseLocalizations',
465
+ attributes: {
466
+ locale,
467
+ name: input.displayName,
468
+ description: input.description,
469
+ },
470
+ relationships: {
471
+ inAppPurchaseV2: { data: { type: 'inAppPurchases', id: iapId } },
472
+ },
473
+ },
474
+ });
475
+ summary.localizationId = loc?.data?.id;
476
+ }
477
+ catch (err) {
478
+ summary.failedStep = 'localization';
479
+ summary.error = err.message;
480
+ return summary;
481
+ }
482
+ // 3) priceSchedule (territory pricePoint 매칭 후 생성)
483
+ try {
484
+ const matched = await findClosestPricePoint(`/inAppPurchases/${iapId}/pricePoints`, baseTerritory, input.priceUsd);
485
+ if (!matched) {
486
+ summary.failedStep = 'priceSchedule';
487
+ summary.error = `${baseTerritory}에서 가격 point를 찾지 못함 — Console에서 수동 설정 필요.`;
488
+ return summary;
489
+ }
490
+ summary.priceMatchWarning = priceMatchWarning(input.priceUsd, matched.customerPrice);
491
+ const priceRefId = '${INAPP_PRICE}';
492
+ const sched = await apiPost('/inAppPurchasePriceSchedules', {
493
+ data: {
494
+ type: 'inAppPurchasePriceSchedules',
495
+ relationships: {
496
+ inAppPurchase: { data: { type: 'inAppPurchases', id: iapId } },
497
+ baseTerritory: { data: { type: 'territories', id: baseTerritory } },
498
+ manualPrices: { data: [{ type: 'inAppPurchasePrices', id: priceRefId }] },
499
+ },
500
+ },
501
+ included: [
502
+ {
503
+ type: 'inAppPurchasePrices',
504
+ id: priceRefId,
505
+ attributes: { startDate: null },
506
+ relationships: {
507
+ inAppPurchasePricePoint: {
508
+ data: { type: 'inAppPurchasePricePoints', id: matched.id },
509
+ },
510
+ inAppPurchaseV2: { data: { type: 'inAppPurchases', id: iapId } },
511
+ territory: { data: { type: 'territories', id: baseTerritory } },
512
+ },
513
+ },
514
+ ],
515
+ });
516
+ summary.priceScheduleId = sched?.data?.id;
517
+ }
518
+ catch (err) {
519
+ summary.failedStep = 'priceSchedule';
520
+ summary.error = err.message;
521
+ return summary;
522
+ }
523
+ // 4) 자동 제출 (옵션) — 실패해도 draft는 유지
524
+ if (input.autoSubmit !== false) {
525
+ try {
526
+ const sub = await apiPost('/inAppPurchaseSubmissions', {
527
+ data: {
528
+ type: 'inAppPurchaseSubmissions',
529
+ relationships: {
530
+ inAppPurchaseV2: { data: { type: 'inAppPurchases', id: iapId } },
531
+ },
532
+ },
533
+ });
534
+ summary.submissionId = sub?.data?.id;
535
+ summary.submissionState = sub?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW';
536
+ }
537
+ catch (err) {
538
+ summary.failedStep = 'submission';
539
+ summary.error = err.message;
540
+ }
541
+ }
542
+ return summary;
543
+ }
544
+ async function findOrCreateSubscriptionGroup(appId, referenceName) {
545
+ // 기존 그룹 검색
546
+ const existing = await apiGet(`/apps/${appId}/subscriptionGroups`, {
547
+ 'fields[subscriptionGroups]': 'referenceName',
548
+ 'limit': '200',
549
+ });
550
+ const found = (existing?.data ?? []).find((g) => g.attributes?.referenceName === referenceName);
551
+ if (found)
552
+ return found.id;
553
+ const created = await apiPost('/subscriptionGroups', {
554
+ data: {
555
+ type: 'subscriptionGroups',
556
+ attributes: { referenceName },
557
+ relationships: {
558
+ app: { data: { type: 'apps', id: appId } },
559
+ },
560
+ },
561
+ });
562
+ const id = created?.data?.id;
563
+ if (!id)
564
+ throw new Error(`subscriptionGroup 생성 실패: ${JSON.stringify(created)}`);
565
+ return id;
566
+ }
567
+ export async function createAutoRenewableSubscription(input) {
568
+ const locale = input.locale ?? 'en-US';
569
+ const baseTerritory = input.baseTerritory ?? 'USA';
570
+ // 1) group 찾거나 생성
571
+ const groupId = await findOrCreateSubscriptionGroup(input.appId, input.groupReferenceName);
572
+ // 2) subscription draft 생성
573
+ // groupLevel은 그룹 내 unique여야 함 — 미지정 시 attribute 자체를 안 보내고 Apple이 자동 부여하게.
574
+ const subAttributes = {
575
+ name: input.referenceName,
576
+ productId: input.productId,
577
+ subscriptionPeriod: input.subscriptionPeriod,
578
+ familySharable: input.familySharable ?? false,
579
+ };
580
+ if (input.reviewNote !== undefined)
581
+ subAttributes.reviewNote = input.reviewNote;
582
+ if (input.groupLevel !== undefined)
583
+ subAttributes.groupLevel = input.groupLevel;
584
+ const created = await apiPost('/subscriptions', {
585
+ data: {
586
+ type: 'subscriptions',
587
+ attributes: subAttributes,
588
+ relationships: {
589
+ group: { data: { type: 'subscriptionGroups', id: groupId } },
590
+ },
591
+ },
592
+ });
593
+ const subscriptionId = created?.data?.id;
594
+ if (!subscriptionId) {
595
+ throw new Error(`subscription 생성 응답에 id 없음: ${JSON.stringify(created)}`);
596
+ }
597
+ const consoleUrl = `https://appstoreconnect.apple.com/apps/${input.appId}/distribution/subscriptions/${subscriptionId}`;
598
+ const summary = {
599
+ subscriptionId,
600
+ groupId,
601
+ productId: input.productId,
602
+ state: created?.data?.attributes?.state,
603
+ consoleUrl,
604
+ };
605
+ // 3) localization
606
+ try {
607
+ const loc = await apiPost('/subscriptionLocalizations', {
608
+ data: {
609
+ type: 'subscriptionLocalizations',
610
+ attributes: {
611
+ locale,
612
+ name: input.displayName,
613
+ description: input.description,
614
+ },
615
+ relationships: {
616
+ subscription: { data: { type: 'subscriptions', id: subscriptionId } },
617
+ },
618
+ },
619
+ });
620
+ summary.localizationId = loc?.data?.id;
621
+ }
622
+ catch (err) {
623
+ summary.failedStep = 'localization';
624
+ summary.error = err.message;
625
+ return summary;
626
+ }
627
+ // 4) price (subscriptionPrices — IAP의 priceSchedule보다 단순)
628
+ try {
629
+ const matched = await findClosestPricePoint(`/subscriptions/${subscriptionId}/pricePoints`, baseTerritory, input.priceUsd);
630
+ if (!matched) {
631
+ summary.failedStep = 'priceSchedule';
632
+ summary.error = `${baseTerritory}에서 가격 point를 찾지 못함 — Console에서 수동 설정 필요.`;
633
+ return summary;
634
+ }
635
+ summary.priceMatchWarning = priceMatchWarning(input.priceUsd, matched.customerPrice);
636
+ await apiPost('/subscriptionPrices', {
637
+ data: {
638
+ type: 'subscriptionPrices',
639
+ relationships: {
640
+ subscription: { data: { type: 'subscriptions', id: subscriptionId } },
641
+ subscriptionPricePoint: {
642
+ data: { type: 'subscriptionPricePoints', id: matched.id },
643
+ },
644
+ territory: { data: { type: 'territories', id: baseTerritory } },
645
+ },
646
+ },
647
+ });
648
+ summary.pricesCreated = true;
649
+ }
650
+ catch (err) {
651
+ summary.failedStep = 'priceSchedule';
652
+ summary.error = err.message;
653
+ return summary;
654
+ }
655
+ // 5) auto-submit
656
+ if (input.autoSubmit !== false) {
657
+ try {
658
+ const sub = await apiPost('/subscriptionSubmissions', {
659
+ data: {
660
+ type: 'subscriptionSubmissions',
661
+ relationships: {
662
+ subscription: { data: { type: 'subscriptions', id: subscriptionId } },
663
+ },
664
+ },
665
+ });
666
+ summary.submissionId = sub?.data?.id;
667
+ summary.submissionState = sub?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW';
668
+ }
669
+ catch (err) {
670
+ summary.failedStep = 'submission';
671
+ summary.error = err.message;
672
+ }
673
+ }
674
+ return summary;
675
+ }
@@ -0,0 +1,4 @@
1
+ import { JWT } from 'google-auth-library';
2
+ export declare function getServiceAccountJson(): string | null;
3
+ export declare function saveServiceAccountJson(json: string): void;
4
+ export declare function getServiceAccountClient(): JWT | null;
@@ -0,0 +1,37 @@
1
+ import { JWT } from 'google-auth-library';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ const SA_PATH = path.join(os.homedir(), '.mimi-seed', 'play-service-account.json');
6
+ export function getServiceAccountJson() {
7
+ if (!fs.existsSync(SA_PATH))
8
+ return null;
9
+ try {
10
+ return fs.readFileSync(SA_PATH, 'utf-8');
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function saveServiceAccountJson(json) {
17
+ const dir = path.dirname(SA_PATH);
18
+ if (!fs.existsSync(dir))
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ fs.writeFileSync(SA_PATH, json, { mode: 0o600 });
21
+ }
22
+ export function getServiceAccountClient() {
23
+ const json = getServiceAccountJson();
24
+ if (!json)
25
+ return null;
26
+ try {
27
+ const parsed = JSON.parse(json);
28
+ return new JWT({
29
+ email: parsed.client_email,
30
+ key: parsed.private_key,
31
+ scopes: ['https://www.googleapis.com/auth/androidpublisher'],
32
+ });
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import { saveServiceAccountJson, getServiceAccountJson } from './playstore-auth.js';
3
+ import readline from 'node:readline';
4
+ import fs from 'node:fs';
5
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6
+ const ask = (q) => new Promise((r) => rl.question(q, r));
7
+ async function main() {
8
+ console.log('');
9
+ console.log(' 🤖 Mimi Seed — Google Play 서비스 계정 연결');
10
+ console.log('');
11
+ const existing = getServiceAccountJson();
12
+ if (existing) {
13
+ try {
14
+ const parsed = JSON.parse(existing);
15
+ console.log(` ✅ 이미 연결됨 (${parsed.client_email ?? '?'})`);
16
+ }
17
+ catch {
18
+ console.log(' ⚠️ 저장된 서비스 계정이 있지만 파싱 오류');
19
+ }
20
+ const answer = await ask(' 다시 설정할래? (y/N): ');
21
+ if (answer.toLowerCase() !== 'y') {
22
+ rl.close();
23
+ return;
24
+ }
25
+ }
26
+ console.log(' Google Play 서비스 계정 키 JSON 파일이 필요해:');
27
+ console.log(' 1. Google Cloud Console → IAM & Admin → Service Accounts');
28
+ console.log(' 2. 서비스 계정 선택 → Keys → Add Key → Create new key → JSON');
29
+ console.log(' 3. 다운로드한 JSON 파일 경로를 입력해');
30
+ console.log('');
31
+ const jsonPath = await ask(' 서비스 계정 JSON 파일 경로: ');
32
+ const trimmedPath = jsonPath.trim().replace(/^["']|["']$/g, '');
33
+ if (!fs.existsSync(trimmedPath)) {
34
+ console.log(` ❌ 파일 없음: ${trimmedPath}`);
35
+ rl.close();
36
+ process.exit(1);
37
+ }
38
+ const json = fs.readFileSync(trimmedPath, 'utf-8');
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(json);
42
+ }
43
+ catch {
44
+ console.log(' ❌ JSON 파싱 실패 — 올바른 서비스 계정 키 파일인지 확인해줘');
45
+ rl.close();
46
+ process.exit(1);
47
+ }
48
+ if (parsed.type !== 'service_account' || !parsed.client_email || !parsed.private_key) {
49
+ console.log(' ❌ 서비스 계정 JSON 형식이 아니야.');
50
+ console.log(' type="service_account", client_email, private_key 필드가 있어야 해.');
51
+ rl.close();
52
+ process.exit(1);
53
+ }
54
+ saveServiceAccountJson(json);
55
+ console.log('');
56
+ console.log(` ✅ 저장 완료! (${parsed.client_email})`);
57
+ console.log('');
58
+ console.log(' 이제 Claude Code에서:');
59
+ console.log(' "내 Play 스토어 앱 리스팅 보여줘"');
60
+ console.log(' "Play 구독 상품 목록 보여줘"');
61
+ console.log('');
62
+ console.log(' ⚠️ Play Console 권한 확인:');
63
+ console.log(' Play Console → Users and permissions → 이 서비스 계정 추가');
64
+ console.log(' 앱 권한에서 "View financial data" + "Manage store listing" 체크');
65
+ console.log('');
66
+ rl.close();
67
+ }
68
+ main();
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@ 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, getStoredTokens, startAuth } from './auth/google-auth.js';
6
+ import { getServiceAccountClient, getServiceAccountJson } from './auth/playstore-auth.js';
7
+ import { getAppStoreCredentials } from './appstore/auth.js';
8
+ import { createGoogleOneTimePurchase, createGoogleSubscription, createAppleOneTimePurchase, createAppleSubscription } from '@onesub/providers';
6
9
  import { MIMI_SEED_CLIENT_ID, MIMI_SEED_CLIENT_SECRET } from './auth/constants.js';
7
10
  import * as firebase from './firebase/tools.js';
8
11
  import * as admob from './admob/tools.js';
@@ -35,6 +38,44 @@ function requireAuth() {
35
38
  }
36
39
  return client;
37
40
  }
41
+ const PLAY_AUTH_HINT = [
42
+ '❌ Google Play 서비스 계정이 연결되지 않았어.',
43
+ '',
44
+ '터미널에서 이것만 실행하면 돼:',
45
+ '',
46
+ ' npx -y @yoonion/mimi-seed-mcp mimi-seed-playstore-auth',
47
+ '',
48
+ '서비스 계정 JSON 파일 경로를 입력하면 저장 완료.',
49
+ '그 다음에 다시 물어봐줘.',
50
+ ].join('\n');
51
+ const APPSTORE_AUTH_HINT = [
52
+ '❌ App Store Connect 인증이 설정되지 않았어.',
53
+ '',
54
+ '터미널에서 이것만 실행하면 돼:',
55
+ '',
56
+ ' npx -y @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
57
+ '',
58
+ 'Issuer ID, Key ID, .p8 파일 경로를 입력하면 저장 완료.',
59
+ '그 다음에 다시 물어봐줘.',
60
+ ].join('\n');
61
+ function requirePlayStoreAuth() {
62
+ const client = getServiceAccountClient();
63
+ if (!client)
64
+ throw new Error(PLAY_AUTH_HINT);
65
+ return client;
66
+ }
67
+ function requireServiceAccountJson() {
68
+ const json = getServiceAccountJson();
69
+ if (!json)
70
+ throw new Error(PLAY_AUTH_HINT);
71
+ return json;
72
+ }
73
+ function requireAppStoreCreds() {
74
+ const creds = getAppStoreCredentials();
75
+ if (!creds)
76
+ throw new Error(APPSTORE_AUTH_HINT);
77
+ return creds;
78
+ }
38
79
  // ══════════════════════════════════════════════════
39
80
  // Firebase Tools
40
81
  // ══════════════════════════════════════════════════
@@ -258,7 +299,7 @@ server.tool('admob_create_ad_unit', 'AdMob 광고 단위 생성 (v1beta — Limi
258
299
  // Google Play Store Tools
259
300
  // ══════════════════════════════════════════════════
260
301
  server.tool('playstore_get_app', 'Google Play 앱 상세 정보 조회', { packageName: z.string().describe('패키지명 (예: com.findthem.app)') }, async ({ packageName }) => {
261
- const auth = requireAuth();
302
+ const auth = requirePlayStoreAuth();
262
303
  const details = await playstore.getAppDetails(auth, packageName);
263
304
  return { content: [{ type: 'text', text: JSON.stringify(details, null, 2) }] };
264
305
  });
@@ -266,7 +307,7 @@ server.tool('playstore_get_listing', 'Google Play 스토어 리스팅 조회 (
266
307
  packageName: z.string().describe('패키지명'),
267
308
  language: z.string().default('ko-KR').describe('언어 코드 (기본: ko-KR)'),
268
309
  }, async ({ packageName, language }) => {
269
- const auth = requireAuth();
310
+ const auth = requirePlayStoreAuth();
270
311
  const listing = await playstore.getListing(auth, packageName, language);
271
312
  return { content: [{ type: 'text', text: JSON.stringify(listing, null, 2) }] };
272
313
  });
@@ -277,14 +318,14 @@ server.tool('playstore_update_listing', 'Google Play 스토어 리스팅 수정
277
318
  shortDescription: z.string().optional().describe('짧은 설명 (80자 이내)'),
278
319
  fullDescription: z.string().optional().describe('전체 설명 (4000자 이내)'),
279
320
  }, async ({ packageName, language, title, shortDescription, fullDescription }) => {
280
- const auth = requireAuth();
321
+ const auth = requirePlayStoreAuth();
281
322
  const result = await playstore.updateListing(auth, packageName, language, {
282
323
  title, shortDescription, fullDescription,
283
324
  });
284
325
  return { content: [{ type: 'text', text: `수정 완료:\n${JSON.stringify(result, null, 2)}` }] };
285
326
  });
286
327
  server.tool('playstore_list_tracks', 'Google Play 릴리스 트랙 현황 (프로덕션/베타/알파/내부)', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
287
- const auth = requireAuth();
328
+ const auth = requirePlayStoreAuth();
288
329
  const tracks = await playstore.listTracks(auth, packageName);
289
330
  return { content: [{ type: 'text', text: JSON.stringify(tracks, null, 2) }] };
290
331
  });
@@ -293,7 +334,7 @@ server.tool('playstore_list_images', 'Google Play 리스팅 이미지 목록 조
293
334
  language: z.string().describe('언어 코드 (예: ko-KR)'),
294
335
  imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']).describe('이미지 타입'),
295
336
  }, async ({ packageName, language, imageType }) => {
296
- const auth = requireAuth();
337
+ const auth = requirePlayStoreAuth();
297
338
  const images = await playstore.listImages(auth, packageName, language, imageType);
298
339
  return { content: [{ type: 'text', text: JSON.stringify(images, null, 2) }] };
299
340
  });
@@ -303,7 +344,7 @@ server.tool('playstore_upload_image', 'Google Play 리스팅 이미지 단일
303
344
  imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
304
345
  filePath: z.string().describe('업로드할 이미지 절대 경로'),
305
346
  }, async ({ packageName, language, imageType, filePath }) => {
306
- const auth = requireAuth();
347
+ const auth = requirePlayStoreAuth();
307
348
  const result = await playstore.uploadImage(auth, packageName, language, imageType, filePath);
308
349
  return { content: [{ type: 'text', text: `✅ 업로드 완료\n${JSON.stringify(result, null, 2)}` }] };
309
350
  });
@@ -312,7 +353,7 @@ server.tool('playstore_delete_all_images', 'Google Play 리스팅 특정 imageTy
312
353
  language: z.string().describe('언어 코드'),
313
354
  imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
314
355
  }, async ({ packageName, language, imageType }) => {
315
- const auth = requireAuth();
356
+ const auth = requirePlayStoreAuth();
316
357
  const result = await playstore.deleteAllImages(auth, packageName, language, imageType);
317
358
  return { content: [{ type: 'text', text: `✅ 전체 삭제\n${JSON.stringify(result, null, 2)}` }] };
318
359
  });
@@ -322,7 +363,7 @@ server.tool('playstore_replace_images', 'Google Play 리스팅 이미지 일괄
322
363
  imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
323
364
  filePaths: z.array(z.string()).describe('업로드할 이미지 절대 경로 배열 (순서 = 노출 순서)'),
324
365
  }, async ({ packageName, language, imageType, filePaths }) => {
325
- const auth = requireAuth();
366
+ const auth = requirePlayStoreAuth();
326
367
  const result = await playstore.replaceImages(auth, packageName, language, imageType, filePaths);
327
368
  return { content: [{ type: 'text', text: `✅ ${result.count}장 교체 완료\n${JSON.stringify(result, null, 2)}` }] };
328
369
  });
@@ -333,7 +374,7 @@ server.tool('playstore_update_release_notes', "Google Play 트랙 릴리스의 '
333
374
  language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
334
375
  text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
335
376
  }, async ({ packageName, track, versionCode, language, text }) => {
336
- const auth = requireAuth();
377
+ const auth = requirePlayStoreAuth();
337
378
  const result = await playstore.updateReleaseNotes(auth, packageName, track, versionCode, language, text);
338
379
  return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode} ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
339
380
  });
@@ -343,12 +384,12 @@ server.tool('playstore_update_latest_release_notes', "Google Play 트랙의 최
343
384
  language: z.string().describe('언어 코드 (예: ko-KR)'),
344
385
  text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
345
386
  }, async ({ packageName, track, language, text }) => {
346
- const auth = requireAuth();
387
+ const auth = requirePlayStoreAuth();
347
388
  const result = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
348
389
  return { content: [{ type: 'text', text: `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(result.updatedVersionCodes)}) ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
349
390
  });
350
391
  server.tool('playstore_list_reviews', 'Google Play 리뷰 목록 조회', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
351
- const auth = requireAuth();
392
+ const auth = requirePlayStoreAuth();
352
393
  const reviews = await playstore.listReviews(auth, packageName);
353
394
  return { content: [{ type: 'text', text: JSON.stringify(reviews, null, 2) }] };
354
395
  });
@@ -357,20 +398,122 @@ server.tool('playstore_reply_review', 'Google Play 리뷰에 답변', {
357
398
  reviewId: z.string().describe('리뷰 ID'),
358
399
  replyText: z.string().describe('답변 내용'),
359
400
  }, async ({ packageName, reviewId, replyText }) => {
360
- const auth = requireAuth();
401
+ const auth = requirePlayStoreAuth();
361
402
  const result = await playstore.replyToReview(auth, packageName, reviewId, replyText);
362
403
  return { content: [{ type: 'text', text: `답변 완료:\n${JSON.stringify(result, null, 2)}` }] };
363
404
  });
364
405
  server.tool('playstore_list_inapp_products', 'Google Play 인앱 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
365
- const auth = requireAuth();
406
+ const auth = requirePlayStoreAuth();
366
407
  const products = await playstore.listInAppProducts(auth, packageName);
367
408
  return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
368
409
  });
369
410
  server.tool('playstore_list_subscriptions', 'Google Play 구독 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
370
- const auth = requireAuth();
411
+ const auth = requirePlayStoreAuth();
371
412
  const subs = await playstore.listSubscriptions(auth, packageName);
372
413
  return { content: [{ type: 'text', text: JSON.stringify(subs, null, 2) }] };
373
414
  });
415
+ server.tool('playstore_create_onetime_product', [
416
+ 'Google Play에 일회성 인앱 상품(소비성 또는 비소비성)을 생성.',
417
+ 'Play Console 권한: "Manage store presence" 필요. 생성 후 Console에서 활성화 필요.',
418
+ ].join(' '), {
419
+ packageName: z.string().describe('패키지명 (예: com.example.app)'),
420
+ productId: z
421
+ .string()
422
+ .describe('상품 ID (소문자/숫자/언더스코어/점, 예: premium_unlock). 한 번 정하면 변경 불가.'),
423
+ name: z.string().describe('상품 이름 (스토어 노출 제목)'),
424
+ price: z.number().int().describe('주 통화 기준 가격 (최소 단위: USD/EUR이면 cents, KRW/JPY이면 원화 정수. 예: USD $4.99 → 499, KRW ₩5,900 → 5900)'),
425
+ currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
426
+ type: z
427
+ .enum(['consumable', 'non_consumable'])
428
+ .default('non_consumable')
429
+ .describe('상품 유형 (소비성/비소비성). 앱이 consumePurchase 호출하면 소비성.'),
430
+ extraRegions: z
431
+ .array(z.object({
432
+ currency: z.string().describe('ISO 4217 통화 코드 (예: KRW, JPY, GBP)'),
433
+ price: z.number().describe('가격 (최소 단위: KRW ₩1,100 → 1100, USD $0.99 → 99)'),
434
+ }))
435
+ .optional()
436
+ .describe('추가 지역별 명시 가격. 자동 환산이 부정확한 KRW/JPY 등에 직접 지정.'),
437
+ }, async (args) => {
438
+ const json = requireServiceAccountJson();
439
+ const result = await createGoogleOneTimePurchase({
440
+ packageName: args.packageName,
441
+ productId: args.productId,
442
+ name: args.name,
443
+ price: args.price,
444
+ currency: args.currency,
445
+ type: args.type,
446
+ ...(args.extraRegions && { extraRegions: args.extraRegions }),
447
+ serviceAccountKey: json,
448
+ });
449
+ if (!result.success) {
450
+ return { content: [{ type: 'text', text: `❌ 상품 생성 실패: ${result.error}` }] };
451
+ }
452
+ return {
453
+ content: [{
454
+ type: 'text',
455
+ text: [
456
+ `✓ Play 일회성 상품 생성 완료`,
457
+ `productId: ${result.productId}`,
458
+ `price: ${args.price} ${args.currency}`,
459
+ '',
460
+ 'Play Console에서 활성화 확인:',
461
+ `https://play.google.com/console/u/0/developers/-/app/-/managed-products?package=${encodeURIComponent(args.packageName)}`,
462
+ ].join('\n'),
463
+ }],
464
+ };
465
+ });
466
+ server.tool('playstore_create_subscription', [
467
+ 'Google Play에 자동 갱신 구독을 생성하고 baseplan을 활성화.',
468
+ '구독 생성 → baseplan(가격·주기) 추가 → 자동 활성화.',
469
+ '이미 같은 productId가 있으면 생성 실패 (Play API 특성상 upsert 미지원).',
470
+ ].join(' '), {
471
+ packageName: z.string().describe('패키지명'),
472
+ productId: z
473
+ .string()
474
+ .describe('구독 상품 ID (소문자/숫자/언더스코어/점, 예: premium_monthly)'),
475
+ name: z.string().describe('구독 제목 (스토어 노출)'),
476
+ price: z.number().int().describe('주 통화 기준 가격 (최소 단위: USD cents. 예: $4.99 → 499, ₩5,900 → 5900)'),
477
+ currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
478
+ period: z
479
+ .enum(['monthly', 'yearly'])
480
+ .describe('청구 주기'),
481
+ extraRegions: z
482
+ .array(z.object({
483
+ currency: z.string().describe('ISO 4217 통화 코드 (예: KRW, JPY)'),
484
+ price: z.number().describe('가격 (최소 단위)'),
485
+ }))
486
+ .optional()
487
+ .describe('추가 지역별 명시 가격'),
488
+ }, async (args) => {
489
+ const json = requireServiceAccountJson();
490
+ const result = await createGoogleSubscription({
491
+ packageName: args.packageName,
492
+ productId: args.productId,
493
+ name: args.name,
494
+ price: args.price,
495
+ currency: args.currency,
496
+ period: args.period,
497
+ ...(args.extraRegions && { extraRegions: args.extraRegions }),
498
+ serviceAccountKey: json,
499
+ });
500
+ if (!result.success) {
501
+ return { content: [{ type: 'text', text: `❌ 구독 생성 실패: ${result.error}` }] };
502
+ }
503
+ return {
504
+ content: [{
505
+ type: 'text',
506
+ text: [
507
+ `✓ Play 구독 생성 완료`,
508
+ `productId: ${result.productId}`,
509
+ `price: ${args.price} ${args.currency} / ${args.period}`,
510
+ '',
511
+ 'Play Console:',
512
+ `https://play.google.com/console/u/0/developers/-/app/-/subscriptions?package=${encodeURIComponent(args.packageName)}`,
513
+ ].join('\n'),
514
+ }],
515
+ };
516
+ });
374
517
  server.tool('playstore_verify_service_account', [
375
518
  "서비스 계정 JSON이 주어진 packageName에 대해 Play Developer API 호출 가능한지 + 'View financial data' 권한까지 있는지 검증.",
376
519
  'onesub 같은 서버가 Play 영수증을 백그라운드로 검증하려면 OAuth 토큰 대신 서비스 계정 JSON이 필요 — 이 도구로 붙여넣기 전에 유효성 확인.',
@@ -688,6 +831,129 @@ server.tool('appstore_reply_review', [
688
831
  const result = await appstore.createReviewResponse(reviewId, responseBody);
689
832
  return { content: [{ type: 'text', text: `✅ 리뷰 ${reviewId}에 답변 등록됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
690
833
  });
834
+ // --- App Store IAP / 구독 생성 ---
835
+ server.tool('appstore_create_inapp_purchase', [
836
+ 'App Store에 일회성 인앱 구매(IAP)를 생성 — CONSUMABLE (소비성) / NON_CONSUMABLE (비소비성).',
837
+ '생성 후 App Store Connect에서 스크린샷·리뷰 노트를 추가해야 심사 제출 가능.',
838
+ ].join(' '), {
839
+ appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과의 id, 숫자형)'),
840
+ productId: z
841
+ .string()
842
+ .describe('상품 ID (글로벌 unique 권장: 예 com.example.coins_100)'),
843
+ name: z.string().describe('상품 이름 (스토어 노출, 최대 30자)'),
844
+ price: z.number().int().describe('가격 (최소 단위: USD cents. 예: $0.99 → 99, ₩1,100 → 1100)'),
845
+ currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
846
+ type: z
847
+ .enum(['consumable', 'non_consumable'])
848
+ .default('non_consumable')
849
+ .describe('IAP 유형 (소비성/비소비성)'),
850
+ extraRegions: z
851
+ .array(z.object({
852
+ currency: z.string().describe('ISO 4217 통화 코드 (예: KRW)'),
853
+ price: z.number().describe('가격 (최소 단위)'),
854
+ }))
855
+ .optional()
856
+ .describe('추가 지역별 명시 가격'),
857
+ bundleId: z.string().optional().describe('번들 ID (appId 대신 사용 가능)'),
858
+ }, async (args) => {
859
+ const creds = requireAppStoreCreds();
860
+ const result = await createAppleOneTimePurchase({
861
+ appId: args.appId,
862
+ bundleId: args.bundleId,
863
+ productId: args.productId,
864
+ name: args.name,
865
+ price: args.price,
866
+ currency: args.currency,
867
+ type: args.type,
868
+ ...(args.extraRegions && { extraRegions: args.extraRegions }),
869
+ keyId: creds.keyId,
870
+ issuerId: creds.issuerId,
871
+ privateKey: creds.privateKey,
872
+ });
873
+ if (!result.success) {
874
+ const hint = result.errorType === 'DUPLICATE'
875
+ ? '\n이미 같은 productId가 존재해. App Store Connect에서 확인해줘.'
876
+ : result.errorType === 'PRICE_NOT_FOUND'
877
+ ? `\n가장 가까운 가격: ${JSON.stringify(result.priceNearest)}`
878
+ : '';
879
+ return { content: [{ type: 'text', text: `❌ IAP 생성 실패: ${result.error}${hint}` }] };
880
+ }
881
+ return {
882
+ content: [{
883
+ type: 'text',
884
+ text: [
885
+ `✓ App Store IAP 생성 완료`,
886
+ `productId: ${result.productId}`,
887
+ `internalId: ${result.internalId}`,
888
+ result.priceSet ? `✓ 가격 설정됨` : `⚠ 가격 미설정 (가장 가까운 가격: ${JSON.stringify(result.priceNearest)})`,
889
+ result.extraRegionsSet?.length ? `✓ 추가 지역: ${result.extraRegionsSet.join(', ')}` : '',
890
+ '',
891
+ '스크린샷·리뷰 노트를 추가한 후 App Store Connect에서 심사 제출:',
892
+ `https://appstoreconnect.apple.com/apps/${args.appId}/distribution/iaps`,
893
+ ].filter(Boolean).join('\n'),
894
+ }],
895
+ };
896
+ });
897
+ server.tool('appstore_create_subscription', [
898
+ 'App Store에 자동 갱신 구독을 생성 — Subscription Group 자동 생성 포함.',
899
+ '생성 후 App Store Connect에서 스크린샷·리뷰 노트를 추가해야 심사 제출 가능.',
900
+ ].join(' '), {
901
+ appId: z.string().describe('App Store 앱 ID'),
902
+ productId: z.string().describe('구독 productId (예: com.example.premium.monthly)'),
903
+ name: z.string().describe('구독 이름 (스토어 노출)'),
904
+ price: z.number().int().describe('가격 (최소 단위: USD cents. 예: $9.99 → 999, ₩9,900 → 9900)'),
905
+ currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
906
+ period: z
907
+ .enum(['monthly', 'yearly'])
908
+ .describe('구독 주기'),
909
+ extraRegions: z
910
+ .array(z.object({
911
+ currency: z.string().describe('ISO 4217 통화 코드 (예: KRW)'),
912
+ price: z.number().describe('가격 (최소 단위)'),
913
+ }))
914
+ .optional()
915
+ .describe('추가 지역별 명시 가격'),
916
+ bundleId: z.string().optional().describe('번들 ID (appId 대신 사용 가능)'),
917
+ }, async (args) => {
918
+ const creds = requireAppStoreCreds();
919
+ const result = await createAppleSubscription({
920
+ appId: args.appId,
921
+ bundleId: args.bundleId,
922
+ productId: args.productId,
923
+ name: args.name,
924
+ price: args.price,
925
+ currency: args.currency,
926
+ period: args.period,
927
+ ...(args.extraRegions && { extraRegions: args.extraRegions }),
928
+ keyId: creds.keyId,
929
+ issuerId: creds.issuerId,
930
+ privateKey: creds.privateKey,
931
+ });
932
+ if (!result.success) {
933
+ const hint = result.errorType === 'DUPLICATE'
934
+ ? '\n이미 같은 productId가 존재해. App Store Connect에서 확인해줘.'
935
+ : result.errorType === 'PRICE_NOT_FOUND'
936
+ ? `\n가장 가까운 가격: ${JSON.stringify(result.priceNearest)}`
937
+ : '';
938
+ return { content: [{ type: 'text', text: `❌ 구독 생성 실패: ${result.error}${hint}` }] };
939
+ }
940
+ return {
941
+ content: [{
942
+ type: 'text',
943
+ text: [
944
+ `✓ App Store 구독 생성 완료`,
945
+ `productId: ${result.productId}`,
946
+ `internalId: ${result.internalId}`,
947
+ result.priceSet ? `✓ 가격 설정됨` : `⚠ 가격 미설정 (가장 가까운 가격: ${JSON.stringify(result.priceNearest)})`,
948
+ result.extraRegionsSet?.length ? `✓ 추가 지역: ${result.extraRegionsSet.join(', ')}` : '',
949
+ result.localizationAdded ? '✓ KRW 한국어 로컬라이제이션 추가됨' : '',
950
+ '',
951
+ 'App Store Connect에서 심사 제출:',
952
+ `https://appstoreconnect.apple.com/apps/${args.appId}/distribution/subscriptions`,
953
+ ].filter(Boolean).join('\n'),
954
+ }],
955
+ };
956
+ });
691
957
  // --- App Store 심사 제출 (Submit for Review) ---
692
958
  server.tool('appstore_submit_for_review', [
693
959
  'App Store 버전을 심사에 제출 — 새 reviewSubmissions API 사용 (옛 /appStoreVersionSubmissions는 2024-01 deprecated).',
@@ -714,7 +980,7 @@ server.tool('playstore_submit_release', [
714
980
  versionCode: z.string().describe('대상 versionCode (문자열)'),
715
981
  status: z.enum(['draft', 'inProgress', 'completed', 'halted']).optional().describe('새 status (기본: completed)'),
716
982
  }, async ({ packageName, track, versionCode, status }) => {
717
- const auth = requireAuth();
983
+ const auth = requirePlayStoreAuth();
718
984
  const result = await playstore.submitRelease(auth, packageName, track, versionCode, status);
719
985
  return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode}: ${result.previousStatus} → ${result.newStatus}\n\n${JSON.stringify(result, null, 2)}` }] };
720
986
  });
@@ -740,7 +1006,7 @@ server.tool('playstore_promote_release', [
740
1006
  text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
741
1007
  })).optional().describe('대상 트랙용 릴리스 노트 (미지정 + copyReleaseNotes=true면 source 그대로)'),
742
1008
  }, async ({ packageName, fromTrack, toTrack, versionCode, status, userFraction, releaseName, copyReleaseNotes, releaseNotes }) => {
743
- const auth = requireAuth();
1009
+ const auth = requirePlayStoreAuth();
744
1010
  const result = await playstore.promoteRelease(auth, packageName, fromTrack, toTrack, versionCode, {
745
1011
  status,
746
1012
  userFraction,
@@ -1,4 +1,5 @@
1
1
  import type { OAuth2Client } from 'google-auth-library';
2
+ import { JWT } from 'google-auth-library';
2
3
  export type PlayImageType = 'featureGraphic' | 'icon' | 'phoneScreenshots' | 'promoGraphic' | 'sevenInchScreenshots' | 'tenInchScreenshots' | 'tvBanner' | 'tvScreenshots' | 'wearScreenshots';
3
4
  /**
4
5
  * Google Play Developer API (Android Publisher API v3) 래퍼
@@ -7,15 +8,15 @@ export type PlayImageType = 'featureGraphic' | 'icon' | 'phoneScreenshots' | 'pr
7
8
  * 여기서는 기존 앱의 메타데이터, 빌드, 출시를 관리.
8
9
  */
9
10
  export declare const publisher: () => import("googleapis").androidpublisher_v3.Androidpublisher;
10
- export declare function withEdit<T>(auth: OAuth2Client, packageName: string, fn: (editId: string) => Promise<T>, commit?: boolean): Promise<T>;
11
- export declare function getAppDetails(auth: OAuth2Client, packageName: string): Promise<import("googleapis").androidpublisher_v3.Schema$AppDetails>;
12
- export declare function getListing(auth: OAuth2Client, packageName: string, language?: string): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
13
- export declare function updateListing(auth: OAuth2Client, packageName: string, language: string, data: {
11
+ export declare function withEdit<T>(auth: OAuth2Client | JWT, packageName: string, fn: (editId: string) => Promise<T>, commit?: boolean): Promise<T>;
12
+ export declare function getAppDetails(auth: OAuth2Client | JWT, packageName: string): Promise<import("googleapis").androidpublisher_v3.Schema$AppDetails>;
13
+ export declare function getListing(auth: OAuth2Client | JWT, packageName: string, language?: string): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
14
+ export declare function updateListing(auth: OAuth2Client | JWT, packageName: string, language: string, data: {
14
15
  title?: string;
15
16
  shortDescription?: string;
16
17
  fullDescription?: string;
17
18
  }): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
18
- export declare function listTracks(auth: OAuth2Client, packageName: string): Promise<{
19
+ export declare function listTracks(auth: OAuth2Client | JWT, packageName: string): Promise<{
19
20
  track: string | null | undefined;
20
21
  releases: {
21
22
  name: string | null | undefined;
@@ -24,20 +25,20 @@ export declare function listTracks(auth: OAuth2Client, packageName: string): Pro
24
25
  releaseNotes: import("googleapis").androidpublisher_v3.Schema$LocalizedText[] | undefined;
25
26
  }[];
26
27
  }[]>;
27
- export declare function updateReleaseNotes(auth: OAuth2Client, packageName: string, track: string, versionCode: string, language: string, text: string): Promise<import("googleapis").androidpublisher_v3.Schema$Track>;
28
+ export declare function updateReleaseNotes(auth: OAuth2Client | JWT, packageName: string, track: string, versionCode: string, language: string, text: string): Promise<import("googleapis").androidpublisher_v3.Schema$Track>;
28
29
  /**
29
30
  * 최신 release (versionCode 최대값)의 releaseNotes[language]를 교체/추가.
30
31
  * versionCode를 모를 때 편의용.
31
32
  */
32
- export declare function updateLatestReleaseNotes(auth: OAuth2Client, packageName: string, track: string, language: string, text: string): Promise<{
33
+ export declare function updateLatestReleaseNotes(auth: OAuth2Client | JWT, packageName: string, track: string, language: string, text: string): Promise<{
33
34
  updatedVersionCodes: string[] | null | undefined;
34
35
  updatedReleaseName: string | null | undefined;
35
36
  releases?: import("googleapis").androidpublisher_v3.Schema$TrackRelease[];
36
37
  track?: string | null;
37
38
  }>;
38
- export declare function listImages(auth: OAuth2Client, packageName: string, language: string, imageType: PlayImageType): Promise<import("googleapis").androidpublisher_v3.Schema$Image[]>;
39
- export declare function uploadImage(auth: OAuth2Client, packageName: string, language: string, imageType: PlayImageType, filePath: string): Promise<import("googleapis").androidpublisher_v3.Schema$Image | undefined>;
40
- export declare function deleteAllImages(auth: OAuth2Client, packageName: string, language: string, imageType: PlayImageType): Promise<{
39
+ export declare function listImages(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType): Promise<import("googleapis").androidpublisher_v3.Schema$Image[]>;
40
+ export declare function uploadImage(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType, filePath: string): Promise<import("googleapis").androidpublisher_v3.Schema$Image | undefined>;
41
+ export declare function deleteAllImages(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType): Promise<{
41
42
  ok: boolean;
42
43
  imageType: PlayImageType;
43
44
  }>;
@@ -45,7 +46,7 @@ export declare function deleteAllImages(auth: OAuth2Client, packageName: string,
45
46
  * 한 edit 세션 내에서 기존 이미지 전체 삭제 + 새 이미지 순서대로 업로드 + commit.
46
47
  * phoneScreenshots처럼 여러 장 교체 시 효율적 (단일 edit).
47
48
  */
48
- export declare function replaceImages(auth: OAuth2Client, packageName: string, language: string, imageType: PlayImageType, filePaths: string[]): Promise<{
49
+ export declare function replaceImages(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType, filePaths: string[]): Promise<{
49
50
  imageType: PlayImageType;
50
51
  count: number;
51
52
  uploaded: {
@@ -54,7 +55,7 @@ export declare function replaceImages(auth: OAuth2Client, packageName: string, l
54
55
  sha256?: string | null;
55
56
  }[];
56
57
  }>;
57
- export declare function submitRelease(auth: OAuth2Client, packageName: string, track: string, versionCode: string, status?: 'completed' | 'draft' | 'inProgress' | 'halted'): Promise<{
58
+ export declare function submitRelease(auth: OAuth2Client | JWT, packageName: string, track: string, versionCode: string, status?: 'completed' | 'draft' | 'inProgress' | 'halted'): Promise<{
58
59
  track: string;
59
60
  versionCode: string;
60
61
  previousStatus: string | null | undefined;
@@ -72,7 +73,7 @@ export interface PromoteReleaseOptions {
72
73
  }>;
73
74
  copyReleaseNotes?: boolean;
74
75
  }
75
- export declare function promoteRelease(auth: OAuth2Client, packageName: string, fromTrack: string, toTrack: string, versionCode: string, options?: PromoteReleaseOptions): Promise<{
76
+ export declare function promoteRelease(auth: OAuth2Client | JWT, packageName: string, fromTrack: string, toTrack: string, versionCode: string, options?: PromoteReleaseOptions): Promise<{
76
77
  packageName: string;
77
78
  fromTrack: string;
78
79
  toTrack: string;
@@ -84,7 +85,7 @@ export declare function promoteRelease(auth: OAuth2Client, packageName: string,
84
85
  committed: boolean;
85
86
  result: import("googleapis").androidpublisher_v3.Schema$Track;
86
87
  }>;
87
- export declare function listReviews(auth: OAuth2Client, packageName: string): Promise<{
88
+ export declare function listReviews(auth: OAuth2Client | JWT, packageName: string): Promise<{
88
89
  reviewId: string | null | undefined;
89
90
  authorName: string | null | undefined;
90
91
  comments: {
@@ -94,13 +95,13 @@ export declare function listReviews(auth: OAuth2Client, packageName: string): Pr
94
95
  deviceMetadata: string | null | undefined;
95
96
  }[] | undefined;
96
97
  }[]>;
97
- export declare function replyToReview(auth: OAuth2Client, packageName: string, reviewId: string, replyText: string): Promise<import("googleapis").androidpublisher_v3.Schema$ReviewsReplyResponse>;
98
- export declare function listInAppProducts(auth: OAuth2Client, packageName: string): Promise<{
98
+ export declare function replyToReview(auth: OAuth2Client | JWT, packageName: string, reviewId: string, replyText: string): Promise<import("googleapis").androidpublisher_v3.Schema$ReviewsReplyResponse>;
99
+ export declare function listInAppProducts(auth: OAuth2Client | JWT, packageName: string): Promise<{
99
100
  productId: any;
100
101
  listings: any;
101
102
  purchaseOptions: any;
102
103
  }[]>;
103
- export declare function listSubscriptions(auth: OAuth2Client, packageName: string): Promise<{
104
+ export declare function listSubscriptions(auth: OAuth2Client | JWT, packageName: string): Promise<{
104
105
  productId: string | null | undefined;
105
106
  basePlans: import("googleapis").androidpublisher_v3.Schema$BasePlan[] | undefined;
106
107
  listings: import("googleapis").androidpublisher_v3.Schema$SubscriptionListing[] | undefined;
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
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": {
7
7
  "mimi-seed-mcp": "dist/index.js",
8
8
  "mimi-seed-auth": "dist/auth/cli.js",
9
- "mimi-seed-appstore-auth": "dist/appstore/setup-cli.js"
9
+ "mimi-seed-appstore-auth": "dist/appstore/setup-cli.js",
10
+ "mimi-seed-playstore-auth": "dist/auth/playstore-setup-cli.js"
10
11
  },
11
12
  "files": [
12
13
  "dist"
@@ -44,6 +45,7 @@
44
45
  "dependencies": {
45
46
  "@anthropic-ai/sdk": "^0.52.0",
46
47
  "@modelcontextprotocol/sdk": "^1.12.1",
48
+ "@onesub/providers": "^0.2.0",
47
49
  "googleapis": "^171.4.0",
48
50
  "jsonwebtoken": "^9.0.3",
49
51
  "open": "^10.1.0",