@yoonion/mimi-seed-mcp 0.3.2 → 0.3.3

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
@@ -172,7 +172,7 @@ Claude가 연쇄 호출:
172
172
 
173
173
  ## 레거시 호환성
174
174
 
175
- 패키지 이름이 `@preseed/mcp-server` / `preseed-mcp` 시절의 데이터는 자동으로 이어받음:
175
+ Preseed 시절(`~/.preseed/`) 데이터는 자동으로 이어받음:
176
176
 
177
177
  - `~/.preseed/tokens.json` 있으면 읽음 (재인증 불필요)
178
178
  - `~/.preseed/appstore.json`도 동일
@@ -21,8 +21,7 @@ function authHeadersOrThrow() {
21
21
  '❌ App Store Connect 인증이 필요해.',
22
22
  '',
23
23
  '터미널에서 실행:',
24
- ' cd c:/users/turbo08/app-gen/packages/mcp-server',
25
- ' npx tsx src/appstore/setup-cli.ts',
24
+ ' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
26
25
  ].join('\n'));
27
26
  }
28
27
  return headers;
@@ -20,3 +20,11 @@ export declare function updateVersionWhatsNew(versionId: string, locale: string,
20
20
  export declare function listBuilds(appId: string): Promise<any>;
21
21
  export declare function listBetaGroups(appId: string): Promise<any>;
22
22
  export declare function getAppInfo(appId: string): Promise<any>;
23
+ export interface ListCustomerReviewsOptions {
24
+ limit?: number;
25
+ territory?: string;
26
+ rating?: 1 | 2 | 3 | 4 | 5;
27
+ }
28
+ export declare function listCustomerReviews(appId: string, opts?: ListCustomerReviewsOptions): Promise<any>;
29
+ export declare function createReviewResponse(reviewId: string, responseBody: string): Promise<any>;
30
+ export declare function submitVersionForReview(versionId: string): Promise<any>;
@@ -11,8 +11,7 @@ export async function apiGet(path, params) {
11
11
  '❌ App Store Connect 인증이 필요해.',
12
12
  '',
13
13
  '터미널에서 실행:',
14
- ' cd c:/users/turbo08/app-gen/packages/mcp-server',
15
- ' npx tsx src/appstore/setup-cli.ts',
14
+ ' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
16
15
  '',
17
16
  'API Key가 필요해:',
18
17
  ' App Store Connect > Users and Access > Integrations > Keys',
@@ -36,8 +35,7 @@ async function apiPatch(path, body) {
36
35
  '❌ App Store Connect 인증이 필요해.',
37
36
  '',
38
37
  '터미널에서 실행:',
39
- ' cd c:/users/turbo08/app-gen/packages/mcp-server',
40
- ' npx tsx src/appstore/setup-cli.ts',
38
+ ' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
41
39
  ].join('\n'));
42
40
  const res = await fetch(`${BASE}${path}`, {
43
41
  method: 'PATCH',
@@ -52,6 +50,28 @@ async function apiPatch(path, body) {
52
50
  const text = await res.text();
53
51
  return text ? JSON.parse(text) : { ok: true };
54
52
  }
53
+ async function apiPost(path, body) {
54
+ const headers = getAuthHeaders();
55
+ if (!headers)
56
+ throw new Error([
57
+ '❌ App Store Connect 인증이 필요해.',
58
+ '',
59
+ '터미널에서 실행:',
60
+ ' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
61
+ ].join('\n'));
62
+ const res = await fetch(`${BASE}${path}`, {
63
+ method: 'POST',
64
+ headers: { ...headers, 'Content-Type': 'application/json' },
65
+ body: JSON.stringify(body),
66
+ });
67
+ if (!res.ok) {
68
+ const text = await res.text();
69
+ throw new Error(`App Store API ${res.status}: ${text}`);
70
+ }
71
+ // 201 Created — 본문에 created entity. 일부 엔드포인트는 204
72
+ const text = await res.text();
73
+ return text ? JSON.parse(text) : { ok: true };
74
+ }
55
75
  // ─── 앱 ───
56
76
  export async function listApps() {
57
77
  const data = await apiGet('/apps', {
@@ -164,3 +184,68 @@ export async function getAppInfo(appId) {
164
184
  ageRating: i.attributes?.appStoreAgeRating,
165
185
  }));
166
186
  }
187
+ export async function listCustomerReviews(appId, opts = {}) {
188
+ const params = {
189
+ 'sort': '-createdDate',
190
+ 'limit': String(opts.limit ?? 50),
191
+ 'fields[customerReviews]': 'rating,title,body,reviewerNickname,createdDate,territory',
192
+ 'include': 'response',
193
+ 'fields[customerReviewResponses]': 'responseBody,lastModifiedDate,state',
194
+ };
195
+ if (opts.territory)
196
+ params['filter[territory]'] = opts.territory;
197
+ if (opts.rating != null)
198
+ params['filter[rating]'] = String(opts.rating);
199
+ const data = await apiGet(`/apps/${appId}/customerReviews`, params);
200
+ // include로 가져온 답변 매핑
201
+ const responses = new Map();
202
+ for (const inc of data.included ?? []) {
203
+ if (inc.type === 'customerReviewResponses') {
204
+ responses.set(inc.id, inc.attributes);
205
+ }
206
+ }
207
+ return (data.data ?? []).map((r) => {
208
+ const respId = r.relationships?.response?.data?.id;
209
+ const resp = respId ? responses.get(respId) : null;
210
+ return {
211
+ id: r.id,
212
+ rating: r.attributes?.rating,
213
+ title: r.attributes?.title,
214
+ body: r.attributes?.body,
215
+ nickname: r.attributes?.reviewerNickname,
216
+ createdDate: r.attributes?.createdDate,
217
+ territory: r.attributes?.territory,
218
+ response: resp
219
+ ? {
220
+ body: resp.responseBody,
221
+ lastModifiedDate: resp.lastModifiedDate,
222
+ state: resp.state,
223
+ }
224
+ : null,
225
+ };
226
+ });
227
+ }
228
+ export async function createReviewResponse(reviewId, responseBody) {
229
+ return apiPost('/customerReviewResponses', {
230
+ data: {
231
+ type: 'customerReviewResponses',
232
+ attributes: { responseBody },
233
+ relationships: {
234
+ review: { data: { type: 'customerReviews', id: reviewId } },
235
+ },
236
+ },
237
+ });
238
+ }
239
+ // ─── 심사 제출 (Submit for Review) ───
240
+ export async function submitVersionForReview(versionId) {
241
+ return apiPost('/appStoreVersionSubmissions', {
242
+ data: {
243
+ type: 'appStoreVersionSubmissions',
244
+ relationships: {
245
+ appStoreVersion: {
246
+ data: { type: 'appStoreVersions', id: versionId },
247
+ },
248
+ },
249
+ },
250
+ });
251
+ }
package/dist/index.js CHANGED
@@ -635,6 +635,60 @@ server.tool('appstore_get_app_info', 'App Store 앱 정보 (카테고리, 연령
635
635
  const info = await appstore.getAppInfo(appId);
636
636
  return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
637
637
  });
638
+ // --- App Store 고객 리뷰 ---
639
+ server.tool('appstore_list_reviews', [
640
+ 'App Store 받은 고객 리뷰 조회 (최신순).',
641
+ 'response 필드에 개발자 답변 존재 여부와 내용이 함께 포함됨 (없으면 null).',
642
+ 'territory(예: KOR/USA — ISO 3166 alpha-3) / rating(1~5)으로 필터 가능.',
643
+ ].join(' '), {
644
+ appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과)'),
645
+ limit: z.number().int().positive().max(200).optional().describe('가져올 개수 (기본 50, 최대 200)'),
646
+ territory: z.string().optional().describe("국가 코드 (예: 'KOR', 'USA' — alpha-3)"),
647
+ rating: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional().describe('별점 필터 (1~5)'),
648
+ }, async ({ appId, limit, territory, rating }) => {
649
+ const reviews = await appstore.listCustomerReviews(appId, { limit, territory, rating });
650
+ return { content: [{ type: 'text', text: JSON.stringify(reviews, null, 2) }] };
651
+ });
652
+ server.tool('appstore_reply_review', [
653
+ 'App Store 고객 리뷰에 개발자 답변을 등록 (또는 갱신).',
654
+ '동일 리뷰에 한 번만 답변 가능 — 기존 답변이 있으면 Apple이 새 응답으로 대체함.',
655
+ 'reviewId는 appstore_list_reviews 결과의 id.',
656
+ '답변 본문은 5970자 이내.',
657
+ ].join(' '), {
658
+ reviewId: z.string().describe('리뷰 ID (appstore_list_reviews 결과)'),
659
+ responseBody: z.string().describe('답변 본문 (5970자 이내)'),
660
+ }, async ({ reviewId, responseBody }) => {
661
+ const result = await appstore.createReviewResponse(reviewId, responseBody);
662
+ return { content: [{ type: 'text', text: `✅ 리뷰 ${reviewId}에 답변 등록됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
663
+ });
664
+ // --- App Store 심사 제출 (Submit for Review) ---
665
+ server.tool('appstore_submit_for_review', [
666
+ 'App Store 버전을 심사에 제출 — POST /appStoreVersionSubmissions.',
667
+ '⚠️ 비가역 작업: 제출 후엔 Apple 심사가 시작되며, 메타데이터/스크린샷/빌드를 더 못 바꿈 (REJECTED/METADATA_REJECTED 시 다시 편집 가능).',
668
+ '사전 조건: 버전이 PREPARE_FOR_SUBMISSION 또는 DEVELOPER_REJECTED 상태, 빌드 attached, 모든 필수 메타데이터 채워짐.',
669
+ 'appstore_check_submission_risks로 사전 점검 권장.',
670
+ ].join(' '), {
671
+ versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
672
+ }, async ({ versionId }) => {
673
+ const result = await appstore.submitVersionForReview(versionId);
674
+ return { content: [{ type: 'text', text: `✅ 버전 ${versionId} 심사 제출 완료. App Store Connect에서 진행 상태 확인 가능.\n\n${JSON.stringify(result, null, 2)}` }] };
675
+ });
676
+ // --- Play Store 심사 제출 / release status 변경 ---
677
+ server.tool('playstore_submit_release', [
678
+ 'Google Play 트랙의 release status를 변경 — 일반적으로 draft → completed로 바꿔 검토/배포 큐에 진입시킬 때 사용.',
679
+ '⚠️ status="completed"는 비가역에 가까움 (전체 출시 또는 Google 검토 시작). halted로 일시 중단은 가능하나 한 번 라이브된 release는 되돌리기 어려움.',
680
+ 'status 옵션: draft(검토 미시작) / inProgress(단계 출시) / completed(전체 출시) / halted(중단).',
681
+ 'playstore_check_submission_risks로 사전 점검 권장.',
682
+ ].join(' '), {
683
+ packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
684
+ track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
685
+ versionCode: z.string().describe('대상 versionCode (문자열)'),
686
+ status: z.enum(['draft', 'inProgress', 'completed', 'halted']).optional().describe('새 status (기본: completed)'),
687
+ }, async ({ packageName, track, versionCode, status }) => {
688
+ const auth = requireAuth();
689
+ const result = await playstore.submitRelease(auth, packageName, track, versionCode, status);
690
+ return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode}: ${result.previousStatus} → ${result.newStatus}\n\n${JSON.stringify(result, null, 2)}` }] };
691
+ });
638
692
  // --- 인증 상태 ---
639
693
  server.tool('mimi_seed_auth_start', [
640
694
  'Google OAuth 로그인 링크를 발급하고 백그라운드 콜백 서버를 시작.',
@@ -54,6 +54,14 @@ export declare function replaceImages(auth: OAuth2Client, packageName: string, l
54
54
  sha256?: string | null;
55
55
  }[];
56
56
  }>;
57
+ export declare function submitRelease(auth: OAuth2Client, packageName: string, track: string, versionCode: string, status?: 'completed' | 'draft' | 'inProgress' | 'halted'): Promise<{
58
+ track: string;
59
+ versionCode: string;
60
+ previousStatus: string | null | undefined;
61
+ newStatus: "completed" | "draft" | "inProgress" | "halted";
62
+ committed: boolean;
63
+ result: import("googleapis").androidpublisher_v3.Schema$Track;
64
+ }>;
57
65
  export declare function listReviews(auth: OAuth2Client, packageName: string): Promise<{
58
66
  reviewId: string | null | undefined;
59
67
  authorName: string | null | undefined;
@@ -221,6 +221,47 @@ export async function replaceImages(auth, packageName, language, imageType, file
221
221
  }, true);
222
222
  }
223
223
  // ─── 리뷰 조회 ───
224
+ // ─── 릴리스 상태 변경 / 심사 제출 ───
225
+ //
226
+ // Play Store는 명시적 "Submit for Review" 버튼이 없고, track의 release
227
+ // status를 "completed"로 바꾸면 자동으로 심사 큐에 들어감 (또는 즉시 publish).
228
+ // status: draft → 검토 미시작, inProgress → 단계적 출시, completed → 전체 출시
229
+ // halted → 일시 중단
230
+ // 신중히 사용해야 함 — completed로 바꾸면 되돌리기 어려움.
231
+ export async function submitRelease(auth, packageName, track, versionCode, status = 'completed') {
232
+ return withEdit(auth, packageName, async (editId) => {
233
+ const current = await publisher().edits.tracks.get({
234
+ auth, packageName, editId, track,
235
+ });
236
+ const releases = current.data.releases ?? [];
237
+ if (releases.length === 0) {
238
+ throw new Error(`${track} 트랙에 릴리스가 없어.`);
239
+ }
240
+ const target = releases.find((r) => (r.versionCodes ?? []).some((v) => String(v) === String(versionCode)));
241
+ if (!target) {
242
+ const available = releases.map((r) => ({
243
+ name: r.name,
244
+ versionCodes: r.versionCodes,
245
+ status: r.status,
246
+ }));
247
+ throw new Error(`versionCode "${versionCode}"를 ${track} 트랙에서 찾을 수 없어. 가능한 릴리스: ${JSON.stringify(available)}`);
248
+ }
249
+ const previousStatus = target.status;
250
+ target.status = status;
251
+ const updated = await publisher().edits.tracks.update({
252
+ auth, packageName, editId, track,
253
+ requestBody: { track, releases },
254
+ });
255
+ return {
256
+ track,
257
+ versionCode,
258
+ previousStatus,
259
+ newStatus: status,
260
+ committed: true,
261
+ result: updated.data,
262
+ };
263
+ }, true);
264
+ }
224
265
  export async function listReviews(auth, packageName) {
225
266
  const res = await publisher().reviews.list({ auth, packageName });
226
267
  return (res.data.reviews ?? []).map((r) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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": {