@yoonion/mimi-seed-mcp 0.3.37 → 0.3.39
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/android/keystore.d.ts +12 -0
- package/dist/android/keystore.js +52 -0
- package/dist/index.js +2 -0
- package/dist/prompts.js +5 -5
- package/dist/registers/android.d.ts +2 -0
- package/dist/registers/android.js +226 -0
- package/dist/registers/auth.js +114 -0
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface GeneratedKeystore {
|
|
2
|
+
keystoreBase64: string;
|
|
3
|
+
storePassword: string;
|
|
4
|
+
keyAlias: string;
|
|
5
|
+
keyPassword: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function isKeytoolAvailable(): boolean;
|
|
8
|
+
export declare function generateKeystore(opts: {
|
|
9
|
+
appName: string;
|
|
10
|
+
org?: string;
|
|
11
|
+
country?: string;
|
|
12
|
+
}): GeneratedKeystore;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { readFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
function randomPassword(len = 20) {
|
|
7
|
+
// alphanumeric — avoid quoting/escape issues in shell / Gradle properties
|
|
8
|
+
return randomBytes(32).toString('base64url').replace(/[^a-zA-Z0-9]/g, '').slice(0, len);
|
|
9
|
+
}
|
|
10
|
+
export function isKeytoolAvailable() {
|
|
11
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
12
|
+
const result = spawnSync(cmd, ['keytool'], { encoding: 'utf-8' });
|
|
13
|
+
return result.status === 0;
|
|
14
|
+
}
|
|
15
|
+
export function generateKeystore(opts) {
|
|
16
|
+
const storePassword = randomPassword(20);
|
|
17
|
+
const keyPassword = randomPassword(20);
|
|
18
|
+
const keyAlias = 'upload';
|
|
19
|
+
const org = opts.org ?? 'Supervlabs';
|
|
20
|
+
const country = opts.country ?? 'KR';
|
|
21
|
+
const dname = `CN=${opts.appName}, OU=Engineering, O=${org}, L=Seoul, ST=Seoul, C=${country}`;
|
|
22
|
+
const keystorePath = join(tmpdir(), `mimi-seed-ks-${Date.now()}.jks`);
|
|
23
|
+
try {
|
|
24
|
+
const result = spawnSync('keytool', [
|
|
25
|
+
'-genkeypair', '-v',
|
|
26
|
+
'-keystore', keystorePath,
|
|
27
|
+
'-keyalg', 'RSA',
|
|
28
|
+
'-keysize', '2048',
|
|
29
|
+
'-validity', '10000',
|
|
30
|
+
'-alias', keyAlias,
|
|
31
|
+
'-storepass', storePassword,
|
|
32
|
+
'-keypass', keyPassword,
|
|
33
|
+
'-dname', dname,
|
|
34
|
+
'-storetype', 'JKS',
|
|
35
|
+
], { stdio: 'pipe', encoding: 'buffer' });
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
const stderr = result.stderr?.toString('utf-8') ?? '';
|
|
38
|
+
throw new Error(`keytool 실패:\n${stderr}`);
|
|
39
|
+
}
|
|
40
|
+
const keystoreBuffer = readFileSync(keystorePath);
|
|
41
|
+
return {
|
|
42
|
+
keystoreBase64: keystoreBuffer.toString('base64'),
|
|
43
|
+
storePassword,
|
|
44
|
+
keyAlias,
|
|
45
|
+
keyPassword,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
if (existsSync(keystorePath))
|
|
50
|
+
unlinkSync(keystorePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerFacebookTools } from './registers/facebook.js';
|
|
|
17
17
|
import { registerGoogleAdsTools } from './registers/googleads.js';
|
|
18
18
|
import { registerGscTools } from './registers/gsc.js';
|
|
19
19
|
import { registerJenkinsTools } from './registers/jenkins.js';
|
|
20
|
+
import { registerAndroidTools } from './registers/android.js';
|
|
20
21
|
import { registerPrompts } from './prompts.js';
|
|
21
22
|
import { registerResources } from './resources.js';
|
|
22
23
|
// dist/index.js 기준 ../package.json — npm 패키지 루트의 버전을 단일 출처로 사용.
|
|
@@ -41,6 +42,7 @@ registerFacebookTools(server);
|
|
|
41
42
|
registerGoogleAdsTools(server);
|
|
42
43
|
registerGscTools(server);
|
|
43
44
|
registerJenkinsTools(server);
|
|
45
|
+
registerAndroidTools(server);
|
|
44
46
|
registerPrompts(server);
|
|
45
47
|
registerResources(server);
|
|
46
48
|
// `npx -y @yoonion/mimi-seed-mcp <subcommand>` 처리.
|
package/dist/prompts.js
CHANGED
|
@@ -25,7 +25,7 @@ export function registerPrompts(server) {
|
|
|
25
25
|
},
|
|
26
26
|
}],
|
|
27
27
|
}));
|
|
28
|
-
server.prompt('health', '
|
|
28
|
+
server.prompt('health', '전체 연결 상태 스캔 + 앱 출시 준비도 요약', {}, async () => ({
|
|
29
29
|
messages: [{
|
|
30
30
|
role: 'user',
|
|
31
31
|
content: {
|
|
@@ -34,10 +34,10 @@ export function registerPrompts(server) {
|
|
|
34
34
|
'현재 Mimi Seed 연결 상태와 앱 출시 준비도를 요약해줘.',
|
|
35
35
|
'',
|
|
36
36
|
'확인 순서:',
|
|
37
|
-
'1.
|
|
38
|
-
'2.
|
|
39
|
-
'3. firebase_list_projects 로 연결된 Firebase 프로젝트 확인 (
|
|
40
|
-
'4. playstore_check_submission_risks / appstore_check_submission_risks 로
|
|
37
|
+
'1. mimi_seed_status 호출 — 9개 서비스 전체 연결 상태 스캔',
|
|
38
|
+
'2. ❌ 필수 항목(Google OAuth / Play SA / App Store)이 있으면 먼저 설정 안내',
|
|
39
|
+
'3. firebase_list_projects 로 연결된 Firebase 프로젝트 확인 (OAuth 연결 시)',
|
|
40
|
+
'4. playstore_check_submission_risks / appstore_check_submission_risks 로 블로커 점검',
|
|
41
41
|
'5. 다음 권장 액션 제안 (블로커 수정 / 릴리즈 노트 생성 / 출시 실행)',
|
|
42
42
|
].join('\n'),
|
|
43
43
|
},
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { requirePlayStoreAuth } from '../helpers.js';
|
|
6
|
+
import { getServiceAccountJson } from '../auth/playstore-auth.js';
|
|
7
|
+
import { getAppDetails } from '../playstore/tools.js';
|
|
8
|
+
import { generateKeystore, isKeytoolAvailable } from '../android/keystore.js';
|
|
9
|
+
import { loadJenkinsConfig, requireJenkinsConfig } from '../jenkins/config.js';
|
|
10
|
+
import { upsertSecretFile } from '../jenkins/credentials.js';
|
|
11
|
+
const SA_DIR = join(homedir(), '.mimi-seed', 'play-service-accounts');
|
|
12
|
+
export function registerAndroidTools(server) {
|
|
13
|
+
// ── 0. 설정 마법사 ─────────────────────────────────────────────────────────
|
|
14
|
+
server.tool('android_signing_setup', [
|
|
15
|
+
'⭐ Android 앱 서명/빌드 설정을 시작할 때 가장 먼저 호출하세요.',
|
|
16
|
+
'Play Console 업로드 이력을 확인해 신규 앱인지 기존 앱인지 판별하고,',
|
|
17
|
+
'Jenkins credential 등록과 Play Store SA 연결까지 단계별 액션 플랜을 반환합니다.',
|
|
18
|
+
'"Jenkins에 keystore 등록해줘", "Android 빌드 설정 해줘", "서명 키 설정" 요청 시 이 도구를 먼저 호출하세요.',
|
|
19
|
+
].join(' '), {
|
|
20
|
+
package_name: z.string().describe('Android 패키지명 (예: gg.pryzm.speakmoney)'),
|
|
21
|
+
project_id: z.string().optional().describe('GCP 프로젝트 ID (SA 생성 시 필요, 선택)'),
|
|
22
|
+
}, async ({ package_name, project_id }) => {
|
|
23
|
+
const jenkinsCfg = loadJenkinsConfig();
|
|
24
|
+
const saJson = getServiceAccountJson(package_name);
|
|
25
|
+
// Play Console 확인
|
|
26
|
+
let appStatus = 'unknown';
|
|
27
|
+
let playNote = '';
|
|
28
|
+
if (saJson || getServiceAccountJson()) {
|
|
29
|
+
try {
|
|
30
|
+
const auth = requirePlayStoreAuth(package_name);
|
|
31
|
+
await getAppDetails(auth, package_name);
|
|
32
|
+
appStatus = 'existing';
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
const msg = e.message ?? String(e);
|
|
36
|
+
if (msg.includes('404') || msg.includes('notFound') || msg.includes('not found')) {
|
|
37
|
+
appStatus = 'new';
|
|
38
|
+
}
|
|
39
|
+
else if (msg.includes('403') || msg.includes('forbidden')) {
|
|
40
|
+
appStatus = 'new'; // SA는 있지만 권한 없음 = 신규 앱에 아직 초대 안 됨
|
|
41
|
+
playNote = '(Play Console SA 권한 없음 — 신규 앱으로 처리)';
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
playNote = `Play Console 확인 불가: ${msg.slice(0, 100)}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
playNote = 'Play Store SA 없음 — 사용자에게 신규/기존 여부를 직접 확인하세요.';
|
|
50
|
+
}
|
|
51
|
+
const jenkinsStatus = jenkinsCfg
|
|
52
|
+
? `✅ Jenkins 연결됨 (${jenkinsCfg.url})`
|
|
53
|
+
: '⚠️ Jenkins 미설정 — jenkins_status 호출 후 jenkins_save_config로 먼저 설정하세요.';
|
|
54
|
+
if (appStatus === 'existing') {
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: 'text',
|
|
58
|
+
text: [
|
|
59
|
+
`📦 ${package_name} — **기존 앱** (Play Console에 이미 존재)`,
|
|
60
|
+
'',
|
|
61
|
+
'기존 upload keystore와 비밀번호가 있어야 합니다.',
|
|
62
|
+
'분실한 경우 새 앱으로 다시 등록하거나 Play App Signing으로 마이그레이션해야 합니다.',
|
|
63
|
+
'',
|
|
64
|
+
`Jenkins: ${jenkinsStatus}`,
|
|
65
|
+
'',
|
|
66
|
+
'── 필요한 정보 ──────────────────────────────────',
|
|
67
|
+
'사용자에게 아래 항목을 확인하세요:',
|
|
68
|
+
' 1. upload.jks / upload.keystore 파일 경로 (base64로 변환 필요)',
|
|
69
|
+
' 2. store password',
|
|
70
|
+
' 3. key alias',
|
|
71
|
+
' 4. key password',
|
|
72
|
+
' 5. Play Store SA JSON 파일 (없으면 setup_playstore_connection으로 생성)',
|
|
73
|
+
'',
|
|
74
|
+
'── 등록 순서 ─────────────────────────────────────',
|
|
75
|
+
' 1. jenkins_upload_keystore(id="speakmoney-android-keystore", keystore_base64=..., file_name="upload.jks")',
|
|
76
|
+
' 2. jenkins_create_credential(id="speakmoney-android-store-password", secret=...)',
|
|
77
|
+
' 3. jenkins_create_credential(id="speakmoney-android-key-alias", secret=...)',
|
|
78
|
+
' 4. jenkins_create_credential(id="speakmoney-android-key-password", secret=...)',
|
|
79
|
+
' 5. jenkins_upload_playstore_sa(package_name="gg.pryzm.speakmoney", credential_id="speakmoney-app-key")',
|
|
80
|
+
' └ SA JSON이 없으면 먼저: setup_playstore_connection(packageName="gg.pryzm.speakmoney", projectId="...")',
|
|
81
|
+
].join('\n'),
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// 신규 앱 또는 미확인
|
|
86
|
+
const keytoolOk = isKeytoolAvailable();
|
|
87
|
+
const appLabel = appStatus === 'new' ? '**신규 앱**' : '**신규 앱으로 처리** (Play Console 확인 불가)';
|
|
88
|
+
return {
|
|
89
|
+
content: [{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: [
|
|
92
|
+
`📦 ${package_name} — ${appLabel}`,
|
|
93
|
+
playNote ? ` ${playNote}` : '',
|
|
94
|
+
'',
|
|
95
|
+
`Jenkins: ${jenkinsStatus}`,
|
|
96
|
+
`keytool(Java JDK): ${keytoolOk ? '✅ 설치됨 — 자동 생성 가능' : '❌ 미설치 — android_generate_keystore 호출 불가, 수동 생성 필요'}`,
|
|
97
|
+
'',
|
|
98
|
+
'── 신규 앱 설정 순서 ─────────────────────────────',
|
|
99
|
+
jenkinsCfg ? '' : ' 0. jenkins_status → jenkins_save_config (Jenkins 먼저 설정)',
|
|
100
|
+
keytoolOk
|
|
101
|
+
? ' 1. android_generate_keystore(app_name="SpeakMoney") → keystore + 비밀번호 자동 생성'
|
|
102
|
+
: ' 1. ⚠️ 수동 keystore 생성 후 base64로 인코딩해서 제공 (keytool -genkeypair ...)',
|
|
103
|
+
' 2. jenkins_upload_keystore(id="speakmoney-android-keystore", keystore_base64=..., file_name="upload.jks")',
|
|
104
|
+
' 3. jenkins_create_credential(id="speakmoney-android-store-password", secret=...)',
|
|
105
|
+
' 4. jenkins_create_credential(id="speakmoney-android-key-alias", secret=...)',
|
|
106
|
+
' 5. jenkins_create_credential(id="speakmoney-android-key-password", secret=...)',
|
|
107
|
+
project_id
|
|
108
|
+
? ` 6. setup_playstore_connection(packageName="${package_name}", projectId="${project_id}")`
|
|
109
|
+
: ` 6. setup_playstore_connection(packageName="${package_name}", projectId="<GCP 프로젝트 ID>")`,
|
|
110
|
+
' └ GCP 프로젝트 ID를 모르면 사용자에게 확인하세요.',
|
|
111
|
+
` 7. jenkins_upload_playstore_sa(package_name="${package_name}", credential_id="speakmoney-app-key")`,
|
|
112
|
+
' 8. Play Console에서 서비스 계정 초대 (수동, 1회)',
|
|
113
|
+
' → Play Console → 사용자 및 권한 → SA 이메일 → 릴리즈 관리자 권한 부여',
|
|
114
|
+
' 9. 첫 AAB 빌드 후 Play Console에 내부 테스트용으로 수동 업로드 (신규 앱 첫 번째만)',
|
|
115
|
+
].filter((l) => l !== undefined).join('\n'),
|
|
116
|
+
}],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
// ── 1. keystore 자동 생성 ──────────────────────────────────────────────────
|
|
120
|
+
server.tool('android_generate_keystore', [
|
|
121
|
+
'새 Android upload keystore (.jks)를 자동 생성합니다.',
|
|
122
|
+
'Java JDK의 keytool이 설치돼 있어야 합니다.',
|
|
123
|
+
'생성 후 반환된 keystoreBase64 / storePassword / keyAlias / keyPassword를',
|
|
124
|
+
'jenkins_upload_keystore 와 jenkins_create_credential 로 Jenkins에 등록하세요.',
|
|
125
|
+
'비밀번호는 이 응답에서만 확인 가능하니 반드시 Jenkins에 즉시 등록하세요.',
|
|
126
|
+
].join(' '), {
|
|
127
|
+
app_name: z.string().describe('앱 이름 — keystore dname CN에 사용 (예: SpeakMoney)'),
|
|
128
|
+
org: z.string().optional().default('Supervlabs').describe('조직명 (기본: Supervlabs)'),
|
|
129
|
+
country: z.string().optional().default('KR').describe('국가 코드 (기본: KR)'),
|
|
130
|
+
}, async ({ app_name, org, country }) => {
|
|
131
|
+
if (!isKeytoolAvailable()) {
|
|
132
|
+
return {
|
|
133
|
+
content: [{
|
|
134
|
+
type: 'text',
|
|
135
|
+
text: [
|
|
136
|
+
'❌ keytool이 설치되지 않았습니다.',
|
|
137
|
+
'',
|
|
138
|
+
'Java JDK를 설치하면 keytool이 포함됩니다:',
|
|
139
|
+
' macOS: brew install openjdk',
|
|
140
|
+
' Ubuntu: sudo apt install default-jdk',
|
|
141
|
+
' Windows: https://adoptium.net/',
|
|
142
|
+
'',
|
|
143
|
+
'설치 후 다시 android_generate_keystore를 호출하세요.',
|
|
144
|
+
].join('\n'),
|
|
145
|
+
}],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const ks = generateKeystore({ appName: app_name, org, country });
|
|
149
|
+
return {
|
|
150
|
+
content: [{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: [
|
|
153
|
+
'✅ Android upload keystore 생성 완료',
|
|
154
|
+
'',
|
|
155
|
+
'── 생성된 값 (지금 바로 Jenkins에 등록하세요) ────',
|
|
156
|
+
`keyAlias: ${ks.keyAlias}`,
|
|
157
|
+
`storePassword: ${ks.storePassword}`,
|
|
158
|
+
`keyPassword: ${ks.keyPassword}`,
|
|
159
|
+
`keystoreBase64 길이: ${ks.keystoreBase64.length}자 (파일 기준 약 ${Math.round(ks.keystoreBase64.length * 0.75 / 1024)}KB)`,
|
|
160
|
+
'',
|
|
161
|
+
'── 다음 단계 — 아래 순서대로 호출하세요 ──────────',
|
|
162
|
+
` jenkins_upload_keystore(`,
|
|
163
|
+
` id="speakmoney-android-keystore",`,
|
|
164
|
+
` keystore_base64="${ks.keystoreBase64.slice(0, 20)}...",`,
|
|
165
|
+
` file_name="upload.jks"`,
|
|
166
|
+
` )`,
|
|
167
|
+
` jenkins_create_credential(id="speakmoney-android-store-password", secret="${ks.storePassword}")`,
|
|
168
|
+
` jenkins_create_credential(id="speakmoney-android-key-alias", secret="${ks.keyAlias}")`,
|
|
169
|
+
` jenkins_create_credential(id="speakmoney-android-key-password", secret="${ks.keyPassword}")`,
|
|
170
|
+
'',
|
|
171
|
+
'⚠️ keystoreBase64 전체 값은 아래 별도 블록으로 제공합니다.',
|
|
172
|
+
`KEYSTORE_BASE64=${ks.keystoreBase64}`,
|
|
173
|
+
].join('\n'),
|
|
174
|
+
}],
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
// ── 2. Play SA JSON → Jenkins 업로드 ─────────────────────────────────────
|
|
178
|
+
server.tool('jenkins_upload_playstore_sa', [
|
|
179
|
+
'setup_playstore_connection으로 생성한 Play Store 서비스 계정 JSON을',
|
|
180
|
+
'Jenkins Secret File credential로 업로드합니다.',
|
|
181
|
+
'~/.mimi-seed/play-service-accounts/{package_name}.json 을 읽어 base64로 변환 후 등록합니다.',
|
|
182
|
+
'setup_playstore_connection 실행 후 반드시 이 도구를 호출하세요.',
|
|
183
|
+
].join(' '), {
|
|
184
|
+
package_name: z.string().describe('Android 패키지명 (예: gg.pryzm.speakmoney)'),
|
|
185
|
+
credential_id: z.string().default('speakmoney-app-key').describe('Jenkins Credential ID (기본: speakmoney-app-key)'),
|
|
186
|
+
}, async ({ package_name, credential_id }) => {
|
|
187
|
+
const saPath = join(SA_DIR, `${package_name}.json`);
|
|
188
|
+
if (!existsSync(saPath)) {
|
|
189
|
+
return {
|
|
190
|
+
content: [{
|
|
191
|
+
type: 'text',
|
|
192
|
+
text: [
|
|
193
|
+
`❌ ${package_name} 서비스 계정 JSON이 없습니다.`,
|
|
194
|
+
` 경로: ${saPath}`,
|
|
195
|
+
'',
|
|
196
|
+
'먼저 setup_playstore_connection을 호출해 서비스 계정을 생성하세요.',
|
|
197
|
+
].join('\n'),
|
|
198
|
+
}],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const saJsonRaw = readFileSync(saPath, 'utf-8');
|
|
202
|
+
let clientEmail = '(파싱 실패)';
|
|
203
|
+
try {
|
|
204
|
+
clientEmail = JSON.parse(saJsonRaw).client_email ?? clientEmail;
|
|
205
|
+
}
|
|
206
|
+
catch { /* ignore */ }
|
|
207
|
+
const saBase64 = Buffer.from(saJsonRaw, 'utf-8').toString('base64');
|
|
208
|
+
const cfg = requireJenkinsConfig();
|
|
209
|
+
const result = await upsertSecretFile(cfg, credential_id, saBase64, `${package_name}-sa.json`);
|
|
210
|
+
return {
|
|
211
|
+
content: [{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: [
|
|
214
|
+
`✅ Play Store SA JSON → Jenkins ${result}: \`${credential_id}\``,
|
|
215
|
+
` 서비스 계정: ${clientEmail}`,
|
|
216
|
+
` 파일 크기: ${saJsonRaw.length}자`,
|
|
217
|
+
'',
|
|
218
|
+
'다음 단계:',
|
|
219
|
+
' Play Console → 설정 → 사용자 및 권한 → 서비스 계정에서',
|
|
220
|
+
` ${clientEmail} 를 찾아 "릴리즈 관리자" 권한을 부여하세요.`,
|
|
221
|
+
' (수동 1회 작업)',
|
|
222
|
+
].join('\n'),
|
|
223
|
+
}],
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
package/dist/registers/auth.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { getMcpOAuthClient } from '../auth/constants.js';
|
|
2
2
|
import { startAuth, ensureFreshAccessToken, getTokensLastRefreshMs } from '../auth/google-auth.js';
|
|
3
|
+
import { listRegisteredServiceAccounts } from '../auth/playstore-auth.js';
|
|
4
|
+
import { getAppStoreCredentials } from '../appstore/auth.js';
|
|
5
|
+
import { loadJenkinsConfig } from '../jenkins/config.js';
|
|
6
|
+
import { loadCiConfig } from '../ci/config.js';
|
|
7
|
+
import { loadConfig as loadGoogleAdsConfig } from '../googleads/config.js';
|
|
8
|
+
import { loadFacebookConfig } from '../facebook/config.js';
|
|
9
|
+
import { loadInstagramConfig } from '../instagram/config.js';
|
|
10
|
+
import { getBigQueryServiceAccountKey } from '../auth/bigquery-auth.js';
|
|
3
11
|
/**
|
|
4
12
|
* tokens.json mtime → "Nd Hh ago" 형식 문자열 + 재인증 권고.
|
|
5
13
|
* Google 정책: 7일 미사용 시 미인증 앱 refresh_token revoke 가능.
|
|
@@ -27,6 +35,112 @@ function formatLastRefreshHint(lastMs) {
|
|
|
27
35
|
return { label };
|
|
28
36
|
}
|
|
29
37
|
export function registerAuthTools(server) {
|
|
38
|
+
// ── 전체 연결 상태 진단 ────────────────────────────────────────────────────
|
|
39
|
+
server.tool('mimi_seed_status', [
|
|
40
|
+
'⭐ 새 세션을 시작하거나 "뭐가 연결됐지?" 라는 질문엔 이 도구를 먼저 호출하세요.',
|
|
41
|
+
'9개 서비스(Google OAuth / Play SA / App Store / Jenkins / CI / Google Ads / Facebook / Instagram / BigQuery)',
|
|
42
|
+
'설정 상태를 한 번에 스캔해 ✅ / ❌ 트래픽 라이트 리포트와 번호 매긴 다음 단계를 반환합니다.',
|
|
43
|
+
'미설정 서비스마다 어떤 도구를 호출하면 되는지 구체적으로 알려줍니다.',
|
|
44
|
+
].join(' '), {}, async () => {
|
|
45
|
+
const lines = ['🌱 Mimi Seed 연결 상태', ''];
|
|
46
|
+
// 1. Google OAuth
|
|
47
|
+
const oauthResult = await ensureFreshAccessToken();
|
|
48
|
+
if (oauthResult.status === 'fresh' || oauthResult.status === 'refreshed') {
|
|
49
|
+
const min = Math.round(oauthResult.msUntilExpiry / 60_000);
|
|
50
|
+
const hint = formatLastRefreshHint(getTokensLastRefreshMs());
|
|
51
|
+
lines.push(`✅ Google OAuth — 연결됨 (${min}분 남음, ${hint.label})`);
|
|
52
|
+
if (hint.recommendation)
|
|
53
|
+
lines.push(` ${hint.recommendation}`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
lines.push('❌ Google OAuth — 미연결 → mimi_seed_auth_start');
|
|
57
|
+
}
|
|
58
|
+
// 2. Play Store SA
|
|
59
|
+
const saInfo = listRegisteredServiceAccounts();
|
|
60
|
+
const saCount = saInfo.perPackage.length + (saInfo.default ? 1 : 0);
|
|
61
|
+
if (saCount > 0) {
|
|
62
|
+
const pkgs = saInfo.perPackage.map((p) => p.packageName).join(', ');
|
|
63
|
+
const detail = pkgs || saInfo.default?.clientEmail || '(default)';
|
|
64
|
+
lines.push(`✅ Play Store SA — ${saCount}개 등록 (${detail})`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
lines.push('❌ Play Store SA — 미설정 → playstore_register_service_account 또는 setup_playstore_connection');
|
|
68
|
+
}
|
|
69
|
+
// 3. App Store Connect
|
|
70
|
+
const asc = getAppStoreCredentials();
|
|
71
|
+
if (asc) {
|
|
72
|
+
lines.push(`✅ App Store Connect — 연결됨 (keyId: ${asc.keyId})`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
lines.push('❌ App Store Connect — 미설정 → npx @yoonion/mimi-seed-mcp mimi-seed-appstore-auth');
|
|
76
|
+
}
|
|
77
|
+
// 4. Jenkins
|
|
78
|
+
const jenkins = loadJenkinsConfig();
|
|
79
|
+
if (jenkins) {
|
|
80
|
+
lines.push(`✅ Jenkins — 연결됨 (${jenkins.url})`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
lines.push('❌ Jenkins — 미설정 → jenkins_status → jenkins_save_config (선택)');
|
|
84
|
+
}
|
|
85
|
+
// 5. CI (GitHub/GitLab)
|
|
86
|
+
const ci = loadCiConfig();
|
|
87
|
+
if (ci) {
|
|
88
|
+
lines.push(`✅ CI — ${ci.provider} (${ci.owner}/${ci.repo})`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
lines.push('❌ CI — 미설정 → ci_save_config (선택)');
|
|
92
|
+
}
|
|
93
|
+
// 6. Google Ads
|
|
94
|
+
const gads = loadGoogleAdsConfig();
|
|
95
|
+
if (gads) {
|
|
96
|
+
lines.push(`⚠️ Google Ads — 설정됨 (customerId: ${gads.customerId})`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
lines.push('❌ Google Ads — 미설정 → googleads_save_config (선택)');
|
|
100
|
+
}
|
|
101
|
+
// 7. Facebook
|
|
102
|
+
const fb = loadFacebookConfig();
|
|
103
|
+
if (fb) {
|
|
104
|
+
lines.push(`✅ Facebook — 연결됨 (pageId: ${fb.pageId})`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
lines.push('❌ Facebook — 미설정 → facebook_save_config (선택)');
|
|
108
|
+
}
|
|
109
|
+
// 8. Instagram
|
|
110
|
+
const ig = loadInstagramConfig();
|
|
111
|
+
if (ig) {
|
|
112
|
+
lines.push(`✅ Instagram — 연결됨 (userId: ${ig.userId})`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
lines.push('❌ Instagram — 미설정 → instagram_save_config (선택)');
|
|
116
|
+
}
|
|
117
|
+
// 9. BigQuery
|
|
118
|
+
const bq = getBigQueryServiceAccountKey();
|
|
119
|
+
if (bq) {
|
|
120
|
+
lines.push(`✅ BigQuery — 서비스 계정 연결됨 (${bq.client_email ?? ''})`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
lines.push('❌ BigQuery — 미설정 → npx @yoonion/mimi-seed-mcp mimi-seed-bigquery-auth (선택)');
|
|
124
|
+
}
|
|
125
|
+
// 다음 단계 안내
|
|
126
|
+
const missing = [];
|
|
127
|
+
if (oauthResult.status !== 'fresh' && oauthResult.status !== 'refreshed') {
|
|
128
|
+
missing.push(' 1. mimi_seed_auth_start (Google 계정 로그인 — Firebase/AdMob/GSC 등 필수)');
|
|
129
|
+
}
|
|
130
|
+
if (saCount === 0) {
|
|
131
|
+
missing.push(' 2. setup_playstore_connection(packageName=..., projectId=...) (Play Store 배포 필수)');
|
|
132
|
+
}
|
|
133
|
+
if (!asc) {
|
|
134
|
+
missing.push(' 3. npx @yoonion/mimi-seed-mcp mimi-seed-appstore-auth (App Store 배포 필수)');
|
|
135
|
+
}
|
|
136
|
+
if (missing.length > 0) {
|
|
137
|
+
lines.push('', '── 다음 단계 (필수) ─────────────────────────────', ...missing);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
lines.push('', '✅ 필수 연결 모두 완료. 앱 출시 작업을 시작할 수 있습니다.');
|
|
141
|
+
}
|
|
142
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
143
|
+
});
|
|
30
144
|
server.tool('mimi_seed_auth_start', [
|
|
31
145
|
'Google OAuth 로그인 링크를 발급하고 백그라운드 콜백 서버를 시작.',
|
|
32
146
|
'응답에 포함된 URL을 브라우저에서 열고 승인하면 localhost:9876으로 자동 콜백 → 토큰이 ~/.mimi-seed/tokens.json에 저장됨.',
|
package/package.json
CHANGED