@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.
@@ -0,0 +1,197 @@
1
+ import { google } from 'googleapis';
2
+ import { JWT } from 'google-auth-library';
3
+ /**
4
+ * Google Play Developer API (Android Publisher API v3) 래퍼
5
+ *
6
+ * 주의: 최초 앱 생성은 API로 불가 (Play Console에서만).
7
+ * 여기서는 기존 앱의 메타데이터, 빌드, 출시를 관리.
8
+ */
9
+ const publisher = () => google.androidpublisher('v3');
10
+ // ─── withEdit 헬퍼 ───
11
+ async function withEdit(auth, packageName, fn, commit = false) {
12
+ const res = await publisher().edits.insert({ auth, packageName });
13
+ const editId = res.data.id;
14
+ if (!editId)
15
+ throw new Error('Failed to create edit session');
16
+ try {
17
+ const result = await fn(editId);
18
+ if (commit) {
19
+ await publisher().edits.commit({ auth, packageName, editId });
20
+ }
21
+ return result;
22
+ }
23
+ finally {
24
+ if (!commit) {
25
+ await publisher().edits.delete({ auth, packageName, editId }).catch(() => { });
26
+ }
27
+ }
28
+ }
29
+ // ─── 앱 목록 ───
30
+ export async function getAppDetails(auth, packageName) {
31
+ return withEdit(auth, packageName, async (editId) => {
32
+ const details = await publisher().edits.details.get({
33
+ auth,
34
+ packageName,
35
+ editId,
36
+ });
37
+ return details.data;
38
+ });
39
+ }
40
+ // ─── 스토어 리스팅 조회 ───
41
+ export async function getListing(auth, packageName, language = 'ko-KR') {
42
+ return withEdit(auth, packageName, async (editId) => {
43
+ const listing = await publisher().edits.listings.get({
44
+ auth,
45
+ packageName,
46
+ editId,
47
+ language,
48
+ });
49
+ return listing.data;
50
+ });
51
+ }
52
+ // ─── 스토어 리스팅 수정 ───
53
+ export async function updateListing(auth, packageName, language, data) {
54
+ return withEdit(auth, packageName, async (editId) => {
55
+ const updated = await publisher().edits.listings.update({
56
+ auth,
57
+ packageName,
58
+ editId,
59
+ language,
60
+ requestBody: data,
61
+ });
62
+ return updated.data;
63
+ }, true);
64
+ }
65
+ // ─── 트랙 목록 (릴리스 현황) ───
66
+ export async function listTracks(auth, packageName) {
67
+ return withEdit(auth, packageName, async (editId) => {
68
+ const tracks = await publisher().edits.tracks.list({
69
+ auth,
70
+ packageName,
71
+ editId,
72
+ });
73
+ return (tracks.data.tracks ?? []).map((t) => ({
74
+ track: t.track,
75
+ releases: (t.releases ?? []).map((r) => ({
76
+ name: r.name,
77
+ status: r.status,
78
+ versionCodes: r.versionCodes,
79
+ releaseNotes: r.releaseNotes,
80
+ })),
81
+ }));
82
+ });
83
+ }
84
+ // ─── 리뷰 조회 ───
85
+ export async function listReviews(auth, packageName) {
86
+ const res = await publisher().reviews.list({ auth, packageName });
87
+ return (res.data.reviews ?? []).map((r) => ({
88
+ reviewId: r.reviewId,
89
+ authorName: r.authorName,
90
+ comments: r.comments?.map((c) => ({
91
+ text: c.userComment?.text,
92
+ starRating: c.userComment?.starRating,
93
+ lastModified: c.userComment?.lastModified?.seconds,
94
+ deviceMetadata: c.userComment?.deviceMetadata?.productName,
95
+ })),
96
+ }));
97
+ }
98
+ // ─── 리뷰 답변 ───
99
+ export async function replyToReview(auth, packageName, reviewId, replyText) {
100
+ const res = await publisher().reviews.reply({
101
+ auth,
102
+ packageName,
103
+ reviewId,
104
+ requestBody: { replyText },
105
+ });
106
+ return res.data;
107
+ }
108
+ // ─── 인앱 상품 조회 ───
109
+ export async function listInAppProducts(auth, packageName) {
110
+ const res = await publisher().inappproducts.list({
111
+ auth,
112
+ packageName,
113
+ });
114
+ return (res.data.inappproduct ?? []).map((p) => ({
115
+ sku: p.sku,
116
+ status: p.status,
117
+ purchaseType: p.purchaseType,
118
+ defaultPrice: p.defaultPrice,
119
+ listings: p.listings,
120
+ }));
121
+ }
122
+ // ─── 구독 조회 ───
123
+ export async function listSubscriptions(auth, packageName) {
124
+ const res = await publisher().monetization.subscriptions.list({
125
+ auth,
126
+ packageName,
127
+ pageSize: 100,
128
+ });
129
+ return (res.data.subscriptions ?? []).map((s) => ({
130
+ productId: s.productId,
131
+ basePlans: s.basePlans,
132
+ listings: s.listings,
133
+ }));
134
+ }
135
+ export async function verifyServiceAccountJson(serviceAccountJson, packageName) {
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(serviceAccountJson);
139
+ }
140
+ catch (err) {
141
+ return { ok: false, stage: 'parse', message: `Invalid JSON: ${err.message}` };
142
+ }
143
+ for (const field of ['type', 'client_email', 'private_key', 'project_id']) {
144
+ if (!parsed[field]) {
145
+ return { ok: false, stage: 'parse', message: `Missing required field: ${field}` };
146
+ }
147
+ }
148
+ if (parsed.type !== 'service_account') {
149
+ return {
150
+ ok: false,
151
+ stage: 'parse',
152
+ message: `Expected type="service_account", got "${parsed.type}" — make sure you downloaded a service account key, not an OAuth client.`,
153
+ };
154
+ }
155
+ const jwt = new JWT({
156
+ email: parsed.client_email,
157
+ key: parsed.private_key,
158
+ scopes: ['https://www.googleapis.com/auth/androidpublisher'],
159
+ });
160
+ try {
161
+ await jwt.authorize();
162
+ }
163
+ catch (err) {
164
+ return {
165
+ ok: false,
166
+ stage: 'auth',
167
+ message: `OAuth token request failed: ${err.message}`,
168
+ };
169
+ }
170
+ // `monetization.subscriptions.list`는 'View financial data' 권한이 있어야
171
+ // 호출 가능. 권한 없이 androidpublisher scope만 있으면 403.
172
+ try {
173
+ await publisher().monetization.subscriptions.list({
174
+ auth: jwt,
175
+ packageName,
176
+ pageSize: 1,
177
+ });
178
+ }
179
+ catch (err) {
180
+ const e = err;
181
+ const httpStatus = e.code ?? e.status;
182
+ let hint = e.message ?? 'unknown error';
183
+ if (httpStatus === 401 || httpStatus === 403) {
184
+ hint +=
185
+ ' — the service account authenticated but lacks permission. In Google Play Console → Users and permissions, grant this service account "View financial data, orders, and cancellation survey responses" on the app.';
186
+ }
187
+ else if (httpStatus === 404) {
188
+ hint += ` — package "${packageName}" not found or not owned by this developer account.`;
189
+ }
190
+ return { ok: false, stage: 'api', httpStatus, message: hint };
191
+ }
192
+ return {
193
+ ok: true,
194
+ clientEmail: parsed.client_email,
195
+ projectId: parsed.project_id,
196
+ };
197
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@yoonion/mimi-seed-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Cursor / any MCP client.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mimi-seed-mcp": "./dist/index.js",
8
+ "mimi-seed-auth": "./dist/auth/cli.js",
9
+ "mimi-seed-appstore-auth": "./dist/appstore/setup-cli.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsx src/index.ts",
17
+ "auth": "tsx src/auth/cli.ts",
18
+ "prepublishOnly": "tsc"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "mcp-server",
23
+ "model-context-protocol",
24
+ "claude",
25
+ "firebase",
26
+ "admob",
27
+ "google-play",
28
+ "app-store",
29
+ "mimi-seed"
30
+ ],
31
+ "homepage": "https://mimi-seed.pryzm.gg",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/jeonghwanko/app-gen.git",
35
+ "directory": "packages/mcp-server"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.12.1",
46
+ "googleapis": "^148.0.0",
47
+ "jsonwebtoken": "^9.0.3",
48
+ "open": "^10.1.0",
49
+ "zod": "^3.24.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/jsonwebtoken": "^9.0.10",
53
+ "@types/node": "^22.0.0",
54
+ "tsx": "^4.19.0",
55
+ "typescript": "^5.7.0"
56
+ }
57
+ }