@yoonion/mimi-seed-mcp 0.6.0 → 0.6.1
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/firebase/tools.d.ts +37 -0
- package/dist/firebase/tools.js +99 -0
- package/dist/lib/google-errors.js +21 -6
- package/dist/registers/firebase.js +37 -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/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({
|
|
@@ -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/package.json
CHANGED