@yoonion/mimi-seed-mcp 0.1.0 → 0.3.0

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/dist/index.js CHANGED
@@ -2,11 +2,18 @@
2
2
  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
- import { getAuthenticatedClient, getStoredTokens } from './auth/google-auth.js';
5
+ import { getAuthenticatedClient, getStoredTokens, startAuth } from './auth/google-auth.js';
6
+ import { MIMI_SEED_CLIENT_ID, MIMI_SEED_CLIENT_SECRET } from './auth/constants.js';
6
7
  import * as firebase from './firebase/tools.js';
7
8
  import * as admob from './admob/tools.js';
8
9
  import * as playstore from './playstore/tools.js';
9
10
  import * as appstore from './appstore/tools.js';
11
+ import * as appstoreScreenshots from './appstore/screenshots.js';
12
+ import * as iam from './iam/tools.js';
13
+ import { checkPlayStoreRisks, checkAppStoreRisks, formatRisks } from './checks/risks.js';
14
+ import { validateAppStoreScreenshots, validatePlayStoreScreenshots, formatValidationResults } from './checks/screenshots.js';
15
+ import { generateReleaseNotesFromCommits, formatGeneratedNotes } from './ai/notes.js';
16
+ import { generateReviewReply, formatReviewReply } from './ai/review.js';
10
17
  const server = new McpServer({
11
18
  name: 'mimi-seed',
12
19
  version: '0.1.0',
@@ -281,6 +288,65 @@ server.tool('playstore_list_tracks', 'Google Play 릴리스 트랙 현황 (프
281
288
  const tracks = await playstore.listTracks(auth, packageName);
282
289
  return { content: [{ type: 'text', text: JSON.stringify(tracks, null, 2) }] };
283
290
  });
291
+ server.tool('playstore_list_images', 'Google Play 리스팅 이미지 목록 조회 (imageType별)', {
292
+ packageName: z.string().describe('패키지명'),
293
+ language: z.string().describe('언어 코드 (예: ko-KR)'),
294
+ imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']).describe('이미지 타입'),
295
+ }, async ({ packageName, language, imageType }) => {
296
+ const auth = requireAuth();
297
+ const images = await playstore.listImages(auth, packageName, language, imageType);
298
+ return { content: [{ type: 'text', text: JSON.stringify(images, null, 2) }] };
299
+ });
300
+ server.tool('playstore_upload_image', 'Google Play 리스팅 이미지 단일 업로드 (기존 이미지 유지). featureGraphic 1024x500 / icon 512x512 / phoneScreenshots 320~3840px', {
301
+ packageName: z.string().describe('패키지명'),
302
+ language: z.string().describe('언어 코드'),
303
+ imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
304
+ filePath: z.string().describe('업로드할 이미지 절대 경로'),
305
+ }, async ({ packageName, language, imageType, filePath }) => {
306
+ const auth = requireAuth();
307
+ const result = await playstore.uploadImage(auth, packageName, language, imageType, filePath);
308
+ return { content: [{ type: 'text', text: `✅ 업로드 완료\n${JSON.stringify(result, null, 2)}` }] };
309
+ });
310
+ server.tool('playstore_delete_all_images', 'Google Play 리스팅 특정 imageType의 이미지 전체 삭제 (교체 전 정리)', {
311
+ packageName: z.string().describe('패키지명'),
312
+ language: z.string().describe('언어 코드'),
313
+ imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
314
+ }, async ({ packageName, language, imageType }) => {
315
+ const auth = requireAuth();
316
+ const result = await playstore.deleteAllImages(auth, packageName, language, imageType);
317
+ return { content: [{ type: 'text', text: `✅ 전체 삭제\n${JSON.stringify(result, null, 2)}` }] };
318
+ });
319
+ server.tool('playstore_replace_images', 'Google Play 리스팅 이미지 일괄 교체 (한 edit 세션: deleteall → 순서대로 upload → commit). 스크린샷 5~8장 한 번에 교체 시 효율적. 업로드 순서가 스토어 노출 순서', {
320
+ packageName: z.string().describe('패키지명'),
321
+ language: z.string().describe('언어 코드'),
322
+ imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
323
+ filePaths: z.array(z.string()).describe('업로드할 이미지 절대 경로 배열 (순서 = 노출 순서)'),
324
+ }, async ({ packageName, language, imageType, filePaths }) => {
325
+ const auth = requireAuth();
326
+ const result = await playstore.replaceImages(auth, packageName, language, imageType, filePaths);
327
+ return { content: [{ type: 'text', text: `✅ ${result.count}장 교체 완료\n${JSON.stringify(result, null, 2)}` }] };
328
+ });
329
+ server.tool('playstore_update_release_notes', "Google Play 트랙 릴리스의 '최근 변경사항'(releaseNotes) 업데이트. versionCode로 타겟 릴리스 지정. 다른 언어/release는 보존. 이미 라이브(completed) 상태도 noteOnly 편집 가능", {
330
+ packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
331
+ track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
332
+ versionCode: z.string().describe('대상 versionCode (문자열, 예: "40")'),
333
+ language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
334
+ text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
335
+ }, async ({ packageName, track, versionCode, language, text }) => {
336
+ const auth = requireAuth();
337
+ const result = await playstore.updateReleaseNotes(auth, packageName, track, versionCode, language, text);
338
+ return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode} ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
339
+ });
340
+ server.tool('playstore_update_latest_release_notes', "Google Play 트랙의 최신 릴리스(versionCode 최대) '최근 변경사항' 업데이트 — versionCode를 모를 때 편의용", {
341
+ packageName: z.string().describe('패키지명'),
342
+ track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
343
+ language: z.string().describe('언어 코드 (예: ko-KR)'),
344
+ text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
345
+ }, async ({ packageName, track, language, text }) => {
346
+ const auth = requireAuth();
347
+ const result = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
348
+ return { content: [{ type: 'text', text: `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(result.updatedVersionCodes)}) ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
349
+ });
284
350
  server.tool('playstore_list_reviews', 'Google Play 리뷰 목록 조회', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
285
351
  const auth = requireAuth();
286
352
  const reviews = await playstore.listReviews(auth, packageName);
@@ -374,6 +440,113 @@ server.tool('playstore_verify_service_account', [
374
440
  return { content: [{ type: 'text', text: lines.join('\n') }] };
375
441
  });
376
442
  // ══════════════════════════════════════════════════
443
+ // Google Cloud IAM Tools
444
+ // ══════════════════════════════════════════════════
445
+ // 서비스 계정 생성 → JSON 키 발급 → 프로젝트 IAM 역할 부여까지 자동화.
446
+ // Play Console의 'View financial data' 권한은 Cloud IAM이 아니라 Play Console
447
+ // Users and permissions에서 별도로 부여해야 함 (androidpublisher.users API
448
+ // 또는 수동 UI).
449
+ server.tool('iam_list_service_accounts', '주어진 projectId의 서비스 계정 목록. 이메일 / displayName / disabled 상태 반환.', {
450
+ projectId: z.string().describe('Google Cloud 프로젝트 ID'),
451
+ }, async ({ projectId }) => {
452
+ const auth = requireAuth();
453
+ const accounts = await iam.listServiceAccounts(auth, projectId);
454
+ return { content: [{ type: 'text', text: JSON.stringify(accounts, null, 2) }] };
455
+ });
456
+ server.tool('iam_create_service_account', '새 서비스 계정 생성. accountId는 이메일의 로컬 파트 (예: "onesub-play-verifier" → onesub-play-verifier@<project>.iam.gserviceaccount.com).', {
457
+ projectId: z.string().describe('Google Cloud 프로젝트 ID'),
458
+ accountId: z
459
+ .string()
460
+ .regex(/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/, 'lowercase letters, digits, hyphens; 6-30 chars')
461
+ .describe('서비스 계정 ID (이메일 로컬 파트)'),
462
+ displayName: z.string().describe('사람이 읽을 표시 이름 (예: "onesub Play verifier")'),
463
+ }, async ({ projectId, accountId, displayName }) => {
464
+ const auth = requireAuth();
465
+ const account = await iam.createServiceAccount(auth, projectId, accountId, displayName);
466
+ return {
467
+ content: [
468
+ {
469
+ type: 'text',
470
+ text: [
471
+ '✓ 서비스 계정 생성 완료',
472
+ '',
473
+ `**email**: \`${account.email}\``,
474
+ `**displayName**: ${account.displayName}`,
475
+ `**uniqueId**: \`${account.uniqueId}\``,
476
+ '',
477
+ '다음 단계:',
478
+ `1. \`iam_create_key("${account.email}")\` — JSON 키 발급`,
479
+ `2. \`playstore_verify_service_account\` 로 Play Console 권한까지 확인 (Play Console에서 수동으로 이메일 초대 + 'View financial data' 권한 부여는 별도)`,
480
+ ].join('\n'),
481
+ },
482
+ ],
483
+ };
484
+ });
485
+ server.tool('iam_list_keys', '주어진 서비스 계정의 기존 키 목록. keyId / keyType / 만료시간 반환. 회수할 키 찾을 때 사용.', {
486
+ serviceAccount: z.string().describe('서비스 계정 이메일'),
487
+ }, async ({ serviceAccount }) => {
488
+ const auth = requireAuth();
489
+ const keys = await iam.listServiceAccountKeys(auth, serviceAccount);
490
+ return { content: [{ type: 'text', text: JSON.stringify(keys, null, 2) }] };
491
+ });
492
+ server.tool('iam_create_key', [
493
+ '주어진 서비스 계정의 새 JSON 키를 발급. 응답에 전체 JSON 포함 — 그대로 onesub의 GOOGLE_SERVICE_ACCOUNT_KEY 환경변수로 쓸 수 있음.',
494
+ '주의: 반환된 JSON은 영구 자격증명이므로 안전하게 보관. 한 서비스 계정당 키 최대 10개 (기존 키 회수 후 발급 필요하면 iam_list_keys로 먼저 확인).',
495
+ ].join(' '), {
496
+ serviceAccount: z.string().describe('서비스 계정 이메일'),
497
+ }, async ({ serviceAccount }) => {
498
+ const auth = requireAuth();
499
+ const key = await iam.createServiceAccountKey(auth, serviceAccount);
500
+ return {
501
+ content: [
502
+ {
503
+ type: 'text',
504
+ text: [
505
+ '✓ 새 키 발급 완료 — 아래 JSON을 안전하게 보관하세요. **다시 볼 수 없습니다.**',
506
+ '',
507
+ `**keyId**: \`${key.keyId}\``,
508
+ `**clientEmail**: \`${key.clientEmail}\``,
509
+ `**projectId**: \`${key.projectId}\``,
510
+ '',
511
+ '## Service account JSON',
512
+ '```json',
513
+ key.json,
514
+ '```',
515
+ '',
516
+ 'onesub 서버 `GOOGLE_SERVICE_ACCOUNT_KEY` env에 한 줄로 넣을 때:',
517
+ '```bash',
518
+ "cat service-account.json | tr -d '\\n' | jq -c .",
519
+ '```',
520
+ ].join('\n'),
521
+ },
522
+ ],
523
+ };
524
+ });
525
+ server.tool('iam_add_iam_policy_binding', [
526
+ '프로젝트 IAM 정책에 (member → role) 바인딩 추가. 이미 같은 조합이 있으면 no-op.',
527
+ 'member 형식: `serviceAccount:<email>` / `user:<email>` / `group:<email>`.',
528
+ '주의: 이건 Google Cloud IAM 역할입니다. Play Console의 "View financial data" 권한은 별도 — Play Console → Users and permissions에서 부여.',
529
+ ].join(' '), {
530
+ projectId: z.string().describe('Google Cloud 프로젝트 ID'),
531
+ member: z
532
+ .string()
533
+ .describe('권한 받을 주체 (예: serviceAccount:my-sa@my-project.iam.gserviceaccount.com)'),
534
+ role: z.string().describe('IAM 역할 (예: roles/iam.serviceAccountTokenCreator)'),
535
+ }, async ({ projectId, member, role }) => {
536
+ const auth = requireAuth();
537
+ const result = await iam.addProjectIamPolicyBinding(auth, projectId, member, role);
538
+ return {
539
+ content: [
540
+ {
541
+ type: 'text',
542
+ text: result.added
543
+ ? `✓ 바인딩 추가: \`${member}\` → \`${role}\``
544
+ : `• 이미 존재: \`${member}\` → \`${role}\` (no-op)`,
545
+ },
546
+ ],
547
+ };
548
+ });
549
+ // ══════════════════════════════════════════════════
377
550
  // App Store Connect Tools
378
551
  // ══════════════════════════════════════════════════
379
552
  server.tool('appstore_list_apps', 'App Store Connect 앱 목록 조회', {}, async () => {
@@ -392,6 +565,64 @@ server.tool('appstore_get_metadata', 'App Store 버전 메타데이터 (설명
392
565
  const localizations = await appstore.getVersionLocalizations(versionId);
393
566
  return { content: [{ type: 'text', text: JSON.stringify(localizations, null, 2) }] };
394
567
  });
568
+ server.tool('appstore_update_localization', "App Store 버전 로컬라이제이션(메타데이터) 수정 — localizationId 직접 지정. 이 버전의 새로운 기능(whatsNew), 설명(description), 키워드, 프로모션 텍스트를 편집. 수정 가능한 상태(PREPARE_FOR_SUBMISSION 등)인 버전에서만 반영됨", {
569
+ localizationId: z.string().describe('로컬라이제이션 ID (appstore_get_metadata 결과의 id)'),
570
+ whatsNew: z.string().optional().describe('이 버전의 새로운 기능 (4000자 이내)'),
571
+ description: z.string().optional().describe('앱 설명 (4000자 이내)'),
572
+ keywords: z.string().optional().describe('키워드 (쉼표 구분, 100자 이내)'),
573
+ promotionalText: z.string().optional().describe('프로모션 텍스트 (170자 이내)'),
574
+ supportUrl: z.string().url().optional().describe('지원 URL'),
575
+ marketingUrl: z.string().url().optional().describe('마케팅 URL'),
576
+ }, async ({ localizationId, ...fields }) => {
577
+ const cleaned = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
578
+ if (Object.keys(cleaned).length === 0) {
579
+ throw new Error('수정할 필드를 하나 이상 지정해줘 (whatsNew, description, keywords, promotionalText, supportUrl, marketingUrl).');
580
+ }
581
+ const result = await appstore.updateVersionLocalization(localizationId, cleaned);
582
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
583
+ });
584
+ server.tool('appstore_list_screenshots', "App Store 스크린샷 셋 + 이미지 목록 조회 (로컬라이제이션 단위). 디스플레이 타입별로 그룹핑됨", { localizationId: z.string().describe('로컬라이제이션 ID (appstore_get_metadata 결과의 id)') }, async ({ localizationId }) => {
585
+ const sets = await appstoreScreenshots.listScreenshotSets(localizationId);
586
+ return { content: [{ type: 'text', text: JSON.stringify(sets, null, 2) }] };
587
+ });
588
+ server.tool('appstore_upload_screenshot', [
589
+ "App Store 스크린샷 업로드 (자동 4단계: 셋 확보 → 예약 → 청크 업로드 → 커밋).",
590
+ "파일 경로는 로컬 절대경로. displayType은 Apple 공식 enum:",
591
+ "APP_IPHONE_69 (6.9\", 1290x2796) / APP_IPHONE_67 (6.7\", 1290x2796) /",
592
+ "APP_IPHONE_65 (6.5\", 1242x2688) / APP_IPHONE_61 (6.1\", 1170x2532) /",
593
+ "APP_IPHONE_58 (5.8\") / APP_IPHONE_55 (5.5\", 1242x2208) /",
594
+ "APP_IPAD_PRO_3GEN_129 (13\", 2064x2752) / APP_IPAD_PRO_3GEN_11 (11\", 1668x2388) /",
595
+ "APP_IPAD_PRO_129 (12.9\", 2048x2732) / APP_DESKTOP (2560x1600+).",
596
+ "해상도 틀리면 검수에서 리젝됨. 수정 가능한 버전 상태에서만 반영됨.",
597
+ ].join(' '), {
598
+ localizationId: z.string().describe('로컬라이제이션 ID'),
599
+ displayType: z.string().describe('Apple 스크린샷 디스플레이 타입 (예: APP_IPHONE_69)'),
600
+ filePath: z.string().describe('업로드할 이미지 파일의 절대경로'),
601
+ }, async ({ localizationId, displayType, filePath }) => {
602
+ const result = await appstoreScreenshots.uploadScreenshot(localizationId, displayType, filePath);
603
+ return {
604
+ content: [{
605
+ type: 'text',
606
+ text: `✅ 스크린샷 업로드 완료 (${displayType})\n\n${JSON.stringify(result, null, 2)}`,
607
+ }],
608
+ };
609
+ });
610
+ server.tool('appstore_delete_screenshot', 'App Store 개별 스크린샷 삭제', { screenshotId: z.string().describe('스크린샷 ID (appstore_list_screenshots 결과)') }, async ({ screenshotId }) => {
611
+ const result = await appstoreScreenshots.deleteScreenshot(screenshotId);
612
+ return { content: [{ type: 'text', text: `✅ 스크린샷 삭제됨\n${JSON.stringify(result, null, 2)}` }] };
613
+ });
614
+ server.tool('appstore_delete_screenshot_set', 'App Store 스크린샷 셋 전체 삭제 (디스플레이 타입 교체 시 먼저 정리)', { setId: z.string().describe('스크린샷 셋 ID (appstore_list_screenshots 결과)') }, async ({ setId }) => {
615
+ const result = await appstoreScreenshots.deleteScreenshotSet(setId);
616
+ return { content: [{ type: 'text', text: `✅ 셋 삭제됨\n${JSON.stringify(result, null, 2)}` }] };
617
+ });
618
+ server.tool('appstore_update_whats_new', "App Store '이 버전의 새로운 기능' 편집 — versionId + locale만 주면 자동으로 로컬라이제이션을 찾아 PATCH. 가장 흔한 사용 케이스. 수정 가능한 상태(PREPARE_FOR_SUBMISSION 등)인 버전에서만 반영됨", {
619
+ versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
620
+ locale: z.string().describe('로캘 (예: ko, en-US, ja)'),
621
+ whatsNew: z.string().describe("'이 버전의 새로운 기능' 텍스트 (4000자 이내)"),
622
+ }, async ({ versionId, locale, whatsNew }) => {
623
+ const result = await appstore.updateVersionWhatsNew(versionId, locale, { whatsNew });
624
+ return { content: [{ type: 'text', text: `✅ ${locale} 로캘의 What's New가 업데이트됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
625
+ });
395
626
  server.tool('appstore_list_builds', 'TestFlight 빌드 목록', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
396
627
  const builds = await appstore.listBuilds(appId);
397
628
  return { content: [{ type: 'text', text: JSON.stringify(builds, null, 2) }] };
@@ -405,6 +636,30 @@ server.tool('appstore_get_app_info', 'App Store 앱 정보 (카테고리, 연령
405
636
  return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
406
637
  });
407
638
  // --- 인증 상태 ---
639
+ server.tool('mimi_seed_auth_start', [
640
+ 'Google OAuth 로그인 링크를 발급하고 백그라운드 콜백 서버를 시작.',
641
+ '응답에 포함된 URL을 브라우저에서 열고 승인하면 localhost:9876으로 자동 콜백 → 토큰이 ~/.mimi-seed/tokens.json에 저장됨.',
642
+ '이후 playstore_*, firebase_*, admob_* 등 다른 MCP 도구 바로 호출 가능.',
643
+ '토큰 만료(invalid_rapt) / 재인증 필요 시 사용. 10분 내 완료해야 함.',
644
+ ].join(' '), {}, async () => {
645
+ const { url, wait } = startAuth(MIMI_SEED_CLIENT_ID, MIMI_SEED_CLIENT_SECRET);
646
+ // fire-and-forget — 토큰은 콜백 서버가 자동 저장
647
+ wait.then(() => { }, (err) => { console.error('[mimi-seed auth]', err.message); });
648
+ return {
649
+ content: [{
650
+ type: 'text',
651
+ text: [
652
+ '🔐 Google 로그인 링크 (10분 유효):',
653
+ '',
654
+ url,
655
+ '',
656
+ '이 URL을 브라우저에서 열고 Google 계정으로 승인해줘.',
657
+ '완료되면 localhost:9876으로 자동 리다이렉트되고 토큰이 저장돼.',
658
+ '이후 바로 다른 MCP 도구(playstore_*, firebase_* 등) 호출 가능.',
659
+ ].join('\n'),
660
+ }],
661
+ };
662
+ });
408
663
  server.tool('mimi_seed_auth_status', 'Mimi Seed MCP 인증 상태 확인', {}, async () => {
409
664
  const client = getAuthenticatedClient();
410
665
  if (!client) {
@@ -435,6 +690,118 @@ server.tool('mimi_seed_auth_status', 'Mimi Seed MCP 인증 상태 확인', {}, a
435
690
  };
436
691
  });
437
692
  // ══════════════════════════════════════════════════
693
+ // 제출 위험 점검 (Submission Risk Check)
694
+ // ══════════════════════════════════════════════════
695
+ server.tool('playstore_check_submission_risks', [
696
+ 'Google Play 제출 전 위험 요소를 자동으로 점검합니다.',
697
+ '블로커(반드시 수정)와 경고(권장 수정)로 분류하여 반환합니다.',
698
+ '점검 항목: 리스팅 완성도(제목/설명/짧은설명), 스크린샷 수, 아이콘, 빌드 존재 여부, 연락처.',
699
+ ].join(' '), {
700
+ packageName: z.string().describe('Android 패키지명 (예: com.example.myapp)'),
701
+ language: z.string().optional().describe('언어 코드 (기본: ko-KR)'),
702
+ }, async ({ packageName, language }) => {
703
+ const auth = requireAuth();
704
+ const risks = await checkPlayStoreRisks(auth, packageName, language ?? 'ko-KR');
705
+ const text = formatRisks(risks, 'Google Play');
706
+ return { content: [{ type: 'text', text }] };
707
+ });
708
+ server.tool('appstore_check_submission_risks', [
709
+ 'App Store 제출 전 위험 요소를 자동으로 점검합니다.',
710
+ '블로커(반드시 수정)와 경고(권장 수정)로 분류하여 반환합니다.',
711
+ '점검 항목: 메타데이터 완성도(설명/What\'s New/키워드), 스크린샷, TestFlight 빌드, 개인정보처리방침 URL.',
712
+ 'App Store 인증이 필요합니다 (appstore_* 도구 사용 가능 상태여야 함).',
713
+ ].join(' '), {
714
+ appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과의 id 필드)'),
715
+ }, async ({ appId }) => {
716
+ const risks = await checkAppStoreRisks(appId);
717
+ const text = formatRisks(risks, 'App Store');
718
+ return { content: [{ type: 'text', text }] };
719
+ });
720
+ // ══════════════════════════════════════════════════
721
+ // 스크린샷 해상도 검증 (Screenshot Validation)
722
+ // ══════════════════════════════════════════════════
723
+ server.tool('screenshot_validate', [
724
+ '로컬 스크린샷 파일의 해상도·파일크기·종횡비를 App Store / Play Store 규격과 비교 검증합니다.',
725
+ 'PNG/JPEG 파일 경로를 배열로 받아 각 파일의 통과 여부와 문제점을 반환합니다.',
726
+ 'platform: "ios" 또는 "android" (기본: ios)',
727
+ 'iOS displayType 예시: APP_IPHONE_69, APP_IPHONE_67, APP_IPHONE_65, APP_IPAD_PRO_3GEN_129',
728
+ 'Android imageType 예시: phoneScreenshots, sevenInchScreenshots, tenInchScreenshots, featureGraphic',
729
+ '업로드 전 미리 검사하여 리젝 방지에 활용하세요.',
730
+ ].join(' '), {
731
+ filePaths: z.array(z.string()).describe('검증할 이미지 파일 절대경로 배열'),
732
+ platform: z.enum(['ios', 'android']).optional().describe('플랫폼 (기본: ios)'),
733
+ displayType: z.string().optional().describe('iOS 디스플레이 타입 (예: APP_IPHONE_67)'),
734
+ imageType: z.string().optional().describe('Android 이미지 타입 (예: phoneScreenshots)'),
735
+ }, async ({ filePaths, platform, displayType, imageType }) => {
736
+ const plat = platform ?? 'ios';
737
+ if (plat === 'ios') {
738
+ const results = validateAppStoreScreenshots(filePaths, displayType);
739
+ const text = formatValidationResults(results, 'App Store');
740
+ return { content: [{ type: 'text', text }] };
741
+ }
742
+ else {
743
+ const results = validatePlayStoreScreenshots(filePaths, imageType ?? 'phoneScreenshots');
744
+ const text = formatValidationResults(results, 'Google Play');
745
+ return { content: [{ type: 'text', text }] };
746
+ }
747
+ });
748
+ // ══════════════════════════════════════════════════
749
+ // AI 릴리즈 노트 생성 (ANTHROPIC_API_KEY 필요)
750
+ // ══════════════════════════════════════════════════
751
+ server.tool('generate_release_notes_from_commits', [
752
+ 'git 커밋 내역을 받아 Claude AI로 앱 스토어용 릴리즈 노트를 생성합니다.',
753
+ '3가지 톤(간결/상세/마케팅)과 다국어 버전을 동시에 생성합니다.',
754
+ 'ANTHROPIC_API_KEY 환경변수가 필요합니다.',
755
+ 'commits: [{ message, author?, date? }] 형태로 전달하세요.',
756
+ 'locales: 다국어 생성할 로케일 배열 (예: ["ko", "en-US", "ja"])',
757
+ '생성 후 playstore_update_release_notes 또는 appstore_update_whats_new로 적용하세요.',
758
+ ].join(' '), {
759
+ commits: z.array(z.object({
760
+ message: z.string(),
761
+ hash: z.string().optional(),
762
+ author: z.string().optional(),
763
+ date: z.string().optional(),
764
+ })).describe('git 커밋 배열'),
765
+ appName: z.string().optional().describe('앱 이름 (프롬프트 맥락용)'),
766
+ locales: z.array(z.string()).optional().describe('다국어 로케일 목록 (예: ["ko", "en-US", "ja"])'),
767
+ }, async ({ commits, appName, locales }) => {
768
+ const result = await generateReleaseNotesFromCommits(commits, {
769
+ appName,
770
+ locales: locales ?? [],
771
+ });
772
+ const text = formatGeneratedNotes(result);
773
+ return { content: [{ type: 'text', text }] };
774
+ });
775
+ // ══════════════════════════════════════════════════
776
+ // AI 리뷰 답변 생성 (ANTHROPIC_API_KEY 필요)
777
+ // ══════════════════════════════════════════════════
778
+ server.tool('generate_review_reply', [
779
+ 'Play Store / App Store 리뷰에 대한 AI 답변 초안을 생성합니다.',
780
+ 'ANTHROPIC_API_KEY 환경변수가 필요합니다.',
781
+ 'tone: friendly(친근) / professional(정중) / empathetic(공감) / brief(간결) — 기본: friendly',
782
+ 'language: ko / en / ja / zh 등 — 기본: ko',
783
+ '⚠ 생성된 답변은 초안입니다. 게시 전 반드시 검토하세요.',
784
+ '답변 게시는 playstore_reply_to_review 도구를 사용하세요.',
785
+ ].join(' '), {
786
+ reviewText: z.string().describe('리뷰 원문'),
787
+ rating: z.number().min(1).max(5).optional().describe('별점 (1~5)'),
788
+ appName: z.string().optional().describe('앱 이름'),
789
+ tone: z.enum(['friendly', 'professional', 'empathetic', 'brief']).optional().describe('답변 톤'),
790
+ language: z.string().optional().describe('답변 언어 코드 (기본: ko)'),
791
+ developerName: z.string().optional().describe('개발자/팀 이름'),
792
+ }, async ({ reviewText, rating, appName, tone, language, developerName }) => {
793
+ const result = await generateReviewReply({
794
+ reviewText,
795
+ rating,
796
+ appName,
797
+ tone: tone ?? 'friendly',
798
+ language: language ?? 'ko',
799
+ developerName,
800
+ });
801
+ const text = formatReviewReply(result);
802
+ return { content: [{ type: 'text', text }] };
803
+ });
804
+ // ══════════════════════════════════════════════════
438
805
  // Start
439
806
  // ══════════════════════════════════════════════════
440
807
  async function main() {
@@ -1,4 +1,13 @@
1
1
  import type { OAuth2Client } from 'google-auth-library';
2
+ export type PlayImageType = 'featureGraphic' | 'icon' | 'phoneScreenshots' | 'promoGraphic' | 'sevenInchScreenshots' | 'tenInchScreenshots' | 'tvBanner' | 'tvScreenshots' | 'wearScreenshots';
3
+ /**
4
+ * Google Play Developer API (Android Publisher API v3) 래퍼
5
+ *
6
+ * 주의: 최초 앱 생성은 API로 불가 (Play Console에서만).
7
+ * 여기서는 기존 앱의 메타데이터, 빌드, 출시를 관리.
8
+ */
9
+ 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>;
2
11
  export declare function getAppDetails(auth: OAuth2Client, packageName: string): Promise<import("googleapis").androidpublisher_v3.Schema$AppDetails>;
3
12
  export declare function getListing(auth: OAuth2Client, packageName: string, language?: string): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
4
13
  export declare function updateListing(auth: OAuth2Client, packageName: string, language: string, data: {
@@ -15,6 +24,36 @@ export declare function listTracks(auth: OAuth2Client, packageName: string): Pro
15
24
  releaseNotes: import("googleapis").androidpublisher_v3.Schema$LocalizedText[] | undefined;
16
25
  }[];
17
26
  }[]>;
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
+ /**
29
+ * 최신 release (versionCode 최대값)의 releaseNotes[language]를 교체/추가.
30
+ * versionCode를 모를 때 편의용.
31
+ */
32
+ export declare function updateLatestReleaseNotes(auth: OAuth2Client, packageName: string, track: string, language: string, text: string): Promise<{
33
+ updatedVersionCodes: string[] | null | undefined;
34
+ updatedReleaseName: string | null | undefined;
35
+ releases?: import("googleapis").androidpublisher_v3.Schema$TrackRelease[];
36
+ track?: string | null;
37
+ }>;
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<{
41
+ ok: boolean;
42
+ imageType: PlayImageType;
43
+ }>;
44
+ /**
45
+ * 한 edit 세션 내에서 기존 이미지 전체 삭제 + 새 이미지 순서대로 업로드 + commit.
46
+ * phoneScreenshots처럼 여러 장 교체 시 효율적 (단일 edit).
47
+ */
48
+ export declare function replaceImages(auth: OAuth2Client, packageName: string, language: string, imageType: PlayImageType, filePaths: string[]): Promise<{
49
+ imageType: PlayImageType;
50
+ count: number;
51
+ uploaded: {
52
+ id?: string | null;
53
+ url?: string | null;
54
+ sha256?: string | null;
55
+ }[];
56
+ }>;
18
57
  export declare function listReviews(auth: OAuth2Client, packageName: string): Promise<{
19
58
  reviewId: string | null | undefined;
20
59
  authorName: string | null | undefined;
@@ -27,13 +66,9 @@ export declare function listReviews(auth: OAuth2Client, packageName: string): Pr
27
66
  }[]>;
28
67
  export declare function replyToReview(auth: OAuth2Client, packageName: string, reviewId: string, replyText: string): Promise<import("googleapis").androidpublisher_v3.Schema$ReviewsReplyResponse>;
29
68
  export declare function listInAppProducts(auth: OAuth2Client, packageName: string): Promise<{
30
- sku: string | null | undefined;
31
- status: string | null | undefined;
32
- purchaseType: string | null | undefined;
33
- defaultPrice: import("googleapis").androidpublisher_v3.Schema$Price | undefined;
34
- listings: {
35
- [key: string]: import("googleapis").androidpublisher_v3.Schema$InAppProductListing;
36
- } | null | undefined;
69
+ productId: any;
70
+ listings: any;
71
+ purchaseOptions: any;
37
72
  }[]>;
38
73
  export declare function listSubscriptions(auth: OAuth2Client, packageName: string): Promise<{
39
74
  productId: string | null | undefined;