@yoonion/mimi-seed-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/admob/tools.d.ts +39 -0
- package/dist/admob/tools.js +122 -0
- package/dist/appstore/auth.d.ts +9 -0
- package/dist/appstore/auth.js +53 -0
- package/dist/appstore/setup-cli.d.ts +2 -0
- package/dist/appstore/setup-cli.js +51 -0
- package/dist/appstore/tools.d.ts +7 -0
- package/dist/appstore/tools.js +119 -0
- package/dist/auth/cli.d.ts +2 -0
- package/dist/auth/cli.js +40 -0
- package/dist/auth/constants.d.ts +2 -0
- package/dist/auth/constants.js +11 -0
- package/dist/auth/google-auth.d.ts +22 -0
- package/dist/auth/google-auth.js +159 -0
- package/dist/firebase/tools.d.ts +60 -0
- package/dist/firebase/tools.js +183 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +447 -0
- package/dist/playstore/tools.d.ts +53 -0
- package/dist/playstore/tools.js +197 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { getAuthenticatedClient, getStoredTokens } from './auth/google-auth.js';
|
|
6
|
+
import * as firebase from './firebase/tools.js';
|
|
7
|
+
import * as admob from './admob/tools.js';
|
|
8
|
+
import * as playstore from './playstore/tools.js';
|
|
9
|
+
import * as appstore from './appstore/tools.js';
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: 'mimi-seed',
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
});
|
|
14
|
+
// ─── Helper ───
|
|
15
|
+
function requireAuth() {
|
|
16
|
+
const client = getAuthenticatedClient();
|
|
17
|
+
if (!client) {
|
|
18
|
+
throw new Error([
|
|
19
|
+
'❌ Google 계정이 연결되지 않았어.',
|
|
20
|
+
'',
|
|
21
|
+
'터미널에서 이것만 실행하면 돼:',
|
|
22
|
+
'',
|
|
23
|
+
' npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
|
|
24
|
+
'',
|
|
25
|
+
'브라우저가 열리면 Google 로그인 → 끝.',
|
|
26
|
+
'그 다음에 다시 물어봐줘.',
|
|
27
|
+
].join('\n'));
|
|
28
|
+
}
|
|
29
|
+
return client;
|
|
30
|
+
}
|
|
31
|
+
// ══════════════════════════════════════════════════
|
|
32
|
+
// Firebase Tools
|
|
33
|
+
// ══════════════════════════════════════════════════
|
|
34
|
+
// --- 프로젝트 ---
|
|
35
|
+
server.tool('firebase_list_projects', '내 Firebase 프로젝트 목록 조회', {}, async () => {
|
|
36
|
+
const auth = requireAuth();
|
|
37
|
+
const projects = await firebase.listProjects(auth);
|
|
38
|
+
return { content: [{ type: 'text', text: JSON.stringify(projects, null, 2) }] };
|
|
39
|
+
});
|
|
40
|
+
server.tool('firebase_get_project', 'Firebase 프로젝트 상세 정보 조회', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
41
|
+
const auth = requireAuth();
|
|
42
|
+
const project = await firebase.getProject(auth, projectId);
|
|
43
|
+
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
44
|
+
});
|
|
45
|
+
// --- Android 앱 ---
|
|
46
|
+
server.tool('firebase_list_android_apps', 'Firebase 프로젝트의 Android 앱 목록', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
47
|
+
const auth = requireAuth();
|
|
48
|
+
const apps = await firebase.listAndroidApps(auth, projectId);
|
|
49
|
+
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
50
|
+
});
|
|
51
|
+
server.tool('firebase_create_android_app', 'Firebase에 새 Android 앱 등록', {
|
|
52
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
53
|
+
packageName: z.string().describe('Android 패키지명 (예: com.example.myapp)'),
|
|
54
|
+
displayName: z.string().describe('앱 표시 이름'),
|
|
55
|
+
}, async ({ projectId, packageName, displayName }) => {
|
|
56
|
+
const auth = requireAuth();
|
|
57
|
+
const result = await firebase.createAndroidApp(auth, projectId, packageName, displayName);
|
|
58
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
59
|
+
});
|
|
60
|
+
server.tool('firebase_get_android_config', 'google-services.json 다운로드', {
|
|
61
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
62
|
+
appId: z.string().describe('Firebase 앱 ID'),
|
|
63
|
+
}, async ({ projectId, appId }) => {
|
|
64
|
+
const auth = requireAuth();
|
|
65
|
+
const config = await firebase.getAndroidConfig(auth, projectId, appId);
|
|
66
|
+
return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] };
|
|
67
|
+
});
|
|
68
|
+
server.tool('firebase_delete_android_app', 'Firebase Android 앱 삭제', {
|
|
69
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
70
|
+
appId: z.string().describe('삭제할 Firebase 앱 ID'),
|
|
71
|
+
}, async ({ projectId, appId }) => {
|
|
72
|
+
const auth = requireAuth();
|
|
73
|
+
const result = await firebase.deleteAndroidApp(auth, projectId, appId);
|
|
74
|
+
return { content: [{ type: 'text', text: `삭제 완료: ${JSON.stringify(result)}` }] };
|
|
75
|
+
});
|
|
76
|
+
// --- iOS 앱 ---
|
|
77
|
+
server.tool('firebase_list_ios_apps', 'Firebase 프로젝트의 iOS 앱 목록', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
78
|
+
const auth = requireAuth();
|
|
79
|
+
const apps = await firebase.listIosApps(auth, projectId);
|
|
80
|
+
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
81
|
+
});
|
|
82
|
+
server.tool('firebase_create_ios_app', 'Firebase에 새 iOS 앱 등록', {
|
|
83
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
84
|
+
bundleId: z.string().describe('iOS Bundle ID (예: com.example.myapp)'),
|
|
85
|
+
displayName: z.string().describe('앱 표시 이름'),
|
|
86
|
+
}, async ({ projectId, bundleId, displayName }) => {
|
|
87
|
+
const auth = requireAuth();
|
|
88
|
+
const result = await firebase.createIosApp(auth, projectId, bundleId, displayName);
|
|
89
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
90
|
+
});
|
|
91
|
+
server.tool('firebase_get_ios_config', 'GoogleService-Info.plist 다운로드', {
|
|
92
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
93
|
+
appId: z.string().describe('Firebase 앱 ID'),
|
|
94
|
+
}, async ({ projectId, appId }) => {
|
|
95
|
+
const auth = requireAuth();
|
|
96
|
+
const config = await firebase.getIosConfig(auth, projectId, appId);
|
|
97
|
+
return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] };
|
|
98
|
+
});
|
|
99
|
+
server.tool('firebase_delete_ios_app', 'Firebase iOS 앱 삭제', {
|
|
100
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
101
|
+
appId: z.string().describe('삭제할 Firebase 앱 ID'),
|
|
102
|
+
}, async ({ projectId, appId }) => {
|
|
103
|
+
const auth = requireAuth();
|
|
104
|
+
const result = await firebase.deleteIosApp(auth, projectId, appId);
|
|
105
|
+
return { content: [{ type: 'text', text: `삭제 완료: ${JSON.stringify(result)}` }] };
|
|
106
|
+
});
|
|
107
|
+
// --- Web 앱 ---
|
|
108
|
+
server.tool('firebase_list_web_apps', 'Firebase 프로젝트의 Web 앱 목록', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
109
|
+
const auth = requireAuth();
|
|
110
|
+
const apps = await firebase.listWebApps(auth, projectId);
|
|
111
|
+
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
112
|
+
});
|
|
113
|
+
server.tool('firebase_create_web_app', 'Firebase에 새 Web 앱 등록', {
|
|
114
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
115
|
+
displayName: z.string().describe('앱 표시 이름'),
|
|
116
|
+
}, async ({ projectId, displayName }) => {
|
|
117
|
+
const auth = requireAuth();
|
|
118
|
+
const result = await firebase.createWebApp(auth, projectId, displayName);
|
|
119
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
120
|
+
});
|
|
121
|
+
server.tool('firebase_get_web_config', 'Firebase Web 설정 (firebaseConfig 객체) 조회', {
|
|
122
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
123
|
+
appId: z.string().describe('Firebase 앱 ID'),
|
|
124
|
+
}, async ({ projectId, appId }) => {
|
|
125
|
+
const auth = requireAuth();
|
|
126
|
+
const config = await firebase.getWebConfig(auth, projectId, appId);
|
|
127
|
+
return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] };
|
|
128
|
+
});
|
|
129
|
+
server.tool('firebase_delete_web_app', 'Firebase Web 앱 삭제', {
|
|
130
|
+
projectId: z.string().describe('Firebase 프로젝트 ID'),
|
|
131
|
+
appId: z.string().describe('삭제할 Firebase 앱 ID'),
|
|
132
|
+
}, async ({ projectId, appId }) => {
|
|
133
|
+
const auth = requireAuth();
|
|
134
|
+
const result = await firebase.deleteWebApp(auth, projectId, appId);
|
|
135
|
+
return { content: [{ type: 'text', text: `삭제 완료: ${JSON.stringify(result)}` }] };
|
|
136
|
+
});
|
|
137
|
+
// --- 서비스 관리 ---
|
|
138
|
+
server.tool('firebase_enable_service', 'GCP 서비스 활성화 (예: firestore.googleapis.com)', {
|
|
139
|
+
projectId: z.string().describe('프로젝트 ID'),
|
|
140
|
+
serviceId: z.string().describe('서비스 ID (예: firestore.googleapis.com)'),
|
|
141
|
+
}, async ({ projectId, serviceId }) => {
|
|
142
|
+
const auth = requireAuth();
|
|
143
|
+
const result = await firebase.enableService(auth, projectId, serviceId);
|
|
144
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
145
|
+
});
|
|
146
|
+
server.tool('firebase_enable_common_services', 'Firebase 기본 서비스 일괄 활성화 (Firestore, Auth, Storage, FCM 등)', { projectId: z.string().describe('프로젝트 ID') }, async ({ projectId }) => {
|
|
147
|
+
const auth = requireAuth();
|
|
148
|
+
const results = await firebase.enableCommonServices(auth, projectId);
|
|
149
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
150
|
+
});
|
|
151
|
+
server.tool('firebase_list_enabled_services', '프로젝트에서 활성화된 GCP 서비스 목록', { projectId: z.string().describe('프로젝트 ID') }, async ({ projectId }) => {
|
|
152
|
+
const auth = requireAuth();
|
|
153
|
+
const services = await firebase.listEnabledServices(auth, projectId);
|
|
154
|
+
return { content: [{ type: 'text', text: JSON.stringify(services, null, 2) }] };
|
|
155
|
+
});
|
|
156
|
+
// ══════════════════════════════════════════════════
|
|
157
|
+
// AdMob Tools
|
|
158
|
+
// ══════════════════════════════════════════════════
|
|
159
|
+
server.tool('admob_list_accounts', 'AdMob 계정 목록 조회', {}, async () => {
|
|
160
|
+
const auth = requireAuth();
|
|
161
|
+
const accounts = await admob.listAccounts(auth);
|
|
162
|
+
return { content: [{ type: 'text', text: JSON.stringify(accounts, null, 2) }] };
|
|
163
|
+
});
|
|
164
|
+
server.tool('admob_list_apps', 'AdMob에 등록된 앱 목록', { accountId: z.string().describe('AdMob 계정 ID (예: accounts/pub-XXXX)') }, async ({ accountId }) => {
|
|
165
|
+
const auth = requireAuth();
|
|
166
|
+
const apps = await admob.listApps(auth, accountId);
|
|
167
|
+
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
168
|
+
});
|
|
169
|
+
server.tool('admob_list_ad_units', 'AdMob 광고 단위 목록', { accountId: z.string().describe('AdMob 계정 ID') }, async ({ accountId }) => {
|
|
170
|
+
const auth = requireAuth();
|
|
171
|
+
const units = await admob.listAdUnits(auth, accountId);
|
|
172
|
+
return { content: [{ type: 'text', text: JSON.stringify(units, null, 2) }] };
|
|
173
|
+
});
|
|
174
|
+
server.tool('admob_get_today_earnings', '오늘 AdMob 수익 요약', { accountId: z.string().describe('AdMob 계정 ID') }, async ({ accountId }) => {
|
|
175
|
+
const auth = requireAuth();
|
|
176
|
+
const report = await admob.getTodayEarnings(auth, accountId);
|
|
177
|
+
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
178
|
+
});
|
|
179
|
+
server.tool('admob_get_report', 'AdMob 수익 리포트 (기간 지정)', {
|
|
180
|
+
accountId: z.string().describe('AdMob 계정 ID'),
|
|
181
|
+
startYear: z.number().describe('시작 연도'),
|
|
182
|
+
startMonth: z.number().describe('시작 월 (1-12)'),
|
|
183
|
+
startDay: z.number().describe('시작 일'),
|
|
184
|
+
endYear: z.number().describe('종료 연도'),
|
|
185
|
+
endMonth: z.number().describe('종료 월 (1-12)'),
|
|
186
|
+
endDay: z.number().describe('종료 일'),
|
|
187
|
+
}, async ({ accountId, startYear, startMonth, startDay, endYear, endMonth, endDay }) => {
|
|
188
|
+
const auth = requireAuth();
|
|
189
|
+
const report = await admob.getNetworkReport(auth, accountId, { year: startYear, month: startMonth, day: startDay }, { year: endYear, month: endMonth, day: endDay });
|
|
190
|
+
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
191
|
+
});
|
|
192
|
+
server.tool('admob_create_app', 'AdMob에 새 앱 등록 (v1beta — Limited Access)', {
|
|
193
|
+
accountId: z.string().describe('AdMob 계정 ID'),
|
|
194
|
+
platform: z.enum(['ANDROID', 'IOS']).describe('플랫폼'),
|
|
195
|
+
displayName: z.string().describe('앱 이름'),
|
|
196
|
+
}, async ({ accountId, platform, displayName }) => {
|
|
197
|
+
const auth = requireAuth();
|
|
198
|
+
try {
|
|
199
|
+
const result = await admob.createApp(auth, accountId, platform, displayName);
|
|
200
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
if (err.code === 403) {
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: [
|
|
208
|
+
'❌ AdMob 앱 생성 API 접근 불가 (403).',
|
|
209
|
+
'',
|
|
210
|
+
'AdMob v1beta 쓰기 API는 Google Account Manager 승인이 필요합니다.',
|
|
211
|
+
'대신 AdMob 콘솔에서 수동 등록하세요:',
|
|
212
|
+
' https://admob.google.com/home',
|
|
213
|
+
'',
|
|
214
|
+
'등록 후 admob_list_apps로 확인할 수 있습니다.',
|
|
215
|
+
].join('\n'),
|
|
216
|
+
}],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
server.tool('admob_create_ad_unit', 'AdMob 광고 단위 생성 (v1beta — Limited Access)', {
|
|
223
|
+
accountId: z.string().describe('AdMob 계정 ID'),
|
|
224
|
+
appId: z.string().describe('AdMob 앱 ID'),
|
|
225
|
+
displayName: z.string().describe('광고 단위 이름'),
|
|
226
|
+
adFormat: z.enum(['BANNER', 'INTERSTITIAL', 'REWARDED', 'REWARDED_INTERSTITIAL', 'APP_OPEN', 'NATIVE']).describe('광고 형식'),
|
|
227
|
+
}, async ({ accountId, appId, displayName, adFormat }) => {
|
|
228
|
+
const auth = requireAuth();
|
|
229
|
+
try {
|
|
230
|
+
const result = await admob.createAdUnit(auth, accountId, appId, displayName, adFormat);
|
|
231
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err.code === 403) {
|
|
235
|
+
return {
|
|
236
|
+
content: [{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: [
|
|
239
|
+
'❌ 광고 단위 생성 API 접근 불가 (403).',
|
|
240
|
+
'',
|
|
241
|
+
'AdMob 콘솔에서 수동 생성하세요:',
|
|
242
|
+
' https://admob.google.com/home',
|
|
243
|
+
].join('\n'),
|
|
244
|
+
}],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
// ══════════════════════════════════════════════════
|
|
251
|
+
// Google Play Store Tools
|
|
252
|
+
// ══════════════════════════════════════════════════
|
|
253
|
+
server.tool('playstore_get_app', 'Google Play 앱 상세 정보 조회', { packageName: z.string().describe('패키지명 (예: com.findthem.app)') }, async ({ packageName }) => {
|
|
254
|
+
const auth = requireAuth();
|
|
255
|
+
const details = await playstore.getAppDetails(auth, packageName);
|
|
256
|
+
return { content: [{ type: 'text', text: JSON.stringify(details, null, 2) }] };
|
|
257
|
+
});
|
|
258
|
+
server.tool('playstore_get_listing', 'Google Play 스토어 리스팅 조회 (제목, 설명문 등)', {
|
|
259
|
+
packageName: z.string().describe('패키지명'),
|
|
260
|
+
language: z.string().default('ko-KR').describe('언어 코드 (기본: ko-KR)'),
|
|
261
|
+
}, async ({ packageName, language }) => {
|
|
262
|
+
const auth = requireAuth();
|
|
263
|
+
const listing = await playstore.getListing(auth, packageName, language);
|
|
264
|
+
return { content: [{ type: 'text', text: JSON.stringify(listing, null, 2) }] };
|
|
265
|
+
});
|
|
266
|
+
server.tool('playstore_update_listing', 'Google Play 스토어 리스팅 수정 (제목, 설명문 변경)', {
|
|
267
|
+
packageName: z.string().describe('패키지명'),
|
|
268
|
+
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
269
|
+
title: z.string().optional().describe('앱 제목 (30자 이내)'),
|
|
270
|
+
shortDescription: z.string().optional().describe('짧은 설명 (80자 이내)'),
|
|
271
|
+
fullDescription: z.string().optional().describe('전체 설명 (4000자 이내)'),
|
|
272
|
+
}, async ({ packageName, language, title, shortDescription, fullDescription }) => {
|
|
273
|
+
const auth = requireAuth();
|
|
274
|
+
const result = await playstore.updateListing(auth, packageName, language, {
|
|
275
|
+
title, shortDescription, fullDescription,
|
|
276
|
+
});
|
|
277
|
+
return { content: [{ type: 'text', text: `수정 완료:\n${JSON.stringify(result, null, 2)}` }] };
|
|
278
|
+
});
|
|
279
|
+
server.tool('playstore_list_tracks', 'Google Play 릴리스 트랙 현황 (프로덕션/베타/알파/내부)', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
280
|
+
const auth = requireAuth();
|
|
281
|
+
const tracks = await playstore.listTracks(auth, packageName);
|
|
282
|
+
return { content: [{ type: 'text', text: JSON.stringify(tracks, null, 2) }] };
|
|
283
|
+
});
|
|
284
|
+
server.tool('playstore_list_reviews', 'Google Play 리뷰 목록 조회', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
285
|
+
const auth = requireAuth();
|
|
286
|
+
const reviews = await playstore.listReviews(auth, packageName);
|
|
287
|
+
return { content: [{ type: 'text', text: JSON.stringify(reviews, null, 2) }] };
|
|
288
|
+
});
|
|
289
|
+
server.tool('playstore_reply_review', 'Google Play 리뷰에 답변', {
|
|
290
|
+
packageName: z.string().describe('패키지명'),
|
|
291
|
+
reviewId: z.string().describe('리뷰 ID'),
|
|
292
|
+
replyText: z.string().describe('답변 내용'),
|
|
293
|
+
}, async ({ packageName, reviewId, replyText }) => {
|
|
294
|
+
const auth = requireAuth();
|
|
295
|
+
const result = await playstore.replyToReview(auth, packageName, reviewId, replyText);
|
|
296
|
+
return { content: [{ type: 'text', text: `답변 완료:\n${JSON.stringify(result, null, 2)}` }] };
|
|
297
|
+
});
|
|
298
|
+
server.tool('playstore_list_inapp_products', 'Google Play 인앱 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
299
|
+
const auth = requireAuth();
|
|
300
|
+
const products = await playstore.listInAppProducts(auth, packageName);
|
|
301
|
+
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
|
|
302
|
+
});
|
|
303
|
+
server.tool('playstore_list_subscriptions', 'Google Play 구독 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
304
|
+
const auth = requireAuth();
|
|
305
|
+
const subs = await playstore.listSubscriptions(auth, packageName);
|
|
306
|
+
return { content: [{ type: 'text', text: JSON.stringify(subs, null, 2) }] };
|
|
307
|
+
});
|
|
308
|
+
server.tool('playstore_verify_service_account', [
|
|
309
|
+
"서비스 계정 JSON이 주어진 packageName에 대해 Play Developer API 호출 가능한지 + 'View financial data' 권한까지 있는지 검증.",
|
|
310
|
+
'onesub 같은 서버가 Play 영수증을 백그라운드로 검증하려면 OAuth 토큰 대신 서비스 계정 JSON이 필요 — 이 도구로 붙여넣기 전에 유효성 확인.',
|
|
311
|
+
'성공 시 clientEmail + projectId 반환. 실패 시 어느 단계(parse/auth/api)에서 왜 막혔는지 단계별로 안내.',
|
|
312
|
+
'(이 도구는 서비스 계정 자격증명만 사용 — 로그인한 사용자의 OAuth 토큰은 건드리지 않음)',
|
|
313
|
+
].join(' '), {
|
|
314
|
+
serviceAccountJson: z
|
|
315
|
+
.string()
|
|
316
|
+
.describe('서비스 계정 JSON 전체 내용 (문자열). Google Cloud Console → IAM & Admin → Service Accounts → Keys → Create new key → JSON으로 다운받은 파일의 내용'),
|
|
317
|
+
packageName: z
|
|
318
|
+
.string()
|
|
319
|
+
.describe('검증할 Android 앱의 패키지명 (예: com.findthem.app)'),
|
|
320
|
+
}, async ({ serviceAccountJson, packageName }) => {
|
|
321
|
+
const result = await playstore.verifyServiceAccountJson(serviceAccountJson, packageName);
|
|
322
|
+
if (result.ok) {
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: 'text',
|
|
327
|
+
text: [
|
|
328
|
+
'✓ 서비스 계정 유효 — Play Developer API 호출 가능',
|
|
329
|
+
'',
|
|
330
|
+
`**clientEmail**: \`${result.clientEmail}\``,
|
|
331
|
+
`**projectId**: \`${result.projectId}\``,
|
|
332
|
+
`**packageName**: \`${packageName}\``,
|
|
333
|
+
'',
|
|
334
|
+
'이제 이 JSON 내용을 onesub 서버의 `GOOGLE_SERVICE_ACCOUNT_KEY` 환경변수에 (한 줄로) 넣으면 됩니다. 예:',
|
|
335
|
+
'```bash',
|
|
336
|
+
'cat service-account.json | tr -d \'\\n\' | jq -c .',
|
|
337
|
+
'```',
|
|
338
|
+
].join('\n'),
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const lines = [
|
|
344
|
+
`✗ 검증 실패 (stage: **${result.stage}**${result.httpStatus ? `, HTTP ${result.httpStatus}` : ''})`,
|
|
345
|
+
'',
|
|
346
|
+
`${result.message}`,
|
|
347
|
+
'',
|
|
348
|
+
];
|
|
349
|
+
if (result.stage === 'parse') {
|
|
350
|
+
lines.push('원인: 붙여넣은 JSON 구조가 올바르지 않음.');
|
|
351
|
+
lines.push('확인: Google Cloud Console → Service Accounts → Keys → **Create new key → JSON** 흐름으로 받은 파일 맞나요?');
|
|
352
|
+
}
|
|
353
|
+
else if (result.stage === 'auth') {
|
|
354
|
+
lines.push('원인: Google이 자격증명 자체를 거부함 (private_key 손상 / 프로젝트 비활성 / 계정 삭제됨).');
|
|
355
|
+
lines.push('확인: 새 키를 다시 발급 (기존 키 회수 후).');
|
|
356
|
+
}
|
|
357
|
+
else if (result.stage === 'api') {
|
|
358
|
+
if (result.httpStatus === 401 || result.httpStatus === 403) {
|
|
359
|
+
lines.push('원인: 토큰은 받았지만 Play Console에서 이 서비스 계정에 권한 없음.');
|
|
360
|
+
lines.push('확인 순서:');
|
|
361
|
+
lines.push('1. Play Console → Users and permissions → 이 서비스 계정 이메일을 초대');
|
|
362
|
+
lines.push('2. App permissions에서 해당 패키지명 앱 선택');
|
|
363
|
+
lines.push('3. Account permissions에 **View financial data, orders, and cancellation survey responses** 체크');
|
|
364
|
+
lines.push('4. 권한 적용까지 **~5분 대기** 후 재시도 (너무 빨리 시도하면 계속 403)');
|
|
365
|
+
}
|
|
366
|
+
else if (result.httpStatus === 404) {
|
|
367
|
+
lines.push('원인: 패키지명이 이 Play Console 개발자 계정 소유가 아님.');
|
|
368
|
+
lines.push(`확인: packageName이 Play Console에 등록된 앱의 것과 정확히 일치하나요? ("\`${packageName}\`")`);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
lines.push('원인: Play Developer API 호출 중 예외. 네트워크 또는 Google 쪽 문제일 수 있음.');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
375
|
+
});
|
|
376
|
+
// ══════════════════════════════════════════════════
|
|
377
|
+
// App Store Connect Tools
|
|
378
|
+
// ══════════════════════════════════════════════════
|
|
379
|
+
server.tool('appstore_list_apps', 'App Store Connect 앱 목록 조회', {}, async () => {
|
|
380
|
+
const apps = await appstore.listApps();
|
|
381
|
+
return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
|
|
382
|
+
});
|
|
383
|
+
server.tool('appstore_get_app', 'App Store Connect 앱 상세 정보', { appId: z.string().describe('앱 ID (숫자)') }, async ({ appId }) => {
|
|
384
|
+
const app = await appstore.getApp(appId);
|
|
385
|
+
return { content: [{ type: 'text', text: JSON.stringify(app, null, 2) }] };
|
|
386
|
+
});
|
|
387
|
+
server.tool('appstore_list_versions', 'App Store 버전 목록 (심사 상태 포함)', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
388
|
+
const versions = await appstore.listVersions(appId);
|
|
389
|
+
return { content: [{ type: 'text', text: JSON.stringify(versions, null, 2) }] };
|
|
390
|
+
});
|
|
391
|
+
server.tool('appstore_get_metadata', 'App Store 버전 메타데이터 (설명문, 키워드, What\'s New)', { versionId: z.string().describe('버전 ID') }, async ({ versionId }) => {
|
|
392
|
+
const localizations = await appstore.getVersionLocalizations(versionId);
|
|
393
|
+
return { content: [{ type: 'text', text: JSON.stringify(localizations, null, 2) }] };
|
|
394
|
+
});
|
|
395
|
+
server.tool('appstore_list_builds', 'TestFlight 빌드 목록', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
396
|
+
const builds = await appstore.listBuilds(appId);
|
|
397
|
+
return { content: [{ type: 'text', text: JSON.stringify(builds, null, 2) }] };
|
|
398
|
+
});
|
|
399
|
+
server.tool('appstore_list_beta_groups', 'TestFlight 베타 그룹 목록', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
400
|
+
const groups = await appstore.listBetaGroups(appId);
|
|
401
|
+
return { content: [{ type: 'text', text: JSON.stringify(groups, null, 2) }] };
|
|
402
|
+
});
|
|
403
|
+
server.tool('appstore_get_app_info', 'App Store 앱 정보 (카테고리, 연령 등급)', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
404
|
+
const info = await appstore.getAppInfo(appId);
|
|
405
|
+
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
|
|
406
|
+
});
|
|
407
|
+
// --- 인증 상태 ---
|
|
408
|
+
server.tool('mimi_seed_auth_status', 'Mimi Seed MCP 인증 상태 확인', {}, async () => {
|
|
409
|
+
const client = getAuthenticatedClient();
|
|
410
|
+
if (!client) {
|
|
411
|
+
return {
|
|
412
|
+
content: [{
|
|
413
|
+
type: 'text',
|
|
414
|
+
text: '❌ 인증되지 않음.\n\n터미널에서 실행:\n npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
|
|
415
|
+
}],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const tokens = getStoredTokens();
|
|
419
|
+
if (tokens?.expiry_date) {
|
|
420
|
+
const now = Date.now();
|
|
421
|
+
const msUntilExpiry = tokens.expiry_date - now;
|
|
422
|
+
if (msUntilExpiry <= 0) {
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: 'text', text: '⚠️ 토큰 만료. 다시 인증 필요.\n\n터미널에서 실행:\n npx -y @yoonion/mimi-seed-mcp mimi-seed-auth' }],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (msUntilExpiry <= 30 * 60 * 1000) {
|
|
428
|
+
return {
|
|
429
|
+
content: [{ type: 'text', text: '⚠️ 토큰 곧 만료. 자동 갱신 예정.' }],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: 'text', text: '✅ 인증 완료.' }],
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
// ══════════════════════════════════════════════════
|
|
438
|
+
// Start
|
|
439
|
+
// ══════════════════════════════════════════════════
|
|
440
|
+
async function main() {
|
|
441
|
+
const transport = new StdioServerTransport();
|
|
442
|
+
await server.connect(transport);
|
|
443
|
+
}
|
|
444
|
+
main().catch((err) => {
|
|
445
|
+
console.error('MCP server error:', err);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
export declare function getAppDetails(auth: OAuth2Client, packageName: string): Promise<import("googleapis").androidpublisher_v3.Schema$AppDetails>;
|
|
3
|
+
export declare function getListing(auth: OAuth2Client, packageName: string, language?: string): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
|
|
4
|
+
export declare function updateListing(auth: OAuth2Client, packageName: string, language: string, data: {
|
|
5
|
+
title?: string;
|
|
6
|
+
shortDescription?: string;
|
|
7
|
+
fullDescription?: string;
|
|
8
|
+
}): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
|
|
9
|
+
export declare function listTracks(auth: OAuth2Client, packageName: string): Promise<{
|
|
10
|
+
track: string | null | undefined;
|
|
11
|
+
releases: {
|
|
12
|
+
name: string | null | undefined;
|
|
13
|
+
status: string | null | undefined;
|
|
14
|
+
versionCodes: string[] | null | undefined;
|
|
15
|
+
releaseNotes: import("googleapis").androidpublisher_v3.Schema$LocalizedText[] | undefined;
|
|
16
|
+
}[];
|
|
17
|
+
}[]>;
|
|
18
|
+
export declare function listReviews(auth: OAuth2Client, packageName: string): Promise<{
|
|
19
|
+
reviewId: string | null | undefined;
|
|
20
|
+
authorName: string | null | undefined;
|
|
21
|
+
comments: {
|
|
22
|
+
text: string | null | undefined;
|
|
23
|
+
starRating: number | null | undefined;
|
|
24
|
+
lastModified: string | null | undefined;
|
|
25
|
+
deviceMetadata: string | null | undefined;
|
|
26
|
+
}[] | undefined;
|
|
27
|
+
}[]>;
|
|
28
|
+
export declare function replyToReview(auth: OAuth2Client, packageName: string, reviewId: string, replyText: string): Promise<import("googleapis").androidpublisher_v3.Schema$ReviewsReplyResponse>;
|
|
29
|
+
export declare function listInAppProducts(auth: OAuth2Client, packageName: string): Promise<{
|
|
30
|
+
sku: string | null | undefined;
|
|
31
|
+
status: string | null | undefined;
|
|
32
|
+
purchaseType: string | null | undefined;
|
|
33
|
+
defaultPrice: import("googleapis").androidpublisher_v3.Schema$Price | undefined;
|
|
34
|
+
listings: {
|
|
35
|
+
[key: string]: import("googleapis").androidpublisher_v3.Schema$InAppProductListing;
|
|
36
|
+
} | null | undefined;
|
|
37
|
+
}[]>;
|
|
38
|
+
export declare function listSubscriptions(auth: OAuth2Client, packageName: string): Promise<{
|
|
39
|
+
productId: string | null | undefined;
|
|
40
|
+
basePlans: import("googleapis").androidpublisher_v3.Schema$BasePlan[] | undefined;
|
|
41
|
+
listings: import("googleapis").androidpublisher_v3.Schema$SubscriptionListing[] | undefined;
|
|
42
|
+
}[]>;
|
|
43
|
+
export type ServiceAccountVerifyResult = {
|
|
44
|
+
ok: true;
|
|
45
|
+
clientEmail: string;
|
|
46
|
+
projectId: string;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
stage: 'parse' | 'auth' | 'api';
|
|
50
|
+
httpStatus?: number;
|
|
51
|
+
message: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function verifyServiceAccountJson(serviceAccountJson: string, packageName: string): Promise<ServiceAccountVerifyResult>;
|