@yoonion/mimi-seed-mcp 0.3.3 → 0.3.4

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.
@@ -20,6 +20,25 @@ 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 AppInfoLocalizationFields {
24
+ name?: string;
25
+ subtitle?: string;
26
+ privacyPolicyUrl?: string;
27
+ privacyPolicyText?: string;
28
+ }
29
+ export declare function listAppInfoLocalizations(appId: string, locale?: string): Promise<{
30
+ appInfoId: string;
31
+ appInfoState: string;
32
+ localizations: {
33
+ id: any;
34
+ locale: any;
35
+ name: any;
36
+ subtitle: any;
37
+ privacyPolicyUrl: any;
38
+ privacyPolicyText: any;
39
+ }[];
40
+ }>;
41
+ export declare function updateAppInfoLocalization(localizationId: string, fields: AppInfoLocalizationFields): Promise<any>;
23
42
  export interface ListCustomerReviewsOptions {
24
43
  limit?: number;
25
44
  territory?: string;
@@ -27,4 +46,12 @@ export interface ListCustomerReviewsOptions {
27
46
  }
28
47
  export declare function listCustomerReviews(appId: string, opts?: ListCustomerReviewsOptions): Promise<any>;
29
48
  export declare function createReviewResponse(reviewId: string, responseBody: string): Promise<any>;
30
- export declare function submitVersionForReview(versionId: string): Promise<any>;
49
+ export declare function submitVersionForReview(versionId: string): Promise<{
50
+ submissionId: string;
51
+ appId: string;
52
+ platform: string;
53
+ versionId: string;
54
+ reusedSubmission: boolean;
55
+ itemAttached: boolean;
56
+ state: any;
57
+ }>;
@@ -174,16 +174,64 @@ export async function listBetaGroups(appId) {
174
174
  }));
175
175
  }
176
176
  // ─── 앱 정보 (카테고리 등) ───
177
+ // AppInfoState — READY_FOR_DISTRIBUTION이 라이브 버전, 그 외(PREPARE_FOR_SUBMISSION /
178
+ // DEVELOPER_REJECTED 등)가 편집 가능한 appInfo.
179
+ const APP_INFO_LIVE_STATE = 'READY_FOR_DISTRIBUTION';
177
180
  export async function getAppInfo(appId) {
178
181
  const data = await apiGet(`/apps/${appId}/appInfos`, {
179
- 'fields[appInfos]': 'appStoreState,appStoreAgeRating,brazilAgeRating',
182
+ 'fields[appInfos]': 'state,appStoreAgeRating,brazilAgeRating',
180
183
  });
181
184
  return (data.data ?? []).map((i) => ({
182
185
  id: i.id,
183
- state: i.attributes?.appStoreState,
186
+ // 새 필드명 state, 옛 필드명 appStoreState 모두 케어
187
+ state: i.attributes?.state ?? i.attributes?.appStoreState,
184
188
  ageRating: i.attributes?.appStoreAgeRating,
185
189
  }));
186
190
  }
191
+ async function findEditableAppInfoId(appId) {
192
+ const data = await apiGet(`/apps/${appId}/appInfos`, {
193
+ 'fields[appInfos]': 'state',
194
+ 'limit': '10',
195
+ });
196
+ const infos = (data?.data ?? []);
197
+ if (infos.length === 0) {
198
+ throw new Error(`앱 ${appId}에 appInfos가 없어. 앱 ID 확인 필요.`);
199
+ }
200
+ const stateOf = (i) => i.attributes?.state ?? i.attributes?.appStoreState ?? '';
201
+ const editable = infos.find((i) => stateOf(i) !== APP_INFO_LIVE_STATE);
202
+ const target = editable ?? infos[0];
203
+ return { appInfoId: target.id, state: stateOf(target) };
204
+ }
205
+ export async function listAppInfoLocalizations(appId, locale) {
206
+ const { appInfoId, state } = await findEditableAppInfoId(appId);
207
+ const data = await apiGet(`/appInfos/${appInfoId}/appInfoLocalizations`, {
208
+ 'fields[appInfoLocalizations]': 'locale,name,subtitle,privacyPolicyUrl,privacyPolicyText',
209
+ 'limit': '200',
210
+ });
211
+ const all = (data?.data ?? []).map((l) => ({
212
+ id: l.id,
213
+ locale: l.attributes?.locale,
214
+ name: l.attributes?.name,
215
+ subtitle: l.attributes?.subtitle,
216
+ privacyPolicyUrl: l.attributes?.privacyPolicyUrl,
217
+ privacyPolicyText: l.attributes?.privacyPolicyText,
218
+ }));
219
+ const filtered = locale ? all.filter((l) => l.locale === locale) : all;
220
+ return { appInfoId, appInfoState: state, localizations: filtered };
221
+ }
222
+ export async function updateAppInfoLocalization(localizationId, fields) {
223
+ const attributes = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
224
+ if (Object.keys(attributes).length === 0) {
225
+ throw new Error('수정할 필드가 없어 (name / subtitle / privacyPolicyUrl / privacyPolicyText 중 하나 이상).');
226
+ }
227
+ return apiPatch(`/appInfoLocalizations/${localizationId}`, {
228
+ data: {
229
+ type: 'appInfoLocalizations',
230
+ id: localizationId,
231
+ attributes,
232
+ },
233
+ });
234
+ }
187
235
  export async function listCustomerReviews(appId, opts = {}) {
188
236
  const params = {
189
237
  'sort': '-createdDate',
@@ -237,15 +285,92 @@ export async function createReviewResponse(reviewId, responseBody) {
237
285
  });
238
286
  }
239
287
  // ─── 심사 제출 (Submit for Review) ───
288
+ // 2024년 1월부터 옛 /appStoreVersionSubmissions가 deprecated 됐고,
289
+ // 새 모델은 /reviewSubmissions + /reviewSubmissionItems 2단계 + PATCH submitted=true.
290
+ // 참고: https://developer.apple.com/documentation/appstoreconnectapi/submit-an-app-for-review
291
+ async function getVersionAppAndPlatform(versionId) {
292
+ const data = await apiGet(`/appStoreVersions/${versionId}`, {
293
+ 'fields[appStoreVersions]': 'platform,app',
294
+ 'include': 'app',
295
+ });
296
+ const platform = data?.data?.attributes?.platform;
297
+ const appId = data?.data?.relationships?.app?.data?.id;
298
+ if (!platform || !appId) {
299
+ throw new Error(`appStoreVersion ${versionId}에서 app 또는 platform을 찾지 못했어. 버전 ID 확인 필요.`);
300
+ }
301
+ return { appId, platform };
302
+ }
303
+ async function findOpenReviewSubmission(appId, platform) {
304
+ const data = await apiGet('/reviewSubmissions', {
305
+ 'filter[app]': appId,
306
+ 'filter[platform]': platform,
307
+ 'filter[state]': 'READY_FOR_REVIEW,COMPLETING,UNRESOLVED_ISSUES',
308
+ 'limit': '1',
309
+ });
310
+ // CREATED 상태(아직 제출 안 된)도 별도 조회해서 우선 재사용
311
+ const created = await apiGet('/reviewSubmissions', {
312
+ 'filter[app]': appId,
313
+ 'filter[platform]': platform,
314
+ 'filter[state]': 'CREATED',
315
+ 'limit': '1',
316
+ });
317
+ return created?.data?.[0]?.id ?? data?.data?.[0]?.id ?? null;
318
+ }
319
+ async function isVersionAttached(submissionId, versionId) {
320
+ const data = await apiGet(`/reviewSubmissions/${submissionId}/items`, {
321
+ 'limit': '50',
322
+ });
323
+ const items = (data?.data ?? []);
324
+ return items.some((it) => it?.relationships?.appStoreVersion?.data?.id === versionId);
325
+ }
240
326
  export async function submitVersionForReview(versionId) {
241
- return apiPost('/appStoreVersionSubmissions', {
242
- data: {
243
- type: 'appStoreVersionSubmissions',
244
- relationships: {
245
- appStoreVersion: {
246
- data: { type: 'appStoreVersions', id: versionId },
327
+ const { appId, platform } = await getVersionAppAndPlatform(versionId);
328
+ // 1. CREATED 상태의 reviewSubmission이 있으면 재사용, 없으면 새로 생성
329
+ let submissionId = await findOpenReviewSubmission(appId, platform);
330
+ const reusedSubmission = Boolean(submissionId);
331
+ if (!submissionId) {
332
+ const created = await apiPost('/reviewSubmissions', {
333
+ data: {
334
+ type: 'reviewSubmissions',
335
+ attributes: { platform },
336
+ relationships: {
337
+ app: { data: { type: 'apps', id: appId } },
247
338
  },
248
339
  },
340
+ });
341
+ submissionId = created?.data?.id;
342
+ if (!submissionId) {
343
+ throw new Error(`reviewSubmission 생성 응답에 id가 없어: ${JSON.stringify(created)}`);
344
+ }
345
+ }
346
+ // 2. 버전을 reviewSubmissionItems로 attach (이미 붙어있으면 skip)
347
+ const alreadyAttached = reusedSubmission ? await isVersionAttached(submissionId, versionId) : false;
348
+ if (!alreadyAttached) {
349
+ await apiPost('/reviewSubmissionItems', {
350
+ data: {
351
+ type: 'reviewSubmissionItems',
352
+ relationships: {
353
+ reviewSubmission: { data: { type: 'reviewSubmissions', id: submissionId } },
354
+ appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
355
+ },
356
+ },
357
+ });
358
+ }
359
+ // 3. PATCH submitted=true → state: CREATED → WAITING_FOR_REVIEW
360
+ const submitted = await apiPatch(`/reviewSubmissions/${submissionId}`, {
361
+ data: {
362
+ type: 'reviewSubmissions',
363
+ id: submissionId,
364
+ attributes: { submitted: true },
249
365
  },
250
366
  });
367
+ return {
368
+ submissionId,
369
+ appId,
370
+ platform,
371
+ versionId,
372
+ reusedSubmission,
373
+ itemAttached: !alreadyAttached,
374
+ state: submitted?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW',
375
+ };
251
376
  }
package/dist/index.js CHANGED
@@ -631,10 +631,37 @@ server.tool('appstore_list_beta_groups', 'TestFlight 베타 그룹 목록', { ap
631
631
  const groups = await appstore.listBetaGroups(appId);
632
632
  return { content: [{ type: 'text', text: JSON.stringify(groups, null, 2) }] };
633
633
  });
634
- server.tool('appstore_get_app_info', 'App Store 앱 정보 (카테고리, 연령 등급)', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
634
+ server.tool('appstore_get_app_info', 'App Store 앱 정보 (카테고리, 연령 등급, state). state=READY_FOR_DISTRIBUTION이 라이브, 그 외가 편집 가능 appInfo.', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
635
635
  const info = await appstore.getAppInfo(appId);
636
636
  return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
637
637
  });
638
+ server.tool('appstore_list_app_info_localizations', [
639
+ '편집 가능한 appInfo의 로컬라이제이션 목록 조회 — 앱 이름(name), 부제(subtitle), 개인정보 URL/텍스트.',
640
+ 'appInfo.relationships.appInfoLocalizations가 빈 배열로 오는 케이스를 우회하려고 /appInfos/{id}/appInfoLocalizations 직접 호출.',
641
+ 'locale을 주면 해당 언어만 반환 (예: "ko", "en-US").',
642
+ '※ versionLocalization(설명/키워드/whatsNew)과 다름 — 그건 appstore_get_metadata 사용.',
643
+ ].join(' '), {
644
+ appId: z.string().describe('앱 ID (appstore_list_apps 결과)'),
645
+ locale: z.string().optional().describe('언어 필터 (예: "ko", "en-US"). 생략 시 전체 반환.'),
646
+ }, async ({ appId, locale }) => {
647
+ const result = await appstore.listAppInfoLocalizations(appId, locale);
648
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
649
+ });
650
+ server.tool('appstore_update_app_info_localization', [
651
+ 'appInfo 로컬라이제이션(앱 이름/부제/개인정보 URL/텍스트) 수정 — PATCH /appInfoLocalizations/{id}.',
652
+ 'localizationId는 appstore_list_app_info_localizations 결과의 id.',
653
+ '편집 가능 상태(PREPARE_FOR_SUBMISSION / DEVELOPER_REJECTED 등)에서만 반영됨.',
654
+ '제한: name 30자, subtitle 30자.',
655
+ ].join(' '), {
656
+ localizationId: z.string().describe('appInfoLocalization ID'),
657
+ name: z.string().optional().describe('앱 이름 (30자 이내)'),
658
+ subtitle: z.string().optional().describe('부제 (30자 이내)'),
659
+ privacyPolicyUrl: z.string().url().optional().describe('개인정보 처리방침 URL'),
660
+ privacyPolicyText: z.string().optional().describe('개인정보 처리방침 텍스트'),
661
+ }, async ({ localizationId, ...fields }) => {
662
+ const result = await appstore.updateAppInfoLocalization(localizationId, fields);
663
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
664
+ });
638
665
  // --- App Store 고객 리뷰 ---
639
666
  server.tool('appstore_list_reviews', [
640
667
  'App Store 받은 고객 리뷰 조회 (최신순).',
@@ -663,7 +690,9 @@ server.tool('appstore_reply_review', [
663
690
  });
664
691
  // --- App Store 심사 제출 (Submit for Review) ---
665
692
  server.tool('appstore_submit_for_review', [
666
- 'App Store 버전을 심사에 제출 — POST /appStoreVersionSubmissions.',
693
+ 'App Store 버전을 심사에 제출 — reviewSubmissions API 사용 (옛 /appStoreVersionSubmissions는 2024-01 deprecated).',
694
+ '내부 흐름: POST /reviewSubmissions(또는 CREATED 상태 재사용) → POST /reviewSubmissionItems(version attach) → PATCH submitted=true.',
695
+ 'appId와 platform은 versionId에서 자동 조회 — 별도 입력 불필요.',
667
696
  '⚠️ 비가역 작업: 제출 후엔 Apple 심사가 시작되며, 메타데이터/스크린샷/빌드를 더 못 바꿈 (REJECTED/METADATA_REJECTED 시 다시 편집 가능).',
668
697
  '사전 조건: 버전이 PREPARE_FOR_SUBMISSION 또는 DEVELOPER_REJECTED 상태, 빌드 attached, 모든 필수 메타데이터 채워짐.',
669
698
  'appstore_check_submission_risks로 사전 점검 권장.',
@@ -671,7 +700,7 @@ server.tool('appstore_submit_for_review', [
671
700
  versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
672
701
  }, async ({ versionId }) => {
673
702
  const result = await appstore.submitVersionForReview(versionId);
674
- return { content: [{ type: 'text', text: `✅ 버전 ${versionId} 심사 제출 완료. App Store Connect에서 진행 상태 확인 가능.\n\n${JSON.stringify(result, null, 2)}` }] };
703
+ return { content: [{ type: 'text', text: `✅ 버전 ${versionId} 심사 제출 완료 (state: ${result.state}). App Store Connect에서 진행 상태 확인 가능.\n\n${JSON.stringify(result, null, 2)}` }] };
675
704
  });
676
705
  // --- Play Store 심사 제출 / release status 변경 ---
677
706
  server.tool('playstore_submit_release', [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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": {