@yoonion/mimi-seed-mcp 0.3.38 → 0.3.40
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/jenkins/credentials.d.ts +4 -0
- package/dist/jenkins/credentials.js +67 -28
- package/dist/prompts.js +5 -5
- package/dist/registers/android.js +4 -0
- package/dist/registers/auth.js +114 -0
- package/package.json +1 -1
|
@@ -6,5 +6,9 @@ export interface JenkinsCredentialSummary {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function listCredentials(cfg: JenkinsConfig): Promise<JenkinsCredentialSummary[]>;
|
|
8
8
|
export declare function upsertSecretText(cfg: JenkinsConfig, id: string, secret: string, description?: string): Promise<'created' | 'updated'>;
|
|
9
|
+
/**
|
|
10
|
+
* Secret File credential 생성/교체. Jenkins는 파일을 multipart 로 받고
|
|
11
|
+
* json 본문이 "file": "<필드명>" 으로 참조한다 (secretBytes JSON 직접 입력은 불가).
|
|
12
|
+
*/
|
|
9
13
|
export declare function upsertSecretFile(cfg: JenkinsConfig, id: string, fileBase64: string, fileName: string, description?: string): Promise<'created' | 'updated'>;
|
|
10
14
|
export declare function deleteCredential(cfg: JenkinsConfig, id: string): Promise<void>;
|
|
@@ -1,17 +1,45 @@
|
|
|
1
|
+
const FILE_CLASS = 'org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl';
|
|
2
|
+
const TEXT_CLASS = 'org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl';
|
|
1
3
|
function basicAuth(username, token) {
|
|
2
4
|
return 'Basic ' + Buffer.from(`${username}:${token}`).toString('base64');
|
|
3
5
|
}
|
|
4
|
-
|
|
6
|
+
// 도메인(_ = 전역) 레벨 — 목록 조회 / createCredentials 의 베이스
|
|
7
|
+
function storeBase(url) {
|
|
5
8
|
return `${url.replace(/\/$/, '')}/credentials/store/system/domain/_`;
|
|
6
9
|
}
|
|
10
|
+
// 개별 credential 레벨 — 반드시 /credential/<id> 세그먼트 필요 (조회/업데이트/삭제)
|
|
11
|
+
function credentialBase(url, id) {
|
|
12
|
+
return `${storeBase(url)}/credential/${encodeURIComponent(id)}`;
|
|
13
|
+
}
|
|
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
|
+
}
|
|
7
35
|
async function credentialExists(cfg, id) {
|
|
8
|
-
const res = await fetch(`${
|
|
36
|
+
const res = await fetch(`${credentialBase(cfg.url, id)}/api/json`, {
|
|
9
37
|
headers: { Authorization: basicAuth(cfg.username, cfg.token) },
|
|
10
38
|
});
|
|
11
39
|
return res.ok;
|
|
12
40
|
}
|
|
13
41
|
export async function listCredentials(cfg) {
|
|
14
|
-
const res = await fetch(`${
|
|
42
|
+
const res = await fetch(`${storeBase(cfg.url)}/api/json?depth=1`, {
|
|
15
43
|
headers: { Authorization: basicAuth(cfg.username, cfg.token) },
|
|
16
44
|
});
|
|
17
45
|
if (!res.ok)
|
|
@@ -24,60 +52,67 @@ export async function listCredentials(cfg) {
|
|
|
24
52
|
}));
|
|
25
53
|
}
|
|
26
54
|
export async function upsertSecretText(cfg, id, secret, description = '') {
|
|
27
|
-
const
|
|
28
|
-
|
|
55
|
+
const exists = await credentialExists(cfg, id);
|
|
56
|
+
const payload = {
|
|
29
57
|
credentials: {
|
|
30
58
|
scope: 'GLOBAL',
|
|
31
59
|
id,
|
|
32
60
|
description,
|
|
33
61
|
secret,
|
|
34
|
-
|
|
35
|
-
'
|
|
62
|
+
$class: TEXT_CLASS,
|
|
63
|
+
'stapler-class': TEXT_CLASS,
|
|
36
64
|
},
|
|
37
|
-
}
|
|
38
|
-
const base = credentialsBase(cfg.url);
|
|
39
|
-
const exists = await credentialExists(cfg, id);
|
|
65
|
+
};
|
|
40
66
|
const endpoint = exists
|
|
41
|
-
? `${
|
|
42
|
-
: `${
|
|
67
|
+
? `${credentialBase(cfg.url, id)}/updateSubmit`
|
|
68
|
+
: `${storeBase(cfg.url)}/createCredentials`;
|
|
69
|
+
const crumb = await getCrumb(cfg);
|
|
43
70
|
const res = await fetch(endpoint, {
|
|
44
71
|
method: 'POST',
|
|
45
72
|
headers: {
|
|
46
73
|
Authorization: basicAuth(cfg.username, cfg.token),
|
|
47
74
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
75
|
+
...crumb,
|
|
48
76
|
},
|
|
49
|
-
body: new URLSearchParams({ json: payload }).toString(),
|
|
77
|
+
body: new URLSearchParams({ json: JSON.stringify(payload) }).toString(),
|
|
50
78
|
});
|
|
51
79
|
if (!res.ok && res.status !== 302) {
|
|
52
80
|
throw new Error(`Jenkins credential ${exists ? 'update' : 'create'} 실패 (${res.status})`);
|
|
53
81
|
}
|
|
54
82
|
return exists ? 'updated' : 'created';
|
|
55
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Secret File credential 생성/교체. Jenkins는 파일을 multipart 로 받고
|
|
86
|
+
* json 본문이 "file": "<필드명>" 으로 참조한다 (secretBytes JSON 직접 입력은 불가).
|
|
87
|
+
*/
|
|
56
88
|
export async function upsertSecretFile(cfg, id, fileBase64, fileName, description = '') {
|
|
57
|
-
const
|
|
58
|
-
|
|
89
|
+
const exists = await credentialExists(cfg, id);
|
|
90
|
+
const payload = {
|
|
59
91
|
credentials: {
|
|
60
92
|
scope: 'GLOBAL',
|
|
61
93
|
id,
|
|
62
94
|
description,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'stapler-class':
|
|
66
|
-
'$class': 'org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl',
|
|
95
|
+
file: 'file0',
|
|
96
|
+
$class: FILE_CLASS,
|
|
97
|
+
'stapler-class': FILE_CLASS,
|
|
67
98
|
},
|
|
68
|
-
}
|
|
69
|
-
const
|
|
70
|
-
|
|
99
|
+
};
|
|
100
|
+
const form = new FormData();
|
|
101
|
+
form.append('json', JSON.stringify(payload));
|
|
102
|
+
const bytes = Buffer.from(fileBase64, 'base64');
|
|
103
|
+
form.append('file0', new Blob([bytes]), fileName);
|
|
71
104
|
const endpoint = exists
|
|
72
|
-
? `${
|
|
73
|
-
: `${
|
|
105
|
+
? `${credentialBase(cfg.url, id)}/updateSubmit`
|
|
106
|
+
: `${storeBase(cfg.url)}/createCredentials`;
|
|
107
|
+
const crumb = await getCrumb(cfg);
|
|
108
|
+
// Content-Type 은 fetch 가 multipart boundary 와 함께 자동 설정 — 수동 지정 금지
|
|
74
109
|
const res = await fetch(endpoint, {
|
|
75
110
|
method: 'POST',
|
|
76
111
|
headers: {
|
|
77
112
|
Authorization: basicAuth(cfg.username, cfg.token),
|
|
78
|
-
|
|
113
|
+
...crumb,
|
|
79
114
|
},
|
|
80
|
-
body:
|
|
115
|
+
body: form,
|
|
81
116
|
});
|
|
82
117
|
if (!res.ok && res.status !== 302) {
|
|
83
118
|
throw new Error(`Jenkins secret file credential ${exists ? 'update' : 'create'} 실패 (${res.status})`);
|
|
@@ -85,9 +120,13 @@ export async function upsertSecretFile(cfg, id, fileBase64, fileName, descriptio
|
|
|
85
120
|
return exists ? 'updated' : 'created';
|
|
86
121
|
}
|
|
87
122
|
export async function deleteCredential(cfg, id) {
|
|
88
|
-
const
|
|
123
|
+
const crumb = await getCrumb(cfg);
|
|
124
|
+
const res = await fetch(`${credentialBase(cfg.url, id)}/doDelete`, {
|
|
89
125
|
method: 'POST',
|
|
90
|
-
headers: {
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: basicAuth(cfg.username, cfg.token),
|
|
128
|
+
...crumb,
|
|
129
|
+
},
|
|
91
130
|
});
|
|
92
131
|
if (!res.ok && res.status !== 302) {
|
|
93
132
|
throw new Error(`Jenkins credential 삭제 실패 (${res.status})`);
|
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
|
},
|
|
@@ -152,6 +152,10 @@ export function registerAndroidTools(server) {
|
|
|
152
152
|
text: [
|
|
153
153
|
'✅ Android upload keystore 생성 완료',
|
|
154
154
|
'',
|
|
155
|
+
'🔒 아래 비밀번호는 채팅 기록에 평문으로 남습니다.',
|
|
156
|
+
' Jenkins 등록을 마친 뒤에는 이 대화/세션을 삭제하는 것을 권장합니다.',
|
|
157
|
+
' keystore 파일과 비밀번호는 분실 시 앱 서명을 영구히 잃으니 별도 안전한 곳에도 백업하세요.',
|
|
158
|
+
'',
|
|
155
159
|
'── 생성된 값 (지금 바로 Jenkins에 등록하세요) ────',
|
|
156
160
|
`keyAlias: ${ks.keyAlias}`,
|
|
157
161
|
`storePassword: ${ks.storePassword}`,
|
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