@yoonion/mimi-seed-mcp 0.3.17 → 0.3.19
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/LICENSE +75 -0
- package/dist/auth/cli.js +3 -2
- package/dist/auth/constants.d.ts +4 -2
- package/dist/auth/constants.js +17 -11
- package/dist/auth/google-auth.js +5 -4
- package/dist/helpers.d.ts +8 -0
- package/dist/helpers.js +58 -0
- package/dist/index.js +22 -1562
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +71 -0
- package/dist/registers/admob.d.ts +2 -0
- package/dist/registers/admob.js +96 -0
- package/dist/registers/ai.d.ts +2 -0
- package/dist/registers/ai.js +55 -0
- package/dist/registers/appstore.d.ts +2 -0
- package/dist/registers/appstore.js +464 -0
- package/dist/registers/auth.d.ts +2 -0
- package/dist/registers/auth.js +67 -0
- package/dist/registers/bigquery.d.ts +2 -0
- package/dist/registers/bigquery.js +38 -0
- package/dist/registers/checks.d.ts +2 -0
- package/dist/registers/checks.js +56 -0
- package/dist/registers/firebase.d.ts +2 -0
- package/dist/registers/firebase.js +122 -0
- package/dist/registers/iam.d.ts +2 -0
- package/dist/registers/iam.js +105 -0
- package/dist/registers/playstore.d.ts +2 -0
- package/dist/registers/playstore.js +487 -0
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +51 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,1572 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import * as iam from './iam/tools.js';
|
|
16
|
-
import * as bigquery from './bigquery/tools.js';
|
|
17
|
-
import { checkPlayStoreRisks, checkAppStoreRisks, formatRisks } from './checks/risks.js';
|
|
18
|
-
import { buildPlayStoreReleasePlan, buildAppStoreReleasePlan } from './checks/plan.js';
|
|
19
|
-
import { validateAppStoreScreenshots, validatePlayStoreScreenshots, formatValidationResults } from './checks/screenshots.js';
|
|
20
|
-
import { generateReleaseNotesFromCommits, formatGeneratedNotes } from './ai/notes.js';
|
|
21
|
-
import { generateReviewReply, formatReviewReply } from './ai/review.js';
|
|
4
|
+
import { registerFirebaseTools } from './registers/firebase.js';
|
|
5
|
+
import { registerAdmobTools } from './registers/admob.js';
|
|
6
|
+
import { registerPlaystoreTools } from './registers/playstore.js';
|
|
7
|
+
import { registerIamTools } from './registers/iam.js';
|
|
8
|
+
import { registerAppstoreTools } from './registers/appstore.js';
|
|
9
|
+
import { registerChecksTools } from './registers/checks.js';
|
|
10
|
+
import { registerAiTools } from './registers/ai.js';
|
|
11
|
+
import { registerBigqueryTools } from './registers/bigquery.js';
|
|
12
|
+
import { registerAuthTools } from './registers/auth.js';
|
|
13
|
+
import { registerPrompts } from './prompts.js';
|
|
14
|
+
import { registerResources } from './resources.js';
|
|
22
15
|
const server = new McpServer({
|
|
23
16
|
name: 'mimi-seed',
|
|
24
17
|
version: '0.1.0',
|
|
25
18
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'브라우저가 열리면 Google 로그인 → 끝.',
|
|
38
|
-
'그 다음에 다시 물어봐줘.',
|
|
39
|
-
].join('\n'));
|
|
40
|
-
}
|
|
41
|
-
return client;
|
|
42
|
-
}
|
|
43
|
-
const PLAY_AUTH_HINT = [
|
|
44
|
-
'❌ Google Play 서비스 계정이 연결되지 않았어.',
|
|
45
|
-
'',
|
|
46
|
-
'터미널에서 이것만 실행하면 돼:',
|
|
47
|
-
'',
|
|
48
|
-
' npx -y @yoonion/mimi-seed-mcp mimi-seed-playstore-auth',
|
|
49
|
-
'',
|
|
50
|
-
'서비스 계정 JSON 파일 경로를 입력하면 저장 완료.',
|
|
51
|
-
'그 다음에 다시 물어봐줘.',
|
|
52
|
-
].join('\n');
|
|
53
|
-
const APPSTORE_AUTH_HINT = [
|
|
54
|
-
'❌ App Store Connect 인증이 설정되지 않았어.',
|
|
55
|
-
'',
|
|
56
|
-
'터미널에서 이것만 실행하면 돼:',
|
|
57
|
-
'',
|
|
58
|
-
' npx -y @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
|
|
59
|
-
'',
|
|
60
|
-
'Issuer ID, Key ID, .p8 파일 경로를 입력하면 저장 완료.',
|
|
61
|
-
'그 다음에 다시 물어봐줘.',
|
|
62
|
-
].join('\n');
|
|
63
|
-
function requirePlayStoreAuth(packageName) {
|
|
64
|
-
const client = getServiceAccountClient(packageName);
|
|
65
|
-
if (!client)
|
|
66
|
-
throw new Error(PLAY_AUTH_HINT);
|
|
67
|
-
return client;
|
|
68
|
-
}
|
|
69
|
-
function requireServiceAccountJson(packageName) {
|
|
70
|
-
const json = getServiceAccountJson(packageName);
|
|
71
|
-
if (!json)
|
|
72
|
-
throw new Error(PLAY_AUTH_HINT);
|
|
73
|
-
return json;
|
|
74
|
-
}
|
|
75
|
-
function requireAppStoreCreds() {
|
|
76
|
-
const creds = getAppStoreCredentials();
|
|
77
|
-
if (!creds)
|
|
78
|
-
throw new Error(APPSTORE_AUTH_HINT);
|
|
79
|
-
return creds;
|
|
80
|
-
}
|
|
81
|
-
// ══════════════════════════════════════════════════
|
|
82
|
-
// Firebase Tools
|
|
83
|
-
// ══════════════════════════════════════════════════
|
|
84
|
-
// --- 프로젝트 ---
|
|
85
|
-
server.tool('firebase_list_projects', '내 Firebase 프로젝트 목록 조회', {}, async () => {
|
|
86
|
-
const auth = requireAuth();
|
|
87
|
-
const projects = await firebase.listProjects(auth);
|
|
88
|
-
return { content: [{ type: 'text', text: JSON.stringify(projects, null, 2) }] };
|
|
89
|
-
});
|
|
90
|
-
server.tool('firebase_get_project', 'Firebase 프로젝트 상세 정보 조회', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
91
|
-
const auth = requireAuth();
|
|
92
|
-
const project = await firebase.getProject(auth, projectId);
|
|
93
|
-
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
94
|
-
});
|
|
95
|
-
// --- Android 앱 ---
|
|
96
|
-
server.tool('firebase_list_android_apps', 'Firebase 프로젝트의 Android 앱 목록', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
97
|
-
const auth = requireAuth();
|
|
98
|
-
const apps = await firebase.listAndroidApps(auth, projectId);
|
|
99
|
-
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
100
|
-
});
|
|
101
|
-
server.tool('firebase_create_android_app', 'Firebase에 새 Android 앱 등록', {
|
|
102
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
103
|
-
packageName: z.string().describe('Android 패키지명 (예: com.example.myapp)'),
|
|
104
|
-
displayName: z.string().describe('앱 표시 이름'),
|
|
105
|
-
}, async ({ projectId, packageName, displayName }) => {
|
|
106
|
-
const auth = requireAuth();
|
|
107
|
-
const result = await firebase.createAndroidApp(auth, projectId, packageName, displayName);
|
|
108
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
109
|
-
});
|
|
110
|
-
server.tool('firebase_get_android_config', 'google-services.json 다운로드', {
|
|
111
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
112
|
-
appId: z.string().describe('Firebase 앱 ID'),
|
|
113
|
-
}, async ({ projectId, appId }) => {
|
|
114
|
-
const auth = requireAuth();
|
|
115
|
-
const config = await firebase.getAndroidConfig(auth, projectId, appId);
|
|
116
|
-
return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] };
|
|
117
|
-
});
|
|
118
|
-
server.tool('firebase_delete_android_app', 'Firebase Android 앱 삭제', {
|
|
119
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
120
|
-
appId: z.string().describe('삭제할 Firebase 앱 ID'),
|
|
121
|
-
}, async ({ projectId, appId }) => {
|
|
122
|
-
const auth = requireAuth();
|
|
123
|
-
const result = await firebase.deleteAndroidApp(auth, projectId, appId);
|
|
124
|
-
return { content: [{ type: 'text', text: `삭제 완료: ${JSON.stringify(result)}` }] };
|
|
125
|
-
});
|
|
126
|
-
// --- iOS 앱 ---
|
|
127
|
-
server.tool('firebase_list_ios_apps', 'Firebase 프로젝트의 iOS 앱 목록', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
128
|
-
const auth = requireAuth();
|
|
129
|
-
const apps = await firebase.listIosApps(auth, projectId);
|
|
130
|
-
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
131
|
-
});
|
|
132
|
-
server.tool('firebase_create_ios_app', 'Firebase에 새 iOS 앱 등록', {
|
|
133
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
134
|
-
bundleId: z.string().describe('iOS Bundle ID (예: com.example.myapp)'),
|
|
135
|
-
displayName: z.string().describe('앱 표시 이름'),
|
|
136
|
-
}, async ({ projectId, bundleId, displayName }) => {
|
|
137
|
-
const auth = requireAuth();
|
|
138
|
-
const result = await firebase.createIosApp(auth, projectId, bundleId, displayName);
|
|
139
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
140
|
-
});
|
|
141
|
-
server.tool('firebase_get_ios_config', 'GoogleService-Info.plist 다운로드', {
|
|
142
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
143
|
-
appId: z.string().describe('Firebase 앱 ID'),
|
|
144
|
-
}, async ({ projectId, appId }) => {
|
|
145
|
-
const auth = requireAuth();
|
|
146
|
-
const config = await firebase.getIosConfig(auth, projectId, appId);
|
|
147
|
-
return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] };
|
|
148
|
-
});
|
|
149
|
-
server.tool('firebase_delete_ios_app', 'Firebase iOS 앱 삭제', {
|
|
150
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
151
|
-
appId: z.string().describe('삭제할 Firebase 앱 ID'),
|
|
152
|
-
}, async ({ projectId, appId }) => {
|
|
153
|
-
const auth = requireAuth();
|
|
154
|
-
const result = await firebase.deleteIosApp(auth, projectId, appId);
|
|
155
|
-
return { content: [{ type: 'text', text: `삭제 완료: ${JSON.stringify(result)}` }] };
|
|
156
|
-
});
|
|
157
|
-
// --- Web 앱 ---
|
|
158
|
-
server.tool('firebase_list_web_apps', 'Firebase 프로젝트의 Web 앱 목록', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
159
|
-
const auth = requireAuth();
|
|
160
|
-
const apps = await firebase.listWebApps(auth, projectId);
|
|
161
|
-
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
162
|
-
});
|
|
163
|
-
server.tool('firebase_create_web_app', 'Firebase에 새 Web 앱 등록', {
|
|
164
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
165
|
-
displayName: z.string().describe('앱 표시 이름'),
|
|
166
|
-
}, async ({ projectId, displayName }) => {
|
|
167
|
-
const auth = requireAuth();
|
|
168
|
-
const result = await firebase.createWebApp(auth, projectId, displayName);
|
|
169
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
170
|
-
});
|
|
171
|
-
server.tool('firebase_get_web_config', 'Firebase Web 설정 (firebaseConfig 객체) 조회', {
|
|
172
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
173
|
-
appId: z.string().describe('Firebase 앱 ID'),
|
|
174
|
-
}, async ({ projectId, appId }) => {
|
|
175
|
-
const auth = requireAuth();
|
|
176
|
-
const config = await firebase.getWebConfig(auth, projectId, appId);
|
|
177
|
-
return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] };
|
|
178
|
-
});
|
|
179
|
-
server.tool('firebase_delete_web_app', 'Firebase Web 앱 삭제', {
|
|
180
|
-
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
181
|
-
appId: z.string().describe('삭제할 Firebase 앱 ID'),
|
|
182
|
-
}, async ({ projectId, appId }) => {
|
|
183
|
-
const auth = requireAuth();
|
|
184
|
-
const result = await firebase.deleteWebApp(auth, projectId, appId);
|
|
185
|
-
return { content: [{ type: 'text', text: `삭제 완료: ${JSON.stringify(result)}` }] };
|
|
186
|
-
});
|
|
187
|
-
// --- 서비스 관리 ---
|
|
188
|
-
server.tool('firebase_enable_service', 'GCP 서비스 활성화 (예: firestore.googleapis.com)', {
|
|
189
|
-
projectId: z.string().describe('프로젝트 ID'),
|
|
190
|
-
serviceId: z.string().describe('서비스 ID (예: firestore.googleapis.com)'),
|
|
191
|
-
}, async ({ projectId, serviceId }) => {
|
|
192
|
-
const auth = requireAuth();
|
|
193
|
-
const result = await firebase.enableService(auth, projectId, serviceId);
|
|
194
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
195
|
-
});
|
|
196
|
-
server.tool('firebase_enable_common_services', 'Firebase 기본 서비스 일괄 활성화 (Firestore, Auth, Storage, FCM 등)', { projectId: z.string().describe('프로젝트 ID') }, async ({ projectId }) => {
|
|
197
|
-
const auth = requireAuth();
|
|
198
|
-
const results = await firebase.enableCommonServices(auth, projectId);
|
|
199
|
-
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
200
|
-
});
|
|
201
|
-
server.tool('firebase_list_enabled_services', '프로젝트에서 활성화된 GCP 서비스 목록', { projectId: z.string().describe('프로젝트 ID') }, async ({ projectId }) => {
|
|
202
|
-
const auth = requireAuth();
|
|
203
|
-
const services = await firebase.listEnabledServices(auth, projectId);
|
|
204
|
-
return { content: [{ type: 'text', text: JSON.stringify(services, null, 2) }] };
|
|
205
|
-
});
|
|
206
|
-
// ══════════════════════════════════════════════════
|
|
207
|
-
// AdMob Tools
|
|
208
|
-
// ══════════════════════════════════════════════════
|
|
209
|
-
server.tool('admob_list_accounts', 'AdMob 계정 목록 조회', {}, async () => {
|
|
210
|
-
const auth = requireAuth();
|
|
211
|
-
const accounts = await admob.listAccounts(auth);
|
|
212
|
-
return { content: [{ type: 'text', text: JSON.stringify(accounts, null, 2) }] };
|
|
213
|
-
});
|
|
214
|
-
server.tool('admob_list_apps', 'AdMob에 등록된 앱 목록', { accountId: z.string().describe('AdMob 계정 ID (예: accounts/pub-XXXX)') }, async ({ accountId }) => {
|
|
215
|
-
const auth = requireAuth();
|
|
216
|
-
const apps = await admob.listApps(auth, accountId);
|
|
217
|
-
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
218
|
-
});
|
|
219
|
-
server.tool('admob_list_ad_units', 'AdMob 광고 단위 목록', { accountId: z.string().describe('AdMob 계정 ID') }, async ({ accountId }) => {
|
|
220
|
-
const auth = requireAuth();
|
|
221
|
-
const units = await admob.listAdUnits(auth, accountId);
|
|
222
|
-
return { content: [{ type: 'text', text: JSON.stringify(units, null, 2) }] };
|
|
223
|
-
});
|
|
224
|
-
server.tool('admob_get_today_earnings', '오늘 AdMob 수익 요약', { accountId: z.string().describe('AdMob 계정 ID') }, async ({ accountId }) => {
|
|
225
|
-
const auth = requireAuth();
|
|
226
|
-
const report = await admob.getTodayEarnings(auth, accountId);
|
|
227
|
-
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
228
|
-
});
|
|
229
|
-
server.tool('admob_get_report', 'AdMob 수익 리포트 (기간 지정)', {
|
|
230
|
-
accountId: z.string().describe('AdMob 계정 ID'),
|
|
231
|
-
startYear: z.number().describe('시작 연도'),
|
|
232
|
-
startMonth: z.number().describe('시작 월 (1-12)'),
|
|
233
|
-
startDay: z.number().describe('시작 일'),
|
|
234
|
-
endYear: z.number().describe('종료 연도'),
|
|
235
|
-
endMonth: z.number().describe('종료 월 (1-12)'),
|
|
236
|
-
endDay: z.number().describe('종료 일'),
|
|
237
|
-
}, async ({ accountId, startYear, startMonth, startDay, endYear, endMonth, endDay }) => {
|
|
238
|
-
const auth = requireAuth();
|
|
239
|
-
const report = await admob.getNetworkReport(auth, accountId, { year: startYear, month: startMonth, day: startDay }, { year: endYear, month: endMonth, day: endDay });
|
|
240
|
-
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
241
|
-
});
|
|
242
|
-
server.tool('admob_create_app', 'AdMob에 새 앱 등록 (v1beta — Limited Access)', {
|
|
243
|
-
accountId: z.string().describe('AdMob 계정 ID'),
|
|
244
|
-
platform: z.enum(['ANDROID', 'IOS']).describe('플랫폼'),
|
|
245
|
-
displayName: z.string().describe('앱 이름'),
|
|
246
|
-
}, async ({ accountId, platform, displayName }) => {
|
|
247
|
-
const auth = requireAuth();
|
|
248
|
-
try {
|
|
249
|
-
const result = await admob.createApp(auth, accountId, platform, displayName);
|
|
250
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
if (err.code === 403) {
|
|
254
|
-
return {
|
|
255
|
-
content: [{
|
|
256
|
-
type: 'text',
|
|
257
|
-
text: [
|
|
258
|
-
'❌ AdMob 앱 생성 API 접근 불가 (403).',
|
|
259
|
-
'',
|
|
260
|
-
'AdMob v1beta 쓰기 API는 Google Account Manager 승인이 필요합니다.',
|
|
261
|
-
'대신 AdMob 콘솔에서 수동 등록하세요:',
|
|
262
|
-
' https://admob.google.com/home',
|
|
263
|
-
'',
|
|
264
|
-
'등록 후 admob_list_apps로 확인할 수 있습니다.',
|
|
265
|
-
].join('\n'),
|
|
266
|
-
}],
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
throw err;
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
server.tool('admob_create_ad_unit', 'AdMob 광고 단위 생성 (v1beta — Limited Access)', {
|
|
273
|
-
accountId: z.string().describe('AdMob 계정 ID'),
|
|
274
|
-
appId: z.string().describe('AdMob 앱 ID'),
|
|
275
|
-
displayName: z.string().describe('광고 단위 이름'),
|
|
276
|
-
adFormat: z.enum(['BANNER', 'INTERSTITIAL', 'REWARDED', 'REWARDED_INTERSTITIAL', 'APP_OPEN', 'NATIVE']).describe('광고 형식'),
|
|
277
|
-
}, async ({ accountId, appId, displayName, adFormat }) => {
|
|
278
|
-
const auth = requireAuth();
|
|
279
|
-
try {
|
|
280
|
-
const result = await admob.createAdUnit(auth, accountId, appId, displayName, adFormat);
|
|
281
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
282
|
-
}
|
|
283
|
-
catch (err) {
|
|
284
|
-
if (err.code === 403) {
|
|
285
|
-
return {
|
|
286
|
-
content: [{
|
|
287
|
-
type: 'text',
|
|
288
|
-
text: [
|
|
289
|
-
'❌ 광고 단위 생성 API 접근 불가 (403).',
|
|
290
|
-
'',
|
|
291
|
-
'AdMob 콘솔에서 수동 생성하세요:',
|
|
292
|
-
' https://admob.google.com/home',
|
|
293
|
-
].join('\n'),
|
|
294
|
-
}],
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
throw err;
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
// ══════════════════════════════════════════════════
|
|
301
|
-
// Google Play Store Tools
|
|
302
|
-
// ══════════════════════════════════════════════════
|
|
303
|
-
server.tool('playstore_get_app', 'Google Play 앱 상세 정보 조회', { packageName: z.string().describe('패키지명 (예: com.findthem.app)') }, async ({ packageName }) => {
|
|
304
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
305
|
-
const details = await playstore.getAppDetails(auth, packageName);
|
|
306
|
-
return { content: [{ type: 'text', text: JSON.stringify(details, null, 2) }] };
|
|
307
|
-
});
|
|
308
|
-
server.tool('playstore_get_listing', 'Google Play 스토어 리스팅 조회 (제목, 설명문 등)', {
|
|
309
|
-
packageName: z.string().describe('패키지명'),
|
|
310
|
-
language: z.string().default('ko-KR').describe('언어 코드 (기본: ko-KR)'),
|
|
311
|
-
}, async ({ packageName, language }) => {
|
|
312
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
313
|
-
const listing = await playstore.getListing(auth, packageName, language);
|
|
314
|
-
return { content: [{ type: 'text', text: JSON.stringify(listing, null, 2) }] };
|
|
315
|
-
});
|
|
316
|
-
server.tool('playstore_update_listing', 'Google Play 스토어 리스팅 수정 (제목, 설명문 변경)', {
|
|
317
|
-
packageName: z.string().describe('패키지명'),
|
|
318
|
-
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
319
|
-
title: z.string().optional().describe('앱 제목 (30자 이내)'),
|
|
320
|
-
shortDescription: z.string().optional().describe('짧은 설명 (80자 이내)'),
|
|
321
|
-
fullDescription: z.string().optional().describe('전체 설명 (4000자 이내)'),
|
|
322
|
-
}, async ({ packageName, language, title, shortDescription, fullDescription }) => {
|
|
323
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
324
|
-
const result = await playstore.updateListing(auth, packageName, language, {
|
|
325
|
-
title, shortDescription, fullDescription,
|
|
326
|
-
});
|
|
327
|
-
return { content: [{ type: 'text', text: `수정 완료:\n${JSON.stringify(result, null, 2)}` }] };
|
|
328
|
-
});
|
|
329
|
-
server.tool('playstore_list_tracks', 'Google Play 릴리스 트랙 현황 (프로덕션/베타/알파/내부)', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
330
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
331
|
-
const tracks = await playstore.listTracks(auth, packageName);
|
|
332
|
-
return { content: [{ type: 'text', text: JSON.stringify(tracks, null, 2) }] };
|
|
333
|
-
});
|
|
334
|
-
server.tool('playstore_list_images', 'Google Play 리스팅 이미지 목록 조회 (imageType별)', {
|
|
335
|
-
packageName: z.string().describe('패키지명'),
|
|
336
|
-
language: z.string().describe('언어 코드 (예: ko-KR)'),
|
|
337
|
-
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']).describe('이미지 타입'),
|
|
338
|
-
}, async ({ packageName, language, imageType }) => {
|
|
339
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
340
|
-
const images = await playstore.listImages(auth, packageName, language, imageType);
|
|
341
|
-
return { content: [{ type: 'text', text: JSON.stringify(images, null, 2) }] };
|
|
342
|
-
});
|
|
343
|
-
server.tool('playstore_upload_image', 'Google Play 리스팅 이미지 단일 업로드 (기존 이미지 유지). featureGraphic 1024x500 / icon 512x512 / phoneScreenshots 320~3840px', {
|
|
344
|
-
packageName: z.string().describe('패키지명'),
|
|
345
|
-
language: z.string().describe('언어 코드'),
|
|
346
|
-
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
347
|
-
filePath: z.string().describe('업로드할 이미지 절대 경로'),
|
|
348
|
-
}, async ({ packageName, language, imageType, filePath }) => {
|
|
349
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
350
|
-
const result = await playstore.uploadImage(auth, packageName, language, imageType, filePath);
|
|
351
|
-
return { content: [{ type: 'text', text: `✅ 업로드 완료\n${JSON.stringify(result, null, 2)}` }] };
|
|
352
|
-
});
|
|
353
|
-
server.tool('playstore_delete_all_images', 'Google Play 리스팅 특정 imageType의 이미지 전체 삭제 (교체 전 정리)', {
|
|
354
|
-
packageName: z.string().describe('패키지명'),
|
|
355
|
-
language: z.string().describe('언어 코드'),
|
|
356
|
-
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
357
|
-
}, async ({ packageName, language, imageType }) => {
|
|
358
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
359
|
-
const result = await playstore.deleteAllImages(auth, packageName, language, imageType);
|
|
360
|
-
return { content: [{ type: 'text', text: `✅ 전체 삭제\n${JSON.stringify(result, null, 2)}` }] };
|
|
361
|
-
});
|
|
362
|
-
server.tool('playstore_replace_images', 'Google Play 리스팅 이미지 일괄 교체 (한 edit 세션: deleteall → 순서대로 upload → commit). 스크린샷 5~8장 한 번에 교체 시 효율적. 업로드 순서가 스토어 노출 순서', {
|
|
363
|
-
packageName: z.string().describe('패키지명'),
|
|
364
|
-
language: z.string().describe('언어 코드'),
|
|
365
|
-
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
366
|
-
filePaths: z.array(z.string()).describe('업로드할 이미지 절대 경로 배열 (순서 = 노출 순서)'),
|
|
367
|
-
}, async ({ packageName, language, imageType, filePaths }) => {
|
|
368
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
369
|
-
const result = await playstore.replaceImages(auth, packageName, language, imageType, filePaths);
|
|
370
|
-
return { content: [{ type: 'text', text: `✅ ${result.count}장 교체 완료\n${JSON.stringify(result, null, 2)}` }] };
|
|
371
|
-
});
|
|
372
|
-
server.tool('playstore_update_release_notes', "Google Play 트랙 릴리스의 '최근 변경사항'(releaseNotes) 업데이트. versionCode로 타겟 릴리스 지정. 다른 언어/release는 보존. 이미 라이브(completed) 상태도 noteOnly 편집 가능", {
|
|
373
|
-
packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
|
|
374
|
-
track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
|
|
375
|
-
versionCode: z.string().describe('대상 versionCode (문자열, 예: "40")'),
|
|
376
|
-
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
377
|
-
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
378
|
-
}, async ({ packageName, track, versionCode, language, text }) => {
|
|
379
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
380
|
-
const result = await playstore.updateReleaseNotes(auth, packageName, track, versionCode, language, text);
|
|
381
|
-
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode} ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
382
|
-
});
|
|
383
|
-
server.tool('playstore_update_latest_release_notes', "Google Play 트랙의 최신 릴리스(versionCode 최대) '최근 변경사항' 업데이트 — versionCode를 모를 때 편의용", {
|
|
384
|
-
packageName: z.string().describe('패키지명'),
|
|
385
|
-
track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
|
|
386
|
-
language: z.string().describe('언어 코드 (예: ko-KR)'),
|
|
387
|
-
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
388
|
-
}, async ({ packageName, track, language, text }) => {
|
|
389
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
390
|
-
const result = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
|
|
391
|
-
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(result.updatedVersionCodes)}) ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
392
|
-
});
|
|
393
|
-
server.tool('playstore_list_reviews', 'Google Play 리뷰 목록 조회', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
394
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
395
|
-
const reviews = await playstore.listReviews(auth, packageName);
|
|
396
|
-
return { content: [{ type: 'text', text: JSON.stringify(reviews, null, 2) }] };
|
|
397
|
-
});
|
|
398
|
-
server.tool('playstore_reply_review', 'Google Play 리뷰에 답변', {
|
|
399
|
-
packageName: z.string().describe('패키지명'),
|
|
400
|
-
reviewId: z.string().describe('리뷰 ID'),
|
|
401
|
-
replyText: z.string().describe('답변 내용'),
|
|
402
|
-
}, async ({ packageName, reviewId, replyText }) => {
|
|
403
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
404
|
-
const result = await playstore.replyToReview(auth, packageName, reviewId, replyText);
|
|
405
|
-
return { content: [{ type: 'text', text: `답변 완료:\n${JSON.stringify(result, null, 2)}` }] };
|
|
406
|
-
});
|
|
407
|
-
server.tool('playstore_list_inapp_products', 'Google Play 인앱 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
408
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
409
|
-
const products = await playstore.listInAppProducts(auth, packageName);
|
|
410
|
-
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
|
|
411
|
-
});
|
|
412
|
-
server.tool('playstore_list_subscriptions', 'Google Play 구독 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
413
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
414
|
-
const subs = await playstore.listSubscriptions(auth, packageName);
|
|
415
|
-
return { content: [{ type: 'text', text: JSON.stringify(subs, null, 2) }] };
|
|
416
|
-
});
|
|
417
|
-
server.tool('playstore_create_onetime_product', [
|
|
418
|
-
'Google Play에 일회성 인앱 상품(소비성 또는 비소비성)을 생성.',
|
|
419
|
-
'Play Console 권한: "Manage store presence" 필요. 생성 후 Console에서 활성화 필요.',
|
|
420
|
-
].join(' '), {
|
|
421
|
-
packageName: z.string().describe('패키지명 (예: com.example.app)'),
|
|
422
|
-
productId: z
|
|
423
|
-
.string()
|
|
424
|
-
.describe('상품 ID (소문자/숫자/언더스코어/점, 예: premium_unlock). 한 번 정하면 변경 불가.'),
|
|
425
|
-
name: z.string().describe('상품 이름 (스토어 노출 제목)'),
|
|
426
|
-
price: z.number().int().describe('주 통화 기준 가격 (최소 단위: USD/EUR이면 cents, KRW/JPY이면 원화 정수. 예: USD $4.99 → 499, KRW ₩5,900 → 5900)'),
|
|
427
|
-
currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
|
|
428
|
-
type: z
|
|
429
|
-
.enum(['consumable', 'non_consumable'])
|
|
430
|
-
.default('non_consumable')
|
|
431
|
-
.describe('상품 유형 (소비성/비소비성). 앱이 consumePurchase 호출하면 소비성.'),
|
|
432
|
-
extraRegions: z
|
|
433
|
-
.array(z.object({
|
|
434
|
-
currency: z.string().describe('ISO 4217 통화 코드 (예: KRW, JPY, GBP)'),
|
|
435
|
-
price: z.number().describe('가격 (최소 단위: KRW ₩1,100 → 1100, USD $0.99 → 99)'),
|
|
436
|
-
}))
|
|
437
|
-
.optional()
|
|
438
|
-
.describe('추가 지역별 명시 가격. 자동 환산이 부정확한 KRW/JPY 등에 직접 지정.'),
|
|
439
|
-
}, async (args) => {
|
|
440
|
-
const json = requireServiceAccountJson(args.packageName);
|
|
441
|
-
const result = await createGoogleOneTimePurchase({
|
|
442
|
-
packageName: args.packageName,
|
|
443
|
-
productId: args.productId,
|
|
444
|
-
name: args.name,
|
|
445
|
-
price: args.price,
|
|
446
|
-
currency: args.currency,
|
|
447
|
-
type: args.type,
|
|
448
|
-
...(args.extraRegions && { extraRegions: args.extraRegions }),
|
|
449
|
-
serviceAccountKey: json,
|
|
450
|
-
});
|
|
451
|
-
if (!result.success) {
|
|
452
|
-
return { content: [{ type: 'text', text: `❌ 상품 생성 실패: ${result.error}` }] };
|
|
453
|
-
}
|
|
454
|
-
return {
|
|
455
|
-
content: [{
|
|
456
|
-
type: 'text',
|
|
457
|
-
text: [
|
|
458
|
-
`✓ Play 일회성 상품 생성 완료`,
|
|
459
|
-
`productId: ${result.productId}`,
|
|
460
|
-
`price: ${args.price} ${args.currency}`,
|
|
461
|
-
'',
|
|
462
|
-
'Play Console에서 활성화 확인:',
|
|
463
|
-
`https://play.google.com/console/u/0/developers/-/app/-/managed-products?package=${encodeURIComponent(args.packageName)}`,
|
|
464
|
-
].join('\n'),
|
|
465
|
-
}],
|
|
466
|
-
};
|
|
467
|
-
});
|
|
468
|
-
server.tool('playstore_create_subscription', [
|
|
469
|
-
'Google Play에 자동 갱신 구독을 생성하고 baseplan을 활성화.',
|
|
470
|
-
'구독 생성 → baseplan(가격·주기) 추가 → 자동 활성화.',
|
|
471
|
-
'이미 같은 productId가 있으면 생성 실패 (Play API 특성상 upsert 미지원).',
|
|
472
|
-
].join(' '), {
|
|
473
|
-
packageName: z.string().describe('패키지명'),
|
|
474
|
-
productId: z
|
|
475
|
-
.string()
|
|
476
|
-
.describe('구독 상품 ID (소문자/숫자/언더스코어/점, 예: premium_monthly)'),
|
|
477
|
-
name: z.string().describe('구독 제목 (스토어 노출)'),
|
|
478
|
-
price: z.number().int().describe('주 통화 기준 가격 (최소 단위: USD cents. 예: $4.99 → 499, ₩5,900 → 5900)'),
|
|
479
|
-
currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
|
|
480
|
-
period: z
|
|
481
|
-
.enum(['monthly', 'yearly'])
|
|
482
|
-
.describe('청구 주기'),
|
|
483
|
-
extraRegions: z
|
|
484
|
-
.array(z.object({
|
|
485
|
-
currency: z.string().describe('ISO 4217 통화 코드 (예: KRW, JPY)'),
|
|
486
|
-
price: z.number().describe('가격 (최소 단위)'),
|
|
487
|
-
}))
|
|
488
|
-
.optional()
|
|
489
|
-
.describe('추가 지역별 명시 가격'),
|
|
490
|
-
}, async (args) => {
|
|
491
|
-
const json = requireServiceAccountJson(args.packageName);
|
|
492
|
-
const result = await createGoogleSubscription({
|
|
493
|
-
packageName: args.packageName,
|
|
494
|
-
productId: args.productId,
|
|
495
|
-
name: args.name,
|
|
496
|
-
price: args.price,
|
|
497
|
-
currency: args.currency,
|
|
498
|
-
period: args.period,
|
|
499
|
-
...(args.extraRegions && { extraRegions: args.extraRegions }),
|
|
500
|
-
serviceAccountKey: json,
|
|
501
|
-
});
|
|
502
|
-
if (!result.success) {
|
|
503
|
-
return { content: [{ type: 'text', text: `❌ 구독 생성 실패: ${result.error}` }] };
|
|
504
|
-
}
|
|
505
|
-
return {
|
|
506
|
-
content: [{
|
|
507
|
-
type: 'text',
|
|
508
|
-
text: [
|
|
509
|
-
`✓ Play 구독 생성 완료`,
|
|
510
|
-
`productId: ${result.productId}`,
|
|
511
|
-
`price: ${args.price} ${args.currency} / ${args.period}`,
|
|
512
|
-
'',
|
|
513
|
-
'Play Console:',
|
|
514
|
-
`https://play.google.com/console/u/0/developers/-/app/-/subscriptions?package=${encodeURIComponent(args.packageName)}`,
|
|
515
|
-
].join('\n'),
|
|
516
|
-
}],
|
|
517
|
-
};
|
|
518
|
-
});
|
|
519
|
-
server.tool('playstore_verify_service_account', [
|
|
520
|
-
"서비스 계정 JSON이 주어진 packageName에 대해 Play Developer API 호출 가능한지 + 'View financial data' 권한까지 있는지 검증.",
|
|
521
|
-
'onesub 같은 서버가 Play 영수증을 백그라운드로 검증하려면 OAuth 토큰 대신 서비스 계정 JSON이 필요 — 이 도구로 붙여넣기 전에 유효성 확인.',
|
|
522
|
-
'성공 시 clientEmail + projectId 반환. 실패 시 어느 단계(parse/auth/api)에서 왜 막혔는지 단계별로 안내.',
|
|
523
|
-
'(이 도구는 서비스 계정 자격증명만 사용 — 로그인한 사용자의 OAuth 토큰은 건드리지 않음)',
|
|
524
|
-
].join(' '), {
|
|
525
|
-
serviceAccountJson: z
|
|
526
|
-
.string()
|
|
527
|
-
.describe('서비스 계정 JSON 전체 내용 (문자열). Google Cloud Console → IAM & Admin → Service Accounts → Keys → Create new key → JSON으로 다운받은 파일의 내용'),
|
|
528
|
-
packageName: z
|
|
529
|
-
.string()
|
|
530
|
-
.describe('검증할 Android 앱의 패키지명 (예: com.findthem.app)'),
|
|
531
|
-
}, async ({ serviceAccountJson, packageName }) => {
|
|
532
|
-
const result = await playstore.verifyServiceAccountJson(serviceAccountJson, packageName);
|
|
533
|
-
if (result.ok) {
|
|
534
|
-
return {
|
|
535
|
-
content: [
|
|
536
|
-
{
|
|
537
|
-
type: 'text',
|
|
538
|
-
text: [
|
|
539
|
-
'✓ 서비스 계정 유효 — Play Developer API 호출 가능',
|
|
540
|
-
'',
|
|
541
|
-
`**clientEmail**: \`${result.clientEmail}\``,
|
|
542
|
-
`**projectId**: \`${result.projectId}\``,
|
|
543
|
-
`**packageName**: \`${packageName}\``,
|
|
544
|
-
'',
|
|
545
|
-
'이제 이 JSON 내용을 onesub 서버의 `GOOGLE_SERVICE_ACCOUNT_KEY` 환경변수에 (한 줄로) 넣으면 됩니다. 예:',
|
|
546
|
-
'```bash',
|
|
547
|
-
'cat service-account.json | tr -d \'\\n\' | jq -c .',
|
|
548
|
-
'```',
|
|
549
|
-
].join('\n'),
|
|
550
|
-
},
|
|
551
|
-
],
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
const lines = [
|
|
555
|
-
`✗ 검증 실패 (stage: **${result.stage}**${result.httpStatus ? `, HTTP ${result.httpStatus}` : ''})`,
|
|
556
|
-
'',
|
|
557
|
-
`${result.message}`,
|
|
558
|
-
'',
|
|
559
|
-
];
|
|
560
|
-
if (result.stage === 'parse') {
|
|
561
|
-
lines.push('원인: 붙여넣은 JSON 구조가 올바르지 않음.');
|
|
562
|
-
lines.push('확인: Google Cloud Console → Service Accounts → Keys → **Create new key → JSON** 흐름으로 받은 파일 맞나요?');
|
|
563
|
-
}
|
|
564
|
-
else if (result.stage === 'auth') {
|
|
565
|
-
lines.push('원인: Google이 자격증명 자체를 거부함 (private_key 손상 / 프로젝트 비활성 / 계정 삭제됨).');
|
|
566
|
-
lines.push('확인: 새 키를 다시 발급 (기존 키 회수 후).');
|
|
567
|
-
}
|
|
568
|
-
else if (result.stage === 'api') {
|
|
569
|
-
if (result.httpStatus === 401 || result.httpStatus === 403) {
|
|
570
|
-
lines.push('원인: 토큰은 받았지만 Play Console에서 이 서비스 계정에 권한 없음.');
|
|
571
|
-
lines.push('확인 순서:');
|
|
572
|
-
lines.push('1. Play Console → Users and permissions → 이 서비스 계정 이메일을 초대');
|
|
573
|
-
lines.push('2. App permissions에서 해당 패키지명 앱 선택');
|
|
574
|
-
lines.push('3. Account permissions에 **View financial data, orders, and cancellation survey responses** 체크');
|
|
575
|
-
lines.push('4. 권한 적용까지 **~5분 대기** 후 재시도 (너무 빨리 시도하면 계속 403)');
|
|
576
|
-
}
|
|
577
|
-
else if (result.httpStatus === 404) {
|
|
578
|
-
lines.push('원인: 패키지명이 이 Play Console 개발자 계정 소유가 아님.');
|
|
579
|
-
lines.push(`확인: packageName이 Play Console에 등록된 앱의 것과 정확히 일치하나요? ("\`${packageName}\`")`);
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
lines.push('원인: Play Developer API 호출 중 예외. 네트워크 또는 Google 쪽 문제일 수 있음.');
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
586
|
-
});
|
|
587
|
-
server.tool('playstore_register_service_account', [
|
|
588
|
-
'Google Play 서비스 계정 JSON을 패키지 단위로 등록 (~/.mimi-seed/play-service-accounts/{packageName}.json, 0600 mode).',
|
|
589
|
-
'여러 앱이 서로 다른 GCP 프로젝트의 SA를 쓰는 환경 지원. 등록 후 해당 packageName으로 호출하는 모든 playstore_* 도구가 이 SA를 사용 (등록 안 된 패키지는 ~/.mimi-seed/play-service-account.json 의 default SA로 폴백).',
|
|
590
|
-
'먼저 playstore_verify_service_account 로 권한 확인 후 등록을 권장.',
|
|
591
|
-
].join(' '), {
|
|
592
|
-
packageName: z.string().describe('Android 패키지명 (예: gg.pryzm.weather)'),
|
|
593
|
-
serviceAccountJson: z.string().describe('서비스 계정 JSON 전체 내용 (문자열)'),
|
|
594
|
-
skipVerify: z.boolean().optional().describe('true면 사전 검증 건너뜀 (기본 false: 등록 전 verifyServiceAccountJson 실행)'),
|
|
595
|
-
}, async ({ packageName, serviceAccountJson, skipVerify }) => {
|
|
596
|
-
if (!skipVerify) {
|
|
597
|
-
const verify = await playstore.verifyServiceAccountJson(serviceAccountJson, packageName);
|
|
598
|
-
if (!verify.ok) {
|
|
599
|
-
return {
|
|
600
|
-
content: [{
|
|
601
|
-
type: 'text',
|
|
602
|
-
text: [
|
|
603
|
-
`❌ 검증 실패 (stage: ${verify.stage})로 등록 중단.`,
|
|
604
|
-
verify.message,
|
|
605
|
-
'',
|
|
606
|
-
`검증을 건너뛰고 강제 등록하려면 skipVerify=true 옵션 추가.`,
|
|
607
|
-
].join('\n'),
|
|
608
|
-
}],
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
let clientEmail = '';
|
|
613
|
-
let projectId = '';
|
|
614
|
-
try {
|
|
615
|
-
const parsed = JSON.parse(serviceAccountJson);
|
|
616
|
-
clientEmail = parsed.client_email ?? '';
|
|
617
|
-
projectId = parsed.project_id ?? '';
|
|
618
|
-
}
|
|
619
|
-
catch {
|
|
620
|
-
return { content: [{ type: 'text', text: '❌ JSON 파싱 실패 — 서비스 계정 JSON 형식이 올바르지 않음.' }] };
|
|
621
|
-
}
|
|
622
|
-
saveServiceAccountJsonForPackage(packageName, serviceAccountJson);
|
|
623
|
-
return {
|
|
624
|
-
content: [{
|
|
625
|
-
type: 'text',
|
|
626
|
-
text: [
|
|
627
|
-
`✓ ${packageName} 서비스 계정 등록 완료`,
|
|
628
|
-
'',
|
|
629
|
-
`**clientEmail**: \`${clientEmail}\``,
|
|
630
|
-
`**projectId**: \`${projectId}\``,
|
|
631
|
-
`**저장 경로**: \`~/.mimi-seed/play-service-accounts/${packageName}.json\` (0600)`,
|
|
632
|
-
'',
|
|
633
|
-
'이후 이 packageName으로 호출하는 모든 playstore_* 도구가 자동으로 이 SA 사용.',
|
|
634
|
-
].join('\n'),
|
|
635
|
-
}],
|
|
636
|
-
};
|
|
637
|
-
});
|
|
638
|
-
server.tool('playstore_list_service_accounts', '등록된 패키지별 서비스 계정 + default(레거시) SA 정보 요약. clientEmail / projectId 만 노출 (private_key 미노출).', {}, async () => {
|
|
639
|
-
const info = listRegisteredServiceAccounts();
|
|
640
|
-
const lines = [];
|
|
641
|
-
if (info.default) {
|
|
642
|
-
lines.push('**Default (legacy)**: `~/.mimi-seed/play-service-account.json`');
|
|
643
|
-
lines.push(` - clientEmail: \`${info.default.clientEmail ?? '(parse error)'}\``);
|
|
644
|
-
lines.push(` - projectId: \`${info.default.projectId ?? '(parse error)'}\``);
|
|
645
|
-
lines.push('');
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
lines.push('**Default (legacy)**: 미등록');
|
|
649
|
-
lines.push('');
|
|
650
|
-
}
|
|
651
|
-
if (info.perPackage.length === 0) {
|
|
652
|
-
lines.push('**Per-package**: 없음');
|
|
653
|
-
lines.push('');
|
|
654
|
-
lines.push('등록 방법: `playstore_register_service_account(packageName, serviceAccountJson)`');
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
lines.push(`**Per-package** (${info.perPackage.length}개):`);
|
|
658
|
-
for (const item of info.perPackage) {
|
|
659
|
-
lines.push(`- \`${item.packageName}\` → \`${item.clientEmail ?? '(parse error)'}\` (project: \`${item.projectId ?? 'unknown'}\`)`);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
663
|
-
});
|
|
664
|
-
server.tool('playstore_delete_service_account', '등록된 패키지별 서비스 계정 삭제. default(레거시) SA는 영향 없음.', {
|
|
665
|
-
packageName: z.string().describe('삭제할 패키지명'),
|
|
666
|
-
}, async ({ packageName }) => {
|
|
667
|
-
const deleted = deleteServiceAccountJsonForPackage(packageName);
|
|
668
|
-
return {
|
|
669
|
-
content: [{
|
|
670
|
-
type: 'text',
|
|
671
|
-
text: deleted
|
|
672
|
-
? `✓ ${packageName} 서비스 계정 파일 삭제 완료. 이후 이 패키지는 default SA로 폴백.`
|
|
673
|
-
: `(skip) ${packageName} 등록된 패키지별 SA 없음.`,
|
|
674
|
-
}],
|
|
675
|
-
};
|
|
676
|
-
});
|
|
677
|
-
// ══════════════════════════════════════════════════
|
|
678
|
-
// Google Cloud IAM Tools
|
|
679
|
-
// ══════════════════════════════════════════════════
|
|
680
|
-
// 서비스 계정 생성 → JSON 키 발급 → 프로젝트 IAM 역할 부여까지 자동화.
|
|
681
|
-
// Play Console의 'View financial data' 권한은 Cloud IAM이 아니라 Play Console
|
|
682
|
-
// Users and permissions에서 별도로 부여해야 함 (androidpublisher.users API
|
|
683
|
-
// 또는 수동 UI).
|
|
684
|
-
server.tool('iam_list_service_accounts', '주어진 projectId의 서비스 계정 목록. 이메일 / displayName / disabled 상태 반환.', {
|
|
685
|
-
projectId: z.string().describe('Google Cloud 프로젝트 ID'),
|
|
686
|
-
}, async ({ projectId }) => {
|
|
687
|
-
const auth = requireAuth();
|
|
688
|
-
const accounts = await iam.listServiceAccounts(auth, projectId);
|
|
689
|
-
return { content: [{ type: 'text', text: JSON.stringify(accounts, null, 2) }] };
|
|
690
|
-
});
|
|
691
|
-
server.tool('iam_create_service_account', '새 서비스 계정 생성. accountId는 이메일의 로컬 파트 (예: "onesub-play-verifier" → onesub-play-verifier@<project>.iam.gserviceaccount.com).', {
|
|
692
|
-
projectId: z.string().describe('Google Cloud 프로젝트 ID'),
|
|
693
|
-
accountId: z
|
|
694
|
-
.string()
|
|
695
|
-
.regex(/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/, 'lowercase letters, digits, hyphens; 6-30 chars')
|
|
696
|
-
.describe('서비스 계정 ID (이메일 로컬 파트)'),
|
|
697
|
-
displayName: z.string().describe('사람이 읽을 표시 이름 (예: "onesub Play verifier")'),
|
|
698
|
-
}, async ({ projectId, accountId, displayName }) => {
|
|
699
|
-
const auth = requireAuth();
|
|
700
|
-
const account = await iam.createServiceAccount(auth, projectId, accountId, displayName);
|
|
701
|
-
return {
|
|
702
|
-
content: [
|
|
703
|
-
{
|
|
704
|
-
type: 'text',
|
|
705
|
-
text: [
|
|
706
|
-
'✓ 서비스 계정 생성 완료',
|
|
707
|
-
'',
|
|
708
|
-
`**email**: \`${account.email}\``,
|
|
709
|
-
`**displayName**: ${account.displayName}`,
|
|
710
|
-
`**uniqueId**: \`${account.uniqueId}\``,
|
|
711
|
-
'',
|
|
712
|
-
'다음 단계:',
|
|
713
|
-
`1. \`iam_create_key("${account.email}")\` — JSON 키 발급`,
|
|
714
|
-
`2. \`playstore_verify_service_account\` 로 Play Console 권한까지 확인 (Play Console에서 수동으로 이메일 초대 + 'View financial data' 권한 부여는 별도)`,
|
|
715
|
-
].join('\n'),
|
|
716
|
-
},
|
|
717
|
-
],
|
|
718
|
-
};
|
|
719
|
-
});
|
|
720
|
-
server.tool('iam_list_keys', '주어진 서비스 계정의 기존 키 목록. keyId / keyType / 만료시간 반환. 회수할 키 찾을 때 사용.', {
|
|
721
|
-
serviceAccount: z.string().describe('서비스 계정 이메일'),
|
|
722
|
-
}, async ({ serviceAccount }) => {
|
|
723
|
-
const auth = requireAuth();
|
|
724
|
-
const keys = await iam.listServiceAccountKeys(auth, serviceAccount);
|
|
725
|
-
return { content: [{ type: 'text', text: JSON.stringify(keys, null, 2) }] };
|
|
726
|
-
});
|
|
727
|
-
server.tool('iam_create_key', [
|
|
728
|
-
'주어진 서비스 계정의 새 JSON 키를 발급. 응답에 전체 JSON 포함 — 그대로 onesub의 GOOGLE_SERVICE_ACCOUNT_KEY 환경변수로 쓸 수 있음.',
|
|
729
|
-
'주의: 반환된 JSON은 영구 자격증명이므로 안전하게 보관. 한 서비스 계정당 키 최대 10개 (기존 키 회수 후 발급 필요하면 iam_list_keys로 먼저 확인).',
|
|
730
|
-
].join(' '), {
|
|
731
|
-
serviceAccount: z.string().describe('서비스 계정 이메일'),
|
|
732
|
-
}, async ({ serviceAccount }) => {
|
|
733
|
-
const auth = requireAuth();
|
|
734
|
-
const key = await iam.createServiceAccountKey(auth, serviceAccount);
|
|
735
|
-
return {
|
|
736
|
-
content: [
|
|
737
|
-
{
|
|
738
|
-
type: 'text',
|
|
739
|
-
text: [
|
|
740
|
-
'✓ 새 키 발급 완료 — 아래 JSON을 안전하게 보관하세요. **다시 볼 수 없습니다.**',
|
|
741
|
-
'',
|
|
742
|
-
`**keyId**: \`${key.keyId}\``,
|
|
743
|
-
`**clientEmail**: \`${key.clientEmail}\``,
|
|
744
|
-
`**projectId**: \`${key.projectId}\``,
|
|
745
|
-
'',
|
|
746
|
-
'## Service account JSON',
|
|
747
|
-
'```json',
|
|
748
|
-
key.json,
|
|
749
|
-
'```',
|
|
750
|
-
'',
|
|
751
|
-
'onesub 서버 `GOOGLE_SERVICE_ACCOUNT_KEY` env에 한 줄로 넣을 때:',
|
|
752
|
-
'```bash',
|
|
753
|
-
"cat service-account.json | tr -d '\\n' | jq -c .",
|
|
754
|
-
'```',
|
|
755
|
-
].join('\n'),
|
|
756
|
-
},
|
|
757
|
-
],
|
|
758
|
-
};
|
|
759
|
-
});
|
|
760
|
-
server.tool('iam_add_iam_policy_binding', [
|
|
761
|
-
'프로젝트 IAM 정책에 (member → role) 바인딩 추가. 이미 같은 조합이 있으면 no-op.',
|
|
762
|
-
'member 형식: `serviceAccount:<email>` / `user:<email>` / `group:<email>`.',
|
|
763
|
-
'주의: 이건 Google Cloud IAM 역할입니다. Play Console의 "View financial data" 권한은 별도 — Play Console → Users and permissions에서 부여.',
|
|
764
|
-
].join(' '), {
|
|
765
|
-
projectId: z.string().describe('Google Cloud 프로젝트 ID'),
|
|
766
|
-
member: z
|
|
767
|
-
.string()
|
|
768
|
-
.describe('권한 받을 주체 (예: serviceAccount:my-sa@my-project.iam.gserviceaccount.com)'),
|
|
769
|
-
role: z.string().describe('IAM 역할 (예: roles/iam.serviceAccountTokenCreator)'),
|
|
770
|
-
}, async ({ projectId, member, role }) => {
|
|
771
|
-
const auth = requireAuth();
|
|
772
|
-
const result = await iam.addProjectIamPolicyBinding(auth, projectId, member, role);
|
|
773
|
-
return {
|
|
774
|
-
content: [
|
|
775
|
-
{
|
|
776
|
-
type: 'text',
|
|
777
|
-
text: result.added
|
|
778
|
-
? `✓ 바인딩 추가: \`${member}\` → \`${role}\``
|
|
779
|
-
: `• 이미 존재: \`${member}\` → \`${role}\` (no-op)`,
|
|
780
|
-
},
|
|
781
|
-
],
|
|
782
|
-
};
|
|
783
|
-
});
|
|
784
|
-
// ══════════════════════════════════════════════════
|
|
785
|
-
// App Store Connect Tools
|
|
786
|
-
// ══════════════════════════════════════════════════
|
|
787
|
-
server.tool('appstore_list_apps', 'App Store Connect 앱 목록 조회', {}, async () => {
|
|
788
|
-
const apps = await appstore.listApps();
|
|
789
|
-
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
790
|
-
});
|
|
791
|
-
server.tool('appstore_get_app', 'App Store Connect 앱 상세 정보', { appId: z.string().describe('앱 ID (숫자)') }, async ({ appId }) => {
|
|
792
|
-
const app = await appstore.getApp(appId);
|
|
793
|
-
return { content: [{ type: 'text', text: JSON.stringify(app, null, 2) }] };
|
|
794
|
-
});
|
|
795
|
-
server.tool('appstore_list_versions', 'App Store 버전 목록 (심사 상태 포함)', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
796
|
-
const versions = await appstore.listVersions(appId);
|
|
797
|
-
return { content: [{ type: 'text', text: JSON.stringify(versions, null, 2) }] };
|
|
798
|
-
});
|
|
799
|
-
server.tool('appstore_create_version', [
|
|
800
|
-
'App Store 새 버전 레코드 생성 — POST /v1/appStoreVersions.',
|
|
801
|
-
'새 versionString(예: "1.2.3")으로 PREPARE_FOR_SUBMISSION 상태의 버전을 만듦.',
|
|
802
|
-
'buildId를 함께 주면 생성과 동시에 빌드 연결. 나중에 붙이려면 appstore_attach_build 사용.',
|
|
803
|
-
'releaseType: MANUAL(개발자가 출시) / AFTER_APPROVAL(승인 후 자동) / SCHEDULED(earliestReleaseDate 필요).',
|
|
804
|
-
].join(' '), {
|
|
805
|
-
appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과의 id, 숫자형)'),
|
|
806
|
-
versionString: z.string().describe('버전 문자열 (예: "1.2.3")'),
|
|
807
|
-
platform: z
|
|
808
|
-
.enum(['IOS', 'MAC_OS', 'TV_OS', 'VISION_OS'])
|
|
809
|
-
.default('IOS')
|
|
810
|
-
.describe('플랫폼 (기본 IOS)'),
|
|
811
|
-
copyright: z.string().optional().describe('저작권 표기 (예: "© 2026 Foo Inc.")'),
|
|
812
|
-
releaseType: z
|
|
813
|
-
.enum(['MANUAL', 'AFTER_APPROVAL', 'SCHEDULED'])
|
|
814
|
-
.optional()
|
|
815
|
-
.describe('출시 방식 (생략 시 Apple 기본값)'),
|
|
816
|
-
earliestReleaseDate: z
|
|
817
|
-
.string()
|
|
818
|
-
.optional()
|
|
819
|
-
.describe('SCHEDULED일 때 가장 빠른 출시 시각 (ISO 8601, 예: "2026-05-01T00:00:00Z")'),
|
|
820
|
-
buildId: z
|
|
821
|
-
.string()
|
|
822
|
-
.optional()
|
|
823
|
-
.describe('연결할 빌드 ID (appstore_list_builds 결과). 생략 시 버전만 생성하고 나중에 attach.'),
|
|
824
|
-
}, async ({ appId, versionString, platform, copyright, releaseType, earliestReleaseDate, buildId }) => {
|
|
825
|
-
const result = await appstore.createVersion({
|
|
826
|
-
appId,
|
|
827
|
-
versionString,
|
|
828
|
-
platform,
|
|
829
|
-
copyright,
|
|
830
|
-
releaseType,
|
|
831
|
-
earliestReleaseDate,
|
|
832
|
-
buildId,
|
|
833
|
-
});
|
|
834
|
-
return {
|
|
835
|
-
content: [
|
|
836
|
-
{
|
|
837
|
-
type: 'text',
|
|
838
|
-
text: `✅ 버전 ${versionString} (${platform}) 생성됨${buildId ? ` + 빌드 ${buildId} 연결됨` : ''}.\n\n${JSON.stringify(result, null, 2)}`,
|
|
839
|
-
},
|
|
840
|
-
],
|
|
841
|
-
};
|
|
842
|
-
});
|
|
843
|
-
server.tool('appstore_attach_build', [
|
|
844
|
-
'App Store 버전에 업로드된 빌드를 연결 — PATCH /v1/appStoreVersions/{id}/relationships/build.',
|
|
845
|
-
'TestFlight에 업로드되어 processingState=VALID 상태인 빌드만 연결 가능.',
|
|
846
|
-
'편집 가능한 버전(PREPARE_FOR_SUBMISSION 등)에서만 변경됨.',
|
|
847
|
-
'buildId는 appstore_list_builds 결과 사용.',
|
|
848
|
-
].join(' '), {
|
|
849
|
-
versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 또는 appstore_create_version 결과)'),
|
|
850
|
-
buildId: z.string().describe('빌드 ID (appstore_list_builds 결과)'),
|
|
851
|
-
}, async ({ versionId, buildId }) => {
|
|
852
|
-
const result = await appstore.attachBuildToVersion(versionId, buildId);
|
|
853
|
-
return {
|
|
854
|
-
content: [
|
|
855
|
-
{
|
|
856
|
-
type: 'text',
|
|
857
|
-
text: `✅ 빌드 ${buildId}가 버전 ${versionId}에 연결됐어.\n\n${JSON.stringify(result, null, 2)}`,
|
|
858
|
-
},
|
|
859
|
-
],
|
|
860
|
-
};
|
|
861
|
-
});
|
|
862
|
-
server.tool('appstore_get_metadata', 'App Store 버전 메타데이터 (설명문, 키워드, What\'s New)', { versionId: z.string().describe('버전 ID') }, async ({ versionId }) => {
|
|
863
|
-
const localizations = await appstore.getVersionLocalizations(versionId);
|
|
864
|
-
return { content: [{ type: 'text', text: JSON.stringify(localizations, null, 2) }] };
|
|
865
|
-
});
|
|
866
|
-
server.tool('appstore_update_localization', "App Store 버전 로컬라이제이션(메타데이터) 수정 — localizationId 직접 지정. 이 버전의 새로운 기능(whatsNew), 설명(description), 키워드, 프로모션 텍스트를 편집. 수정 가능한 상태(PREPARE_FOR_SUBMISSION 등)인 버전에서만 반영됨", {
|
|
867
|
-
localizationId: z.string().describe('로컬라이제이션 ID (appstore_get_metadata 결과의 id)'),
|
|
868
|
-
whatsNew: z.string().optional().describe('이 버전의 새로운 기능 (4000자 이내)'),
|
|
869
|
-
description: z.string().optional().describe('앱 설명 (4000자 이내)'),
|
|
870
|
-
keywords: z.string().optional().describe('키워드 (쉼표 구분, 100자 이내)'),
|
|
871
|
-
promotionalText: z.string().optional().describe('프로모션 텍스트 (170자 이내)'),
|
|
872
|
-
supportUrl: z.string().url().optional().describe('지원 URL'),
|
|
873
|
-
marketingUrl: z.string().url().optional().describe('마케팅 URL'),
|
|
874
|
-
}, async ({ localizationId, ...fields }) => {
|
|
875
|
-
const cleaned = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
876
|
-
if (Object.keys(cleaned).length === 0) {
|
|
877
|
-
throw new Error('수정할 필드를 하나 이상 지정해줘 (whatsNew, description, keywords, promotionalText, supportUrl, marketingUrl).');
|
|
878
|
-
}
|
|
879
|
-
const result = await appstore.updateVersionLocalization(localizationId, cleaned);
|
|
880
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
881
|
-
});
|
|
882
|
-
server.tool('appstore_list_screenshots', "App Store 스크린샷 셋 + 이미지 목록 조회 (로컬라이제이션 단위). 디스플레이 타입별로 그룹핑됨", { localizationId: z.string().describe('로컬라이제이션 ID (appstore_get_metadata 결과의 id)') }, async ({ localizationId }) => {
|
|
883
|
-
const sets = await appstoreScreenshots.listScreenshotSets(localizationId);
|
|
884
|
-
return { content: [{ type: 'text', text: JSON.stringify(sets, null, 2) }] };
|
|
885
|
-
});
|
|
886
|
-
server.tool('appstore_upload_screenshot', [
|
|
887
|
-
"App Store 스크린샷 업로드 (자동 4단계: 셋 확보 → 예약 → 청크 업로드 → 커밋).",
|
|
888
|
-
"파일 경로는 로컬 절대경로. displayType은 Apple 공식 enum:",
|
|
889
|
-
"APP_IPHONE_69 (6.9\", 1290x2796) / APP_IPHONE_67 (6.7\", 1290x2796) /",
|
|
890
|
-
"APP_IPHONE_65 (6.5\", 1242x2688) / APP_IPHONE_61 (6.1\", 1170x2532) /",
|
|
891
|
-
"APP_IPHONE_58 (5.8\") / APP_IPHONE_55 (5.5\", 1242x2208) /",
|
|
892
|
-
"APP_IPAD_PRO_3GEN_129 (13\", 2064x2752) / APP_IPAD_PRO_3GEN_11 (11\", 1668x2388) /",
|
|
893
|
-
"APP_IPAD_PRO_129 (12.9\", 2048x2732) / APP_DESKTOP (2560x1600+).",
|
|
894
|
-
"해상도 틀리면 검수에서 리젝됨. 수정 가능한 버전 상태에서만 반영됨.",
|
|
895
|
-
].join(' '), {
|
|
896
|
-
localizationId: z.string().describe('로컬라이제이션 ID'),
|
|
897
|
-
displayType: z.string().describe('Apple 스크린샷 디스플레이 타입 (예: APP_IPHONE_69)'),
|
|
898
|
-
filePath: z.string().describe('업로드할 이미지 파일의 절대경로'),
|
|
899
|
-
}, async ({ localizationId, displayType, filePath }) => {
|
|
900
|
-
const result = await appstoreScreenshots.uploadScreenshot(localizationId, displayType, filePath);
|
|
901
|
-
return {
|
|
902
|
-
content: [{
|
|
903
|
-
type: 'text',
|
|
904
|
-
text: `✅ 스크린샷 업로드 완료 (${displayType})\n\n${JSON.stringify(result, null, 2)}`,
|
|
905
|
-
}],
|
|
906
|
-
};
|
|
907
|
-
});
|
|
908
|
-
server.tool('appstore_delete_screenshot', 'App Store 개별 스크린샷 삭제', { screenshotId: z.string().describe('스크린샷 ID (appstore_list_screenshots 결과)') }, async ({ screenshotId }) => {
|
|
909
|
-
const result = await appstoreScreenshots.deleteScreenshot(screenshotId);
|
|
910
|
-
return { content: [{ type: 'text', text: `✅ 스크린샷 삭제됨\n${JSON.stringify(result, null, 2)}` }] };
|
|
911
|
-
});
|
|
912
|
-
server.tool('appstore_delete_screenshot_set', 'App Store 스크린샷 셋 전체 삭제 (디스플레이 타입 교체 시 먼저 정리)', { setId: z.string().describe('스크린샷 셋 ID (appstore_list_screenshots 결과)') }, async ({ setId }) => {
|
|
913
|
-
const result = await appstoreScreenshots.deleteScreenshotSet(setId);
|
|
914
|
-
return { content: [{ type: 'text', text: `✅ 셋 삭제됨\n${JSON.stringify(result, null, 2)}` }] };
|
|
915
|
-
});
|
|
916
|
-
server.tool('appstore_update_whats_new', "App Store '이 버전의 새로운 기능' 편집 — versionId + locale만 주면 자동으로 로컬라이제이션을 찾아 PATCH. 가장 흔한 사용 케이스. 수정 가능한 상태(PREPARE_FOR_SUBMISSION 등)인 버전에서만 반영됨", {
|
|
917
|
-
versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
|
|
918
|
-
locale: z.string().describe('로캘 (예: ko, en-US, ja)'),
|
|
919
|
-
whatsNew: z.string().describe("'이 버전의 새로운 기능' 텍스트 (4000자 이내)"),
|
|
920
|
-
}, async ({ versionId, locale, whatsNew }) => {
|
|
921
|
-
const result = await appstore.updateVersionWhatsNew(versionId, locale, { whatsNew });
|
|
922
|
-
return { content: [{ type: 'text', text: `✅ ${locale} 로캘의 What's New가 업데이트됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
923
|
-
});
|
|
924
|
-
server.tool('appstore_update_review_notes', "App Store 심사 리뷰어 노트(Notes for App Review) 등록/수정. versionId 버전에 appStoreReviewDetail.notes를 PATCH하거나 없으면 POST로 생성. 심사 시 리뷰어에게 전달되는 테스트 계정·기능 안내 텍스트 작성에 사용. 4000자 권장 한도.", {
|
|
925
|
-
versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
|
|
926
|
-
notes: z.string().min(1).max(4000).describe('리뷰어에게 전달할 메모 (테스트 계정, 주요 변경사항, 접근 방법 등). 4000자 이내.'),
|
|
927
|
-
}, async ({ versionId, notes }) => {
|
|
928
|
-
const result = await appstore.updateReviewNotes(versionId, notes);
|
|
929
|
-
const action = result.created ? 'created' : 'updated';
|
|
930
|
-
const summary = `✅ 리뷰어 노트 ${result.created ? '신규 등록' : '수정'} 완료 (reviewDetailId: ${result.reviewDetailId})`;
|
|
931
|
-
return {
|
|
932
|
-
content: [{
|
|
933
|
-
type: 'text',
|
|
934
|
-
text: `${summary}\n\n${JSON.stringify({ ok: true, action, ...result }, null, 2)}`,
|
|
935
|
-
}],
|
|
936
|
-
};
|
|
937
|
-
});
|
|
938
|
-
server.tool('appstore_get_review_notes', "App Store 심사 리뷰어 노트(Notes for App Review) 조회. 현재 등록된 notes, contactEmail 확인용.", {
|
|
939
|
-
versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
|
|
940
|
-
}, async ({ versionId }) => {
|
|
941
|
-
const result = await appstore.getReviewNotes(versionId);
|
|
942
|
-
if (!result.reviewDetailId) {
|
|
943
|
-
return {
|
|
944
|
-
content: [{
|
|
945
|
-
type: 'text',
|
|
946
|
-
text: `이 버전에는 아직 리뷰어 노트가 없어. appstore_update_review_notes로 등록해줘.\n\n${JSON.stringify({ ok: true, exists: false }, null, 2)}`,
|
|
947
|
-
}],
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
const summary = `reviewDetailId: ${result.reviewDetailId}\ncontactEmail: ${result.contactEmail ?? '(없음)'}\n\n노트:\n${result.notes ?? '(비어있음)'}`;
|
|
951
|
-
return {
|
|
952
|
-
content: [{
|
|
953
|
-
type: 'text',
|
|
954
|
-
text: `${summary}\n\n${JSON.stringify({ ok: true, exists: true, ...result }, null, 2)}`,
|
|
955
|
-
}],
|
|
956
|
-
};
|
|
957
|
-
});
|
|
958
|
-
server.tool('appstore_list_builds', 'TestFlight 빌드 목록', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
959
|
-
const builds = await appstore.listBuilds(appId);
|
|
960
|
-
return { content: [{ type: 'text', text: JSON.stringify(builds, null, 2) }] };
|
|
961
|
-
});
|
|
962
|
-
server.tool('appstore_list_beta_groups', 'TestFlight 베타 그룹 목록', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
963
|
-
const groups = await appstore.listBetaGroups(appId);
|
|
964
|
-
return { content: [{ type: 'text', text: JSON.stringify(groups, null, 2) }] };
|
|
965
|
-
});
|
|
966
|
-
server.tool('appstore_get_app_info', 'App Store 앱 정보 (카테고리, 연령 등급, state). state=READY_FOR_DISTRIBUTION이 라이브, 그 외가 편집 가능 appInfo.', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
967
|
-
const info = await appstore.getAppInfo(appId);
|
|
968
|
-
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
|
|
969
|
-
});
|
|
970
|
-
server.tool('appstore_list_app_info_localizations', [
|
|
971
|
-
'편집 가능한 appInfo의 로컬라이제이션 목록 조회 — 앱 이름(name), 부제(subtitle), 개인정보 URL/텍스트.',
|
|
972
|
-
'appInfo.relationships.appInfoLocalizations가 빈 배열로 오는 케이스를 우회하려고 /appInfos/{id}/appInfoLocalizations 직접 호출.',
|
|
973
|
-
'locale을 주면 해당 언어만 반환 (예: "ko", "en-US").',
|
|
974
|
-
'※ versionLocalization(설명/키워드/whatsNew)과 다름 — 그건 appstore_get_metadata 사용.',
|
|
975
|
-
].join(' '), {
|
|
976
|
-
appId: z.string().describe('앱 ID (appstore_list_apps 결과)'),
|
|
977
|
-
locale: z.string().optional().describe('언어 필터 (예: "ko", "en-US"). 생략 시 전체 반환.'),
|
|
978
|
-
}, async ({ appId, locale }) => {
|
|
979
|
-
const result = await appstore.listAppInfoLocalizations(appId, locale);
|
|
980
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
981
|
-
});
|
|
982
|
-
server.tool('appstore_update_app_info_localization', [
|
|
983
|
-
'appInfo 로컬라이제이션(앱 이름/부제/개인정보 URL/텍스트) 수정 — PATCH /appInfoLocalizations/{id}.',
|
|
984
|
-
'localizationId는 appstore_list_app_info_localizations 결과의 id.',
|
|
985
|
-
'편집 가능 상태(PREPARE_FOR_SUBMISSION / DEVELOPER_REJECTED 등)에서만 반영됨.',
|
|
986
|
-
'제한: name 30자, subtitle 30자.',
|
|
987
|
-
].join(' '), {
|
|
988
|
-
localizationId: z.string().describe('appInfoLocalization ID'),
|
|
989
|
-
name: z.string().optional().describe('앱 이름 (30자 이내)'),
|
|
990
|
-
subtitle: z.string().optional().describe('부제 (30자 이내)'),
|
|
991
|
-
privacyPolicyUrl: z.string().url().optional().describe('개인정보 처리방침 URL'),
|
|
992
|
-
privacyPolicyText: z.string().optional().describe('개인정보 처리방침 텍스트'),
|
|
993
|
-
}, async ({ localizationId, ...fields }) => {
|
|
994
|
-
const result = await appstore.updateAppInfoLocalization(localizationId, fields);
|
|
995
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
996
|
-
});
|
|
997
|
-
// --- App Store 고객 리뷰 ---
|
|
998
|
-
server.tool('appstore_list_reviews', [
|
|
999
|
-
'App Store 받은 고객 리뷰 조회 (최신순).',
|
|
1000
|
-
'response 필드에 개발자 답변 존재 여부와 내용이 함께 포함됨 (없으면 null).',
|
|
1001
|
-
'territory(예: KOR/USA — ISO 3166 alpha-3) / rating(1~5)으로 필터 가능.',
|
|
1002
|
-
].join(' '), {
|
|
1003
|
-
appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과)'),
|
|
1004
|
-
limit: z.number().int().positive().max(200).optional().describe('가져올 개수 (기본 50, 최대 200)'),
|
|
1005
|
-
territory: z.string().optional().describe("국가 코드 (예: 'KOR', 'USA' — alpha-3)"),
|
|
1006
|
-
rating: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional().describe('별점 필터 (1~5)'),
|
|
1007
|
-
}, async ({ appId, limit, territory, rating }) => {
|
|
1008
|
-
const reviews = await appstore.listCustomerReviews(appId, { limit, territory, rating });
|
|
1009
|
-
return { content: [{ type: 'text', text: JSON.stringify(reviews, null, 2) }] };
|
|
1010
|
-
});
|
|
1011
|
-
server.tool('appstore_reply_review', [
|
|
1012
|
-
'App Store 고객 리뷰에 개발자 답변을 등록 (또는 갱신).',
|
|
1013
|
-
'동일 리뷰에 한 번만 답변 가능 — 기존 답변이 있으면 Apple이 새 응답으로 대체함.',
|
|
1014
|
-
'reviewId는 appstore_list_reviews 결과의 id.',
|
|
1015
|
-
'답변 본문은 5970자 이내.',
|
|
1016
|
-
].join(' '), {
|
|
1017
|
-
reviewId: z.string().describe('리뷰 ID (appstore_list_reviews 결과)'),
|
|
1018
|
-
responseBody: z.string().describe('답변 본문 (5970자 이내)'),
|
|
1019
|
-
}, async ({ reviewId, responseBody }) => {
|
|
1020
|
-
const result = await appstore.createReviewResponse(reviewId, responseBody);
|
|
1021
|
-
return { content: [{ type: 'text', text: `✅ 리뷰 ${reviewId}에 답변 등록됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1022
|
-
});
|
|
1023
|
-
// --- App Store IAP / 구독 생성 ---
|
|
1024
|
-
server.tool('appstore_create_inapp_purchase', [
|
|
1025
|
-
'App Store에 일회성 인앱 구매(IAP)를 생성 — CONSUMABLE (소비성) / NON_CONSUMABLE (비소비성).',
|
|
1026
|
-
'생성 후 App Store Connect에서 스크린샷·리뷰 노트를 추가해야 심사 제출 가능.',
|
|
1027
|
-
].join(' '), {
|
|
1028
|
-
appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과의 id, 숫자형)'),
|
|
1029
|
-
productId: z
|
|
1030
|
-
.string()
|
|
1031
|
-
.describe('상품 ID (글로벌 unique 권장: 예 com.example.coins_100)'),
|
|
1032
|
-
name: z.string().describe('상품 이름 (스토어 노출, 최대 30자)'),
|
|
1033
|
-
price: z.number().int().describe('가격 (최소 단위: USD cents. 예: $0.99 → 99, ₩1,100 → 1100)'),
|
|
1034
|
-
currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
|
|
1035
|
-
type: z
|
|
1036
|
-
.enum(['consumable', 'non_consumable'])
|
|
1037
|
-
.default('non_consumable')
|
|
1038
|
-
.describe('IAP 유형 (소비성/비소비성)'),
|
|
1039
|
-
extraRegions: z
|
|
1040
|
-
.array(z.object({
|
|
1041
|
-
currency: z.string().describe('ISO 4217 통화 코드 (예: KRW)'),
|
|
1042
|
-
price: z.number().describe('가격 (최소 단위)'),
|
|
1043
|
-
}))
|
|
1044
|
-
.optional()
|
|
1045
|
-
.describe('추가 지역별 명시 가격'),
|
|
1046
|
-
bundleId: z.string().optional().describe('번들 ID (appId 대신 사용 가능)'),
|
|
1047
|
-
}, async (args) => {
|
|
1048
|
-
const creds = requireAppStoreCreds();
|
|
1049
|
-
const result = await createAppleOneTimePurchase({
|
|
1050
|
-
appId: args.appId,
|
|
1051
|
-
bundleId: args.bundleId,
|
|
1052
|
-
productId: args.productId,
|
|
1053
|
-
name: args.name,
|
|
1054
|
-
price: args.price,
|
|
1055
|
-
currency: args.currency,
|
|
1056
|
-
type: args.type,
|
|
1057
|
-
...(args.extraRegions && { extraRegions: args.extraRegions }),
|
|
1058
|
-
keyId: creds.keyId,
|
|
1059
|
-
issuerId: creds.issuerId,
|
|
1060
|
-
privateKey: creds.privateKey,
|
|
1061
|
-
});
|
|
1062
|
-
if (!result.success) {
|
|
1063
|
-
const hint = result.errorType === 'DUPLICATE'
|
|
1064
|
-
? '\n이미 같은 productId가 존재해. App Store Connect에서 확인해줘.'
|
|
1065
|
-
: result.errorType === 'PRICE_NOT_FOUND'
|
|
1066
|
-
? `\n가장 가까운 가격: ${JSON.stringify(result.priceNearest)}`
|
|
1067
|
-
: '';
|
|
1068
|
-
return { content: [{ type: 'text', text: `❌ IAP 생성 실패: ${result.error}${hint}` }] };
|
|
1069
|
-
}
|
|
1070
|
-
return {
|
|
1071
|
-
content: [{
|
|
1072
|
-
type: 'text',
|
|
1073
|
-
text: [
|
|
1074
|
-
`✓ App Store IAP 생성 완료`,
|
|
1075
|
-
`productId: ${result.productId}`,
|
|
1076
|
-
`internalId: ${result.internalId}`,
|
|
1077
|
-
result.priceSet ? `✓ 가격 설정됨` : `⚠ 가격 미설정 (가장 가까운 가격: ${JSON.stringify(result.priceNearest)})`,
|
|
1078
|
-
result.extraRegionsSet?.length ? `✓ 추가 지역: ${result.extraRegionsSet.join(', ')}` : '',
|
|
1079
|
-
'',
|
|
1080
|
-
'스크린샷·리뷰 노트를 추가한 후 App Store Connect에서 심사 제출:',
|
|
1081
|
-
`https://appstoreconnect.apple.com/apps/${args.appId}/distribution/iaps`,
|
|
1082
|
-
].filter(Boolean).join('\n'),
|
|
1083
|
-
}],
|
|
1084
|
-
};
|
|
1085
|
-
});
|
|
1086
|
-
server.tool('appstore_create_subscription', [
|
|
1087
|
-
'App Store에 자동 갱신 구독을 생성 — Subscription Group 자동 생성 포함.',
|
|
1088
|
-
'생성 후 App Store Connect에서 스크린샷·리뷰 노트를 추가해야 심사 제출 가능.',
|
|
1089
|
-
].join(' '), {
|
|
1090
|
-
appId: z.string().describe('App Store 앱 ID'),
|
|
1091
|
-
productId: z.string().describe('구독 productId (예: com.example.premium.monthly)'),
|
|
1092
|
-
name: z.string().describe('구독 이름 (스토어 노출)'),
|
|
1093
|
-
price: z.number().int().describe('가격 (최소 단위: USD cents. 예: $9.99 → 999, ₩9,900 → 9900)'),
|
|
1094
|
-
currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
|
|
1095
|
-
period: z
|
|
1096
|
-
.enum(['monthly', 'yearly'])
|
|
1097
|
-
.describe('구독 주기'),
|
|
1098
|
-
extraRegions: z
|
|
1099
|
-
.array(z.object({
|
|
1100
|
-
currency: z.string().describe('ISO 4217 통화 코드 (예: KRW)'),
|
|
1101
|
-
price: z.number().describe('가격 (최소 단위)'),
|
|
1102
|
-
}))
|
|
1103
|
-
.optional()
|
|
1104
|
-
.describe('추가 지역별 명시 가격'),
|
|
1105
|
-
bundleId: z.string().optional().describe('번들 ID (appId 대신 사용 가능)'),
|
|
1106
|
-
}, async (args) => {
|
|
1107
|
-
const creds = requireAppStoreCreds();
|
|
1108
|
-
const result = await createAppleSubscription({
|
|
1109
|
-
appId: args.appId,
|
|
1110
|
-
bundleId: args.bundleId,
|
|
1111
|
-
productId: args.productId,
|
|
1112
|
-
name: args.name,
|
|
1113
|
-
price: args.price,
|
|
1114
|
-
currency: args.currency,
|
|
1115
|
-
period: args.period,
|
|
1116
|
-
...(args.extraRegions && { extraRegions: args.extraRegions }),
|
|
1117
|
-
keyId: creds.keyId,
|
|
1118
|
-
issuerId: creds.issuerId,
|
|
1119
|
-
privateKey: creds.privateKey,
|
|
1120
|
-
});
|
|
1121
|
-
if (!result.success) {
|
|
1122
|
-
const hint = result.errorType === 'DUPLICATE'
|
|
1123
|
-
? '\n이미 같은 productId가 존재해. App Store Connect에서 확인해줘.'
|
|
1124
|
-
: result.errorType === 'PRICE_NOT_FOUND'
|
|
1125
|
-
? `\n가장 가까운 가격: ${JSON.stringify(result.priceNearest)}`
|
|
1126
|
-
: '';
|
|
1127
|
-
return { content: [{ type: 'text', text: `❌ 구독 생성 실패: ${result.error}${hint}` }] };
|
|
1128
|
-
}
|
|
1129
|
-
return {
|
|
1130
|
-
content: [{
|
|
1131
|
-
type: 'text',
|
|
1132
|
-
text: [
|
|
1133
|
-
`✓ App Store 구독 생성 완료`,
|
|
1134
|
-
`productId: ${result.productId}`,
|
|
1135
|
-
`internalId: ${result.internalId}`,
|
|
1136
|
-
result.priceSet ? `✓ 가격 설정됨` : `⚠ 가격 미설정 (가장 가까운 가격: ${JSON.stringify(result.priceNearest)})`,
|
|
1137
|
-
result.extraRegionsSet?.length ? `✓ 추가 지역: ${result.extraRegionsSet.join(', ')}` : '',
|
|
1138
|
-
result.localizationAdded ? '✓ KRW 한국어 로컬라이제이션 추가됨' : '',
|
|
1139
|
-
'',
|
|
1140
|
-
'App Store Connect에서 심사 제출:',
|
|
1141
|
-
`https://appstoreconnect.apple.com/apps/${args.appId}/distribution/subscriptions`,
|
|
1142
|
-
].filter(Boolean).join('\n'),
|
|
1143
|
-
}],
|
|
1144
|
-
};
|
|
1145
|
-
});
|
|
1146
|
-
// --- SKU 운영 (조회/수정/삭제) — onesub 위임 ---
|
|
1147
|
-
server.tool('playstore_list_products', 'Google Play의 모든 IAP 상품(구독 + 일회성) 통합 조회. productId / name / status / type / price / currency 반환.', {
|
|
1148
|
-
packageName: z.string().describe('패키지명'),
|
|
1149
|
-
}, async ({ packageName }) => {
|
|
1150
|
-
const json = requireServiceAccountJson(packageName);
|
|
1151
|
-
const products = await listGoogleProducts({ packageName, serviceAccountKey: json });
|
|
1152
|
-
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
|
|
1153
|
-
});
|
|
1154
|
-
server.tool('playstore_update_product', 'Google Play IAP 상품의 표시 이름 변경 (현재 name 필드만 수정 가능). productId / type / 가격은 변경 불가.', {
|
|
1155
|
-
packageName: z.string().describe('패키지명'),
|
|
1156
|
-
productId: z.string().describe('상품 ID'),
|
|
1157
|
-
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
1158
|
-
name: z.string().describe('새 표시 이름'),
|
|
1159
|
-
}, async ({ packageName, productId, productType, name }) => {
|
|
1160
|
-
const json = requireServiceAccountJson(packageName);
|
|
1161
|
-
const result = await updateGoogleProduct({
|
|
1162
|
-
packageName, productId, productType, name, serviceAccountKey: json,
|
|
1163
|
-
});
|
|
1164
|
-
if (!result.success) {
|
|
1165
|
-
return { content: [{ type: 'text', text: `❌ 수정 실패: ${result.error}` }] };
|
|
1166
|
-
}
|
|
1167
|
-
return { content: [{ type: 'text', text: `✓ 수정 완료 (변경 필드: ${result.updated.join(', ') || 'none'})` }] };
|
|
1168
|
-
});
|
|
1169
|
-
server.tool('playstore_delete_product', '⚠️ 비가역. Google Play IAP 상품 삭제. 활성 baseplan + 구독자 있는 구독은 삭제 불가.', {
|
|
1170
|
-
packageName: z.string().describe('패키지명'),
|
|
1171
|
-
productId: z.string().describe('상품 ID'),
|
|
1172
|
-
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
1173
|
-
}, async ({ packageName, productId, productType }) => {
|
|
1174
|
-
const json = requireServiceAccountJson(packageName);
|
|
1175
|
-
const result = await deleteGoogleProduct({
|
|
1176
|
-
packageName, productId, productType, serviceAccountKey: json,
|
|
1177
|
-
});
|
|
1178
|
-
if (!result.success) {
|
|
1179
|
-
return { content: [{ type: 'text', text: `❌ 삭제 실패: ${result.error}` }] };
|
|
1180
|
-
}
|
|
1181
|
-
return { content: [{ type: 'text', text: `✓ ${productId} 삭제 완료` }] };
|
|
1182
|
-
});
|
|
1183
|
-
server.tool('appstore_list_products', 'App Store의 모든 IAP 상품(구독 + 일회성) 통합 조회. productId / internalId / name / status / type 반환.', {
|
|
1184
|
-
appId: z.string().describe('App Store 앱 ID (숫자형, appstore_list_apps 결과)'),
|
|
1185
|
-
}, async ({ appId }) => {
|
|
1186
|
-
const creds = requireAppStoreCreds();
|
|
1187
|
-
const products = await listAppleProducts({
|
|
1188
|
-
appId, keyId: creds.keyId, issuerId: creds.issuerId, privateKey: creds.privateKey,
|
|
1189
|
-
});
|
|
1190
|
-
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
|
|
1191
|
-
});
|
|
1192
|
-
server.tool('appstore_update_product', 'App Store IAP 상품의 reference name 변경. productId / 유형은 변경 불가.', {
|
|
1193
|
-
appId: z.string().optional().describe('App Store 앱 ID'),
|
|
1194
|
-
bundleId: z.string().optional().describe('번들 ID (appId 대신 사용 가능)'),
|
|
1195
|
-
productId: z.string().describe('상품 ID'),
|
|
1196
|
-
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
1197
|
-
name: z.string().describe('새 reference name'),
|
|
1198
|
-
}, async ({ appId, bundleId, productId, productType, name }) => {
|
|
1199
|
-
if (!appId && !bundleId) {
|
|
1200
|
-
throw new Error('appId 또는 bundleId 중 하나는 반드시 제공해야 합니다.');
|
|
1201
|
-
}
|
|
1202
|
-
const creds = requireAppStoreCreds();
|
|
1203
|
-
const result = await updateAppleProduct({
|
|
1204
|
-
appId, bundleId, productId, productType, name,
|
|
1205
|
-
keyId: creds.keyId, issuerId: creds.issuerId, privateKey: creds.privateKey,
|
|
1206
|
-
});
|
|
1207
|
-
if (!result.success) {
|
|
1208
|
-
return { content: [{ type: 'text', text: `❌ 수정 실패: ${result.error}` }] };
|
|
1209
|
-
}
|
|
1210
|
-
return { content: [{ type: 'text', text: `✓ 수정 완료 (변경 필드: ${result.updated.join(', ') || 'none'})` }] };
|
|
1211
|
-
});
|
|
1212
|
-
server.tool('appstore_delete_product', '⚠️ 비가역. App Store IAP 상품 삭제. MISSING_METADATA / WAITING_FOR_REVIEW 상태만 가능 — 이미 승인(READY_FOR_SALE)된 상품은 Console에서 "Remove from sale" 해야 함.', {
|
|
1213
|
-
appId: z.string().optional().describe('App Store 앱 ID'),
|
|
1214
|
-
bundleId: z.string().optional().describe('번들 ID (appId 대신 사용 가능)'),
|
|
1215
|
-
productId: z.string().describe('상품 ID'),
|
|
1216
|
-
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
1217
|
-
}, async ({ appId, bundleId, productId, productType }) => {
|
|
1218
|
-
if (!appId && !bundleId) {
|
|
1219
|
-
throw new Error('appId 또는 bundleId 중 하나는 반드시 제공해야 합니다.');
|
|
1220
|
-
}
|
|
1221
|
-
const creds = requireAppStoreCreds();
|
|
1222
|
-
const result = await deleteAppleProduct({
|
|
1223
|
-
appId, bundleId, productId, productType,
|
|
1224
|
-
keyId: creds.keyId, issuerId: creds.issuerId, privateKey: creds.privateKey,
|
|
1225
|
-
});
|
|
1226
|
-
if (!result.success) {
|
|
1227
|
-
const hint = result.errorType === 'CANNOT_DELETE'
|
|
1228
|
-
? '\n승인된 상품은 API 삭제 불가 — App Store Connect → 상품 → "Remove from sale"'
|
|
1229
|
-
: '';
|
|
1230
|
-
return { content: [{ type: 'text', text: `❌ 삭제 실패: ${result.error}${hint}` }] };
|
|
1231
|
-
}
|
|
1232
|
-
return { content: [{ type: 'text', text: `✓ ${productId} 삭제 완료` }] };
|
|
1233
|
-
});
|
|
1234
|
-
// --- 배포 플랜 (read-only, AI에게 TodoWrite 지시) ---
|
|
1235
|
-
server.tool('playstore_plan_release', [
|
|
1236
|
-
'Google Play 배포 플랜 — 인증/빌드/메타데이터/스크린샷/정책 위험을 한 번에 점검하고',
|
|
1237
|
-
'체크리스트로 반환합니다 (read-only, 아무것도 변경하지 않음).',
|
|
1238
|
-
'⚠️ AI 호출자 지시: 응답을 받으면 미완료 항목을 반드시 TodoWrite로 사용자에게 먼저 보여주고,',
|
|
1239
|
-
'사용자 동의 후 단계별로 기존 도구(playstore_update_listing, playstore_update_release_notes,',
|
|
1240
|
-
'playstore_submit_release 등)를 호출하세요. submit_release(status=completed)는 비가역이므로 반드시 명시 동의 필요.',
|
|
1241
|
-
].join(' '), {
|
|
1242
|
-
packageName: z.string().describe('Android 패키지명 (예: gg.pryzm.coffee)'),
|
|
1243
|
-
versionCode: z.string().optional().describe('확인할 versionCode. 미지정 시 트랙 최신 release 검사'),
|
|
1244
|
-
track: z.enum(['production', 'beta', 'alpha', 'internal']).optional().describe('대상 트랙 (기본: production)'),
|
|
1245
|
-
language: z.string().optional().describe('점검할 리스팅 언어 (기본: ko-KR)'),
|
|
1246
|
-
}, async ({ packageName, versionCode, track, language }) => {
|
|
1247
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
1248
|
-
const text = await buildPlayStoreReleasePlan({
|
|
1249
|
-
auth,
|
|
1250
|
-
packageName,
|
|
1251
|
-
versionCode,
|
|
1252
|
-
track: track ?? 'production',
|
|
1253
|
-
language: language ?? 'ko-KR',
|
|
1254
|
-
});
|
|
1255
|
-
return { content: [{ type: 'text', text }] };
|
|
1256
|
-
});
|
|
1257
|
-
server.tool('appstore_plan_release', [
|
|
1258
|
-
'App Store 배포 플랜 — 편집 가능한 버전/빌드 attach/메타/스크린샷/정책 위험을 한 번에 점검하고',
|
|
1259
|
-
'체크리스트로 반환합니다 (read-only).',
|
|
1260
|
-
'⚠️ AI 호출자 지시: 응답의 미완료 항목을 반드시 TodoWrite로 사용자에게 먼저 보여주고,',
|
|
1261
|
-
'사용자 동의 후 단계별로 기존 도구(appstore_update_localization, appstore_update_whats_new,',
|
|
1262
|
-
'appstore_submit_for_review 등)를 호출하세요. submit_for_review는 비가역이므로 반드시 명시 동의 필요.',
|
|
1263
|
-
].join(' '), {
|
|
1264
|
-
appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과의 id)'),
|
|
1265
|
-
versionString: z.string().optional().describe('대상 버전 (예: 1.3.0). 미지정 시 가장 최근 편집 가능 버전'),
|
|
1266
|
-
}, async ({ appId, versionString }) => {
|
|
1267
|
-
const text = await buildAppStoreReleasePlan({ appId, versionString });
|
|
1268
|
-
return { content: [{ type: 'text', text }] };
|
|
1269
|
-
});
|
|
1270
|
-
// --- App Store 심사 제출 (Submit for Review) ---
|
|
1271
|
-
server.tool('appstore_submit_for_review', [
|
|
1272
|
-
'App Store 버전을 심사에 제출 — 새 reviewSubmissions API 사용 (옛 /appStoreVersionSubmissions는 2024-01 deprecated).',
|
|
1273
|
-
'내부 흐름: POST /reviewSubmissions(또는 CREATED 상태 재사용) → POST /reviewSubmissionItems(version attach) → PATCH submitted=true.',
|
|
1274
|
-
'appId와 platform은 versionId에서 자동 조회 — 별도 입력 불필요.',
|
|
1275
|
-
'⚠️ 비가역 작업: 제출 후엔 Apple 심사가 시작되며, 메타데이터/스크린샷/빌드를 더 못 바꿈 (REJECTED/METADATA_REJECTED 시 다시 편집 가능).',
|
|
1276
|
-
'사전 조건: 버전이 PREPARE_FOR_SUBMISSION 또는 DEVELOPER_REJECTED 상태, 빌드 attached, 모든 필수 메타데이터 채워짐.',
|
|
1277
|
-
'appstore_check_submission_risks로 사전 점검 권장.',
|
|
1278
|
-
].join(' '), {
|
|
1279
|
-
versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
|
|
1280
|
-
}, async ({ versionId }) => {
|
|
1281
|
-
const result = await appstore.submitVersionForReview(versionId);
|
|
1282
|
-
return { content: [{ type: 'text', text: `✅ 버전 ${versionId} 심사 제출 완료 (state: ${result.state}). App Store Connect에서 진행 상태 확인 가능.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1283
|
-
});
|
|
1284
|
-
// --- App Store 심사 철회 (Cancel Review) ---
|
|
1285
|
-
server.tool('appstore_cancel_review', [
|
|
1286
|
-
'App Store 심사 제출을 철회 — WAITING_FOR_REVIEW 상태에서만 가능.',
|
|
1287
|
-
'submitted=false PATCH → reviewSubmission이 READY_FOR_REVIEW로 복귀, 버전은 PREPARE_FOR_SUBMISSION 상태로 돌아가 메타데이터/빌드 수정 가능.',
|
|
1288
|
-
'⚠️ IN_REVIEW 이상이면 Apple API가 409로 거부함 — 이 경우 App Store Connect 웹에서 직접 처리하거나 심사 결과를 기다려야 함.',
|
|
1289
|
-
'철회 후 수정 완료 시 appstore_submit_for_review로 재제출 가능.',
|
|
1290
|
-
].join(' '), {
|
|
1291
|
-
versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
|
|
1292
|
-
}, async ({ versionId }) => {
|
|
1293
|
-
const result = await appstore.cancelVersionReview(versionId);
|
|
1294
|
-
return {
|
|
1295
|
-
content: [{
|
|
1296
|
-
type: 'text',
|
|
1297
|
-
text: [
|
|
1298
|
-
`✅ 심사 철회 완료`,
|
|
1299
|
-
` submissionId: ${result.submissionId}`,
|
|
1300
|
-
` ${result.previousState} → ${result.newState}`,
|
|
1301
|
-
` 버전 ${result.versionId}이(가) PREPARE_FOR_SUBMISSION 상태로 복귀됨.`,
|
|
1302
|
-
` 메타데이터/빌드 수정 후 appstore_submit_for_review로 재제출 가능.`,
|
|
1303
|
-
].join('\n'),
|
|
1304
|
-
}],
|
|
1305
|
-
};
|
|
1306
|
-
});
|
|
1307
|
-
// --- Play Store 심사 제출 / release status 변경 ---
|
|
1308
|
-
server.tool('playstore_submit_release', [
|
|
1309
|
-
'Google Play 트랙의 release status를 변경 — 일반적으로 draft → completed로 바꿔 검토/배포 큐에 진입시킬 때 사용.',
|
|
1310
|
-
'⚠️ status="completed"는 비가역에 가까움 (전체 출시 또는 Google 검토 시작). halted로 일시 중단은 가능하나 한 번 라이브된 release는 되돌리기 어려움.',
|
|
1311
|
-
'status 옵션: draft(검토 미시작) / inProgress(단계 출시) / completed(전체 출시) / halted(중단).',
|
|
1312
|
-
'playstore_check_submission_risks로 사전 점검 권장.',
|
|
1313
|
-
].join(' '), {
|
|
1314
|
-
packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
|
|
1315
|
-
track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
|
|
1316
|
-
versionCode: z.string().describe('대상 versionCode (문자열)'),
|
|
1317
|
-
status: z.enum(['draft', 'inProgress', 'completed', 'halted']).optional().describe('새 status (기본: completed)'),
|
|
1318
|
-
}, async ({ packageName, track, versionCode, status }) => {
|
|
1319
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
1320
|
-
const result = await playstore.submitRelease(auth, packageName, track, versionCode, status);
|
|
1321
|
-
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode}: ${result.previousStatus} → ${result.newStatus}\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1322
|
-
});
|
|
1323
|
-
// --- Play Store 트랙 간 promote (internal → production 등) ---
|
|
1324
|
-
server.tool('playstore_promote_release', [
|
|
1325
|
-
'Google Play 트랙 간 release promote — fromTrack(예: internal)의 versionCode를 toTrack(예: production)에 새 release로 추가.',
|
|
1326
|
-
'단일 edit session에서 source 조회 → target 업데이트 → commit 까지 한 번에 처리.',
|
|
1327
|
-
'source의 releaseNotes를 자동 복사 (copyReleaseNotes=false 또는 releaseNotes로 덮어쓰기 가능).',
|
|
1328
|
-
'status="completed" + production 으로 전체 출시, status="inProgress" + userFraction 으로 단계 출시(예: 0.1 = 10%).',
|
|
1329
|
-
'target에 같은 versionCode가 이미 있으면 해당 항목을 교체. status="completed"면 target 활성 release를 통째로 새 것으로 대체.',
|
|
1330
|
-
'⚠️ status="completed"는 비가역에 가까움. playstore_check_submission_risks로 사전 점검 권장.',
|
|
1331
|
-
].join(' '), {
|
|
1332
|
-
packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
|
|
1333
|
-
fromTrack: z.enum(['production', 'beta', 'alpha', 'internal']).describe('출처 트랙 (예: internal)'),
|
|
1334
|
-
toTrack: z.enum(['production', 'beta', 'alpha', 'internal']).describe('대상 트랙 (예: production)'),
|
|
1335
|
-
versionCode: z.string().describe('promote할 versionCode (문자열)'),
|
|
1336
|
-
status: z.enum(['completed', 'draft', 'inProgress', 'halted']).optional().describe('대상 트랙에서의 status (기본 completed)'),
|
|
1337
|
-
userFraction: z.number().min(0).max(1).optional().describe('status="inProgress"일 때 단계 출시 비율 (0~1, 예: 0.1)'),
|
|
1338
|
-
releaseName: z.string().optional().describe('release 이름 (미지정 시 source 이름 그대로)'),
|
|
1339
|
-
copyReleaseNotes: z.boolean().optional().describe('source releaseNotes 복사 여부 (기본 true)'),
|
|
1340
|
-
releaseNotes: z.array(z.object({
|
|
1341
|
-
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
1342
|
-
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
1343
|
-
})).optional().describe('대상 트랙용 릴리스 노트 (미지정 + copyReleaseNotes=true면 source 그대로)'),
|
|
1344
|
-
}, async ({ packageName, fromTrack, toTrack, versionCode, status, userFraction, releaseName, copyReleaseNotes, releaseNotes }) => {
|
|
1345
|
-
const auth = requirePlayStoreAuth(packageName);
|
|
1346
|
-
const result = await playstore.promoteRelease(auth, packageName, fromTrack, toTrack, versionCode, {
|
|
1347
|
-
status,
|
|
1348
|
-
userFraction,
|
|
1349
|
-
releaseName,
|
|
1350
|
-
copyReleaseNotes,
|
|
1351
|
-
releaseNotes,
|
|
1352
|
-
});
|
|
1353
|
-
const summary = `✅ ${packageName} ${fromTrack} → ${toTrack} v${versionCode} (status: ${result.newStatus}${result.userFraction != null ? `, userFraction: ${result.userFraction}` : ''})`;
|
|
1354
|
-
return { content: [{ type: 'text', text: `${summary}\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1355
|
-
});
|
|
1356
|
-
// --- 인증 상태 ---
|
|
1357
|
-
server.tool('mimi_seed_auth_start', [
|
|
1358
|
-
'Google OAuth 로그인 링크를 발급하고 백그라운드 콜백 서버를 시작.',
|
|
1359
|
-
'응답에 포함된 URL을 브라우저에서 열고 승인하면 localhost:9876으로 자동 콜백 → 토큰이 ~/.mimi-seed/tokens.json에 저장됨.',
|
|
1360
|
-
'이후 playstore_*, firebase_*, admob_* 등 다른 MCP 도구 바로 호출 가능.',
|
|
1361
|
-
'토큰 만료(invalid_rapt) / 재인증 필요 시 사용. 10분 내 완료해야 함.',
|
|
1362
|
-
].join(' '), {}, async () => {
|
|
1363
|
-
const { url, wait } = startAuth(MIMI_SEED_CLIENT_ID, MIMI_SEED_CLIENT_SECRET);
|
|
1364
|
-
// fire-and-forget — 토큰은 콜백 서버가 자동 저장
|
|
1365
|
-
wait.then(() => { }, (err) => { console.error('[mimi-seed auth]', err.message); });
|
|
1366
|
-
return {
|
|
1367
|
-
content: [{
|
|
1368
|
-
type: 'text',
|
|
1369
|
-
text: [
|
|
1370
|
-
'🔐 Google 로그인 링크 (10분 유효):',
|
|
1371
|
-
'',
|
|
1372
|
-
url,
|
|
1373
|
-
'',
|
|
1374
|
-
'이 URL을 브라우저에서 열고 Google 계정으로 승인해줘.',
|
|
1375
|
-
'완료되면 localhost:9876으로 자동 리다이렉트되고 토큰이 저장돼.',
|
|
1376
|
-
'이후 바로 다른 MCP 도구(playstore_*, firebase_* 등) 호출 가능.',
|
|
1377
|
-
].join('\n'),
|
|
1378
|
-
}],
|
|
1379
|
-
};
|
|
1380
|
-
});
|
|
1381
|
-
server.tool('mimi_seed_auth_status', 'Mimi Seed MCP 인증 상태 확인 (만료 시 refresh_token으로 자동 갱신 시도)', {}, async () => {
|
|
1382
|
-
const r = await ensureFreshAccessToken();
|
|
1383
|
-
switch (r.status) {
|
|
1384
|
-
case 'unauthenticated':
|
|
1385
|
-
return {
|
|
1386
|
-
content: [{
|
|
1387
|
-
type: 'text',
|
|
1388
|
-
text: `❌ [${r.error.code}] ${r.error.message}\n` +
|
|
1389
|
-
(r.error.hint ? `→ ${r.error.hint}\n\n` : '\n') +
|
|
1390
|
-
'터미널에서 실행:\n npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
|
|
1391
|
-
}],
|
|
1392
|
-
};
|
|
1393
|
-
case 'fresh': {
|
|
1394
|
-
const min = Math.round(r.msUntilExpiry / 60000);
|
|
1395
|
-
return { content: [{ type: 'text', text: `✅ 인증 유효 (${min}분 남음).` }] };
|
|
1396
|
-
}
|
|
1397
|
-
case 'refreshed': {
|
|
1398
|
-
const min = Math.round(r.msUntilExpiry / 60000);
|
|
1399
|
-
return {
|
|
1400
|
-
content: [{
|
|
1401
|
-
type: 'text',
|
|
1402
|
-
text: `✅ 토큰 만료 → refresh_token으로 자동 갱신 완료 (${min}분 남음).`,
|
|
1403
|
-
}],
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
case 'expired_refresh_failed':
|
|
1407
|
-
return {
|
|
1408
|
-
content: [{
|
|
1409
|
-
type: 'text',
|
|
1410
|
-
text: `⚠️ 토큰 만료 + 자동 갱신 실패\n` +
|
|
1411
|
-
` 코드: ${r.error.code}\n` +
|
|
1412
|
-
` ${r.error.message}\n` +
|
|
1413
|
-
(r.error.hint ? ` → ${r.error.hint}\n` : '') +
|
|
1414
|
-
'\n터미널에서 재로그인:\n npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
|
|
1415
|
-
}],
|
|
1416
|
-
};
|
|
1417
|
-
}
|
|
1418
|
-
});
|
|
1419
|
-
// ══════════════════════════════════════════════════
|
|
1420
|
-
// 제출 위험 점검 (Submission Risk Check)
|
|
1421
|
-
// ══════════════════════════════════════════════════
|
|
1422
|
-
server.tool('playstore_check_submission_risks', [
|
|
1423
|
-
'Google Play 제출 전 위험 요소를 자동으로 점검합니다.',
|
|
1424
|
-
'블로커(반드시 수정)와 경고(권장 수정)로 분류하여 반환합니다.',
|
|
1425
|
-
'점검 항목: 리스팅 완성도(제목/설명/짧은설명), 스크린샷 수, 아이콘, 빌드 존재 여부, 연락처.',
|
|
1426
|
-
].join(' '), {
|
|
1427
|
-
packageName: z.string().describe('Android 패키지명 (예: com.example.myapp)'),
|
|
1428
|
-
language: z.string().optional().describe('언어 코드 (기본: ko-KR)'),
|
|
1429
|
-
}, async ({ packageName, language }) => {
|
|
1430
|
-
const auth = requireAuth();
|
|
1431
|
-
const risks = await checkPlayStoreRisks(auth, packageName, language ?? 'ko-KR');
|
|
1432
|
-
const text = formatRisks(risks, 'Google Play');
|
|
1433
|
-
return { content: [{ type: 'text', text }] };
|
|
1434
|
-
});
|
|
1435
|
-
server.tool('appstore_check_submission_risks', [
|
|
1436
|
-
'App Store 제출 전 위험 요소를 자동으로 점검합니다.',
|
|
1437
|
-
'블로커(반드시 수정)와 경고(권장 수정)로 분류하여 반환합니다.',
|
|
1438
|
-
'점검 항목: 메타데이터 완성도(설명/What\'s New/키워드), 스크린샷, TestFlight 빌드, 개인정보처리방침 URL.',
|
|
1439
|
-
'App Store 인증이 필요합니다 (appstore_* 도구 사용 가능 상태여야 함).',
|
|
1440
|
-
].join(' '), {
|
|
1441
|
-
appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과의 id 필드)'),
|
|
1442
|
-
}, async ({ appId }) => {
|
|
1443
|
-
const risks = await checkAppStoreRisks(appId);
|
|
1444
|
-
const text = formatRisks(risks, 'App Store');
|
|
1445
|
-
return { content: [{ type: 'text', text }] };
|
|
1446
|
-
});
|
|
1447
|
-
// ══════════════════════════════════════════════════
|
|
1448
|
-
// 스크린샷 해상도 검증 (Screenshot Validation)
|
|
1449
|
-
// ══════════════════════════════════════════════════
|
|
1450
|
-
server.tool('screenshot_validate', [
|
|
1451
|
-
'로컬 스크린샷 파일의 해상도·파일크기·종횡비를 App Store / Play Store 규격과 비교 검증합니다.',
|
|
1452
|
-
'PNG/JPEG 파일 경로를 배열로 받아 각 파일의 통과 여부와 문제점을 반환합니다.',
|
|
1453
|
-
'platform: "ios" 또는 "android" (기본: ios)',
|
|
1454
|
-
'iOS displayType 예시: APP_IPHONE_69, APP_IPHONE_67, APP_IPHONE_65, APP_IPAD_PRO_3GEN_129',
|
|
1455
|
-
'Android imageType 예시: phoneScreenshots, sevenInchScreenshots, tenInchScreenshots, featureGraphic',
|
|
1456
|
-
'업로드 전 미리 검사하여 리젝 방지에 활용하세요.',
|
|
1457
|
-
].join(' '), {
|
|
1458
|
-
filePaths: z.array(z.string()).describe('검증할 이미지 파일 절대경로 배열'),
|
|
1459
|
-
platform: z.enum(['ios', 'android']).optional().describe('플랫폼 (기본: ios)'),
|
|
1460
|
-
displayType: z.string().optional().describe('iOS 디스플레이 타입 (예: APP_IPHONE_67)'),
|
|
1461
|
-
imageType: z.string().optional().describe('Android 이미지 타입 (예: phoneScreenshots)'),
|
|
1462
|
-
}, async ({ filePaths, platform, displayType, imageType }) => {
|
|
1463
|
-
const plat = platform ?? 'ios';
|
|
1464
|
-
if (plat === 'ios') {
|
|
1465
|
-
const results = validateAppStoreScreenshots(filePaths, displayType);
|
|
1466
|
-
const text = formatValidationResults(results, 'App Store');
|
|
1467
|
-
return { content: [{ type: 'text', text }] };
|
|
1468
|
-
}
|
|
1469
|
-
else {
|
|
1470
|
-
const results = validatePlayStoreScreenshots(filePaths, imageType ?? 'phoneScreenshots');
|
|
1471
|
-
const text = formatValidationResults(results, 'Google Play');
|
|
1472
|
-
return { content: [{ type: 'text', text }] };
|
|
1473
|
-
}
|
|
1474
|
-
});
|
|
1475
|
-
// ══════════════════════════════════════════════════
|
|
1476
|
-
// AI 릴리즈 노트 생성 (ANTHROPIC_API_KEY 필요)
|
|
1477
|
-
// ══════════════════════════════════════════════════
|
|
1478
|
-
server.tool('generate_release_notes_from_commits', [
|
|
1479
|
-
'git 커밋 내역을 받아 Claude AI로 앱 스토어용 릴리즈 노트를 생성합니다.',
|
|
1480
|
-
'3가지 톤(간결/상세/마케팅)과 다국어 버전을 동시에 생성합니다.',
|
|
1481
|
-
'ANTHROPIC_API_KEY 환경변수가 필요합니다.',
|
|
1482
|
-
'commits: [{ message, author?, date? }] 형태로 전달하세요.',
|
|
1483
|
-
'locales: 다국어 생성할 로케일 배열 (예: ["ko", "en-US", "ja"])',
|
|
1484
|
-
'생성 후 playstore_update_release_notes 또는 appstore_update_whats_new로 적용하세요.',
|
|
1485
|
-
].join(' '), {
|
|
1486
|
-
commits: z.array(z.object({
|
|
1487
|
-
message: z.string(),
|
|
1488
|
-
hash: z.string().optional(),
|
|
1489
|
-
author: z.string().optional(),
|
|
1490
|
-
date: z.string().optional(),
|
|
1491
|
-
})).describe('git 커밋 배열'),
|
|
1492
|
-
appName: z.string().optional().describe('앱 이름 (프롬프트 맥락용)'),
|
|
1493
|
-
locales: z.array(z.string()).optional().describe('다국어 로케일 목록 (예: ["ko", "en-US", "ja"])'),
|
|
1494
|
-
}, async ({ commits, appName, locales }) => {
|
|
1495
|
-
const result = await generateReleaseNotesFromCommits(commits, {
|
|
1496
|
-
appName,
|
|
1497
|
-
locales: locales ?? [],
|
|
1498
|
-
});
|
|
1499
|
-
const text = formatGeneratedNotes(result);
|
|
1500
|
-
return { content: [{ type: 'text', text }] };
|
|
1501
|
-
});
|
|
1502
|
-
// ══════════════════════════════════════════════════
|
|
1503
|
-
// AI 리뷰 답변 생성 (ANTHROPIC_API_KEY 필요)
|
|
1504
|
-
// ══════════════════════════════════════════════════
|
|
1505
|
-
server.tool('generate_review_reply', [
|
|
1506
|
-
'Play Store / App Store 리뷰에 대한 AI 답변 초안을 생성합니다.',
|
|
1507
|
-
'ANTHROPIC_API_KEY 환경변수가 필요합니다.',
|
|
1508
|
-
'tone: friendly(친근) / professional(정중) / empathetic(공감) / brief(간결) — 기본: friendly',
|
|
1509
|
-
'language: ko / en / ja / zh 등 — 기본: ko',
|
|
1510
|
-
'⚠ 생성된 답변은 초안입니다. 게시 전 반드시 검토하세요.',
|
|
1511
|
-
'답변 게시는 playstore_reply_to_review 도구를 사용하세요.',
|
|
1512
|
-
].join(' '), {
|
|
1513
|
-
reviewText: z.string().describe('리뷰 원문'),
|
|
1514
|
-
rating: z.number().min(1).max(5).optional().describe('별점 (1~5)'),
|
|
1515
|
-
appName: z.string().optional().describe('앱 이름'),
|
|
1516
|
-
tone: z.enum(['friendly', 'professional', 'empathetic', 'brief']).optional().describe('답변 톤'),
|
|
1517
|
-
language: z.string().optional().describe('답변 언어 코드 (기본: ko)'),
|
|
1518
|
-
developerName: z.string().optional().describe('개발자/팀 이름'),
|
|
1519
|
-
}, async ({ reviewText, rating, appName, tone, language, developerName }) => {
|
|
1520
|
-
const result = await generateReviewReply({
|
|
1521
|
-
reviewText,
|
|
1522
|
-
rating,
|
|
1523
|
-
appName,
|
|
1524
|
-
tone: tone ?? 'friendly',
|
|
1525
|
-
language: language ?? 'ko',
|
|
1526
|
-
developerName,
|
|
1527
|
-
});
|
|
1528
|
-
const text = formatReviewReply(result);
|
|
1529
|
-
return { content: [{ type: 'text', text }] };
|
|
1530
|
-
});
|
|
1531
|
-
// ══════════════════════════════════════════════════
|
|
1532
|
-
// BigQuery
|
|
1533
|
-
// ══════════════════════════════════════════════════
|
|
1534
|
-
server.tool('bigquery_run_query', 'BigQuery SQL 쿼리 실행 (SELECT). GA4 analytics_* 테이블 분석에 사용.', {
|
|
1535
|
-
projectId: z.string().describe('GCP 프로젝트 ID (예: ads-coffee)'),
|
|
1536
|
-
query: z.string().describe('실행할 StandardSQL 쿼리'),
|
|
1537
|
-
maxResults: z.number().optional().describe('최대 행 수 (기본 1000)'),
|
|
1538
|
-
}, async ({ projectId, query, maxResults }) => {
|
|
1539
|
-
const auth = requireAuth();
|
|
1540
|
-
const result = await bigquery.runQuery(auth, projectId, query, maxResults ?? 1000);
|
|
1541
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1542
|
-
});
|
|
1543
|
-
server.tool('bigquery_list_datasets', 'BigQuery 프로젝트의 데이터셋 목록 조회', {
|
|
1544
|
-
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
1545
|
-
}, async ({ projectId }) => {
|
|
1546
|
-
const auth = requireAuth();
|
|
1547
|
-
const datasets = await bigquery.listDatasets(auth, projectId);
|
|
1548
|
-
return { content: [{ type: 'text', text: JSON.stringify(datasets, null, 2) }] };
|
|
1549
|
-
});
|
|
1550
|
-
server.tool('bigquery_list_tables', 'BigQuery 데이터셋의 테이블 목록 조회', {
|
|
1551
|
-
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
1552
|
-
datasetId: z.string().describe('데이터셋 ID (예: analytics_530080532)'),
|
|
1553
|
-
}, async ({ projectId, datasetId }) => {
|
|
1554
|
-
const auth = requireAuth();
|
|
1555
|
-
const tables = await bigquery.listTables(auth, projectId, datasetId);
|
|
1556
|
-
return { content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }] };
|
|
1557
|
-
});
|
|
1558
|
-
server.tool('bigquery_get_table_schema', 'BigQuery 테이블 스키마(컬럼 정보) 조회', {
|
|
1559
|
-
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
1560
|
-
datasetId: z.string().describe('데이터셋 ID'),
|
|
1561
|
-
tableId: z.string().describe('테이블 ID'),
|
|
1562
|
-
}, async ({ projectId, datasetId, tableId }) => {
|
|
1563
|
-
const auth = requireAuth();
|
|
1564
|
-
const schema = await bigquery.getTableSchema(auth, projectId, datasetId, tableId);
|
|
1565
|
-
return { content: [{ type: 'text', text: JSON.stringify(schema, null, 2) }] };
|
|
1566
|
-
});
|
|
1567
|
-
// ══════════════════════════════════════════════════
|
|
1568
|
-
// Start
|
|
1569
|
-
// ══════════════════════════════════════════════════
|
|
19
|
+
registerFirebaseTools(server);
|
|
20
|
+
registerAdmobTools(server);
|
|
21
|
+
registerPlaystoreTools(server);
|
|
22
|
+
registerIamTools(server);
|
|
23
|
+
registerAppstoreTools(server);
|
|
24
|
+
registerChecksTools(server);
|
|
25
|
+
registerAiTools(server);
|
|
26
|
+
registerBigqueryTools(server);
|
|
27
|
+
registerAuthTools(server);
|
|
28
|
+
registerPrompts(server);
|
|
29
|
+
registerResources(server);
|
|
1570
30
|
// `npx -y @yoonion/mimi-seed-mcp <subcommand>` 처리.
|
|
1571
31
|
// npx는 스코프 패키지의 basename(`mimi-seed-mcp`)을 매치해 이 bin을 실행하므로,
|
|
1572
32
|
// 추가 인자(`mimi-seed-auth` 등)는 여기 argv로 흘러들어온다. 이전엔 MCP 서버가
|