@yoonion/mimi-seed-mcp 0.6.0 → 0.6.2
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 +2 -2
- package/dist/auth/constants.d.ts +9 -0
- package/dist/auth/constants.js +34 -2
- package/dist/auth/errors.d.ts +1 -1
- package/dist/auth/errors.js +17 -2
- package/dist/auth/google-auth.js +22 -4
- package/dist/firebase/tools.d.ts +37 -0
- package/dist/firebase/tools.js +99 -0
- package/dist/index.js +2 -43
- package/dist/lib/google-errors.js +21 -6
- package/dist/registers/firebase.js +37 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +51 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,13 +62,13 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
|
-
## 제공 도구 (
|
|
65
|
+
## 제공 도구 (148개 · 17개 영역)
|
|
66
66
|
|
|
67
67
|
| 영역 | 도구 수 | 주요 도구 |
|
|
68
68
|
|------|---------|-----------|
|
|
69
69
|
| App Store Connect | 31 | `appstore_submit_for_review` / `appstore_upload_screenshot` / `appstore_update_whats_new` / `appstore_create_version` |
|
|
70
70
|
| Google Play | 28 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
|
|
71
|
-
| Firebase |
|
|
71
|
+
| Firebase | 20 | `firebase_create_project` / `firebase_create_android_app` / `firebase_get_android_config` / `firebase_create_ios_app` |
|
|
72
72
|
| AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
|
|
73
73
|
| CI/CD (GitHub Actions · GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
|
|
74
74
|
| Jenkins (크리덴셜) | 6 | `jenkins_create_credential` / `jenkins_upload_keystore` / `jenkins_save_config` |
|
package/dist/auth/constants.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth client id/secret 해석: env 오버라이드 → 웹 콘솔 원격 조회.
|
|
3
|
+
* (디스크 캐시(credentials.json) 우선 순위는 호출자 — google-auth.ts
|
|
4
|
+
* ensureFreshAccessToken — 가 처리하며, 원격 조회 성공 시 디스크에 저장한다.)
|
|
5
|
+
*
|
|
6
|
+
* 모든 실패는 message 에 'mcp-auth-config' 를 포함해 던진다 —
|
|
7
|
+
* errors.ts classifyError 가 이 마커로 CONFIG_FETCH_FAILED 로 분류해
|
|
8
|
+
* "구글 연결 실패" 같은 오진 대신 정확한 원인/해법을 안내한다.
|
|
9
|
+
*/
|
|
1
10
|
export declare function getMcpOAuthClient(): Promise<{
|
|
2
11
|
clientId: string;
|
|
3
12
|
clientSecret: string;
|
package/dist/auth/constants.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
const WEB_BASE = process.env.MIMI_SEED_WEB_BASE ?? 'https://mimi-seed.pryzm.gg';
|
|
2
2
|
let _cached = null;
|
|
3
|
+
/**
|
|
4
|
+
* OAuth client id/secret 해석: env 오버라이드 → 웹 콘솔 원격 조회.
|
|
5
|
+
* (디스크 캐시(credentials.json) 우선 순위는 호출자 — google-auth.ts
|
|
6
|
+
* ensureFreshAccessToken — 가 처리하며, 원격 조회 성공 시 디스크에 저장한다.)
|
|
7
|
+
*
|
|
8
|
+
* 모든 실패는 message 에 'mcp-auth-config' 를 포함해 던진다 —
|
|
9
|
+
* errors.ts classifyError 가 이 마커로 CONFIG_FETCH_FAILED 로 분류해
|
|
10
|
+
* "구글 연결 실패" 같은 오진 대신 정확한 원인/해법을 안내한다.
|
|
11
|
+
*/
|
|
3
12
|
export async function getMcpOAuthClient() {
|
|
4
13
|
const id = process.env.MIMI_SEED_GOOGLE_CLIENT_ID ??
|
|
5
14
|
process.env.PRESEED_GOOGLE_CLIENT_ID;
|
|
@@ -9,9 +18,32 @@ export async function getMcpOAuthClient() {
|
|
|
9
18
|
return { clientId: id, clientSecret: secret };
|
|
10
19
|
if (_cached)
|
|
11
20
|
return _cached;
|
|
12
|
-
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await fetch(`${WEB_BASE}/api/mcp-auth-config`, {
|
|
24
|
+
signal: AbortSignal.timeout(10_000),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
29
|
+
throw new Error(`mcp-auth-config fetch failed (${WEB_BASE} 접속 불가: ${detail})`);
|
|
30
|
+
}
|
|
13
31
|
if (!res.ok)
|
|
14
32
|
throw new Error(`mcp-auth-config fetch failed (${res.status})`);
|
|
15
|
-
|
|
33
|
+
// body 파싱/검증 실패도 반드시 마커를 달아야 한다 — captive portal 이 200 + HTML 을
|
|
34
|
+
// 돌려주는 첫 설치 환경에서 raw SyntaxError 가 새면 "재로그인하세요" 로 오진된다.
|
|
35
|
+
let data;
|
|
36
|
+
try {
|
|
37
|
+
data = (await res.json());
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
41
|
+
throw new Error(`mcp-auth-config fetch failed (응답이 JSON 이 아님: ${detail})`);
|
|
42
|
+
}
|
|
43
|
+
// 빈 값 200 응답(서버 env 미설정)을 캐싱하면 프로세스 수명 내내 invalid_client 로 오진된다.
|
|
44
|
+
if (!data || typeof data !== 'object' || !data.clientId || !data.clientSecret) {
|
|
45
|
+
throw new Error('mcp-auth-config fetch failed (서버 응답에 clientId/clientSecret 비어 있음)');
|
|
46
|
+
}
|
|
47
|
+
_cached = { clientId: data.clientId, clientSecret: data.clientSecret };
|
|
16
48
|
return _cached;
|
|
17
49
|
}
|
package/dist/auth/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AuthErrorCode = 'UNAUTHENTICATED' | 'NO_REFRESH_TOKEN' | 'INVALID_GRANT' | 'RAPT_REQUIRED' | 'INVALID_CLIENT' | 'UNAUTHORIZED_CLIENT' | 'REFRESH_NETWORK_ERROR' | 'REFRESH_UNKNOWN' | 'INSUFFICIENT_SCOPE' | 'CALLBACK_PORT_IN_USE' | 'CALLBACK_TIMEOUT' | 'BROWSER_OPEN_FAILED' | 'USER_DENIED' | 'CODE_EXCHANGE_FAILED' | 'TOKEN_RESPONSE_INVALID';
|
|
1
|
+
export type AuthErrorCode = 'UNAUTHENTICATED' | 'NO_REFRESH_TOKEN' | 'INVALID_GRANT' | 'RAPT_REQUIRED' | 'INVALID_CLIENT' | 'UNAUTHORIZED_CLIENT' | 'REFRESH_NETWORK_ERROR' | 'CONFIG_FETCH_FAILED' | 'REFRESH_UNKNOWN' | 'INSUFFICIENT_SCOPE' | 'CALLBACK_PORT_IN_USE' | 'CALLBACK_TIMEOUT' | 'BROWSER_OPEN_FAILED' | 'USER_DENIED' | 'CODE_EXCHANGE_FAILED' | 'TOKEN_RESPONSE_INVALID';
|
|
2
2
|
export interface AuthErrorPayload {
|
|
3
3
|
code: AuthErrorCode;
|
|
4
4
|
message: string;
|
package/dist/auth/errors.js
CHANGED
|
@@ -63,13 +63,28 @@ export function classifyError(e, ctx) {
|
|
|
63
63
|
if (oauthCode === 'access_denied') {
|
|
64
64
|
return {
|
|
65
65
|
code: 'USER_DENIED',
|
|
66
|
-
message: '
|
|
67
|
-
hint: '
|
|
66
|
+
message: 'Google 동의가 거부되었습니다 (access_denied).',
|
|
67
|
+
hint: '직접 거부하지 않았다면: 이 OAuth 앱은 Google 미검증(테스트) 상태라 등록된 테스트 사용자만 로그인할 수 있습니다. ' +
|
|
68
|
+
'운영자에게 Google Cloud Console → OAuth 동의 화면 → 테스트 사용자에 이 계정 추가를 요청한 뒤 재시도하세요. ' +
|
|
69
|
+
'직접 거부했다면 다시 시도해 모든 권한에 동의해주세요.',
|
|
68
70
|
retriable: true,
|
|
69
71
|
needsReauth: true,
|
|
70
72
|
cause: raw,
|
|
71
73
|
};
|
|
72
74
|
}
|
|
75
|
+
// OAuth client 설정 조회 실패 — 구글이 아니라 mimi-seed 웹 콘솔 쪽 문제.
|
|
76
|
+
// 네트워크 분류보다 먼저 검사해야 "oauth2.googleapis.com 연결 실패" 오진을 막는다.
|
|
77
|
+
if (/mcp-auth-config/i.test(raw)) {
|
|
78
|
+
return {
|
|
79
|
+
code: 'CONFIG_FETCH_FAILED',
|
|
80
|
+
message: 'Mimi Seed 서버에서 OAuth 클라이언트 설정을 받지 못했습니다.',
|
|
81
|
+
hint: 'mimi-seed.pryzm.gg 접속 가능 여부를 확인하거나, ' +
|
|
82
|
+
'환경변수 MIMI_SEED_GOOGLE_CLIENT_ID / MIMI_SEED_GOOGLE_CLIENT_SECRET 로 자체 OAuth 클라이언트를 지정하세요.',
|
|
83
|
+
retriable: true,
|
|
84
|
+
needsReauth: false,
|
|
85
|
+
cause: raw,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
73
88
|
// 네트워크 / 시스템 레벨
|
|
74
89
|
if (isNetworkError(e, raw)) {
|
|
75
90
|
return {
|
package/dist/auth/google-auth.js
CHANGED
|
@@ -316,11 +316,29 @@ export async function ensureFreshAccessToken(marginMs = 300_000) {
|
|
|
316
316
|
},
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
|
-
// refresh 시도 — credentials.json
|
|
319
|
+
// refresh 시도 — credentials.json(디스크) 우선. 디스크에 있으면 원격 조회 자체를 안 한다 —
|
|
320
|
+
// 매시간 refresh 가 웹 콘솔 생존에 의존하면 콘솔 장애가 모든 로컬 도구 호출을 죽인다.
|
|
321
|
+
// 없을 때만 env → 원격(getMcpOAuthClient) 순으로 받고, 성공 시 디스크에 저장해
|
|
322
|
+
// 원격 의존을 최초 1회로 끝낸다. 조회 실패는 raw throw 가 아니라 분류된 에러로 반환.
|
|
323
|
+
let clientId;
|
|
324
|
+
let clientSecret;
|
|
320
325
|
const stored = getStoredCredentials();
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
326
|
+
if (stored?.clientId && stored?.clientSecret) {
|
|
327
|
+
({ clientId, clientSecret } = stored);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
try {
|
|
331
|
+
({ clientId, clientSecret } = await getMcpOAuthClient());
|
|
332
|
+
saveCredentials(clientId, clientSecret);
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
return {
|
|
336
|
+
status: 'expired_refresh_failed',
|
|
337
|
+
tokens,
|
|
338
|
+
error: classifyError(e, { phase: 'refresh' }),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
324
342
|
const client = createOAuth2Client(clientId, clientSecret);
|
|
325
343
|
client.setCredentials({ refresh_token: tokens.refresh_token });
|
|
326
344
|
try {
|
package/dist/firebase/tools.d.ts
CHANGED
|
@@ -9,6 +9,43 @@ export declare function listProjects(auth: OAuth2Client): Promise<{
|
|
|
9
9
|
projectNumber: string | null | undefined;
|
|
10
10
|
}[]>;
|
|
11
11
|
export declare function getProject(auth: OAuth2Client, projectId: string): Promise<import("googleapis").firebase_v1beta1.Schema$FirebaseProject>;
|
|
12
|
+
/** long-running Operation 하나의 완료 여부를 판정한다. 폴링 루프에서 재사용 + 단독 테스트 가능. */
|
|
13
|
+
export declare function operationOutcome(op: {
|
|
14
|
+
done?: boolean | null;
|
|
15
|
+
error?: {
|
|
16
|
+
message?: string | null;
|
|
17
|
+
} | null;
|
|
18
|
+
}): {
|
|
19
|
+
status: 'pending';
|
|
20
|
+
} | {
|
|
21
|
+
status: 'done';
|
|
22
|
+
} | {
|
|
23
|
+
status: 'error';
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* getOperation()을 완료(done)까지 폴링한다. getOperation() 자체가 튕기는 것(네트워크 blip)과
|
|
28
|
+
* operation이 아직 안 끝난 것(pending)을 구분해서, 전자는 몇 번 더 참아주고 후자만 진행 신호로 본다.
|
|
29
|
+
* intervalMs/maxAttempts는 테스트에서 실시간 대기 없이 검증하기 위한 override.
|
|
30
|
+
*/
|
|
31
|
+
export declare function waitForOperation(getOperation: () => Promise<{
|
|
32
|
+
done?: boolean | null;
|
|
33
|
+
error?: {
|
|
34
|
+
message?: string | null;
|
|
35
|
+
} | null;
|
|
36
|
+
}>, label: string, opts?: {
|
|
37
|
+
intervalMs?: number;
|
|
38
|
+
maxAttempts?: number;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* 새 GCP 프로젝트를 만들고 Firebase를 추가한다 (앱 하나당 전용 Firebase 프로젝트 컨벤션).
|
|
42
|
+
* 둘 다 long-running operation이라 완료까지 폴링한다(최악 케이스 ~2분: 단계당 ~60s × 2단계).
|
|
43
|
+
* parent 생략 시 계정의 기본 정책대로 생성된다 — 조직 소속이 강제된 계정은 parent
|
|
44
|
+
* ('organizations/<id>' 또는 'folders/<id>') 없이 호출하면 에러가 난다.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createProject(auth: OAuth2Client, projectId: string, displayName: string, opts?: {
|
|
47
|
+
parent?: string;
|
|
48
|
+
}): Promise<import("googleapis").firebase_v1beta1.Schema$FirebaseProject>;
|
|
12
49
|
export declare function listAndroidApps(auth: OAuth2Client, projectId: string): Promise<{
|
|
13
50
|
appId: string | null | undefined;
|
|
14
51
|
packageName: string | null | undefined;
|
package/dist/firebase/tools.js
CHANGED
|
@@ -19,6 +19,105 @@ export async function getProject(auth, projectId) {
|
|
|
19
19
|
});
|
|
20
20
|
return res.data;
|
|
21
21
|
}
|
|
22
|
+
const OPERATION_POLL_INTERVAL_MS = 2000;
|
|
23
|
+
const OPERATION_MAX_ATTEMPTS = 30; // ~60s 상한 (operation 하나당) — 프로젝트 생성 + Firebase 추가 두 단계라 최악 ~2분
|
|
24
|
+
const OPERATION_FETCH_RETRY_LIMIT = 3; // getOperation() 자체가 튕기는 경우(네트워크 blip) 허용할 연속 실패 횟수
|
|
25
|
+
function sleep(ms) {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
/** long-running Operation 하나의 완료 여부를 판정한다. 폴링 루프에서 재사용 + 단독 테스트 가능. */
|
|
29
|
+
export function operationOutcome(op) {
|
|
30
|
+
if (!op.done)
|
|
31
|
+
return { status: 'pending' };
|
|
32
|
+
if (op.error)
|
|
33
|
+
return { status: 'error', message: op.error.message ?? JSON.stringify(op.error) };
|
|
34
|
+
return { status: 'done' };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* getOperation()을 완료(done)까지 폴링한다. getOperation() 자체가 튕기는 것(네트워크 blip)과
|
|
38
|
+
* operation이 아직 안 끝난 것(pending)을 구분해서, 전자는 몇 번 더 참아주고 후자만 진행 신호로 본다.
|
|
39
|
+
* intervalMs/maxAttempts는 테스트에서 실시간 대기 없이 검증하기 위한 override.
|
|
40
|
+
*/
|
|
41
|
+
export async function waitForOperation(getOperation, label, opts = {}) {
|
|
42
|
+
const intervalMs = opts.intervalMs ?? OPERATION_POLL_INTERVAL_MS;
|
|
43
|
+
const maxAttempts = opts.maxAttempts ?? OPERATION_MAX_ATTEMPTS;
|
|
44
|
+
let consecutiveFetchErrors = 0;
|
|
45
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
46
|
+
let op;
|
|
47
|
+
try {
|
|
48
|
+
op = await getOperation();
|
|
49
|
+
consecutiveFetchErrors = 0;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
consecutiveFetchErrors += 1;
|
|
53
|
+
if (consecutiveFetchErrors > OPERATION_FETCH_RETRY_LIMIT)
|
|
54
|
+
throw err;
|
|
55
|
+
if (attempt < maxAttempts - 1)
|
|
56
|
+
await sleep(intervalMs);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const outcome = operationOutcome(op);
|
|
60
|
+
if (outcome.status === 'error')
|
|
61
|
+
throw new Error(`${label} 실패: ${outcome.message}`);
|
|
62
|
+
if (outcome.status === 'done')
|
|
63
|
+
return;
|
|
64
|
+
if (attempt < maxAttempts - 1)
|
|
65
|
+
await sleep(intervalMs);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`${label}이(가) 시간 내에 끝나지 않았습니다 — 나중에 firebase_get_project 로 상태를 확인하세요.`);
|
|
68
|
+
}
|
|
69
|
+
/** 방금 만든 프로젝트를 곧바로 조회할 때의 짧은 전파 지연(propagation lag)을 흡수하는 재시도. */
|
|
70
|
+
async function getProjectWithRetry(auth, projectId, attempts = 3, delayMs = 1500) {
|
|
71
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
return await getProject(auth, projectId);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (attempt === attempts - 1)
|
|
77
|
+
throw err;
|
|
78
|
+
await sleep(delayMs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error('unreachable');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 새 GCP 프로젝트를 만들고 Firebase를 추가한다 (앱 하나당 전용 Firebase 프로젝트 컨벤션).
|
|
85
|
+
* 둘 다 long-running operation이라 완료까지 폴링한다(최악 케이스 ~2분: 단계당 ~60s × 2단계).
|
|
86
|
+
* parent 생략 시 계정의 기본 정책대로 생성된다 — 조직 소속이 강제된 계정은 parent
|
|
87
|
+
* ('organizations/<id>' 또는 'folders/<id>') 없이 호출하면 에러가 난다.
|
|
88
|
+
*/
|
|
89
|
+
export async function createProject(auth, projectId, displayName, opts = {}) {
|
|
90
|
+
const crm = google.cloudresourcemanager('v3');
|
|
91
|
+
const createRes = await crm.projects.create({
|
|
92
|
+
auth,
|
|
93
|
+
requestBody: { projectId, displayName, parent: opts.parent },
|
|
94
|
+
});
|
|
95
|
+
const createOpName = createRes.data.name;
|
|
96
|
+
if (!createOpName)
|
|
97
|
+
throw new Error('GCP 프로젝트 생성 응답에 operation name이 없습니다.');
|
|
98
|
+
await waitForOperation(async () => (await crm.operations.get({ auth, name: createOpName })).data, 'GCP 프로젝트 생성');
|
|
99
|
+
// 이 지점부터는 GCP 프로젝트가 이미 존재한다 — 아래서 실패하면 "다른 projectId로 재시도"가
|
|
100
|
+
// 아니라 "같은 projectId로 복구"를 안내해야 한다(안 그러면 고아 프로젝트가 계속 쌓일 수 있음).
|
|
101
|
+
try {
|
|
102
|
+
const fb = google.firebase('v1beta1');
|
|
103
|
+
const addRes = await fb.projects.addFirebase({ auth, project: `projects/${projectId}` });
|
|
104
|
+
const addOpName = addRes.data.name;
|
|
105
|
+
if (!addOpName)
|
|
106
|
+
throw new Error('Firebase 추가 응답에 operation name이 없습니다.');
|
|
107
|
+
await waitForOperation(async () => (await fb.operations.get({ auth, name: addOpName })).data, 'Firebase 추가');
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (err && typeof err === 'object') {
|
|
111
|
+
err.partialFailureNote = [
|
|
112
|
+
`⚠️ GCP 프로젝트 \`${projectId}\`는 이미 생성됐습니다.`,
|
|
113
|
+
'→ 다른 projectId로 재시도하지 말고, 원인을 해결한 뒤 같은 projectId로 다시 시도하거나',
|
|
114
|
+
`firebase_get_project("${projectId}")로 상태를 확인하세요.`,
|
|
115
|
+
].join(' ');
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
return getProjectWithRetry(auth, projectId);
|
|
120
|
+
}
|
|
22
121
|
// ─── Android 앱 ───
|
|
23
122
|
export async function listAndroidApps(auth, projectId) {
|
|
24
123
|
const res = await google.firebase('v1beta1').projects.androidApps.list({
|
package/dist/index.js
CHANGED
|
@@ -1,52 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import {
|
|
6
|
-
import { registerAdmobTools } from './registers/admob.js';
|
|
7
|
-
import { registerPlaystoreTools } from './registers/playstore.js';
|
|
8
|
-
import { registerIamTools } from './registers/iam.js';
|
|
9
|
-
import { registerAppstoreTools } from './registers/appstore.js';
|
|
10
|
-
import { registerChecksTools } from './registers/checks.js';
|
|
11
|
-
import { registerAiTools } from './registers/ai.js';
|
|
12
|
-
import { registerBigqueryTools } from './registers/bigquery.js';
|
|
13
|
-
import { registerAuthTools } from './registers/auth.js';
|
|
14
|
-
import { registerCiTools } from './registers/ci.js';
|
|
15
|
-
import { registerInstagramTools } from './registers/instagram.js';
|
|
16
|
-
import { registerFacebookTools } from './registers/facebook.js';
|
|
17
|
-
import { registerGoogleAdsTools } from './registers/googleads.js';
|
|
18
|
-
import { registerGscTools } from './registers/gsc.js';
|
|
19
|
-
import { registerGa4Tools } from './registers/ga4.js';
|
|
20
|
-
import { registerJenkinsTools } from './registers/jenkins.js';
|
|
21
|
-
import { registerAndroidTools } from './registers/android.js';
|
|
22
|
-
import { registerPrompts } from './prompts.js';
|
|
23
|
-
import { registerResources } from './resources.js';
|
|
4
|
+
import { buildServer } from './server.js';
|
|
24
5
|
// dist/index.js 기준 ../package.json — npm 패키지 루트의 버전을 단일 출처로 사용.
|
|
25
6
|
// 하드코딩하면 publish 때마다 serverInfo.version 이 드리프트하므로 런타임에 읽는다.
|
|
26
7
|
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
27
|
-
const server = new McpServer({
|
|
28
|
-
name: 'mimi-seed',
|
|
29
|
-
version,
|
|
30
|
-
});
|
|
31
|
-
registerFirebaseTools(server);
|
|
32
|
-
registerAdmobTools(server);
|
|
33
|
-
registerPlaystoreTools(server);
|
|
34
|
-
registerIamTools(server);
|
|
35
|
-
registerAppstoreTools(server);
|
|
36
|
-
registerChecksTools(server);
|
|
37
|
-
registerAiTools(server);
|
|
38
|
-
registerBigqueryTools(server);
|
|
39
|
-
registerAuthTools(server);
|
|
40
|
-
registerCiTools(server);
|
|
41
|
-
registerInstagramTools(server);
|
|
42
|
-
registerFacebookTools(server);
|
|
43
|
-
registerGoogleAdsTools(server);
|
|
44
|
-
registerGscTools(server);
|
|
45
|
-
registerGa4Tools(server);
|
|
46
|
-
registerJenkinsTools(server);
|
|
47
|
-
registerAndroidTools(server);
|
|
48
|
-
registerPrompts(server);
|
|
49
|
-
registerResources(server);
|
|
50
8
|
// `npx -y @yoonion/mimi-seed-mcp <subcommand>` 처리.
|
|
51
9
|
// npx는 스코프 패키지의 basename(`mimi-seed-mcp`)을 매치해 이 bin을 실행하므로,
|
|
52
10
|
// 추가 인자(`mimi-seed-auth` 등)는 여기 argv로 흘러들어온다. 이전엔 MCP 서버가
|
|
@@ -80,6 +38,7 @@ async function main() {
|
|
|
80
38
|
].join('\n'));
|
|
81
39
|
process.exit(2);
|
|
82
40
|
}
|
|
41
|
+
const server = buildServer(version);
|
|
83
42
|
const transport = new StdioServerTransport();
|
|
84
43
|
await server.connect(transport);
|
|
85
44
|
}
|
|
@@ -67,6 +67,15 @@ export function withCause(err, cause) {
|
|
|
67
67
|
err.cause = cause;
|
|
68
68
|
return err;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* 일부 호출자(예: firebase_create_project)는 "이미 부분적으로 완료된 작업이 있다"는
|
|
72
|
+
* 사실을 에러 객체에 `partialFailureNote`로 남겨둔다 — 어떤 분기로 친절화되든
|
|
73
|
+
* (또는 인식 못 해 원본 그대로 통과하든) 그 경고가 사라지지 않도록 항상 앞에 붙인다.
|
|
74
|
+
*/
|
|
75
|
+
function withPartialNote(e, message) {
|
|
76
|
+
const note = e?.partialFailureNote;
|
|
77
|
+
return note ? `${note}\n\n${message}` : message;
|
|
78
|
+
}
|
|
70
79
|
/**
|
|
71
80
|
* Firebase / GCP 에러를 친절 메시지로 변환. 신규 사용자가 가장 자주 만나는
|
|
72
81
|
* 'API 미활성화 / 프로젝트 없음 / billing / 권한' 케이스에 다음 행동을 붙인다.
|
|
@@ -76,22 +85,28 @@ export function friendlyGoogleError(e) {
|
|
|
76
85
|
const status = extractHttpStatus(e);
|
|
77
86
|
const reauth = authReauthMessage(text);
|
|
78
87
|
if (reauth)
|
|
79
|
-
return withCause(new Error(reauth), e);
|
|
88
|
+
return withCause(new Error(withPartialNote(e, reauth)), e);
|
|
80
89
|
if (/SERVICE_DISABLED|has not been used in project|is disabled|accessNotConfigured/i.test(text)) {
|
|
81
90
|
const url = extractActivationUrl(text);
|
|
82
|
-
return withCause(new Error([
|
|
91
|
+
return withCause(new Error(withPartialNote(e, [
|
|
83
92
|
'❌ 필요한 Google API가 비활성화돼 있어요.',
|
|
84
93
|
url ? `→ 활성화: ${url}` : '→ Google Cloud Console에서 해당 API를 활성화한 뒤 다시 시도하세요.',
|
|
85
|
-
].join('\n')), e);
|
|
94
|
+
].join('\n'))), e);
|
|
86
95
|
}
|
|
87
96
|
if (/BILLING_DISABLED|billing.*(disabled|not enabled|required)/i.test(text)) {
|
|
88
|
-
return withCause(new Error('❌ 결제(billing)가 비활성화된 프로젝트예요.\n→ Google Cloud Console → 결제 에서 결제 계정을 연결하세요.'), e);
|
|
97
|
+
return withCause(new Error(withPartialNote(e, '❌ 결제(billing)가 비활성화된 프로젝트예요.\n→ Google Cloud Console → 결제 에서 결제 계정을 연결하세요.')), e);
|
|
98
|
+
}
|
|
99
|
+
if (/ALREADY_EXISTS|already exists|already in use/i.test(text)) {
|
|
100
|
+
return withCause(new Error(withPartialNote(e, '❌ 이미 사용 중인 ID예요 (GCP 프로젝트 ID는 전역에서 유일해야 함).\n→ 다른 projectId로 다시 시도하거나, firebase_get_project 로 소유권을 확인하세요.')), e);
|
|
89
101
|
}
|
|
90
102
|
if (status === 404 || /NOT_FOUND|not found/i.test(text)) {
|
|
91
|
-
return withCause(new Error('❌ 프로젝트/리소스를 찾을 수 없어요.\n→ firebase_list_projects 로 유효한 projectId를 확인하세요.'), e);
|
|
103
|
+
return withCause(new Error(withPartialNote(e, '❌ 프로젝트/리소스를 찾을 수 없어요.\n→ firebase_list_projects 로 유효한 projectId를 확인하세요.')), e);
|
|
92
104
|
}
|
|
93
105
|
if (status === 403 || /PERMISSION_DENIED|forbidden|permission/i.test(text)) {
|
|
94
|
-
return withCause(new Error('❌ 권한 부족 — 이 Google 계정이 해당 프로젝트에 접근 권한이 있는지 확인하세요.'), e);
|
|
106
|
+
return withCause(new Error(withPartialNote(e, '❌ 권한 부족 — 이 Google 계정이 해당 프로젝트에 접근 권한이 있는지 확인하세요.')), e);
|
|
95
107
|
}
|
|
108
|
+
const note = e?.partialFailureNote;
|
|
109
|
+
if (note)
|
|
110
|
+
return withCause(new Error(`${note}\n\n${text}`), e);
|
|
96
111
|
return e instanceof Error ? e : new Error(text);
|
|
97
112
|
}
|
|
@@ -37,6 +37,43 @@ export function registerFirebaseTools(server) {
|
|
|
37
37
|
const project = await firebase.getProject(auth, projectId);
|
|
38
38
|
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
39
39
|
});
|
|
40
|
+
server.tool('firebase_create_project', [
|
|
41
|
+
'새 GCP 프로젝트를 만들고 Firebase를 추가한다 (앱 하나당 전용 Firebase 프로젝트 컨벤션 — 기존 프로젝트에',
|
|
42
|
+
'앱만 추가하려면 firebase_create_android_app/firebase_create_ios_app 사용).',
|
|
43
|
+
'두 개의 long-running operation(프로젝트 생성 → Firebase 추가)을 순서대로 완료까지 폴링하므로 최악의 경우 2분 정도 걸릴 수 있다.',
|
|
44
|
+
'projectId는 6-30자, 소문자로 시작, 소문자/숫자/하이픈만 가능(끝에 하이픈 금지).',
|
|
45
|
+
].join(' '), {
|
|
46
|
+
projectId: z
|
|
47
|
+
.string()
|
|
48
|
+
.regex(/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/, '소문자로 시작, 소문자/숫자/하이픈 6-30자 (끝에 하이픈 불가)')
|
|
49
|
+
.describe('새로 만들 GCP/Firebase 프로젝트 ID (예: pryzm-penguinrun)'),
|
|
50
|
+
displayName: z.string().describe('프로젝트 표시 이름'),
|
|
51
|
+
parent: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("조직/폴더 소속이 필요한 계정일 때만: 'organizations/<id>' 또는 'folders/<id>'. 생략 시 계정 기본 정책대로 생성"),
|
|
55
|
+
}, async ({ projectId, displayName, parent }) => {
|
|
56
|
+
const auth = await requireAuth();
|
|
57
|
+
const project = await firebase.createProject(auth, projectId, displayName, { parent });
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: [
|
|
63
|
+
`✓ Firebase 프로젝트 생성 완료: \`${project.projectId}\``,
|
|
64
|
+
'',
|
|
65
|
+
`**displayName**: ${project.displayName}`,
|
|
66
|
+
`**projectNumber**: ${project.projectNumber}`,
|
|
67
|
+
'',
|
|
68
|
+
'다음 단계:',
|
|
69
|
+
`1. firebase_create_android_app("${project.projectId}", packageName, displayName)`,
|
|
70
|
+
`2. firebase_create_ios_app("${project.projectId}", bundleId, displayName)`,
|
|
71
|
+
'3. firebase_get_android_config / firebase_get_ios_config 로 설정 파일 다운로드',
|
|
72
|
+
].join('\n'),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
});
|
|
40
77
|
server.tool('firebase_list_android_apps', 'Firebase 프로젝트의 Android 앱 목록', { projectId: z.string().describe('Firebase 프로젝트 ID') }, async ({ projectId }) => {
|
|
41
78
|
const auth = await requireAuth();
|
|
42
79
|
const apps = await firebase.listAndroidApps(auth, projectId);
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
/**
|
|
3
|
+
* 서버 조립 단일 지점 — stdio 엔트리(index.ts)와 tool-manifest 스모크 테스트
|
|
4
|
+
* (src/__tests__/tool-manifest.test.ts)가 공유한다. 새 register 모듈은 반드시
|
|
5
|
+
* 여기(index.ts 가 아니라)에 추가해야 테스트가 tool-manifest.json 과의 드리프트를 잡는다.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildServer(version: string): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { registerFirebaseTools } from './registers/firebase.js';
|
|
3
|
+
import { registerAdmobTools } from './registers/admob.js';
|
|
4
|
+
import { registerPlaystoreTools } from './registers/playstore.js';
|
|
5
|
+
import { registerIamTools } from './registers/iam.js';
|
|
6
|
+
import { registerAppstoreTools } from './registers/appstore.js';
|
|
7
|
+
import { registerChecksTools } from './registers/checks.js';
|
|
8
|
+
import { registerAiTools } from './registers/ai.js';
|
|
9
|
+
import { registerBigqueryTools } from './registers/bigquery.js';
|
|
10
|
+
import { registerAuthTools } from './registers/auth.js';
|
|
11
|
+
import { registerCiTools } from './registers/ci.js';
|
|
12
|
+
import { registerInstagramTools } from './registers/instagram.js';
|
|
13
|
+
import { registerFacebookTools } from './registers/facebook.js';
|
|
14
|
+
import { registerGoogleAdsTools } from './registers/googleads.js';
|
|
15
|
+
import { registerGscTools } from './registers/gsc.js';
|
|
16
|
+
import { registerGa4Tools } from './registers/ga4.js';
|
|
17
|
+
import { registerJenkinsTools } from './registers/jenkins.js';
|
|
18
|
+
import { registerAndroidTools } from './registers/android.js';
|
|
19
|
+
import { registerPrompts } from './prompts.js';
|
|
20
|
+
import { registerResources } from './resources.js';
|
|
21
|
+
/**
|
|
22
|
+
* 서버 조립 단일 지점 — stdio 엔트리(index.ts)와 tool-manifest 스모크 테스트
|
|
23
|
+
* (src/__tests__/tool-manifest.test.ts)가 공유한다. 새 register 모듈은 반드시
|
|
24
|
+
* 여기(index.ts 가 아니라)에 추가해야 테스트가 tool-manifest.json 과의 드리프트를 잡는다.
|
|
25
|
+
*/
|
|
26
|
+
export function buildServer(version) {
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: 'mimi-seed',
|
|
29
|
+
version,
|
|
30
|
+
});
|
|
31
|
+
registerFirebaseTools(server);
|
|
32
|
+
registerAdmobTools(server);
|
|
33
|
+
registerPlaystoreTools(server);
|
|
34
|
+
registerIamTools(server);
|
|
35
|
+
registerAppstoreTools(server);
|
|
36
|
+
registerChecksTools(server);
|
|
37
|
+
registerAiTools(server);
|
|
38
|
+
registerBigqueryTools(server);
|
|
39
|
+
registerAuthTools(server);
|
|
40
|
+
registerCiTools(server);
|
|
41
|
+
registerInstagramTools(server);
|
|
42
|
+
registerFacebookTools(server);
|
|
43
|
+
registerGoogleAdsTools(server);
|
|
44
|
+
registerGscTools(server);
|
|
45
|
+
registerGa4Tools(server);
|
|
46
|
+
registerJenkinsTools(server);
|
|
47
|
+
registerAndroidTools(server);
|
|
48
|
+
registerPrompts(server);
|
|
49
|
+
registerResources(server);
|
|
50
|
+
return server;
|
|
51
|
+
}
|
package/package.json
CHANGED