@yoonion/mimi-seed-mcp 0.4.0 → 0.6.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.
- package/README.md +11 -5
- package/dist/jenkins/credentials.js +6 -29
- package/dist/jenkins/http.d.ts +22 -0
- package/dist/jenkins/http.js +67 -0
- package/dist/jenkins/jobs.d.ts +28 -0
- package/dist/jenkins/jobs.js +80 -0
- package/dist/lib/project-manifest.d.ts +36 -0
- package/dist/lib/project-manifest.js +52 -0
- package/dist/registers/auth.js +88 -5
- package/dist/registers/jenkins.js +75 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -62,24 +62,30 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
|
-
## 제공 도구 (
|
|
65
|
+
## 제공 도구 (147개 · 17개 영역)
|
|
66
66
|
|
|
67
67
|
| 영역 | 도구 수 | 주요 도구 |
|
|
68
68
|
|------|---------|-----------|
|
|
69
|
-
| App Store Connect |
|
|
70
|
-
| Google Play |
|
|
71
|
-
| Firebase |
|
|
69
|
+
| App Store Connect | 31 | `appstore_submit_for_review` / `appstore_upload_screenshot` / `appstore_update_whats_new` / `appstore_create_version` |
|
|
70
|
+
| Google Play | 28 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
|
|
71
|
+
| Firebase | 19 | `firebase_create_android_app` / `firebase_get_android_config` / `firebase_create_ios_app` / `firebase_enable_service` |
|
|
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
|
+
| Jenkins (크리덴셜) | 6 | `jenkins_create_credential` / `jenkins_upload_keystore` / `jenkins_save_config` |
|
|
75
|
+
| GA4 | 6 | `ga4_create_property` / `ga4_create_data_stream` / `ga4_run_report` |
|
|
76
|
+
| Search Console | 6 | `gsc_inspect_url` / `gsc_search_analytics` / `gsc_submit_sitemap` |
|
|
77
|
+
| Google Ads | 6 | `googleads_list_campaigns` / `googleads_get_uac_report` / `googleads_get_campaign_report` |
|
|
74
78
|
| Facebook | 6 | `facebook_post_photo` / `facebook_post_multi_photo` / `facebook_list_pages` |
|
|
75
79
|
| Google Cloud IAM | 5 | `iam_create_service_account` / `iam_create_key` / `iam_add_iam_policy_binding` |
|
|
76
80
|
| BigQuery | 5 | `bigquery_run_query` / `bigquery_list_datasets` / `bigquery_get_table_schema` |
|
|
77
81
|
| 점검 / 위험 | 4 | `playstore_check_submission_risks` / `appstore_check_submission_risks` / `screenshot_validate` / `release_status` |
|
|
78
82
|
| Instagram | 4 | `instagram_post_image` / `instagram_post_carousel` / `instagram_save_config` |
|
|
83
|
+
| Android 서명 | 3 | `android_signing_setup` / `android_generate_keystore` / `jenkins_upload_playstore_sa` |
|
|
84
|
+
| 인증 | 3 | `mimi_seed_status` / `mimi_seed_auth_start` / `mimi_seed_auth_status` |
|
|
79
85
|
| AI (Claude) | 2 | `generate_release_notes_from_commits` / `generate_review_reply` |
|
|
80
|
-
| 인증 | 2 | `mimi_seed_auth_start` / `mimi_seed_auth_status` |
|
|
81
86
|
|
|
82
87
|
> 인앱 결제(IAP·구독) 도구는 위 Play Store·App Store 카운트에 포함됩니다 — `playstore_create_subscription` · `appstore_create_inapp_purchase` 등 (`@onesub/providers` 위임).
|
|
88
|
+
> 전체 카탈로그(항상 최신): [`docs/domain/tool-catalog.md`](../../docs/domain/tool-catalog.md)
|
|
83
89
|
|
|
84
90
|
---
|
|
85
91
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { authHeaders, getCrumb } from './http.js';
|
|
1
2
|
const FILE_CLASS = 'org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl';
|
|
2
3
|
const TEXT_CLASS = 'org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl';
|
|
3
|
-
function basicAuth(username, token) {
|
|
4
|
-
return 'Basic ' + Buffer.from(`${username}:${token}`).toString('base64');
|
|
5
|
-
}
|
|
6
4
|
// 도메인(_ = 전역) 레벨 — 목록 조회 / createCredentials 의 베이스
|
|
7
5
|
function storeBase(url) {
|
|
8
6
|
return `${url.replace(/\/$/, '')}/credentials/store/system/domain/_`;
|
|
@@ -11,36 +9,15 @@ function storeBase(url) {
|
|
|
11
9
|
function credentialBase(url, id) {
|
|
12
10
|
return `${storeBase(url)}/credential/${encodeURIComponent(id)}`;
|
|
13
11
|
}
|
|
14
|
-
/**
|
|
15
|
-
* CSRF crumb (best-effort). crumb issuer 비활성이거나 API 토큰으로 면제되면 빈 객체.
|
|
16
|
-
* 구버전 Jenkins / 비밀번호 인증 환경에서 POST 403 방지.
|
|
17
|
-
*/
|
|
18
|
-
async function getCrumb(cfg) {
|
|
19
|
-
try {
|
|
20
|
-
const res = await fetch(`${cfg.url.replace(/\/$/, '')}/crumbIssuer/api/json`, {
|
|
21
|
-
headers: { Authorization: basicAuth(cfg.username, cfg.token) },
|
|
22
|
-
});
|
|
23
|
-
if (!res.ok)
|
|
24
|
-
return {};
|
|
25
|
-
const data = (await res.json());
|
|
26
|
-
if (data.crumbRequestField && data.crumb) {
|
|
27
|
-
return { [data.crumbRequestField]: data.crumb };
|
|
28
|
-
}
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
12
|
async function credentialExists(cfg, id) {
|
|
36
13
|
const res = await fetch(`${credentialBase(cfg.url, id)}/api/json`, {
|
|
37
|
-
headers:
|
|
14
|
+
headers: authHeaders(cfg),
|
|
38
15
|
});
|
|
39
16
|
return res.ok;
|
|
40
17
|
}
|
|
41
18
|
export async function listCredentials(cfg) {
|
|
42
19
|
const res = await fetch(`${storeBase(cfg.url)}/api/json?depth=1`, {
|
|
43
|
-
headers:
|
|
20
|
+
headers: authHeaders(cfg),
|
|
44
21
|
});
|
|
45
22
|
if (!res.ok)
|
|
46
23
|
throw new Error(`Jenkins credentials 조회 실패 (${res.status})`);
|
|
@@ -70,7 +47,7 @@ export async function upsertSecretText(cfg, id, secret, description = '') {
|
|
|
70
47
|
const res = await fetch(endpoint, {
|
|
71
48
|
method: 'POST',
|
|
72
49
|
headers: {
|
|
73
|
-
|
|
50
|
+
...authHeaders(cfg),
|
|
74
51
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
75
52
|
...crumb,
|
|
76
53
|
},
|
|
@@ -109,7 +86,7 @@ export async function upsertSecretFile(cfg, id, fileBase64, fileName, descriptio
|
|
|
109
86
|
const res = await fetch(endpoint, {
|
|
110
87
|
method: 'POST',
|
|
111
88
|
headers: {
|
|
112
|
-
|
|
89
|
+
...authHeaders(cfg),
|
|
113
90
|
...crumb,
|
|
114
91
|
},
|
|
115
92
|
body: form,
|
|
@@ -124,7 +101,7 @@ export async function deleteCredential(cfg, id) {
|
|
|
124
101
|
const res = await fetch(`${credentialBase(cfg.url, id)}/doDelete`, {
|
|
125
102
|
method: 'POST',
|
|
126
103
|
headers: {
|
|
127
|
-
|
|
104
|
+
...authHeaders(cfg),
|
|
128
105
|
...crumb,
|
|
129
106
|
},
|
|
130
107
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { JenkinsConfig } from './config.js';
|
|
2
|
+
export declare function basicAuth(username: string, token: string): string;
|
|
3
|
+
export declare function baseUrl(cfg: JenkinsConfig): string;
|
|
4
|
+
export declare function authHeaders(cfg: JenkinsConfig): Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* CSRF crumb (best-effort). crumb issuer 비활성이거나 API 토큰으로 면제되면 빈 객체.
|
|
7
|
+
* 구버전 Jenkins / 비밀번호 인증 환경에서 POST 403 방지.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getCrumb(cfg: JenkinsConfig): Promise<Record<string, string>>;
|
|
10
|
+
/**
|
|
11
|
+
* 잡 경로를 Jenkins URL 로 바꾼다. 폴더는 `/` 로 구분한다.
|
|
12
|
+
* "penguinrun" -> <base>/job/penguinrun
|
|
13
|
+
* "vir-game/client" -> <base>/job/vir-game/job/client
|
|
14
|
+
*/
|
|
15
|
+
export declare function jobUrl(cfg: JenkinsConfig, jobPath: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* createItem 은 "부모 컨테이너" 에 POST 하고 leaf 이름을 쿼리로 넘긴다.
|
|
18
|
+
* 루트 잡이면 부모가 Jenkins 루트, 폴더 안이면 폴더 URL.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createItemUrl(cfg: JenkinsConfig, jobPath: string): string;
|
|
21
|
+
/** 실패 응답의 본문 일부를 붙여 에러를 만든다. Jenkins 는 원인을 본문에만 담는 경우가 많다. */
|
|
22
|
+
export declare function jenkinsError(prefix: string, res: Response): Promise<Error>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function basicAuth(username, token) {
|
|
2
|
+
return 'Basic ' + Buffer.from(`${username}:${token}`).toString('base64');
|
|
3
|
+
}
|
|
4
|
+
export function baseUrl(cfg) {
|
|
5
|
+
return cfg.url.replace(/\/$/, '');
|
|
6
|
+
}
|
|
7
|
+
export function authHeaders(cfg) {
|
|
8
|
+
return { Authorization: basicAuth(cfg.username, cfg.token) };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* CSRF crumb (best-effort). crumb issuer 비활성이거나 API 토큰으로 면제되면 빈 객체.
|
|
12
|
+
* 구버전 Jenkins / 비밀번호 인증 환경에서 POST 403 방지.
|
|
13
|
+
*/
|
|
14
|
+
export async function getCrumb(cfg) {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${baseUrl(cfg)}/crumbIssuer/api/json`, {
|
|
17
|
+
headers: authHeaders(cfg),
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
return {};
|
|
21
|
+
const data = (await res.json());
|
|
22
|
+
if (data.crumbRequestField && data.crumb) {
|
|
23
|
+
return { [data.crumbRequestField]: data.crumb };
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 잡 경로를 Jenkins URL 로 바꾼다. 폴더는 `/` 로 구분한다.
|
|
33
|
+
* "penguinrun" -> <base>/job/penguinrun
|
|
34
|
+
* "vir-game/client" -> <base>/job/vir-game/job/client
|
|
35
|
+
*/
|
|
36
|
+
export function jobUrl(cfg, jobPath) {
|
|
37
|
+
const segments = jobPath.split('/').filter(Boolean);
|
|
38
|
+
if (segments.length === 0)
|
|
39
|
+
throw new Error('잡 이름이 비어 있습니다.');
|
|
40
|
+
const suffix = segments.map((s) => `job/${encodeURIComponent(s)}`).join('/');
|
|
41
|
+
return `${baseUrl(cfg)}/${suffix}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* createItem 은 "부모 컨테이너" 에 POST 하고 leaf 이름을 쿼리로 넘긴다.
|
|
45
|
+
* 루트 잡이면 부모가 Jenkins 루트, 폴더 안이면 폴더 URL.
|
|
46
|
+
*/
|
|
47
|
+
export function createItemUrl(cfg, jobPath) {
|
|
48
|
+
const segments = jobPath.split('/').filter(Boolean);
|
|
49
|
+
if (segments.length === 0)
|
|
50
|
+
throw new Error('잡 이름이 비어 있습니다.');
|
|
51
|
+
const leaf = segments.pop();
|
|
52
|
+
const parent = segments.length
|
|
53
|
+
? `${baseUrl(cfg)}/${segments.map((s) => `job/${encodeURIComponent(s)}`).join('/')}`
|
|
54
|
+
: baseUrl(cfg);
|
|
55
|
+
return `${parent}/createItem?name=${encodeURIComponent(leaf)}`;
|
|
56
|
+
}
|
|
57
|
+
/** 실패 응답의 본문 일부를 붙여 에러를 만든다. Jenkins 는 원인을 본문에만 담는 경우가 많다. */
|
|
58
|
+
export async function jenkinsError(prefix, res) {
|
|
59
|
+
let detail = '';
|
|
60
|
+
try {
|
|
61
|
+
detail = (await res.text()).trim().slice(0, 300);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// 본문 없음
|
|
65
|
+
}
|
|
66
|
+
return new Error(`${prefix} (${res.status})${detail ? `: ${detail}` : ''}`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JenkinsConfig } from './config.js';
|
|
2
|
+
export interface JenkinsJobSummary {
|
|
3
|
+
name: string;
|
|
4
|
+
url: string;
|
|
5
|
+
/** 잡 상태 색상. 폴더는 색이 없다. */
|
|
6
|
+
color?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 잡 목록. folder 를 주면 그 폴더 안을, 없으면 루트를 조회한다.
|
|
10
|
+
* 재귀하지 않는다 (한 단계만).
|
|
11
|
+
*/
|
|
12
|
+
export declare function listJobs(cfg: JenkinsConfig, folder?: string): Promise<JenkinsJobSummary[]>;
|
|
13
|
+
/**
|
|
14
|
+
* 404 만 "없음" 으로 본다. 401/403/500 을 없음으로 삼키면 upsertJob 이 엉뚱하게
|
|
15
|
+
* 생성 분기를 타므로, 그 경우는 원인을 드러내며 throw 한다.
|
|
16
|
+
*/
|
|
17
|
+
export declare function jobExists(cfg: JenkinsConfig, jobPath: string): Promise<boolean>;
|
|
18
|
+
/** 잡의 config.xml 원문. 백업하거나 수정 전 현재 상태를 확인할 때 쓴다. */
|
|
19
|
+
export declare function getJobConfig(cfg: JenkinsConfig, jobPath: string): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* 새 잡 생성. 같은 이름이 이미 있으면 Jenkins 가 400 을 준다.
|
|
22
|
+
* 폴더 안에 만들려면 jobPath 에 "folder/name" 을 넘긴다 (폴더는 미리 존재해야 함).
|
|
23
|
+
*/
|
|
24
|
+
export declare function createJob(cfg: JenkinsConfig, jobPath: string, configXml: string): Promise<void>;
|
|
25
|
+
/** 기존 잡의 config.xml 교체. 잡이 없으면 404. */
|
|
26
|
+
export declare function updateJob(cfg: JenkinsConfig, jobPath: string, configXml: string): Promise<void>;
|
|
27
|
+
/** 있으면 수정, 없으면 생성. Jenkinsfile/job-config.xml 을 레포에서 밀어넣을 때 쓴다. */
|
|
28
|
+
export declare function upsertJob(cfg: JenkinsConfig, jobPath: string, configXml: string): Promise<'created' | 'updated'>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { authHeaders, baseUrl, createItemUrl, getCrumb, jenkinsError, jobUrl } from './http.js';
|
|
2
|
+
/**
|
|
3
|
+
* 잡 목록. folder 를 주면 그 폴더 안을, 없으면 루트를 조회한다.
|
|
4
|
+
* 재귀하지 않는다 (한 단계만).
|
|
5
|
+
*/
|
|
6
|
+
export async function listJobs(cfg, folder) {
|
|
7
|
+
const root = folder ? jobUrl(cfg, folder) : baseUrl(cfg);
|
|
8
|
+
const res = await fetch(`${root}/api/json?tree=jobs[name,url,color]`, {
|
|
9
|
+
headers: authHeaders(cfg),
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok)
|
|
12
|
+
throw await jenkinsError('Jenkins 잡 목록 조회 실패', res);
|
|
13
|
+
const data = (await res.json());
|
|
14
|
+
return data.jobs ?? [];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 404 만 "없음" 으로 본다. 401/403/500 을 없음으로 삼키면 upsertJob 이 엉뚱하게
|
|
18
|
+
* 생성 분기를 타므로, 그 경우는 원인을 드러내며 throw 한다.
|
|
19
|
+
*/
|
|
20
|
+
export async function jobExists(cfg, jobPath) {
|
|
21
|
+
const res = await fetch(`${jobUrl(cfg, jobPath)}/api/json?tree=name`, {
|
|
22
|
+
headers: authHeaders(cfg),
|
|
23
|
+
});
|
|
24
|
+
if (res.ok)
|
|
25
|
+
return true;
|
|
26
|
+
if (res.status === 404)
|
|
27
|
+
return false;
|
|
28
|
+
throw await jenkinsError(`Jenkins 잡 존재 확인 실패: ${jobPath}`, res);
|
|
29
|
+
}
|
|
30
|
+
/** 잡의 config.xml 원문. 백업하거나 수정 전 현재 상태를 확인할 때 쓴다. */
|
|
31
|
+
export async function getJobConfig(cfg, jobPath) {
|
|
32
|
+
const res = await fetch(`${jobUrl(cfg, jobPath)}/config.xml`, {
|
|
33
|
+
headers: authHeaders(cfg),
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
throw await jenkinsError(`Jenkins 잡 config 조회 실패: ${jobPath}`, res);
|
|
37
|
+
return res.text();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 새 잡 생성. 같은 이름이 이미 있으면 Jenkins 가 400 을 준다.
|
|
41
|
+
* 폴더 안에 만들려면 jobPath 에 "folder/name" 을 넘긴다 (폴더는 미리 존재해야 함).
|
|
42
|
+
*/
|
|
43
|
+
export async function createJob(cfg, jobPath, configXml) {
|
|
44
|
+
const crumb = await getCrumb(cfg);
|
|
45
|
+
const res = await fetch(createItemUrl(cfg, jobPath), {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
...authHeaders(cfg),
|
|
49
|
+
'Content-Type': 'application/xml',
|
|
50
|
+
...crumb,
|
|
51
|
+
},
|
|
52
|
+
body: configXml,
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw await jenkinsError(`Jenkins 잡 생성 실패: ${jobPath}`, res);
|
|
56
|
+
}
|
|
57
|
+
/** 기존 잡의 config.xml 교체. 잡이 없으면 404. */
|
|
58
|
+
export async function updateJob(cfg, jobPath, configXml) {
|
|
59
|
+
const crumb = await getCrumb(cfg);
|
|
60
|
+
const res = await fetch(`${jobUrl(cfg, jobPath)}/config.xml`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
...authHeaders(cfg),
|
|
64
|
+
'Content-Type': 'application/xml',
|
|
65
|
+
...crumb,
|
|
66
|
+
},
|
|
67
|
+
body: configXml,
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok)
|
|
70
|
+
throw await jenkinsError(`Jenkins 잡 수정 실패: ${jobPath}`, res);
|
|
71
|
+
}
|
|
72
|
+
/** 있으면 수정, 없으면 생성. Jenkinsfile/job-config.xml 을 레포에서 밀어넣을 때 쓴다. */
|
|
73
|
+
export async function upsertJob(cfg, jobPath, configXml) {
|
|
74
|
+
if (await jobExists(cfg, jobPath)) {
|
|
75
|
+
await updateJob(cfg, jobPath, configXml);
|
|
76
|
+
return 'updated';
|
|
77
|
+
}
|
|
78
|
+
await createJob(cfg, jobPath, configXml);
|
|
79
|
+
return 'created';
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const MANIFEST_FILENAME = ".mimi-seed.json";
|
|
2
|
+
/** 매니페스트가 인지하는 서비스 id. status 매칭 로직이 이 키를 기준으로 동작한다. */
|
|
3
|
+
export type ManifestServiceId = 'oauth' | 'bigquery' | 'playstore' | 'appstore' | 'jenkins';
|
|
4
|
+
export interface ManifestService {
|
|
5
|
+
/** 이 프로젝트에서 필수인지. false(선택)면 미설정이어도 ❌ 대신 정보 표기. */
|
|
6
|
+
required?: boolean;
|
|
7
|
+
/** 사람이 읽을 한 줄 설명. status 출력에 그대로 표시. */
|
|
8
|
+
note?: string;
|
|
9
|
+
projectId?: string;
|
|
10
|
+
dataset?: string;
|
|
11
|
+
packageName?: string;
|
|
12
|
+
keyId?: string;
|
|
13
|
+
issuerId?: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
/** 이 서비스가 워크스페이스 공유 시크릿(provider)으로도 제공되는 경우 그 provider id. */
|
|
16
|
+
workspaceProvider?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ProjectManifest {
|
|
19
|
+
project?: string;
|
|
20
|
+
displayName?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
services?: Partial<Record<ManifestServiceId, ManifestService>>;
|
|
23
|
+
}
|
|
24
|
+
export interface LoadedManifest {
|
|
25
|
+
manifest: ProjectManifest;
|
|
26
|
+
/** 실제로 읽은 파일 절대 경로. */
|
|
27
|
+
filePath: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* startDir 에서 위로 올라가며 첫 번째 `.mimi-seed.json` 을 찾는다.
|
|
31
|
+
* MCP stdio 서버의 cwd 는 보통 프로젝트 루트지만, 하위 폴더에서 실행돼도 잡히게 상향 탐색한다.
|
|
32
|
+
* maxDepth 로 홈 디렉터리 밖까지 무한 상승하는 것을 방지.
|
|
33
|
+
*/
|
|
34
|
+
export declare function findProjectManifest(startDir?: string, maxDepth?: number): LoadedManifest | null;
|
|
35
|
+
/** 매니페스트 services 를 [id, service] 배열로. 없으면 빈 배열. */
|
|
36
|
+
export declare function manifestServiceEntries(m: ProjectManifest): Array<[ManifestServiceId, ManifestService]>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// 프로젝트 매니페스트(.mimi-seed.json) 리더.
|
|
2
|
+
//
|
|
3
|
+
// 프로젝트 루트에 커밋된 `.mimi-seed.json` 이 "이 저장소를 mimi-seed 로 다루려면
|
|
4
|
+
// 어떤 서비스 연결이 필요한가"를 선언한다. mimi_seed_status(MCP)와 mimi-seed doctor(CLI)가
|
|
5
|
+
// 이 파일을 읽어, 범용 9종 스캔 대신 "이 프로젝트에서 너한테 빠진 것 + 정확한 셋업 명령"을 보여준다.
|
|
6
|
+
//
|
|
7
|
+
// 팀 온보딩 목적: 새 팀원이 clone → mimi_seed_status 호출 → 안내 따라가기 만으로
|
|
8
|
+
// 자기 머신에 필요한 자격증명을 채울 수 있게 한다.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
export const MANIFEST_FILENAME = '.mimi-seed.json';
|
|
12
|
+
/**
|
|
13
|
+
* startDir 에서 위로 올라가며 첫 번째 `.mimi-seed.json` 을 찾는다.
|
|
14
|
+
* MCP stdio 서버의 cwd 는 보통 프로젝트 루트지만, 하위 폴더에서 실행돼도 잡히게 상향 탐색한다.
|
|
15
|
+
* maxDepth 로 홈 디렉터리 밖까지 무한 상승하는 것을 방지.
|
|
16
|
+
*/
|
|
17
|
+
export function findProjectManifest(startDir = process.cwd(), maxDepth = 8) {
|
|
18
|
+
let dir = path.resolve(startDir);
|
|
19
|
+
for (let i = 0; i <= maxDepth; i++) {
|
|
20
|
+
const candidate = path.join(dir, MANIFEST_FILENAME);
|
|
21
|
+
if (fs.existsSync(candidate)) {
|
|
22
|
+
const parsed = safeParse(candidate);
|
|
23
|
+
if (parsed)
|
|
24
|
+
return { manifest: parsed, filePath: candidate };
|
|
25
|
+
return null; // 파일은 있으나 형식 오류 — 상위로 더 올라가지 않는다(의도 명확).
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(dir);
|
|
28
|
+
if (parent === dir)
|
|
29
|
+
break; // 루트 도달
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function safeParse(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
37
|
+
const obj = JSON.parse(raw);
|
|
38
|
+
if (obj && typeof obj === 'object')
|
|
39
|
+
return obj;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** 매니페스트 services 를 [id, service] 배열로. 없으면 빈 배열. */
|
|
47
|
+
export function manifestServiceEntries(m) {
|
|
48
|
+
const svc = m.services ?? {};
|
|
49
|
+
return Object.keys(svc)
|
|
50
|
+
.filter((k) => svc[k] != null)
|
|
51
|
+
.map((k) => [k, svc[k]]);
|
|
52
|
+
}
|
package/dist/registers/auth.js
CHANGED
|
@@ -7,7 +7,8 @@ import { loadCiConfig } from '../ci/config.js';
|
|
|
7
7
|
import { loadConfig as loadGoogleAdsConfig } from '../googleads/config.js';
|
|
8
8
|
import { loadFacebookConfig } from '../facebook/config.js';
|
|
9
9
|
import { loadInstagramConfig } from '../instagram/config.js';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveBigQueryAuth } from '../auth/bigquery-auth.js';
|
|
11
|
+
import { findProjectManifest, manifestServiceEntries, } from '../lib/project-manifest.js';
|
|
11
12
|
/**
|
|
12
13
|
* tokens.json mtime → "Nd Hh ago" 형식 문자열 + 재인증 권고.
|
|
13
14
|
* Google 정책: 7일 미사용 시 미인증 앱 refresh_token revoke 가능.
|
|
@@ -34,6 +35,54 @@ function formatLastRefreshHint(lastMs) {
|
|
|
34
35
|
}
|
|
35
36
|
return { label };
|
|
36
37
|
}
|
|
38
|
+
// 매니페스트 서비스별 셋업 명령. status/doctor 가 "정확한 다음 명령"으로 안내한다.
|
|
39
|
+
const MANIFEST_FIX = {
|
|
40
|
+
oauth: () => 'mimi_seed_auth_start (Google 로그인 — Firebase/AdMob/Play/GSC/GA4)',
|
|
41
|
+
bigquery: () => 'npx -y @yoonion/mimi-seed-mcp mimi-seed-bigquery-auth (서비스 계정 권장)',
|
|
42
|
+
playstore: (svc) => svc.packageName
|
|
43
|
+
? `setup_playstore_connection(packageName="${svc.packageName}")`
|
|
44
|
+
: 'setup_playstore_connection(packageName=...)',
|
|
45
|
+
appstore: () => 'npx -y @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
|
|
46
|
+
jenkins: () => 'claude mcp add virgm-jenkins -s user (개인 자격증명 — .mcp.json 금지)',
|
|
47
|
+
};
|
|
48
|
+
/** 서비스별 식별자를 한 줄 detail 로 (예: "ads-coffee / analytics_530080532"). */
|
|
49
|
+
function manifestServiceDetail(id, svc) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (id === 'bigquery') {
|
|
52
|
+
if (svc.projectId)
|
|
53
|
+
parts.push(svc.projectId);
|
|
54
|
+
if (svc.dataset)
|
|
55
|
+
parts.push(svc.dataset);
|
|
56
|
+
}
|
|
57
|
+
else if (id === 'playstore') {
|
|
58
|
+
if (svc.packageName)
|
|
59
|
+
parts.push(svc.packageName);
|
|
60
|
+
}
|
|
61
|
+
else if (id === 'appstore') {
|
|
62
|
+
if (svc.keyId)
|
|
63
|
+
parts.push(`keyId ${svc.keyId}`);
|
|
64
|
+
}
|
|
65
|
+
else if (id === 'jenkins') {
|
|
66
|
+
if (svc.url)
|
|
67
|
+
parts.push(svc.url);
|
|
68
|
+
}
|
|
69
|
+
return parts.join(' / ');
|
|
70
|
+
}
|
|
71
|
+
/** 매니페스트 한 서비스를 status 라인(들)으로 렌더. 미설정+필수면 명령 힌트를 붙인다. */
|
|
72
|
+
function renderManifestLine(id, svc, connected, required) {
|
|
73
|
+
const label = id.padEnd(10);
|
|
74
|
+
const detail = manifestServiceDetail(id, svc);
|
|
75
|
+
const detailSuffix = detail ? ` (${detail})` : '';
|
|
76
|
+
if (connected)
|
|
77
|
+
return `✅ ${label} — 연결됨${detailSuffix}`;
|
|
78
|
+
if (!required) {
|
|
79
|
+
const note = svc.note ? ` — ${svc.note}` : '';
|
|
80
|
+
return `ℹ️ ${label} — (선택)${detailSuffix}${note}`;
|
|
81
|
+
}
|
|
82
|
+
const fix = MANIFEST_FIX[id](svc);
|
|
83
|
+
const noteLine = svc.note ? `\n ${svc.note}` : '';
|
|
84
|
+
return `❌ ${label} — 미설정${detailSuffix}\n → ${fix}${noteLine}`;
|
|
85
|
+
}
|
|
37
86
|
export function registerAuthTools(server) {
|
|
38
87
|
// ── 전체 연결 상태 진단 ────────────────────────────────────────────────────
|
|
39
88
|
server.tool('mimi_seed_status', [
|
|
@@ -114,10 +163,14 @@ export function registerAuthTools(server) {
|
|
|
114
163
|
else {
|
|
115
164
|
lines.push('❌ Instagram — 미설정 → instagram_save_config (선택)');
|
|
116
165
|
}
|
|
117
|
-
// 9. BigQuery
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
166
|
+
// 9. BigQuery — resolveBigQueryAuth 기준(서비스 계정 우선, OAuth fallback).
|
|
167
|
+
// 이전엔 SA 파일만 검사해 OAuth fallback 이 살아있어도 ❌ 로 오표기했다.
|
|
168
|
+
const bqAuth = resolveBigQueryAuth();
|
|
169
|
+
if (bqAuth?.source === 'service-account') {
|
|
170
|
+
lines.push(`✅ BigQuery — 서비스 계정 연결됨 (${bqAuth.clientEmail ?? ''})`);
|
|
171
|
+
}
|
|
172
|
+
else if (bqAuth?.source === 'user-oauth') {
|
|
173
|
+
lines.push('⚠️ BigQuery — OAuth fallback (Workspace 재인증 정책 시 끊길 수 있음 → 서비스 계정 권장: mimi-seed-bigquery-auth)');
|
|
121
174
|
}
|
|
122
175
|
else {
|
|
123
176
|
lines.push('❌ BigQuery — 미설정 → npx @yoonion/mimi-seed-mcp mimi-seed-bigquery-auth (선택)');
|
|
@@ -139,6 +192,36 @@ export function registerAuthTools(server) {
|
|
|
139
192
|
else {
|
|
140
193
|
lines.push('', '✅ 필수 연결 모두 완료. 앱 출시 작업을 시작할 수 있습니다.');
|
|
141
194
|
}
|
|
195
|
+
// ── 프로젝트 매니페스트(.mimi-seed.json) 기반 요구사항 ────────────────────
|
|
196
|
+
// 이 저장소가 "무엇을 필요로 하는가"를 선언해두면, 범용 스캔 대신
|
|
197
|
+
// "이 프로젝트에서 너한테 빠진 것 + 정확한 셋업 명령"을 팀원별로 보여준다.
|
|
198
|
+
const loaded = findProjectManifest();
|
|
199
|
+
if (loaded) {
|
|
200
|
+
const oauthOk = oauthResult.status === 'fresh' || oauthResult.status === 'refreshed';
|
|
201
|
+
const playstoreCovers = (pkg) => saCount > 0 &&
|
|
202
|
+
(!pkg || !!saInfo.default || saInfo.perPackage.some((p) => p.packageName === pkg));
|
|
203
|
+
const state = {
|
|
204
|
+
oauth: oauthOk,
|
|
205
|
+
bigquery: !!bqAuth,
|
|
206
|
+
playstore: playstoreCovers(),
|
|
207
|
+
appstore: !!asc,
|
|
208
|
+
jenkins: !!jenkins,
|
|
209
|
+
};
|
|
210
|
+
const projName = loaded.manifest.displayName ?? loaded.manifest.project ?? '이 프로젝트';
|
|
211
|
+
const reqLines = ['', `── ${projName} 요구사항 (.mimi-seed.json) ─────────`];
|
|
212
|
+
let anyMissing = false;
|
|
213
|
+
for (const [id, svc] of manifestServiceEntries(loaded.manifest)) {
|
|
214
|
+
const required = svc.required !== false;
|
|
215
|
+
const connected = id === 'playstore' ? playstoreCovers(svc.packageName) : state[id];
|
|
216
|
+
reqLines.push(renderManifestLine(id, svc, connected, required));
|
|
217
|
+
if (required && !connected)
|
|
218
|
+
anyMissing = true;
|
|
219
|
+
}
|
|
220
|
+
if (anyMissing) {
|
|
221
|
+
reqLines.push('', 'ℹ️ 팀 공유가 목표라면, 각자 로컬 자격증명을 채우는 대신', ' 워크스페이스(https://mimi-seed.pryzm.gg) 초대 → PAT 발급 → 원격 MCP 연결도 가능.');
|
|
222
|
+
}
|
|
223
|
+
lines.push(...reqLines);
|
|
224
|
+
}
|
|
142
225
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
143
226
|
});
|
|
144
227
|
server.tool('mimi_seed_auth_start', [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { loadJenkinsConfig, requireJenkinsConfig, saveJenkinsConfig } from '../jenkins/config.js';
|
|
3
3
|
import * as creds from '../jenkins/credentials.js';
|
|
4
|
+
import * as jobs from '../jenkins/jobs.js';
|
|
4
5
|
export function registerJenkinsTools(server) {
|
|
5
6
|
// ── 0. 상태 확인 (항상 첫 번째로 호출) ─────────────────────────────────────
|
|
6
7
|
server.tool('jenkins_status', [
|
|
@@ -42,6 +43,7 @@ export function registerJenkinsTools(server) {
|
|
|
42
43
|
'',
|
|
43
44
|
'jenkins_list_credentials 로 등록된 credential 목록을 확인하거나,',
|
|
44
45
|
'jenkins_create_credential / jenkins_upload_keystore 로 credential을 추가하세요.',
|
|
46
|
+
'잡은 jenkins_list_jobs / jenkins_get_job_config / jenkins_create_job / jenkins_update_job 로 다룹니다.',
|
|
45
47
|
].join('\n'),
|
|
46
48
|
}],
|
|
47
49
|
};
|
|
@@ -147,4 +149,77 @@ export function registerJenkinsTools(server) {
|
|
|
147
149
|
}],
|
|
148
150
|
};
|
|
149
151
|
});
|
|
152
|
+
// ── 6. 잡 목록 ─────────────────────────────────────────────────────────────
|
|
153
|
+
server.tool('jenkins_list_jobs', [
|
|
154
|
+
'Jenkins 잡 목록을 조회합니다 (이름 / URL / 상태색).',
|
|
155
|
+
'folder를 주면 그 폴더 안만 조회합니다. 재귀하지 않고 한 단계만 봅니다.',
|
|
156
|
+
'설정이 없으면 jenkins_status를 먼저 호출하세요.',
|
|
157
|
+
].join(' '), {
|
|
158
|
+
folder: z.string().optional().describe('폴더 경로 (예: vir-game). 생략하면 루트'),
|
|
159
|
+
}, async ({ folder }) => {
|
|
160
|
+
const cfg = requireJenkinsConfig();
|
|
161
|
+
const list = await jobs.listJobs(cfg, folder);
|
|
162
|
+
if (list.length === 0) {
|
|
163
|
+
return { content: [{ type: 'text', text: '잡 없음.' }] };
|
|
164
|
+
}
|
|
165
|
+
const lines = list.map((j) => `• ${j.name}${j.color ? ` [${j.color}]` : ' [folder]'}`);
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: `Jenkins jobs (${list.length}개):\n\n${lines.join('\n')}`,
|
|
170
|
+
}],
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
// ── 7. 잡 config.xml 조회 ──────────────────────────────────────────────────
|
|
174
|
+
server.tool('jenkins_get_job_config', [
|
|
175
|
+
'잡의 config.xml 원문을 가져옵니다.',
|
|
176
|
+
'잡을 수정하기 전에 현재 설정을 확인하거나 백업할 때 사용하세요.',
|
|
177
|
+
'폴더 안의 잡은 "folder/job" 형태로 경로를 넘깁니다.',
|
|
178
|
+
].join(' '), {
|
|
179
|
+
job: z.string().describe('잡 경로 (예: penguinrun, vir-game/client)'),
|
|
180
|
+
}, async ({ job }) => {
|
|
181
|
+
const cfg = requireJenkinsConfig();
|
|
182
|
+
const xml = await jobs.getJobConfig(cfg, job);
|
|
183
|
+
return { content: [{ type: 'text', text: xml }] };
|
|
184
|
+
});
|
|
185
|
+
// ── 8. 잡 생성 ─────────────────────────────────────────────────────────────
|
|
186
|
+
server.tool('jenkins_create_job', [
|
|
187
|
+
'config.xml 로 새 Jenkins 잡을 생성합니다 (createItem).',
|
|
188
|
+
'같은 이름의 잡이 이미 있으면 실패합니다. 덮어쓰려면 overwrite=true 를 주거나 jenkins_update_job 을 쓰세요.',
|
|
189
|
+
'폴더 안에 만들려면 job 에 "folder/name" 을 넘깁니다 (폴더는 미리 존재해야 합니다).',
|
|
190
|
+
'config_xml 은 Pipeline job 이면 flow-definition 루트 엘리먼트를 갖는 XML 전문입니다.',
|
|
191
|
+
].join(' '), {
|
|
192
|
+
job: z.string().describe('잡 경로 (예: penguinrun, vir-game/client)'),
|
|
193
|
+
config_xml: z.string().describe('잡 정의 config.xml 전문'),
|
|
194
|
+
overwrite: z.boolean().default(false).describe('이미 존재하면 덮어쓸지 여부 (기본 false)'),
|
|
195
|
+
}, async ({ job, config_xml, overwrite }) => {
|
|
196
|
+
const cfg = requireJenkinsConfig();
|
|
197
|
+
if (overwrite) {
|
|
198
|
+
const result = await jobs.upsertJob(cfg, job, config_xml);
|
|
199
|
+
return { content: [{ type: 'text', text: `✅ Jenkins 잡 ${result}: \`${job}\`` }] };
|
|
200
|
+
}
|
|
201
|
+
if (await jobs.jobExists(cfg, job)) {
|
|
202
|
+
return {
|
|
203
|
+
content: [{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: `⚠️ 잡 \`${job}\` 이(가) 이미 존재합니다. jenkins_update_job 을 쓰거나 overwrite=true 로 다시 호출하세요.`,
|
|
206
|
+
}],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
await jobs.createJob(cfg, job, config_xml);
|
|
210
|
+
return { content: [{ type: 'text', text: `✅ Jenkins 잡 created: \`${job}\`` }] };
|
|
211
|
+
});
|
|
212
|
+
// ── 9. 잡 수정 ─────────────────────────────────────────────────────────────
|
|
213
|
+
server.tool('jenkins_update_job', [
|
|
214
|
+
'기존 Jenkins 잡의 config.xml 을 통째로 교체합니다.',
|
|
215
|
+
'잡이 없으면 실패합니다 (생성은 jenkins_create_job).',
|
|
216
|
+
'기존 설정이 사라지므로 jenkins_get_job_config 로 먼저 백업하는 것을 권장합니다.',
|
|
217
|
+
].join(' '), {
|
|
218
|
+
job: z.string().describe('잡 경로 (예: penguinrun, vir-game/client)'),
|
|
219
|
+
config_xml: z.string().describe('교체할 config.xml 전문'),
|
|
220
|
+
}, async ({ job, config_xml }) => {
|
|
221
|
+
const cfg = requireJenkinsConfig();
|
|
222
|
+
await jobs.updateJob(cfg, job, config_xml);
|
|
223
|
+
return { content: [{ type: 'text', text: `✅ Jenkins 잡 updated: \`${job}\`` }] };
|
|
224
|
+
});
|
|
150
225
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoonion/mimi-seed-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=20"
|
|
52
52
|
},
|
|
53
|
+
"overrides": {
|
|
54
|
+
"gaxios": "7.1.5"
|
|
55
|
+
},
|
|
53
56
|
"dependencies": {
|
|
54
57
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
55
58
|
"@modelcontextprotocol/sdk": "^1.12.1",
|