@yoonion/mimi-seed-mcp 0.3.18 → 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/dist/helpers.d.ts +8 -0
- package/dist/helpers.js +58 -0
- package/dist/index.js +22 -1563
- 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 +1 -1
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as playstore from '../playstore/tools.js';
|
|
3
|
+
import { saveServiceAccountJsonForPackage, listRegisteredServiceAccounts, deleteServiceAccountJsonForPackage, } from '../auth/playstore-auth.js';
|
|
4
|
+
import { createGoogleOneTimePurchase, createGoogleSubscription, updateGoogleProduct, deleteGoogleProduct, listGoogleProducts, } from '@onesub/providers';
|
|
5
|
+
import { requirePlayStoreAuth, requireServiceAccountJson } from '../helpers.js';
|
|
6
|
+
import { buildPlayStoreReleasePlan } from '../checks/plan.js';
|
|
7
|
+
export function registerPlaystoreTools(server) {
|
|
8
|
+
server.tool('playstore_get_app', 'Google Play 앱 상세 정보 조회', { packageName: z.string().describe('패키지명 (예: com.findthem.app)') }, async ({ packageName }) => {
|
|
9
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
10
|
+
const details = await playstore.getAppDetails(auth, packageName);
|
|
11
|
+
return { content: [{ type: 'text', text: JSON.stringify(details, null, 2) }] };
|
|
12
|
+
});
|
|
13
|
+
server.tool('playstore_get_listing', 'Google Play 스토어 리스팅 조회 (제목, 설명문 등)', {
|
|
14
|
+
packageName: z.string().describe('패키지명'),
|
|
15
|
+
language: z.string().default('ko-KR').describe('언어 코드 (기본: ko-KR)'),
|
|
16
|
+
}, async ({ packageName, language }) => {
|
|
17
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
18
|
+
const listing = await playstore.getListing(auth, packageName, language);
|
|
19
|
+
return { content: [{ type: 'text', text: JSON.stringify(listing, null, 2) }] };
|
|
20
|
+
});
|
|
21
|
+
server.tool('playstore_update_listing', 'Google Play 스토어 리스팅 수정 (제목, 설명문 변경)', {
|
|
22
|
+
packageName: z.string().describe('패키지명'),
|
|
23
|
+
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
24
|
+
title: z.string().optional().describe('앱 제목 (30자 이내)'),
|
|
25
|
+
shortDescription: z.string().optional().describe('짧은 설명 (80자 이내)'),
|
|
26
|
+
fullDescription: z.string().optional().describe('전체 설명 (4000자 이내)'),
|
|
27
|
+
}, async ({ packageName, language, title, shortDescription, fullDescription }) => {
|
|
28
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
29
|
+
const result = await playstore.updateListing(auth, packageName, language, {
|
|
30
|
+
title, shortDescription, fullDescription,
|
|
31
|
+
});
|
|
32
|
+
return { content: [{ type: 'text', text: `수정 완료:\n${JSON.stringify(result, null, 2)}` }] };
|
|
33
|
+
});
|
|
34
|
+
server.tool('playstore_list_tracks', 'Google Play 릴리스 트랙 현황 (프로덕션/베타/알파/내부)', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
35
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
36
|
+
const tracks = await playstore.listTracks(auth, packageName);
|
|
37
|
+
return { content: [{ type: 'text', text: JSON.stringify(tracks, null, 2) }] };
|
|
38
|
+
});
|
|
39
|
+
server.tool('playstore_list_images', 'Google Play 리스팅 이미지 목록 조회 (imageType별)', {
|
|
40
|
+
packageName: z.string().describe('패키지명'),
|
|
41
|
+
language: z.string().describe('언어 코드 (예: ko-KR)'),
|
|
42
|
+
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']).describe('이미지 타입'),
|
|
43
|
+
}, async ({ packageName, language, imageType }) => {
|
|
44
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
45
|
+
const images = await playstore.listImages(auth, packageName, language, imageType);
|
|
46
|
+
return { content: [{ type: 'text', text: JSON.stringify(images, null, 2) }] };
|
|
47
|
+
});
|
|
48
|
+
server.tool('playstore_upload_image', 'Google Play 리스팅 이미지 단일 업로드 (기존 이미지 유지). featureGraphic 1024x500 / icon 512x512 / phoneScreenshots 320~3840px', {
|
|
49
|
+
packageName: z.string().describe('패키지명'),
|
|
50
|
+
language: z.string().describe('언어 코드'),
|
|
51
|
+
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
52
|
+
filePath: z.string().describe('업로드할 이미지 절대 경로'),
|
|
53
|
+
}, async ({ packageName, language, imageType, filePath }) => {
|
|
54
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
55
|
+
const result = await playstore.uploadImage(auth, packageName, language, imageType, filePath);
|
|
56
|
+
return { content: [{ type: 'text', text: `✅ 업로드 완료\n${JSON.stringify(result, null, 2)}` }] };
|
|
57
|
+
});
|
|
58
|
+
server.tool('playstore_delete_all_images', 'Google Play 리스팅 특정 imageType의 이미지 전체 삭제 (교체 전 정리)', {
|
|
59
|
+
packageName: z.string().describe('패키지명'),
|
|
60
|
+
language: z.string().describe('언어 코드'),
|
|
61
|
+
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
62
|
+
}, async ({ packageName, language, imageType }) => {
|
|
63
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
64
|
+
const result = await playstore.deleteAllImages(auth, packageName, language, imageType);
|
|
65
|
+
return { content: [{ type: 'text', text: `✅ 전체 삭제\n${JSON.stringify(result, null, 2)}` }] };
|
|
66
|
+
});
|
|
67
|
+
server.tool('playstore_replace_images', 'Google Play 리스팅 이미지 일괄 교체 (한 edit 세션: deleteall → 순서대로 upload → commit). 스크린샷 5~8장 한 번에 교체 시 효율적. 업로드 순서가 스토어 노출 순서', {
|
|
68
|
+
packageName: z.string().describe('패키지명'),
|
|
69
|
+
language: z.string().describe('언어 코드'),
|
|
70
|
+
imageType: z.enum(['featureGraphic', 'icon', 'phoneScreenshots', 'promoGraphic', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvBanner', 'tvScreenshots', 'wearScreenshots']),
|
|
71
|
+
filePaths: z.array(z.string()).describe('업로드할 이미지 절대 경로 배열 (순서 = 노출 순서)'),
|
|
72
|
+
}, async ({ packageName, language, imageType, filePaths }) => {
|
|
73
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
74
|
+
const result = await playstore.replaceImages(auth, packageName, language, imageType, filePaths);
|
|
75
|
+
return { content: [{ type: 'text', text: `✅ ${result.count}장 교체 완료\n${JSON.stringify(result, null, 2)}` }] };
|
|
76
|
+
});
|
|
77
|
+
server.tool('playstore_update_release_notes', "Google Play 트랙 릴리스의 '최근 변경사항'(releaseNotes) 업데이트. versionCode로 타겟 릴리스 지정. 다른 언어/release는 보존. 이미 라이브(completed) 상태도 noteOnly 편집 가능", {
|
|
78
|
+
packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
|
|
79
|
+
track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
|
|
80
|
+
versionCode: z.string().describe('대상 versionCode (문자열, 예: "40")'),
|
|
81
|
+
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
82
|
+
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
83
|
+
}, async ({ packageName, track, versionCode, language, text }) => {
|
|
84
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
85
|
+
const result = await playstore.updateReleaseNotes(auth, packageName, track, versionCode, language, text);
|
|
86
|
+
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode} ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
87
|
+
});
|
|
88
|
+
server.tool('playstore_update_latest_release_notes', "Google Play 트랙의 최신 릴리스(versionCode 최대) '최근 변경사항' 업데이트 — versionCode를 모를 때 편의용", {
|
|
89
|
+
packageName: z.string().describe('패키지명'),
|
|
90
|
+
track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
|
|
91
|
+
language: z.string().describe('언어 코드 (예: ko-KR)'),
|
|
92
|
+
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
93
|
+
}, async ({ packageName, track, language, text }) => {
|
|
94
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
95
|
+
const result = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
|
|
96
|
+
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(result.updatedVersionCodes)}) ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
97
|
+
});
|
|
98
|
+
server.tool('playstore_list_reviews', 'Google Play 리뷰 목록 조회', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
99
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
100
|
+
const reviews = await playstore.listReviews(auth, packageName);
|
|
101
|
+
return { content: [{ type: 'text', text: JSON.stringify(reviews, null, 2) }] };
|
|
102
|
+
});
|
|
103
|
+
server.tool('playstore_reply_review', 'Google Play 리뷰에 답변', {
|
|
104
|
+
packageName: z.string().describe('패키지명'),
|
|
105
|
+
reviewId: z.string().describe('리뷰 ID'),
|
|
106
|
+
replyText: z.string().describe('답변 내용'),
|
|
107
|
+
}, async ({ packageName, reviewId, replyText }) => {
|
|
108
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
109
|
+
const result = await playstore.replyToReview(auth, packageName, reviewId, replyText);
|
|
110
|
+
return { content: [{ type: 'text', text: `답변 완료:\n${JSON.stringify(result, null, 2)}` }] };
|
|
111
|
+
});
|
|
112
|
+
server.tool('playstore_list_inapp_products', 'Google Play 인앱 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
113
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
114
|
+
const products = await playstore.listInAppProducts(auth, packageName);
|
|
115
|
+
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
|
|
116
|
+
});
|
|
117
|
+
server.tool('playstore_list_subscriptions', 'Google Play 구독 상품 목록', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
|
|
118
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
119
|
+
const subs = await playstore.listSubscriptions(auth, packageName);
|
|
120
|
+
return { content: [{ type: 'text', text: JSON.stringify(subs, null, 2) }] };
|
|
121
|
+
});
|
|
122
|
+
server.tool('playstore_create_onetime_product', [
|
|
123
|
+
'Google Play에 일회성 인앱 상품(소비성 또는 비소비성)을 생성.',
|
|
124
|
+
'Play Console 권한: "Manage store presence" 필요. 생성 후 Console에서 활성화 필요.',
|
|
125
|
+
].join(' '), {
|
|
126
|
+
packageName: z.string().describe('패키지명 (예: com.example.app)'),
|
|
127
|
+
productId: z
|
|
128
|
+
.string()
|
|
129
|
+
.describe('상품 ID (소문자/숫자/언더스코어/점, 예: premium_unlock). 한 번 정하면 변경 불가.'),
|
|
130
|
+
name: z.string().describe('상품 이름 (스토어 노출 제목)'),
|
|
131
|
+
price: z.number().int().describe('주 통화 기준 가격 (최소 단위: USD/EUR이면 cents, KRW/JPY이면 원화 정수. 예: USD $4.99 → 499, KRW ₩5,900 → 5900)'),
|
|
132
|
+
currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
|
|
133
|
+
type: z
|
|
134
|
+
.enum(['consumable', 'non_consumable'])
|
|
135
|
+
.default('non_consumable')
|
|
136
|
+
.describe('상품 유형 (소비성/비소비성). 앱이 consumePurchase 호출하면 소비성.'),
|
|
137
|
+
extraRegions: z
|
|
138
|
+
.array(z.object({
|
|
139
|
+
currency: z.string().describe('ISO 4217 통화 코드 (예: KRW, JPY, GBP)'),
|
|
140
|
+
price: z.number().describe('가격 (최소 단위: KRW ₩1,100 → 1100, USD $0.99 → 99)'),
|
|
141
|
+
}))
|
|
142
|
+
.optional()
|
|
143
|
+
.describe('추가 지역별 명시 가격. 자동 환산이 부정확한 KRW/JPY 등에 직접 지정.'),
|
|
144
|
+
}, async (args) => {
|
|
145
|
+
const json = requireServiceAccountJson(args.packageName);
|
|
146
|
+
const result = await createGoogleOneTimePurchase({
|
|
147
|
+
packageName: args.packageName,
|
|
148
|
+
productId: args.productId,
|
|
149
|
+
name: args.name,
|
|
150
|
+
price: args.price,
|
|
151
|
+
currency: args.currency,
|
|
152
|
+
type: args.type,
|
|
153
|
+
...(args.extraRegions && { extraRegions: args.extraRegions }),
|
|
154
|
+
serviceAccountKey: json,
|
|
155
|
+
});
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
return { content: [{ type: 'text', text: `❌ 상품 생성 실패: ${result.error}` }] };
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
content: [{
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: [
|
|
163
|
+
`✓ Play 일회성 상품 생성 완료`,
|
|
164
|
+
`productId: ${result.productId}`,
|
|
165
|
+
`price: ${args.price} ${args.currency}`,
|
|
166
|
+
'',
|
|
167
|
+
'Play Console에서 활성화 확인:',
|
|
168
|
+
`https://play.google.com/console/u/0/developers/-/app/-/managed-products?package=${encodeURIComponent(args.packageName)}`,
|
|
169
|
+
].join('\n'),
|
|
170
|
+
}],
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
server.tool('playstore_create_subscription', [
|
|
174
|
+
'Google Play에 자동 갱신 구독을 생성하고 baseplan을 활성화.',
|
|
175
|
+
'구독 생성 → baseplan(가격·주기) 추가 → 자동 활성화.',
|
|
176
|
+
'이미 같은 productId가 있으면 생성 실패 (Play API 특성상 upsert 미지원).',
|
|
177
|
+
].join(' '), {
|
|
178
|
+
packageName: z.string().describe('패키지명'),
|
|
179
|
+
productId: z
|
|
180
|
+
.string()
|
|
181
|
+
.describe('구독 상품 ID (소문자/숫자/언더스코어/점, 예: premium_monthly)'),
|
|
182
|
+
name: z.string().describe('구독 제목 (스토어 노출)'),
|
|
183
|
+
price: z.number().int().describe('주 통화 기준 가격 (최소 단위: USD cents. 예: $4.99 → 499, ₩5,900 → 5900)'),
|
|
184
|
+
currency: z.string().default('USD').describe('ISO 4217 통화 코드 (기본 USD)'),
|
|
185
|
+
period: z
|
|
186
|
+
.enum(['monthly', 'yearly'])
|
|
187
|
+
.describe('청구 주기'),
|
|
188
|
+
extraRegions: z
|
|
189
|
+
.array(z.object({
|
|
190
|
+
currency: z.string().describe('ISO 4217 통화 코드 (예: KRW, JPY)'),
|
|
191
|
+
price: z.number().describe('가격 (최소 단위)'),
|
|
192
|
+
}))
|
|
193
|
+
.optional()
|
|
194
|
+
.describe('추가 지역별 명시 가격'),
|
|
195
|
+
}, async (args) => {
|
|
196
|
+
const json = requireServiceAccountJson(args.packageName);
|
|
197
|
+
const result = await createGoogleSubscription({
|
|
198
|
+
packageName: args.packageName,
|
|
199
|
+
productId: args.productId,
|
|
200
|
+
name: args.name,
|
|
201
|
+
price: args.price,
|
|
202
|
+
currency: args.currency,
|
|
203
|
+
period: args.period,
|
|
204
|
+
...(args.extraRegions && { extraRegions: args.extraRegions }),
|
|
205
|
+
serviceAccountKey: json,
|
|
206
|
+
});
|
|
207
|
+
if (!result.success) {
|
|
208
|
+
return { content: [{ type: 'text', text: `❌ 구독 생성 실패: ${result.error}` }] };
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
content: [{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: [
|
|
214
|
+
`✓ Play 구독 생성 완료`,
|
|
215
|
+
`productId: ${result.productId}`,
|
|
216
|
+
`price: ${args.price} ${args.currency} / ${args.period}`,
|
|
217
|
+
'',
|
|
218
|
+
'Play Console:',
|
|
219
|
+
`https://play.google.com/console/u/0/developers/-/app/-/subscriptions?package=${encodeURIComponent(args.packageName)}`,
|
|
220
|
+
].join('\n'),
|
|
221
|
+
}],
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
server.tool('playstore_verify_service_account', [
|
|
225
|
+
"서비스 계정 JSON이 주어진 packageName에 대해 Play Developer API 호출 가능한지 + 'View financial data' 권한까지 있는지 검증.",
|
|
226
|
+
'onesub 같은 서버가 Play 영수증을 백그라운드로 검증하려면 OAuth 토큰 대신 서비스 계정 JSON이 필요 — 이 도구로 붙여넣기 전에 유효성 확인.',
|
|
227
|
+
'성공 시 clientEmail + projectId 반환. 실패 시 어느 단계(parse/auth/api)에서 왜 막혔는지 단계별로 안내.',
|
|
228
|
+
'(이 도구는 서비스 계정 자격증명만 사용 — 로그인한 사용자의 OAuth 토큰은 건드리지 않음)',
|
|
229
|
+
].join(' '), {
|
|
230
|
+
serviceAccountJson: z
|
|
231
|
+
.string()
|
|
232
|
+
.describe('서비스 계정 JSON 전체 내용 (문자열). Google Cloud Console → IAM & Admin → Service Accounts → Keys → Create new key → JSON으로 다운받은 파일의 내용'),
|
|
233
|
+
packageName: z
|
|
234
|
+
.string()
|
|
235
|
+
.describe('검증할 Android 앱의 패키지명 (예: com.findthem.app)'),
|
|
236
|
+
}, async ({ serviceAccountJson, packageName }) => {
|
|
237
|
+
const result = await playstore.verifyServiceAccountJson(serviceAccountJson, packageName);
|
|
238
|
+
if (result.ok) {
|
|
239
|
+
return {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: 'text',
|
|
243
|
+
text: [
|
|
244
|
+
'✓ 서비스 계정 유효 — Play Developer API 호출 가능',
|
|
245
|
+
'',
|
|
246
|
+
`**clientEmail**: \`${result.clientEmail}\``,
|
|
247
|
+
`**projectId**: \`${result.projectId}\``,
|
|
248
|
+
`**packageName**: \`${packageName}\``,
|
|
249
|
+
'',
|
|
250
|
+
'이제 이 JSON 내용을 onesub 서버의 `GOOGLE_SERVICE_ACCOUNT_KEY` 환경변수에 (한 줄로) 넣으면 됩니다. 예:',
|
|
251
|
+
'```bash',
|
|
252
|
+
'cat service-account.json | tr -d \'\\n\' | jq -c .',
|
|
253
|
+
'```',
|
|
254
|
+
].join('\n'),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const lines = [
|
|
260
|
+
`✗ 검증 실패 (stage: **${result.stage}**${result.httpStatus ? `, HTTP ${result.httpStatus}` : ''})`,
|
|
261
|
+
'',
|
|
262
|
+
`${result.message}`,
|
|
263
|
+
'',
|
|
264
|
+
];
|
|
265
|
+
if (result.stage === 'parse') {
|
|
266
|
+
lines.push('원인: 붙여넣은 JSON 구조가 올바르지 않음.');
|
|
267
|
+
lines.push('확인: Google Cloud Console → Service Accounts → Keys → **Create new key → JSON** 흐름으로 받은 파일 맞나요?');
|
|
268
|
+
}
|
|
269
|
+
else if (result.stage === 'auth') {
|
|
270
|
+
lines.push('원인: Google이 자격증명 자체를 거부함 (private_key 손상 / 프로젝트 비활성 / 계정 삭제됨).');
|
|
271
|
+
lines.push('확인: 새 키를 다시 발급 (기존 키 회수 후).');
|
|
272
|
+
}
|
|
273
|
+
else if (result.stage === 'api') {
|
|
274
|
+
if (result.httpStatus === 401 || result.httpStatus === 403) {
|
|
275
|
+
lines.push('원인: 토큰은 받았지만 Play Console에서 이 서비스 계정에 권한 없음.');
|
|
276
|
+
lines.push('확인 순서:');
|
|
277
|
+
lines.push('1. Play Console → Users and permissions → 이 서비스 계정 이메일을 초대');
|
|
278
|
+
lines.push('2. App permissions에서 해당 패키지명 앱 선택');
|
|
279
|
+
lines.push('3. Account permissions에 **View financial data, orders, and cancellation survey responses** 체크');
|
|
280
|
+
lines.push('4. 권한 적용까지 **~5분 대기** 후 재시도 (너무 빨리 시도하면 계속 403)');
|
|
281
|
+
}
|
|
282
|
+
else if (result.httpStatus === 404) {
|
|
283
|
+
lines.push('원인: 패키지명이 이 Play Console 개발자 계정 소유가 아님.');
|
|
284
|
+
lines.push(`확인: packageName이 Play Console에 등록된 앱의 것과 정확히 일치하나요? ("\`${packageName}\`")`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
lines.push('원인: Play Developer API 호출 중 예외. 네트워크 또는 Google 쪽 문제일 수 있음.');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
291
|
+
});
|
|
292
|
+
server.tool('playstore_register_service_account', [
|
|
293
|
+
'Google Play 서비스 계정 JSON을 패키지 단위로 등록 (~/.mimi-seed/play-service-accounts/{packageName}.json, 0600 mode).',
|
|
294
|
+
'여러 앱이 서로 다른 GCP 프로젝트의 SA를 쓰는 환경 지원. 등록 후 해당 packageName으로 호출하는 모든 playstore_* 도구가 이 SA를 사용 (등록 안 된 패키지는 ~/.mimi-seed/play-service-account.json 의 default SA로 폴백).',
|
|
295
|
+
'먼저 playstore_verify_service_account 로 권한 확인 후 등록을 권장.',
|
|
296
|
+
].join(' '), {
|
|
297
|
+
packageName: z.string().describe('Android 패키지명 (예: gg.pryzm.weather)'),
|
|
298
|
+
serviceAccountJson: z.string().describe('서비스 계정 JSON 전체 내용 (문자열)'),
|
|
299
|
+
skipVerify: z.boolean().optional().describe('true면 사전 검증 건너뜀 (기본 false: 등록 전 verifyServiceAccountJson 실행)'),
|
|
300
|
+
}, async ({ packageName, serviceAccountJson, skipVerify }) => {
|
|
301
|
+
if (!skipVerify) {
|
|
302
|
+
const verify = await playstore.verifyServiceAccountJson(serviceAccountJson, packageName);
|
|
303
|
+
if (!verify.ok) {
|
|
304
|
+
return {
|
|
305
|
+
content: [{
|
|
306
|
+
type: 'text',
|
|
307
|
+
text: [
|
|
308
|
+
`❌ 검증 실패 (stage: ${verify.stage})로 등록 중단.`,
|
|
309
|
+
verify.message,
|
|
310
|
+
'',
|
|
311
|
+
`검증을 건너뛰고 강제 등록하려면 skipVerify=true 옵션 추가.`,
|
|
312
|
+
].join('\n'),
|
|
313
|
+
}],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
let clientEmail = '';
|
|
318
|
+
let projectId = '';
|
|
319
|
+
try {
|
|
320
|
+
const parsed = JSON.parse(serviceAccountJson);
|
|
321
|
+
clientEmail = parsed.client_email ?? '';
|
|
322
|
+
projectId = parsed.project_id ?? '';
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return { content: [{ type: 'text', text: '❌ JSON 파싱 실패 — 서비스 계정 JSON 형식이 올바르지 않음.' }] };
|
|
326
|
+
}
|
|
327
|
+
saveServiceAccountJsonForPackage(packageName, serviceAccountJson);
|
|
328
|
+
return {
|
|
329
|
+
content: [{
|
|
330
|
+
type: 'text',
|
|
331
|
+
text: [
|
|
332
|
+
`✓ ${packageName} 서비스 계정 등록 완료`,
|
|
333
|
+
'',
|
|
334
|
+
`**clientEmail**: \`${clientEmail}\``,
|
|
335
|
+
`**projectId**: \`${projectId}\``,
|
|
336
|
+
`**저장 경로**: \`~/.mimi-seed/play-service-accounts/${packageName}.json\` (0600)`,
|
|
337
|
+
'',
|
|
338
|
+
'이후 이 packageName으로 호출하는 모든 playstore_* 도구가 자동으로 이 SA 사용.',
|
|
339
|
+
].join('\n'),
|
|
340
|
+
}],
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
server.tool('playstore_list_service_accounts', '등록된 패키지별 서비스 계정 + default(레거시) SA 정보 요약. clientEmail / projectId 만 노출 (private_key 미노출).', {}, async () => {
|
|
344
|
+
const info = listRegisteredServiceAccounts();
|
|
345
|
+
const lines = [];
|
|
346
|
+
if (info.default) {
|
|
347
|
+
lines.push('**Default (legacy)**: `~/.mimi-seed/play-service-account.json`');
|
|
348
|
+
lines.push(` - clientEmail: \`${info.default.clientEmail ?? '(parse error)'}\``);
|
|
349
|
+
lines.push(` - projectId: \`${info.default.projectId ?? '(parse error)'}\``);
|
|
350
|
+
lines.push('');
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
lines.push('**Default (legacy)**: 미등록');
|
|
354
|
+
lines.push('');
|
|
355
|
+
}
|
|
356
|
+
if (info.perPackage.length === 0) {
|
|
357
|
+
lines.push('**Per-package**: 없음');
|
|
358
|
+
lines.push('');
|
|
359
|
+
lines.push('등록 방법: `playstore_register_service_account(packageName, serviceAccountJson)`');
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
lines.push(`**Per-package** (${info.perPackage.length}개):`);
|
|
363
|
+
for (const item of info.perPackage) {
|
|
364
|
+
lines.push(`- \`${item.packageName}\` → \`${item.clientEmail ?? '(parse error)'}\` (project: \`${item.projectId ?? 'unknown'}\`)`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
368
|
+
});
|
|
369
|
+
server.tool('playstore_delete_service_account', '등록된 패키지별 서비스 계정 삭제. default(레거시) SA는 영향 없음.', {
|
|
370
|
+
packageName: z.string().describe('삭제할 패키지명'),
|
|
371
|
+
}, async ({ packageName }) => {
|
|
372
|
+
const deleted = deleteServiceAccountJsonForPackage(packageName);
|
|
373
|
+
return {
|
|
374
|
+
content: [{
|
|
375
|
+
type: 'text',
|
|
376
|
+
text: deleted
|
|
377
|
+
? `✓ ${packageName} 서비스 계정 파일 삭제 완료. 이후 이 패키지는 default SA로 폴백.`
|
|
378
|
+
: `(skip) ${packageName} 등록된 패키지별 SA 없음.`,
|
|
379
|
+
}],
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
server.tool('playstore_plan_release', [
|
|
383
|
+
'Google Play 배포 플랜 — 인증/빌드/메타데이터/스크린샷/정책 위험을 한 번에 점검하고',
|
|
384
|
+
'체크리스트로 반환합니다 (read-only, 아무것도 변경하지 않음).',
|
|
385
|
+
'⚠️ AI 호출자 지시: 응답을 받으면 미완료 항목을 반드시 TodoWrite로 사용자에게 먼저 보여주고,',
|
|
386
|
+
'사용자 동의 후 단계별로 기존 도구(playstore_update_listing, playstore_update_release_notes,',
|
|
387
|
+
'playstore_submit_release 등)를 호출하세요. submit_release(status=completed)는 비가역이므로 반드시 명시 동의 필요.',
|
|
388
|
+
].join(' '), {
|
|
389
|
+
packageName: z.string().describe('Android 패키지명 (예: gg.pryzm.coffee)'),
|
|
390
|
+
versionCode: z.string().optional().describe('확인할 versionCode. 미지정 시 트랙 최신 release 검사'),
|
|
391
|
+
track: z.enum(['production', 'beta', 'alpha', 'internal']).optional().describe('대상 트랙 (기본: production)'),
|
|
392
|
+
language: z.string().optional().describe('점검할 리스팅 언어 (기본: ko-KR)'),
|
|
393
|
+
}, async ({ packageName, versionCode, track, language }) => {
|
|
394
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
395
|
+
const text = await buildPlayStoreReleasePlan({
|
|
396
|
+
auth,
|
|
397
|
+
packageName,
|
|
398
|
+
versionCode,
|
|
399
|
+
track: track ?? 'production',
|
|
400
|
+
language: language ?? 'ko-KR',
|
|
401
|
+
});
|
|
402
|
+
return { content: [{ type: 'text', text }] };
|
|
403
|
+
});
|
|
404
|
+
server.tool('playstore_submit_release', [
|
|
405
|
+
'Google Play 트랙의 release status를 변경 — 일반적으로 draft → completed로 바꿔 검토/배포 큐에 진입시킬 때 사용.',
|
|
406
|
+
'⚠️ status="completed"는 비가역에 가까움 (전체 출시 또는 Google 검토 시작). halted로 일시 중단은 가능하나 한 번 라이브된 release는 되돌리기 어려움.',
|
|
407
|
+
'status 옵션: draft(검토 미시작) / inProgress(단계 출시) / completed(전체 출시) / halted(중단).',
|
|
408
|
+
'playstore_check_submission_risks로 사전 점검 권장.',
|
|
409
|
+
].join(' '), {
|
|
410
|
+
packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
|
|
411
|
+
track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
|
|
412
|
+
versionCode: z.string().describe('대상 versionCode (문자열)'),
|
|
413
|
+
status: z.enum(['draft', 'inProgress', 'completed', 'halted']).optional().describe('새 status (기본: completed)'),
|
|
414
|
+
}, async ({ packageName, track, versionCode, status }) => {
|
|
415
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
416
|
+
const result = await playstore.submitRelease(auth, packageName, track, versionCode, status);
|
|
417
|
+
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode}: ${result.previousStatus} → ${result.newStatus}\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
418
|
+
});
|
|
419
|
+
server.tool('playstore_promote_release', [
|
|
420
|
+
'Google Play 트랙 간 release promote — fromTrack(예: internal)의 versionCode를 toTrack(예: production)에 새 release로 추가.',
|
|
421
|
+
'단일 edit session에서 source 조회 → target 업데이트 → commit 까지 한 번에 처리.',
|
|
422
|
+
'source의 releaseNotes를 자동 복사 (copyReleaseNotes=false 또는 releaseNotes로 덮어쓰기 가능).',
|
|
423
|
+
'status="completed" + production 으로 전체 출시, status="inProgress" + userFraction 으로 단계 출시(예: 0.1 = 10%).',
|
|
424
|
+
'target에 같은 versionCode가 이미 있으면 해당 항목을 교체. status="completed"면 target 활성 release를 통째로 새 것으로 대체.',
|
|
425
|
+
'⚠️ status="completed"는 비가역에 가까움. playstore_check_submission_risks로 사전 점검 권장.',
|
|
426
|
+
].join(' '), {
|
|
427
|
+
packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
|
|
428
|
+
fromTrack: z.enum(['production', 'beta', 'alpha', 'internal']).describe('출처 트랙 (예: internal)'),
|
|
429
|
+
toTrack: z.enum(['production', 'beta', 'alpha', 'internal']).describe('대상 트랙 (예: production)'),
|
|
430
|
+
versionCode: z.string().describe('promote할 versionCode (문자열)'),
|
|
431
|
+
status: z.enum(['completed', 'draft', 'inProgress', 'halted']).optional().describe('대상 트랙에서의 status (기본 completed)'),
|
|
432
|
+
userFraction: z.number().min(0).max(1).optional().describe('status="inProgress"일 때 단계 출시 비율 (0~1, 예: 0.1)'),
|
|
433
|
+
releaseName: z.string().optional().describe('release 이름 (미지정 시 source 이름 그대로)'),
|
|
434
|
+
copyReleaseNotes: z.boolean().optional().describe('source releaseNotes 복사 여부 (기본 true)'),
|
|
435
|
+
releaseNotes: z.array(z.object({
|
|
436
|
+
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
437
|
+
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
438
|
+
})).optional().describe('대상 트랙용 릴리스 노트 (미지정 + copyReleaseNotes=true면 source 그대로)'),
|
|
439
|
+
}, async ({ packageName, fromTrack, toTrack, versionCode, status, userFraction, releaseName, copyReleaseNotes, releaseNotes }) => {
|
|
440
|
+
const auth = requirePlayStoreAuth(packageName);
|
|
441
|
+
const result = await playstore.promoteRelease(auth, packageName, fromTrack, toTrack, versionCode, {
|
|
442
|
+
status,
|
|
443
|
+
userFraction,
|
|
444
|
+
releaseName,
|
|
445
|
+
copyReleaseNotes,
|
|
446
|
+
releaseNotes,
|
|
447
|
+
});
|
|
448
|
+
const summary = `✅ ${packageName} ${fromTrack} → ${toTrack} v${versionCode} (status: ${result.newStatus}${result.userFraction != null ? `, userFraction: ${result.userFraction}` : ''})`;
|
|
449
|
+
return { content: [{ type: 'text', text: `${summary}\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
450
|
+
});
|
|
451
|
+
server.tool('playstore_list_products', 'Google Play의 모든 IAP 상품(구독 + 일회성) 통합 조회. productId / name / status / type / price / currency 반환.', {
|
|
452
|
+
packageName: z.string().describe('패키지명'),
|
|
453
|
+
}, async ({ packageName }) => {
|
|
454
|
+
const json = requireServiceAccountJson(packageName);
|
|
455
|
+
const products = await listGoogleProducts({ packageName, serviceAccountKey: json });
|
|
456
|
+
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
|
|
457
|
+
});
|
|
458
|
+
server.tool('playstore_update_product', 'Google Play IAP 상품의 표시 이름 변경 (현재 name 필드만 수정 가능). productId / type / 가격은 변경 불가.', {
|
|
459
|
+
packageName: z.string().describe('패키지명'),
|
|
460
|
+
productId: z.string().describe('상품 ID'),
|
|
461
|
+
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
462
|
+
name: z.string().describe('새 표시 이름'),
|
|
463
|
+
}, async ({ packageName, productId, productType, name }) => {
|
|
464
|
+
const json = requireServiceAccountJson(packageName);
|
|
465
|
+
const result = await updateGoogleProduct({
|
|
466
|
+
packageName, productId, productType, name, serviceAccountKey: json,
|
|
467
|
+
});
|
|
468
|
+
if (!result.success) {
|
|
469
|
+
return { content: [{ type: 'text', text: `❌ 수정 실패: ${result.error}` }] };
|
|
470
|
+
}
|
|
471
|
+
return { content: [{ type: 'text', text: `✓ 수정 완료 (변경 필드: ${result.updated.join(', ') || 'none'})` }] };
|
|
472
|
+
});
|
|
473
|
+
server.tool('playstore_delete_product', '⚠️ 비가역. Google Play IAP 상품 삭제. 활성 baseplan + 구독자 있는 구독은 삭제 불가.', {
|
|
474
|
+
packageName: z.string().describe('패키지명'),
|
|
475
|
+
productId: z.string().describe('상품 ID'),
|
|
476
|
+
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
477
|
+
}, async ({ packageName, productId, productType }) => {
|
|
478
|
+
const json = requireServiceAccountJson(packageName);
|
|
479
|
+
const result = await deleteGoogleProduct({
|
|
480
|
+
packageName, productId, productType, serviceAccountKey: json,
|
|
481
|
+
});
|
|
482
|
+
if (!result.success) {
|
|
483
|
+
return { content: [{ type: 'text', text: `❌ 삭제 실패: ${result.error}` }] };
|
|
484
|
+
}
|
|
485
|
+
return { content: [{ type: 'text', text: `✓ ${productId} 삭제 완료` }] };
|
|
486
|
+
});
|
|
487
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ensureFreshAccessToken } from './auth/google-auth.js';
|
|
2
|
+
export function registerResources(server) {
|
|
3
|
+
server.resource('auth-status', 'mimi-seed://auth/status', { description: 'Google OAuth 인증 상태 — fresh / refreshed / expired / unauthenticated', mimeType: 'application/json' }, async () => {
|
|
4
|
+
const r = await ensureFreshAccessToken();
|
|
5
|
+
const ok = r.status === 'fresh' || r.status === 'refreshed';
|
|
6
|
+
return {
|
|
7
|
+
contents: [{
|
|
8
|
+
uri: 'mimi-seed://auth/status',
|
|
9
|
+
mimeType: 'application/json',
|
|
10
|
+
text: JSON.stringify({
|
|
11
|
+
status: r.status,
|
|
12
|
+
authenticated: ok,
|
|
13
|
+
msUntilExpiry: ok ? r.msUntilExpiry : null,
|
|
14
|
+
error: !ok ? r.error : null,
|
|
15
|
+
hint: !ok ? 'Run: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth' : null,
|
|
16
|
+
}, null, 2),
|
|
17
|
+
}],
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
server.resource('agent-guide', 'mimi-seed://agent/guide', { description: 'Mimi Seed 에이전트 역할 정의 — 출시 워크플로우 · 주의사항 · 슬래시 커맨드', mimeType: 'text/markdown' }, async () => ({
|
|
21
|
+
contents: [{
|
|
22
|
+
uri: 'mimi-seed://agent/guide',
|
|
23
|
+
mimeType: 'text/markdown',
|
|
24
|
+
text: [
|
|
25
|
+
'# Mimi Seed — 앱 출시·운영 Agent',
|
|
26
|
+
'',
|
|
27
|
+
'당신은 Mimi Seed MCP를 통해 인디 개발자의 앱 출시와 운영을 돕는 에이전트입니다.',
|
|
28
|
+
'Google Play · App Store · Firebase · AdMob을 직접 제어하는 65+ 도구를 사용할 수 있습니다.',
|
|
29
|
+
'',
|
|
30
|
+
'## 출시 요청 처리 순서',
|
|
31
|
+
'',
|
|
32
|
+
'1. **항상** `playstore_check_submission_risks` / `appstore_check_submission_risks` 로 블로커 먼저 확인',
|
|
33
|
+
'2. 릴리즈 노트: `generate_release_notes_from_commits` → 검토 → 적용',
|
|
34
|
+
'3. **쓰기 작업**(submit, apply, reply 등)은 반드시 사용자 명시 동의 후 실행',
|
|
35
|
+
'4. 완료 후 결과 요약 제공',
|
|
36
|
+
'',
|
|
37
|
+
'## 슬래시 커맨드',
|
|
38
|
+
'',
|
|
39
|
+
'- `/mimi-seed:deploy` — 전체 출시 파이프라인',
|
|
40
|
+
'- `/mimi-seed:health` — 연결·인증 상태 빠른 확인',
|
|
41
|
+
'- `/mimi-seed:review-inbox` — 미답변 리뷰 조회 + 답변 생성',
|
|
42
|
+
'',
|
|
43
|
+
'## 주의사항',
|
|
44
|
+
'',
|
|
45
|
+
'- `playstore_submit_release(status=completed)` — 비가역, 반드시 명시 동의 필요',
|
|
46
|
+
'- `appstore_submit_for_review` — 비가역, 반드시 명시 동의 필요',
|
|
47
|
+
'- `playstore_reply_to_review` — 공개 게시, 반드시 검토 후 동의 필요',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
}],
|
|
50
|
+
}));
|
|
51
|
+
}
|
package/package.json
CHANGED