@yoonion/mimi-seed-mcp 0.3.25 → 0.3.27

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.
@@ -1,7 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { checkPlayStoreRisks, checkAppStoreRisks, formatRisks } from '../checks/risks.js';
3
3
  import { validateAppStoreScreenshots, validatePlayStoreScreenshots, formatValidationResults } from '../checks/screenshots.js';
4
- import { requireAuth } from '../helpers.js';
4
+ import { requireAuth, requirePlayStoreAuth } from '../helpers.js';
5
+ import * as appstore from '../appstore/tools.js';
6
+ import * as playstore from '../playstore/tools.js';
5
7
  export function registerChecksTools(server) {
6
8
  server.tool('playstore_check_submission_risks', [
7
9
  'Google Play 제출 전 위험 요소를 자동으로 점검합니다.',
@@ -53,4 +55,90 @@ export function registerChecksTools(server) {
53
55
  return { content: [{ type: 'text', text }] };
54
56
  }
55
57
  });
58
+ server.tool('release_status', [
59
+ '양 스토어의 동일 버전 상태를 한 번에 조회 — 1.4.x 배포 시 "Play 는 production?", "ASC 는 심사중?" 을',
60
+ 'list_versions + list_tracks 2회 호출 + grep 으로 합치던 멘탈 부담을 단일 응답으로 줄임.',
61
+ 'App Store: appId 가 있을 때 listVersions 에서 version 일치 항목의 state/releaseType/attached build/createdDate.',
62
+ 'Play Store: packageName 이 있을 때 listTracks 에서 동일 versionName 또는 versionCode 가 있는 모든 트랙의 status/versionCodes.',
63
+ 'appId 또는 packageName 둘 다 비어 있으면 에러. 둘 중 하나만 줘도 그 스토어 상태만 조회 가능.',
64
+ ].join(' '), {
65
+ version: z.string().describe('조회할 버전명 (예: "1.4.9"). App Store versionString + Play release.name 매칭에 사용.'),
66
+ appId: z.string().optional().describe('App Store appId (appstore_list_apps 결과). 없으면 App Store 영역 skip.'),
67
+ packageName: z.string().optional().describe('Play 패키지명 (예: gg.pryzm.coffee). 없으면 Play 영역 skip.'),
68
+ }, async ({ version, appId, packageName }) => {
69
+ if (!appId && !packageName) {
70
+ throw new Error('appId 또는 packageName 중 최소 하나는 지정해야 해요.');
71
+ }
72
+ let appStoreSection = null;
73
+ if (appId) {
74
+ try {
75
+ const versions = (await appstore.listVersions(appId));
76
+ const match = versions.find((v) => v.version === version);
77
+ if (match) {
78
+ // attached build 조회 — listBuilds 까지 가지 않고 단일 versionId 의 build 만.
79
+ // appstore.tools.ts 의 attachBuildToVersion 인접 헬퍼는 외부 노출 X — 여기서는
80
+ // listBuilds(appId) 의 최근 10개에서 매칭 추정. 일치하는 buildNumber 가 없어도 무방.
81
+ const builds = (await appstore.listBuilds(appId).catch(() => []));
82
+ appStoreSection = {
83
+ versionId: match.id,
84
+ version: match.version,
85
+ state: match.state,
86
+ releaseType: match.releaseType,
87
+ createdDate: match.createdDate,
88
+ recentBuilds: builds.slice(0, 3).map((b) => ({
89
+ id: b.id,
90
+ buildNumber: b.version,
91
+ processingState: b.processingState,
92
+ })),
93
+ };
94
+ }
95
+ else {
96
+ appStoreSection = {
97
+ found: false,
98
+ available: versions.slice(0, 10).map((v) => ({ version: v.version, state: v.state })),
99
+ };
100
+ }
101
+ }
102
+ catch (e) {
103
+ appStoreSection = { error: e instanceof Error ? e.message : String(e) };
104
+ }
105
+ }
106
+ // ── Play Store ────────────────────────────────────────
107
+ let playStoreSection = null;
108
+ if (packageName) {
109
+ try {
110
+ const auth = requirePlayStoreAuth(packageName);
111
+ const tracks = await playstore.listTracks(auth, packageName);
112
+ // 트랙별로 version 이 매칭되는 release 찾기.
113
+ // match 기준: release.name === version (보통 "1.4.9" 또는 "1.4.9 (373)")
114
+ // 완전 일치가 없으면 prefix 매칭으로 폴백 (releaseName 표기 변동성 흡수).
115
+ const byTrack = {};
116
+ for (const t of tracks) {
117
+ const exact = t.releases.find((r) => r.name === version);
118
+ const prefix = exact ?? t.releases.find((r) => typeof r.name === 'string' && r.name?.startsWith(`${version} `));
119
+ if (prefix) {
120
+ byTrack[t.track ?? 'unknown'] = {
121
+ releaseName: prefix.name,
122
+ status: prefix.status,
123
+ versionCodes: prefix.versionCodes,
124
+ };
125
+ }
126
+ }
127
+ playStoreSection = Object.keys(byTrack).length > 0
128
+ ? byTrack
129
+ : {
130
+ found: false,
131
+ available: tracks.map((t) => ({
132
+ track: t.track,
133
+ releases: t.releases.map((r) => ({ name: r.name, status: r.status })),
134
+ })),
135
+ };
136
+ }
137
+ catch (e) {
138
+ playStoreSection = { error: e instanceof Error ? e.message : String(e) };
139
+ }
140
+ }
141
+ const result = { version, appStore: appStoreSection, playStore: playStoreSection };
142
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
143
+ });
56
144
  }
@@ -4,6 +4,7 @@ import { saveServiceAccountJsonForPackage, listRegisteredServiceAccounts, delete
4
4
  import { createGoogleOneTimePurchase, createGoogleSubscription, updateGoogleProduct, deleteGoogleProduct, listGoogleProducts, } from '@onesub/providers';
5
5
  import { requirePlayStoreAuth, requireServiceAccountJson } from '../helpers.js';
6
6
  import { buildPlayStoreReleasePlan } from '../checks/plan.js';
7
+ import { validatePlayReleaseNotes, formatIssuesForUser } from '../lib/text-validators.js';
7
8
  export function registerPlaystoreTools(server) {
8
9
  server.tool('playstore_get_app', 'Google Play 앱 상세 정보 조회', { packageName: z.string().describe('패키지명 (예: com.findthem.app)') }, async ({ packageName }) => {
9
10
  const auth = requirePlayStoreAuth(packageName);
@@ -81,19 +82,73 @@ export function registerPlaystoreTools(server) {
81
82
  language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
82
83
  text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
83
84
  }, async ({ packageName, track, versionCode, language, text }) => {
85
+ // ── 사전 lint — 500자 / HTML / 역슬래시 가격(\5000원) round-trip 차단.
86
+ const validation = validatePlayReleaseNotes(text);
87
+ if (!validation.ok) {
88
+ return {
89
+ content: [{
90
+ type: 'text',
91
+ text: `❌ 릴리스 노트 사전 검증 실패 — API 호출 안 함\n\n${formatIssuesForUser(validation.issues)}\n\n수정 후 다시 호출해주세요.`,
92
+ }],
93
+ isError: true,
94
+ };
95
+ }
84
96
  const auth = requirePlayStoreAuth(packageName);
85
97
  const result = await playstore.updateReleaseNotes(auth, packageName, track, versionCode, language, text);
86
98
  return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode} ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
87
99
  });
88
- server.tool('playstore_update_latest_release_notes', "Google Play 트랙의 최신 릴리스(versionCode 최대) '최근 변경사항' 업데이트 — versionCode를 모를 때 편의용", {
100
+ server.tool('playstore_update_latest_release_notes', [
101
+ "Google Play 트랙의 최신 릴리스(versionCode 최대) '최근 변경사항' 업데이트 — versionCode를 모를 때 편의용.",
102
+ '⚠️ 지정한 단일 트랙에만 적용 — 다른 트랙에는 자동 복사되지 않음 (Google Play 정책: promote_release 시점에 노트 캐리됨).',
103
+ '동일 노트를 여러 트랙에 즉시 반영하려면 syncTracks 옵션 사용 — 지정 트랙들에 대해 순차로 같은 노트 적용.',
104
+ ].join(' '), {
89
105
  packageName: z.string().describe('패키지명'),
90
- track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
106
+ track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('1차 적용 트랙'),
91
107
  language: z.string().describe('언어 코드 (예: ko-KR)'),
92
108
  text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
93
- }, async ({ packageName, track, language, text }) => {
109
+ syncTracks: z.array(z.enum(['production', 'beta', 'alpha', 'internal']))
110
+ .optional()
111
+ .describe('추가로 동일 노트 적용할 트랙 배열 (예: ["production"]). 지정 시 1차 track 반영 후 순차 동기화.'),
112
+ }, async ({ packageName, track, language, text, syncTracks }) => {
113
+ const validation = validatePlayReleaseNotes(text);
114
+ if (!validation.ok) {
115
+ return {
116
+ content: [{
117
+ type: 'text',
118
+ text: `❌ 릴리스 노트 사전 검증 실패 — API 호출 안 함\n\n${formatIssuesForUser(validation.issues)}\n\n수정 후 다시 호출해주세요.`,
119
+ }],
120
+ isError: true,
121
+ };
122
+ }
94
123
  const auth = requirePlayStoreAuth(packageName);
95
- const result = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
96
- return { content: [{ type: 'text', text: `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(result.updatedVersionCodes)}) ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
124
+ // 1차 적용 + 결과 누적.
125
+ const primaryResult = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
126
+ const lines = [
127
+ `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(primaryResult.updatedVersionCodes)}) ${language} 노트 반영`,
128
+ ];
129
+ const allResults = { [track]: primaryResult };
130
+ // 추가 트랙 — 1차와 중복은 skip. 한 트랙 실패해도 나머지 트랙은 계속 시도.
131
+ if (syncTracks && syncTracks.length > 0) {
132
+ const targets = syncTracks.filter((t) => t !== track);
133
+ for (const t of targets) {
134
+ try {
135
+ const r = await playstore.updateLatestReleaseNotes(auth, packageName, t, language, text);
136
+ allResults[t] = r;
137
+ lines.push(` ↳ sync ${t} (versionCodes=${JSON.stringify(r.updatedVersionCodes)}) 반영`);
138
+ }
139
+ catch (e) {
140
+ const msg = e instanceof Error ? e.message : String(e);
141
+ allResults[t] = { error: msg };
142
+ lines.push(` ↳ sync ${t} 실패: ${msg}`);
143
+ }
144
+ }
145
+ }
146
+ return {
147
+ content: [{
148
+ type: 'text',
149
+ text: `${lines.join('\n')}\n\n${JSON.stringify(allResults, null, 2)}`,
150
+ }],
151
+ };
97
152
  });
98
153
  server.tool('playstore_list_reviews', 'Google Play 리뷰 목록 조회', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
99
154
  const auth = requirePlayStoreAuth(packageName);
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.25",
3
+ "version": "0.3.27",
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
9
  "mimi-seed-appstore-auth": "dist/appstore/setup-cli.js",
10
- "mimi-seed-playstore-auth": "dist/auth/playstore-setup-cli.js"
10
+ "mimi-seed-playstore-auth": "dist/auth/playstore-setup-cli.js",
11
+ "mimi-seed-bigquery-auth": "dist/auth/bigquery-setup-cli.js"
11
12
  },
12
13
  "files": [
13
14
  "dist",