@yoonion/mimi-seed-mcp 0.3.14 → 0.3.16
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/tools.js +5 -9
- package/dist/bigquery/tools.d.ts +29 -0
- package/dist/bigquery/tools.js +53 -0
- package/dist/checks/risks.js +65 -11
- package/dist/index.js +65 -0
- package/package.json +1 -1
package/dist/appstore/tools.js
CHANGED
|
@@ -395,20 +395,16 @@ async function getVersionAppAndPlatform(versionId) {
|
|
|
395
395
|
return { appId, platform };
|
|
396
396
|
}
|
|
397
397
|
async function findOpenReviewSubmission(appId, platform) {
|
|
398
|
+
// ASC API는 filter[state]=CREATED를 더 이상 허용하지 않음 (READY_FOR_REVIEW, WAITING_FOR_REVIEW 등만 허용).
|
|
399
|
+
// CREATED 상태 submission은 별도로 조회 불가 → 이미 진행 중인 submission만 재사용.
|
|
400
|
+
// 없으면 submitVersionForReview가 새로 생성.
|
|
398
401
|
const data = await apiGet('/reviewSubmissions', {
|
|
399
402
|
'filter[app]': appId,
|
|
400
403
|
'filter[platform]': platform,
|
|
401
|
-
'filter[state]': 'READY_FOR_REVIEW,COMPLETING,UNRESOLVED_ISSUES',
|
|
404
|
+
'filter[state]': 'READY_FOR_REVIEW,WAITING_FOR_REVIEW,COMPLETING,UNRESOLVED_ISSUES',
|
|
402
405
|
'limit': '1',
|
|
403
406
|
});
|
|
404
|
-
|
|
405
|
-
const created = await apiGet('/reviewSubmissions', {
|
|
406
|
-
'filter[app]': appId,
|
|
407
|
-
'filter[platform]': platform,
|
|
408
|
-
'filter[state]': 'CREATED',
|
|
409
|
-
'limit': '1',
|
|
410
|
-
});
|
|
411
|
-
return created?.data?.[0]?.id ?? data?.data?.[0]?.id ?? null;
|
|
407
|
+
return data?.data?.[0]?.id ?? null;
|
|
412
408
|
}
|
|
413
409
|
async function isVersionAttached(submissionId, versionId) {
|
|
414
410
|
const data = await apiGet(`/reviewSubmissions/${submissionId}/items`, {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
export declare function runQuery(auth: OAuth2Client, projectId: string, query: string, maxResults?: number): Promise<{
|
|
3
|
+
jobComplete: boolean | null | undefined;
|
|
4
|
+
totalRows: string | null | undefined;
|
|
5
|
+
schema: {
|
|
6
|
+
name: string | null | undefined;
|
|
7
|
+
type: string | null | undefined;
|
|
8
|
+
}[];
|
|
9
|
+
rows: {
|
|
10
|
+
[k: string]: any;
|
|
11
|
+
}[];
|
|
12
|
+
}>;
|
|
13
|
+
export declare function listDatasets(auth: OAuth2Client, projectId: string): Promise<{
|
|
14
|
+
datasetId: string | null | undefined;
|
|
15
|
+
location: string | undefined;
|
|
16
|
+
}[]>;
|
|
17
|
+
export declare function listTables(auth: OAuth2Client, projectId: string, datasetId: string): Promise<{
|
|
18
|
+
tableId: string | null | undefined;
|
|
19
|
+
type: string | undefined;
|
|
20
|
+
}[]>;
|
|
21
|
+
export declare function getTableSchema(auth: OAuth2Client, projectId: string, datasetId: string, tableId: string): Promise<{
|
|
22
|
+
tableId: string;
|
|
23
|
+
schema: {
|
|
24
|
+
name: string | null | undefined;
|
|
25
|
+
type: string | null | undefined;
|
|
26
|
+
mode: string | null | undefined;
|
|
27
|
+
description: string | null | undefined;
|
|
28
|
+
}[] | undefined;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
const bq = () => google.bigquery('v2');
|
|
3
|
+
// ─── 쿼리 실행 ───
|
|
4
|
+
export async function runQuery(auth, projectId, query, maxResults = 1000) {
|
|
5
|
+
const res = await bq().jobs.query({
|
|
6
|
+
auth,
|
|
7
|
+
projectId,
|
|
8
|
+
requestBody: {
|
|
9
|
+
query,
|
|
10
|
+
useLegacySql: false,
|
|
11
|
+
maxResults,
|
|
12
|
+
timeoutMs: 30000,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
const data = res.data;
|
|
16
|
+
const schema = data.schema?.fields ?? [];
|
|
17
|
+
const rows = (data.rows ?? []).map((row) => Object.fromEntries((row.f ?? []).map((cell, i) => [schema[i]?.name ?? `col${i}`, cell.v])));
|
|
18
|
+
return {
|
|
19
|
+
jobComplete: data.jobComplete,
|
|
20
|
+
totalRows: data.totalRows,
|
|
21
|
+
schema: schema.map((f) => ({ name: f.name, type: f.type })),
|
|
22
|
+
rows,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// ─── 데이터셋 목록 ───
|
|
26
|
+
export async function listDatasets(auth, projectId) {
|
|
27
|
+
const res = await bq().datasets.list({ auth, projectId });
|
|
28
|
+
return (res.data.datasets ?? []).map((d) => ({
|
|
29
|
+
datasetId: d.datasetReference?.datasetId,
|
|
30
|
+
location: d.location,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
// ─── 테이블 목록 ───
|
|
34
|
+
export async function listTables(auth, projectId, datasetId) {
|
|
35
|
+
const res = await bq().tables.list({ auth, projectId, datasetId });
|
|
36
|
+
return (res.data.tables ?? []).map((t) => ({
|
|
37
|
+
tableId: t.tableReference?.tableId,
|
|
38
|
+
type: t.type,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
// ─── 테이블 스키마 ───
|
|
42
|
+
export async function getTableSchema(auth, projectId, datasetId, tableId) {
|
|
43
|
+
const res = await bq().tables.get({ auth, projectId, datasetId, tableId });
|
|
44
|
+
return {
|
|
45
|
+
tableId,
|
|
46
|
+
schema: res.data.schema?.fields?.map((f) => ({
|
|
47
|
+
name: f.name,
|
|
48
|
+
type: f.type,
|
|
49
|
+
mode: f.mode,
|
|
50
|
+
description: f.description,
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
53
|
+
}
|
package/dist/checks/risks.js
CHANGED
|
@@ -53,21 +53,42 @@ export async function checkPlayStoreRisks(auth, packageName, language = 'ko-KR')
|
|
|
53
53
|
});
|
|
54
54
|
return risks;
|
|
55
55
|
}
|
|
56
|
+
// Apple의 ASC API 호출은 권한·필드명·deprecated endpoint 등으로 실패할 수 있다.
|
|
57
|
+
// 실패를 silent하게 null로 만들면 후속 check가 false-positive blocker를 토해낸다 —
|
|
58
|
+
// 이 헬퍼는 실패 사실을 risks 배열에 warning으로 기록해 사용자에게 노출시키되,
|
|
59
|
+
// 호출부는 null을 받아 blocker 분기를 건너뛸 수 있게 해준다.
|
|
60
|
+
async function safeGet(call, risks, code, title) {
|
|
61
|
+
try {
|
|
62
|
+
return await call();
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
risks.push({
|
|
66
|
+
level: 'warning',
|
|
67
|
+
code: `API_ERROR_${code}`,
|
|
68
|
+
title: `App Store Connect 조회 실패 — ${title}`,
|
|
69
|
+
detail: `이 항목은 위험 분석에서 제외됨. 원인: ${e instanceof Error ? e.message : String(e)}`,
|
|
70
|
+
});
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const APP_INFO_LIVE_STATE = 'READY_FOR_DISTRIBUTION';
|
|
56
75
|
export async function checkAppStoreRisks(appId) {
|
|
57
76
|
const risks = [];
|
|
58
|
-
const versions = await apiGet(`/apps/${appId}/appStoreVersions`, {
|
|
77
|
+
const versions = await safeGet(() => apiGet(`/apps/${appId}/appStoreVersions`, {
|
|
59
78
|
'filter[appStoreState]': APPSTORE_EDITABLE_STATES,
|
|
60
79
|
'limit': '5',
|
|
61
|
-
})
|
|
80
|
+
}), risks, 'VERSIONS', '편집 가능한 버전 목록');
|
|
62
81
|
if (!versions?.data?.length) {
|
|
63
82
|
risks.push({ level: 'warning', code: 'NO_EDITABLE_VERSION', title: '편집 가능한 버전 없음', detail: 'App Store Connect에서 새 버전을 만드세요.', fixUrl: 'https://appstoreconnect.apple.com' });
|
|
64
83
|
return risks;
|
|
65
84
|
}
|
|
66
85
|
const versionId = versions.data[0].id;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
apiGet(`/
|
|
86
|
+
// 1) 메타데이터·스크린샷·버전 첨부 빌드는 versionId 기반으로 조회.
|
|
87
|
+
// 2) 개인정보 URL은 appInfoLocalization 단위(앱 단위)로 조회.
|
|
88
|
+
const [locs, attachedBuildRel, appInfos] = await Promise.all([
|
|
89
|
+
safeGet(() => apiGet(`/appStoreVersions/${versionId}/appStoreVersionLocalizations`), risks, 'LOCALIZATIONS', '버전 로컬라이제이션'),
|
|
90
|
+
safeGet(() => apiGet(`/appStoreVersions/${versionId}/relationships/build`), risks, 'BUILD', '버전 첨부 빌드'),
|
|
91
|
+
safeGet(() => apiGet(`/apps/${appId}/appInfos`, { 'fields[appInfos]': 'state', 'limit': '10' }), risks, 'APP_INFO', '앱 정보'),
|
|
71
92
|
]);
|
|
72
93
|
if (!locs?.data?.length) {
|
|
73
94
|
risks.push({ level: 'blocker', code: 'NO_LOCALIZATIONS', title: '로컬라이제이션 없음', detail: '메타데이터를 입력하세요.' });
|
|
@@ -84,16 +105,49 @@ export async function checkAppStoreRisks(appId) {
|
|
|
84
105
|
}
|
|
85
106
|
// screenshots depend on locs being present
|
|
86
107
|
const locId = locs.data[0].id;
|
|
87
|
-
const screenshots = await apiGet(`/appStoreVersionLocalizations/${locId}/appScreenshotSets`)
|
|
88
|
-
if (!screenshots?.data?.length) {
|
|
108
|
+
const screenshots = await safeGet(() => apiGet(`/appStoreVersionLocalizations/${locId}/appScreenshotSets`), risks, 'SCREENSHOTS', '스크린샷 셋');
|
|
109
|
+
if (screenshots && !screenshots?.data?.length) {
|
|
89
110
|
risks.push({ level: 'blocker', code: 'NO_SCREENSHOTS', title: '스크린샷 없음', detail: 'iPhone 6.5" 또는 6.9" 스크린샷이 필요합니다.' });
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
|
-
|
|
113
|
+
// 빌드 — 1차: 버전에 첨부된 빌드. 2차 폴백: 앱 전체 빌드 (예전 동작).
|
|
114
|
+
// attachedBuildRel?.data가 null이면 "관계 조회 성공 + 첨부 안 됨" 의미.
|
|
115
|
+
// attachedBuildRel 자체가 null이면 safeGet 실패 — fall back.
|
|
116
|
+
let buildVerdict;
|
|
117
|
+
if (attachedBuildRel) {
|
|
118
|
+
buildVerdict = attachedBuildRel.data ? 'present' : 'missing';
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const fallbackBuilds = await safeGet(() => apiGet(`/builds`, {
|
|
122
|
+
'filter[app]': appId,
|
|
123
|
+
'fields[builds]': 'version,uploadedDate,processingState',
|
|
124
|
+
'sort': '-uploadedDate',
|
|
125
|
+
'limit': '5',
|
|
126
|
+
}), risks, 'BUILDS_FALLBACK', '앱 빌드 목록(폴백)');
|
|
127
|
+
buildVerdict = fallbackBuilds?.data?.length ? 'present' : fallbackBuilds ? 'missing' : 'unknown';
|
|
128
|
+
}
|
|
129
|
+
if (buildVerdict === 'missing') {
|
|
93
130
|
risks.push({ level: 'blocker', code: 'NO_BUILD', title: 'TestFlight 빌드 없음', detail: 'Xcode 또는 Fastlane으로 빌드를 업로드하세요.' });
|
|
94
131
|
}
|
|
95
|
-
|
|
96
|
-
|
|
132
|
+
// 개인정보처리방침 — appInfoLocalization 어디 한 곳이라도 URL 또는 in-app 텍스트가 있으면 OK.
|
|
133
|
+
// appInfos 조회 자체가 실패하면 unknown으로 두고 blocker 안 띄움 (warning만 기록됨).
|
|
134
|
+
if (appInfos) {
|
|
135
|
+
const infos = (appInfos.data ?? []);
|
|
136
|
+
const stateOf = (i) => i.attributes?.state ?? i.attributes?.appStoreState ?? '';
|
|
137
|
+
const editable = infos.find((i) => stateOf(i) !== APP_INFO_LIVE_STATE) ?? infos[0];
|
|
138
|
+
if (editable) {
|
|
139
|
+
const localizations = await safeGet(() => apiGet(`/appInfos/${editable.id}/appInfoLocalizations`, {
|
|
140
|
+
'fields[appInfoLocalizations]': 'locale,privacyPolicyUrl,privacyPolicyText',
|
|
141
|
+
'limit': '200',
|
|
142
|
+
}), risks, 'PRIVACY', '개인정보 로컬라이제이션');
|
|
143
|
+
if (localizations) {
|
|
144
|
+
const locs = (localizations.data ?? []);
|
|
145
|
+
const hasPrivacy = locs.some((l) => l.attributes?.privacyPolicyUrl || l.attributes?.privacyPolicyText);
|
|
146
|
+
if (!hasPrivacy) {
|
|
147
|
+
risks.push({ level: 'blocker', code: 'NO_PRIVACY', title: '개인정보처리방침 URL 없음', detail: 'App Store 가이드라인 5.1.1 — 필수 항목입니다. 모든 로컬라이제이션에서 privacyPolicyUrl 또는 privacyPolicyText 중 하나를 입력하세요.' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
97
151
|
}
|
|
98
152
|
return risks;
|
|
99
153
|
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as playstore from './playstore/tools.js';
|
|
|
13
13
|
import * as appstore from './appstore/tools.js';
|
|
14
14
|
import * as appstoreScreenshots from './appstore/screenshots.js';
|
|
15
15
|
import * as iam from './iam/tools.js';
|
|
16
|
+
import * as bigquery from './bigquery/tools.js';
|
|
16
17
|
import { checkPlayStoreRisks, checkAppStoreRisks, formatRisks } from './checks/risks.js';
|
|
17
18
|
import { buildPlayStoreReleasePlan, buildAppStoreReleasePlan } from './checks/plan.js';
|
|
18
19
|
import { validateAppStoreScreenshots, validatePlayStoreScreenshots, formatValidationResults } from './checks/screenshots.js';
|
|
@@ -1407,9 +1408,73 @@ server.tool('generate_review_reply', [
|
|
|
1407
1408
|
return { content: [{ type: 'text', text }] };
|
|
1408
1409
|
});
|
|
1409
1410
|
// ══════════════════════════════════════════════════
|
|
1411
|
+
// BigQuery
|
|
1412
|
+
// ══════════════════════════════════════════════════
|
|
1413
|
+
server.tool('bigquery_run_query', 'BigQuery SQL 쿼리 실행 (SELECT). GA4 analytics_* 테이블 분석에 사용.', {
|
|
1414
|
+
projectId: z.string().describe('GCP 프로젝트 ID (예: ads-coffee)'),
|
|
1415
|
+
query: z.string().describe('실행할 StandardSQL 쿼리'),
|
|
1416
|
+
maxResults: z.number().optional().describe('최대 행 수 (기본 1000)'),
|
|
1417
|
+
}, async ({ projectId, query, maxResults }) => {
|
|
1418
|
+
const auth = requireAuth();
|
|
1419
|
+
const result = await bigquery.runQuery(auth, projectId, query, maxResults ?? 1000);
|
|
1420
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1421
|
+
});
|
|
1422
|
+
server.tool('bigquery_list_datasets', 'BigQuery 프로젝트의 데이터셋 목록 조회', {
|
|
1423
|
+
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
1424
|
+
}, async ({ projectId }) => {
|
|
1425
|
+
const auth = requireAuth();
|
|
1426
|
+
const datasets = await bigquery.listDatasets(auth, projectId);
|
|
1427
|
+
return { content: [{ type: 'text', text: JSON.stringify(datasets, null, 2) }] };
|
|
1428
|
+
});
|
|
1429
|
+
server.tool('bigquery_list_tables', 'BigQuery 데이터셋의 테이블 목록 조회', {
|
|
1430
|
+
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
1431
|
+
datasetId: z.string().describe('데이터셋 ID (예: analytics_530080532)'),
|
|
1432
|
+
}, async ({ projectId, datasetId }) => {
|
|
1433
|
+
const auth = requireAuth();
|
|
1434
|
+
const tables = await bigquery.listTables(auth, projectId, datasetId);
|
|
1435
|
+
return { content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }] };
|
|
1436
|
+
});
|
|
1437
|
+
server.tool('bigquery_get_table_schema', 'BigQuery 테이블 스키마(컬럼 정보) 조회', {
|
|
1438
|
+
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
1439
|
+
datasetId: z.string().describe('데이터셋 ID'),
|
|
1440
|
+
tableId: z.string().describe('테이블 ID'),
|
|
1441
|
+
}, async ({ projectId, datasetId, tableId }) => {
|
|
1442
|
+
const auth = requireAuth();
|
|
1443
|
+
const schema = await bigquery.getTableSchema(auth, projectId, datasetId, tableId);
|
|
1444
|
+
return { content: [{ type: 'text', text: JSON.stringify(schema, null, 2) }] };
|
|
1445
|
+
});
|
|
1446
|
+
// ══════════════════════════════════════════════════
|
|
1410
1447
|
// Start
|
|
1411
1448
|
// ══════════════════════════════════════════════════
|
|
1449
|
+
// `npx -y @yoonion/mimi-seed-mcp <subcommand>` 처리.
|
|
1450
|
+
// npx는 스코프 패키지의 basename(`mimi-seed-mcp`)을 매치해 이 bin을 실행하므로,
|
|
1451
|
+
// 추가 인자(`mimi-seed-auth` 등)는 여기 argv로 흘러들어온다. 이전엔 MCP 서버가
|
|
1452
|
+
// stdin을 기다리며 영구 hang 됐다 — 이제 sub-CLI로 위임한다.
|
|
1453
|
+
const SUBCOMMANDS = {
|
|
1454
|
+
'mimi-seed-auth': () => import('./auth/cli.js'),
|
|
1455
|
+
'mimi-seed-playstore-auth': () => import('./auth/playstore-setup-cli.js'),
|
|
1456
|
+
'mimi-seed-appstore-auth': () => import('./appstore/setup-cli.js'),
|
|
1457
|
+
};
|
|
1412
1458
|
async function main() {
|
|
1459
|
+
const sub = process.argv[2];
|
|
1460
|
+
if (sub && SUBCOMMANDS[sub]) {
|
|
1461
|
+
// argv에서 subcommand 토큰 제거 후 sub-CLI 모듈 import — 모듈 top-level이 main() 실행
|
|
1462
|
+
process.argv.splice(2, 1);
|
|
1463
|
+
await SUBCOMMANDS[sub]();
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (sub && !sub.startsWith('-')) {
|
|
1467
|
+
console.error([
|
|
1468
|
+
`❌ Unknown subcommand: ${sub}`,
|
|
1469
|
+
'',
|
|
1470
|
+
'Available:',
|
|
1471
|
+
...Object.keys(SUBCOMMANDS).map((k) => ` npx -y @yoonion/mimi-seed-mcp ${k}`),
|
|
1472
|
+
'',
|
|
1473
|
+
'Or run the MCP server (no args):',
|
|
1474
|
+
' npx -y @yoonion/mimi-seed-mcp',
|
|
1475
|
+
].join('\n'));
|
|
1476
|
+
process.exit(2);
|
|
1477
|
+
}
|
|
1413
1478
|
const transport = new StdioServerTransport();
|
|
1414
1479
|
await server.connect(transport);
|
|
1415
1480
|
}
|
package/package.json
CHANGED