@yoonion/mimi-seed-mcp 0.9.1 → 0.9.3
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 +1 -1
- package/dist/playstore/tools.js +9 -2
- package/dist/registers/auth.js +25 -0
- package/dist/registers/playstore.js +1 -1
- package/dist/remote-sync.d.ts +24 -0
- package/dist/remote-sync.js +135 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
93
93
|
| 점검 / 위험 | 4 | `playstore_check_submission_risks` / `appstore_check_submission_risks` / `screenshot_validate` / `release_status` |
|
|
94
94
|
| Instagram | 4 | `instagram_post_image` / `instagram_post_carousel` / `instagram_save_config` |
|
|
95
95
|
| Android 서명 | 3 | `android_signing_setup` / `android_generate_keystore` / `jenkins_upload_playstore_sa` |
|
|
96
|
-
| 인증 |
|
|
96
|
+
| 인증 | 4 | `mimi_seed_status` / `mimi_seed_auth_start` / `mimi_seed_auth_status` / `mimi_seed_remote_sync_credentials` |
|
|
97
97
|
| AI (Claude) | 2 | `generate_release_notes_from_commits` / `generate_review_reply` |
|
|
98
98
|
|
|
99
99
|
> 인앱 결제(IAP·구독) 도구는 위 Play Store·App Store 카운트에 포함됩니다 — `appstore_create_inapp_purchase` · `appstore_update_product_review_note` · `appstore_upload_product_review_screenshot` 등.
|
package/dist/playstore/tools.js
CHANGED
|
@@ -141,13 +141,20 @@ export async function getListing(auth, packageName, language = 'ko-KR') {
|
|
|
141
141
|
}
|
|
142
142
|
// ─── 스토어 리스팅 수정 ───
|
|
143
143
|
export async function updateListing(auth, packageName, language, data) {
|
|
144
|
+
// ⚠️ edits.listings.update 는 리소스를 **통째로 교체**한다(PUT). requestBody 에 없는 필드는
|
|
145
|
+
// 지워진다 — 특히 `video`(스토어 프로모 영상)는 이 함수의 시그니처에 아예 없으므로,
|
|
146
|
+
// "제목만 바꾸기"가 조용히 앱의 프로모 영상을 삭제한다. patch 는 넘긴 필드만 부분 갱신한다.
|
|
147
|
+
//
|
|
148
|
+
// undefined 를 걸러내는 이유: 호출자가 title 만 준 경우 { shortDescription: undefined } 가
|
|
149
|
+
// 그대로 실려 나가면 patch 라도 해당 필드를 비우려는 의도로 해석될 여지가 있다.
|
|
150
|
+
const requestBody = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
|
|
144
151
|
return withEdit(auth, packageName, async (editId) => {
|
|
145
|
-
const updated = await publisher().edits.listings.
|
|
152
|
+
const updated = await publisher().edits.listings.patch({
|
|
146
153
|
auth,
|
|
147
154
|
packageName,
|
|
148
155
|
editId,
|
|
149
156
|
language,
|
|
150
|
-
requestBody
|
|
157
|
+
requestBody,
|
|
151
158
|
});
|
|
152
159
|
return updated.data;
|
|
153
160
|
}, true);
|
package/dist/registers/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { getMcpOAuthClient } from '../auth/constants.js';
|
|
3
4
|
import { classifyError } from '../auth/errors.js';
|
|
4
5
|
import { startAuth, ensureFreshAccessToken, getTokensLastRefreshMs } from '../auth/google-auth.js';
|
|
@@ -10,6 +11,7 @@ import { loadConfig as loadGoogleAdsConfig } from '../googleads/config.js';
|
|
|
10
11
|
import { loadFacebookConfig } from '../facebook/config.js';
|
|
11
12
|
import { loadInstagramConfig } from '../instagram/config.js';
|
|
12
13
|
import { resolveBigQueryAuth } from '../auth/bigquery-auth.js';
|
|
14
|
+
import { syncRemoteCredentials } from '../remote-sync.js';
|
|
13
15
|
import { findProjectManifest, manifestServiceEntries, } from '../lib/project-manifest.js';
|
|
14
16
|
/**
|
|
15
17
|
* tokens.json mtime → "Nd Hh ago" 형식 문자열 + 재인증 권고.
|
|
@@ -322,4 +324,27 @@ export function registerAuthTools(server) {
|
|
|
322
324
|
};
|
|
323
325
|
}
|
|
324
326
|
});
|
|
327
|
+
server.tool('mimi_seed_remote_sync_credentials', [
|
|
328
|
+
'로컬 ~/.mimi-seed의 App Store Connect 키와 패키지별 Google Play 서비스 계정을',
|
|
329
|
+
'Mimi Seed 원격 MCP에 검증 후 암호화 동기화합니다.',
|
|
330
|
+
'기본은 미리보기이며 실제 비밀값 전송에는 confirm=true가 필요합니다.',
|
|
331
|
+
'로컬 Google OAuth tokens.json은 보안 및 OAuth 클라이언트 호환성 때문에 복사하지 않습니다.',
|
|
332
|
+
].join(' '), {
|
|
333
|
+
confirm: z.boolean().optional().describe('true일 때만 원격에 비밀값을 전송하고 저장'),
|
|
334
|
+
include_appstore: z.boolean().optional().describe('App Store 키 포함 (기본 true)'),
|
|
335
|
+
include_playstore: z.boolean().optional().describe('Play 서비스 계정 포함 (기본 true)'),
|
|
336
|
+
package_names: z.array(z.string()).optional().describe('특정 Play 패키지만 동기화'),
|
|
337
|
+
}, async ({ confirm, include_appstore, include_playstore, package_names }) => ({
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: 'text',
|
|
341
|
+
text: await syncRemoteCredentials({
|
|
342
|
+
confirm,
|
|
343
|
+
includeAppStore: include_appstore,
|
|
344
|
+
includePlayStore: include_playstore,
|
|
345
|
+
packageNames: package_names,
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
}));
|
|
325
350
|
}
|
|
@@ -59,7 +59,7 @@ export function registerPlaystoreTools(server) {
|
|
|
59
59
|
const listing = await playstore.getListing(auth, packageName, language);
|
|
60
60
|
return { content: [{ type: 'text', text: JSON.stringify(listing, null, 2) }] };
|
|
61
61
|
});
|
|
62
|
-
server.tool('playstore_update_listing', 'Google Play 스토어 리스팅 수정 (제목, 설명문 변경). ⚠️ Play API 편집과 Console 동시 편집은 충돌 — Console에서 같은 리스팅을 편집·미게시 중이면 그 변경이 덮어써질 수 있음.', {
|
|
62
|
+
server.tool('playstore_update_listing', 'Google Play 스토어 리스팅 수정 (제목, 설명문 변경). 넘긴 필드만 부분 갱신(edits.listings.patch) — 생략한 필드와 프로모 영상(video)은 보존된다. ⚠️ Play API 편집과 Console 동시 편집은 충돌 — Console에서 같은 리스팅을 편집·미게시 중이면 그 변경이 덮어써질 수 있음.', {
|
|
63
63
|
packageName: z.string().describe('패키지명'),
|
|
64
64
|
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
65
65
|
title: z.string().optional().describe('앱 제목 (30자 이내)'),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type AppStoreCredentials } from './appstore/auth.js';
|
|
2
|
+
interface RemoteConfig {
|
|
3
|
+
token: string;
|
|
4
|
+
endpoint: string;
|
|
5
|
+
webBase: string;
|
|
6
|
+
}
|
|
7
|
+
export interface RemoteSyncOptions {
|
|
8
|
+
confirm?: boolean;
|
|
9
|
+
includeAppStore?: boolean;
|
|
10
|
+
includePlayStore?: boolean;
|
|
11
|
+
packageNames?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface RemoteSyncDependencies {
|
|
14
|
+
getConfig(): RemoteConfig | null;
|
|
15
|
+
getAppStoreCredentials(): AppStoreCredentials | null;
|
|
16
|
+
listPackageNames(): string[];
|
|
17
|
+
getServiceAccountJson(packageName: string): string | null;
|
|
18
|
+
callRemote(config: RemoteConfig, tool: string, args: Record<string, unknown>): Promise<{
|
|
19
|
+
text: string;
|
|
20
|
+
isError: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export declare function syncRemoteCredentials(options: RemoteSyncOptions, dependencies?: RemoteSyncDependencies): Promise<string>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getAppStoreCredentials } from './appstore/auth.js';
|
|
5
|
+
import { getServiceAccountJson, listRegisteredServiceAccounts, } from './auth/playstore-auth.js';
|
|
6
|
+
function getRemoteConfig() {
|
|
7
|
+
const envToken = process.env.MIMI_SEED_TOKEN;
|
|
8
|
+
if (envToken) {
|
|
9
|
+
const webBase = process.env.MIMI_SEED_WEB_BASE ?? 'https://mimi-seed.pryzm.gg';
|
|
10
|
+
return { token: envToken, endpoint: `${webBase}/api/mcp`, webBase };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const configPath = path.join(os.homedir(), '.mimi-seed', 'config.json');
|
|
14
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
15
|
+
if (!parsed.token || !parsed.endpoint)
|
|
16
|
+
return null;
|
|
17
|
+
const webBase = parsed.webBase ?? parsed.endpoint.replace(/\/api\/mcp\/?$/, '');
|
|
18
|
+
return { token: parsed.token, endpoint: parsed.endpoint, webBase };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function callRemote(config, tool, args) {
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetch(config.endpoint, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
Accept: 'application/json, text/event-stream',
|
|
32
|
+
Authorization: `Bearer ${config.token}`,
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
jsonrpc: '2.0',
|
|
36
|
+
id: 1,
|
|
37
|
+
method: 'tools/call',
|
|
38
|
+
params: { name: tool, arguments: args },
|
|
39
|
+
}),
|
|
40
|
+
signal: AbortSignal.timeout(30_000),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { text: '원격 MCP 네트워크 연결 실패', isError: true };
|
|
45
|
+
}
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
return { text: `원격 MCP HTTP ${response.status}`, isError: true };
|
|
48
|
+
}
|
|
49
|
+
const raw = await response.text();
|
|
50
|
+
let payloadRaw = raw;
|
|
51
|
+
if ((response.headers.get('content-type') ?? '').includes('text/event-stream')) {
|
|
52
|
+
const data = raw
|
|
53
|
+
.split('\n')
|
|
54
|
+
.map((line) => line.trim())
|
|
55
|
+
.find((line) => line.startsWith('data:'));
|
|
56
|
+
if (!data)
|
|
57
|
+
return { text: '원격 MCP 응답을 읽지 못했습니다.', isError: true };
|
|
58
|
+
payloadRaw = data.slice(5).trim();
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const payload = JSON.parse(payloadRaw);
|
|
62
|
+
if (payload.error)
|
|
63
|
+
return { text: payload.error.message ?? '원격 MCP 오류', isError: true };
|
|
64
|
+
return {
|
|
65
|
+
text: (payload.result?.content ?? [])
|
|
66
|
+
.filter((item) => item.type === 'text')
|
|
67
|
+
.map((item) => item.text ?? '')
|
|
68
|
+
.join('\n'),
|
|
69
|
+
isError: payload.result?.isError === true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { text: '원격 MCP 응답 형식이 올바르지 않습니다.', isError: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const defaultDependencies = {
|
|
77
|
+
getConfig: getRemoteConfig,
|
|
78
|
+
getAppStoreCredentials,
|
|
79
|
+
listPackageNames: () => listRegisteredServiceAccounts().perPackage.map((entry) => entry.packageName),
|
|
80
|
+
getServiceAccountJson: (packageName) => getServiceAccountJson(packageName),
|
|
81
|
+
callRemote,
|
|
82
|
+
};
|
|
83
|
+
export async function syncRemoteCredentials(options, dependencies = defaultDependencies) {
|
|
84
|
+
const includeAppStore = options.includeAppStore !== false;
|
|
85
|
+
const includePlayStore = options.includePlayStore !== false;
|
|
86
|
+
const appStore = includeAppStore ? dependencies.getAppStoreCredentials() : null;
|
|
87
|
+
const requestedPackages = options.packageNames?.map((name) => name.trim()).filter(Boolean);
|
|
88
|
+
const packageNames = includePlayStore
|
|
89
|
+
? [...new Set(requestedPackages?.length ? requestedPackages : dependencies.listPackageNames())]
|
|
90
|
+
: [];
|
|
91
|
+
const playCredentials = packageNames
|
|
92
|
+
.map((packageName) => ({ packageName, json: dependencies.getServiceAccountJson(packageName) }))
|
|
93
|
+
.filter((entry) => Boolean(entry.json));
|
|
94
|
+
const lines = [
|
|
95
|
+
'Mimi Seed 로컬 → 원격 자격증명 동기화',
|
|
96
|
+
`- App Store Connect: ${appStore ? '대상 1개' : includeAppStore ? '로컬 키 없음' : '제외'}`,
|
|
97
|
+
`- Google Play: ${playCredentials.length}개 패키지${packageNames.length > playCredentials.length ? ` (자격증명 없는 대상 ${packageNames.length - playCredentials.length}개)` : ''}`,
|
|
98
|
+
...playCredentials.map((entry) => ` - ${entry.packageName}`),
|
|
99
|
+
'- Google OAuth: 복사하지 않음 (원격 웹에서 별도 동의 필요)',
|
|
100
|
+
];
|
|
101
|
+
if (!options.confirm) {
|
|
102
|
+
return [...lines, '', '미리보기만 수행했습니다. 원격 저장은 confirm=true일 때만 실행됩니다.'].join('\n');
|
|
103
|
+
}
|
|
104
|
+
const config = dependencies.getConfig();
|
|
105
|
+
if (!config) {
|
|
106
|
+
return [...lines, '', '원격 연결 정보가 없습니다. 먼저 `mimi-seed init`을 실행하세요.'].join('\n');
|
|
107
|
+
}
|
|
108
|
+
const results = [];
|
|
109
|
+
if (appStore) {
|
|
110
|
+
const result = await dependencies.callRemote(config, 'import_appstore_credentials', {
|
|
111
|
+
key_id: appStore.keyId,
|
|
112
|
+
issuer_id: appStore.issuerId,
|
|
113
|
+
private_key: appStore.privateKey,
|
|
114
|
+
confirm: true,
|
|
115
|
+
});
|
|
116
|
+
results.push(`- App Store: ${result.isError ? '실패' : result.text}`);
|
|
117
|
+
}
|
|
118
|
+
for (const entry of playCredentials) {
|
|
119
|
+
const result = await dependencies.callRemote(config, 'import_playstore_service_account', {
|
|
120
|
+
package_name: entry.packageName,
|
|
121
|
+
service_account_json: entry.json,
|
|
122
|
+
confirm: true,
|
|
123
|
+
});
|
|
124
|
+
results.push(`- Play ${entry.packageName}: ${result.isError ? '실패' : result.text}`);
|
|
125
|
+
}
|
|
126
|
+
if (results.length === 0)
|
|
127
|
+
results.push('- 동기화할 로컬 자격증명이 없습니다.');
|
|
128
|
+
return [
|
|
129
|
+
...lines,
|
|
130
|
+
'',
|
|
131
|
+
...results,
|
|
132
|
+
'',
|
|
133
|
+
`Google 사용자 권한(Firebase/AdMob/vitals): ${config.webBase}/apps 에서 "Google 플랫폼 연결" 동의 필요`,
|
|
134
|
+
].join('\n');
|
|
135
|
+
}
|
package/package.json
CHANGED