@yoonion/mimi-seed-mcp 0.3.12 → 0.3.14
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/appstore/auth.d.ts +2 -2
- package/dist/appstore/auth.js +35 -18
- package/dist/appstore/screenshots.js +3 -3
- package/dist/appstore/tools.d.ts +9 -0
- package/dist/appstore/tools.js +58 -4
- package/dist/index.js +26 -0
- package/package.json +2 -3
package/dist/appstore/auth.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export interface AppStoreCredentials {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function getAppStoreCredentials(): AppStoreCredentials | null;
|
|
7
7
|
export declare function saveAppStoreCredentials(creds: AppStoreCredentials): void;
|
|
8
|
-
export declare function generateToken(creds: AppStoreCredentials): string
|
|
9
|
-
export declare function getAuthHeaders(): Record<string, string> | null
|
|
8
|
+
export declare function generateToken(creds: AppStoreCredentials): Promise<string>;
|
|
9
|
+
export declare function getAuthHeaders(): Promise<Record<string, string> | null>;
|
package/dist/appstore/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SignJWT, importPKCS8 } from 'jose';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -28,26 +28,43 @@ export function saveAppStoreCredentials(creds) {
|
|
|
28
28
|
fs.mkdirSync(dir, { recursive: true });
|
|
29
29
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
});
|
|
31
|
+
function normalizePrivateKey(raw) {
|
|
32
|
+
// Normalize CRLF → LF, strip extra whitespace from lines
|
|
33
|
+
const lines = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
34
|
+
const header = lines.find(l => l.startsWith('-----BEGIN'));
|
|
35
|
+
const footer = lines.find(l => l.startsWith('-----END'));
|
|
36
|
+
if (!header || !footer)
|
|
37
|
+
return raw; // not PEM, pass through and let importPKCS8 error
|
|
38
|
+
const body = lines
|
|
39
|
+
.filter(l => l && !l.startsWith('-----'))
|
|
40
|
+
.join('');
|
|
41
|
+
// Re-chunk into 64-char lines (standard PEM)
|
|
42
|
+
const chunks = body.match(/.{1,64}/g) ?? [];
|
|
43
|
+
return [header, ...chunks, footer, ''].join('\n');
|
|
46
44
|
}
|
|
47
|
-
export function
|
|
45
|
+
export async function generateToken(creds) {
|
|
46
|
+
const normalizedKey = normalizePrivateKey(creds.privateKey);
|
|
47
|
+
let key;
|
|
48
|
+
try {
|
|
49
|
+
key = await importPKCS8(normalizedKey, 'ES256');
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new Error(`App Store 개인 키 파싱 실패 — ~/.mimi-seed/appstore.json의 privateKey 형식 확인 필요.\n원인: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
// Subtract 60s from iat to tolerate local clock running slightly ahead of Apple servers.
|
|
55
|
+
const iat = Math.floor(Date.now() / 1000) - 60;
|
|
56
|
+
return new SignJWT({})
|
|
57
|
+
.setProtectedHeader({ alg: 'ES256', kid: creds.keyId, typ: 'JWT' })
|
|
58
|
+
.setIssuer(creds.issuerId)
|
|
59
|
+
.setIssuedAt(iat)
|
|
60
|
+
.setExpirationTime(iat + 20 * 60)
|
|
61
|
+
.setAudience('appstoreconnect-v1')
|
|
62
|
+
.sign(key);
|
|
63
|
+
}
|
|
64
|
+
export async function getAuthHeaders() {
|
|
48
65
|
const creds = getAppStoreCredentials();
|
|
49
66
|
if (!creds)
|
|
50
67
|
return null;
|
|
51
|
-
const token = generateToken(creds);
|
|
68
|
+
const token = await generateToken(creds);
|
|
52
69
|
return { Authorization: `Bearer ${token}` };
|
|
53
70
|
}
|
|
@@ -14,8 +14,8 @@ import crypto from 'node:crypto';
|
|
|
14
14
|
* 4. commit — PATCH /appScreenshots/{id} { uploaded: true, sourceFileChecksum }
|
|
15
15
|
*/
|
|
16
16
|
const BASE = 'https://api.appstoreconnect.apple.com/v1';
|
|
17
|
-
function authHeadersOrThrow() {
|
|
18
|
-
const headers = getAuthHeaders();
|
|
17
|
+
async function authHeadersOrThrow() {
|
|
18
|
+
const headers = await getAuthHeaders();
|
|
19
19
|
if (!headers) {
|
|
20
20
|
throw new Error([
|
|
21
21
|
'❌ App Store Connect 인증이 필요해.',
|
|
@@ -27,7 +27,7 @@ function authHeadersOrThrow() {
|
|
|
27
27
|
return headers;
|
|
28
28
|
}
|
|
29
29
|
async function req(pathOrUrl, init = {}) {
|
|
30
|
-
const headers = authHeadersOrThrow();
|
|
30
|
+
const headers = await authHeadersOrThrow();
|
|
31
31
|
const url = pathOrUrl.startsWith('http') ? pathOrUrl : `${BASE}${pathOrUrl}`;
|
|
32
32
|
const res = await fetch(url, {
|
|
33
33
|
...init,
|
package/dist/appstore/tools.d.ts
CHANGED
|
@@ -41,6 +41,15 @@ export declare function updateVersionLocalization(localizationId: string, fields
|
|
|
41
41
|
* localizationId를 직접 모를 때 편의용.
|
|
42
42
|
*/
|
|
43
43
|
export declare function updateVersionWhatsNew(versionId: string, locale: string, fields: LocalizationUpdateFields): Promise<any>;
|
|
44
|
+
export declare function updateReviewNotes(versionId: string, notes: string): Promise<{
|
|
45
|
+
reviewDetailId: string;
|
|
46
|
+
notes: string;
|
|
47
|
+
}>;
|
|
48
|
+
export declare function getReviewNotes(versionId: string): Promise<{
|
|
49
|
+
reviewDetailId: string | null;
|
|
50
|
+
notes: string | null;
|
|
51
|
+
contactEmail: string | null;
|
|
52
|
+
}>;
|
|
44
53
|
export declare function listBuilds(appId: string): Promise<any>;
|
|
45
54
|
export declare function listBetaGroups(appId: string): Promise<any>;
|
|
46
55
|
export declare function getAppInfo(appId: string): Promise<any>;
|
package/dist/appstore/tools.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getAuthHeaders } from './auth.js';
|
|
|
5
5
|
*/
|
|
6
6
|
const BASE = 'https://api.appstoreconnect.apple.com/v1';
|
|
7
7
|
export async function apiGet(path, params) {
|
|
8
|
-
const headers = getAuthHeaders();
|
|
8
|
+
const headers = await getAuthHeaders();
|
|
9
9
|
if (!headers)
|
|
10
10
|
throw new Error([
|
|
11
11
|
'❌ App Store Connect 인증이 필요해.',
|
|
@@ -29,7 +29,7 @@ export async function apiGet(path, params) {
|
|
|
29
29
|
return res.json();
|
|
30
30
|
}
|
|
31
31
|
async function apiPatch(path, body) {
|
|
32
|
-
const headers = getAuthHeaders();
|
|
32
|
+
const headers = await getAuthHeaders();
|
|
33
33
|
if (!headers)
|
|
34
34
|
throw new Error([
|
|
35
35
|
'❌ App Store Connect 인증이 필요해.',
|
|
@@ -51,7 +51,7 @@ async function apiPatch(path, body) {
|
|
|
51
51
|
return text ? JSON.parse(text) : { ok: true };
|
|
52
52
|
}
|
|
53
53
|
async function apiPost(path, body) {
|
|
54
|
-
const headers = getAuthHeaders();
|
|
54
|
+
const headers = await getAuthHeaders();
|
|
55
55
|
if (!headers)
|
|
56
56
|
throw new Error([
|
|
57
57
|
'❌ App Store Connect 인증이 필요해.',
|
|
@@ -185,6 +185,60 @@ export async function updateVersionWhatsNew(versionId, locale, fields) {
|
|
|
185
185
|
}
|
|
186
186
|
return updateVersionLocalization(target.id, fields);
|
|
187
187
|
}
|
|
188
|
+
// ─── 리뷰어 노트 (appStoreReviewDetail.notes) ───
|
|
189
|
+
export async function updateReviewNotes(versionId, notes) {
|
|
190
|
+
// 1. 기존 reviewDetail 조회
|
|
191
|
+
let reviewDetailId = null;
|
|
192
|
+
try {
|
|
193
|
+
const existing = await apiGet(`/appStoreVersions/${versionId}/appStoreReviewDetail`, {
|
|
194
|
+
'fields[appStoreReviewDetails]': 'notes,contactFirstName,contactLastName,contactPhone,contactEmail',
|
|
195
|
+
});
|
|
196
|
+
reviewDetailId = existing?.data?.id ?? null;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// 없으면 새로 생성
|
|
200
|
+
}
|
|
201
|
+
if (reviewDetailId) {
|
|
202
|
+
// 2a. 있으면 PATCH
|
|
203
|
+
const updated = await apiPatch(`/appStoreReviewDetails/${reviewDetailId}`, {
|
|
204
|
+
data: {
|
|
205
|
+
type: 'appStoreReviewDetails',
|
|
206
|
+
id: reviewDetailId,
|
|
207
|
+
attributes: { notes },
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
return { reviewDetailId, notes: updated?.data?.attributes?.notes ?? notes };
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// 2b. 없으면 POST (version과 relationship 연결)
|
|
214
|
+
const created = await apiPost('/appStoreReviewDetails', {
|
|
215
|
+
data: {
|
|
216
|
+
type: 'appStoreReviewDetails',
|
|
217
|
+
attributes: { notes },
|
|
218
|
+
relationships: {
|
|
219
|
+
appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const newId = created?.data?.id ?? '';
|
|
224
|
+
return { reviewDetailId: newId, notes: created?.data?.attributes?.notes ?? notes };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export async function getReviewNotes(versionId) {
|
|
228
|
+
try {
|
|
229
|
+
const data = await apiGet(`/appStoreVersions/${versionId}/appStoreReviewDetail`, {
|
|
230
|
+
'fields[appStoreReviewDetails]': 'notes,contactEmail,demoAccountName,demoAccountRequired',
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
reviewDetailId: data?.data?.id ?? null,
|
|
234
|
+
notes: data?.data?.attributes?.notes ?? null,
|
|
235
|
+
contactEmail: data?.data?.attributes?.contactEmail ?? null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return { reviewDetailId: null, notes: null, contactEmail: null };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
188
242
|
// ─── 빌드 ───
|
|
189
243
|
export async function listBuilds(appId) {
|
|
190
244
|
const data = await apiGet(`/builds`, {
|
|
@@ -421,7 +475,7 @@ export async function submitVersionForReview(versionId) {
|
|
|
421
475
|
// 자동 제출(submission)은 권장 가이드라인 (스크린샷·리뷰 노트 등)이 충족돼야 통과.
|
|
422
476
|
const IAP_V2_BASE = 'https://api.appstoreconnect.apple.com/v2';
|
|
423
477
|
async function apiPostV2(path, body) {
|
|
424
|
-
const headers = getAuthHeaders();
|
|
478
|
+
const headers = await getAuthHeaders();
|
|
425
479
|
if (!headers) {
|
|
426
480
|
throw new Error('App Store Connect 인증 필요 — npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth');
|
|
427
481
|
}
|
package/dist/index.js
CHANGED
|
@@ -830,6 +830,32 @@ server.tool('appstore_update_whats_new', "App Store '이 버전의 새로운 기
|
|
|
830
830
|
const result = await appstore.updateVersionWhatsNew(versionId, locale, { whatsNew });
|
|
831
831
|
return { content: [{ type: 'text', text: `✅ ${locale} 로캘의 What's New가 업데이트됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
832
832
|
});
|
|
833
|
+
server.tool('appstore_update_review_notes', "App Store 심사 리뷰어 노트(Notes for App Review) 등록/수정. versionId 버전에 appStoreReviewDetail.notes를 PATCH하거나 없으면 POST로 생성. 심사 시 리뷰어에게 전달되는 테스트 계정·기능 안내 텍스트 작성에 사용.", {
|
|
834
|
+
versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
|
|
835
|
+
notes: z.string().describe('리뷰어에게 전달할 메모 (테스트 계정, 주요 변경사항, 접근 방법 등). 4000자 이내 권장.'),
|
|
836
|
+
}, async ({ versionId, notes }) => {
|
|
837
|
+
const result = await appstore.updateReviewNotes(versionId, notes);
|
|
838
|
+
return {
|
|
839
|
+
content: [{
|
|
840
|
+
type: 'text',
|
|
841
|
+
text: `✅ 리뷰어 노트가 저장됐어.\n\nreviewDetailId: ${result.reviewDetailId}\n\n${result.notes}`,
|
|
842
|
+
}],
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
server.tool('appstore_get_review_notes', "App Store 심사 리뷰어 노트(Notes for App Review) 조회. 현재 등록된 notes, contactEmail 확인용.", {
|
|
846
|
+
versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
|
|
847
|
+
}, async ({ versionId }) => {
|
|
848
|
+
const result = await appstore.getReviewNotes(versionId);
|
|
849
|
+
if (!result.reviewDetailId) {
|
|
850
|
+
return { content: [{ type: 'text', text: '이 버전에는 아직 리뷰어 노트가 없어. appstore_update_review_notes로 등록해줘.' }] };
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
content: [{
|
|
854
|
+
type: 'text',
|
|
855
|
+
text: `reviewDetailId: ${result.reviewDetailId}\ncontactEmail: ${result.contactEmail ?? '(없음)'}\n\n노트:\n${result.notes ?? '(비어있음)'}`,
|
|
856
|
+
}],
|
|
857
|
+
};
|
|
858
|
+
});
|
|
833
859
|
server.tool('appstore_list_builds', 'TestFlight 빌드 목록', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
834
860
|
const builds = await appstore.listBuilds(appId);
|
|
835
861
|
return { content: [{ type: 'text', text: JSON.stringify(builds, null, 2) }] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoonion/mimi-seed-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
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": {
|
|
@@ -47,12 +47,11 @@
|
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
48
48
|
"@onesub/providers": "^0.2.0",
|
|
49
49
|
"googleapis": "^171.4.0",
|
|
50
|
-
"
|
|
50
|
+
"jose": "^5.10.0",
|
|
51
51
|
"open": "^10.1.0",
|
|
52
52
|
"zod": "^3.24.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@types/jsonwebtoken": "^9.0.10",
|
|
56
55
|
"@types/node": "^22.0.0",
|
|
57
56
|
"tsx": "^4.19.0",
|
|
58
57
|
"typescript": "^5.7.0"
|