@yoonion/mimi-seed-mcp 0.15.1 → 0.15.3

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.
@@ -10,11 +10,39 @@ function storeBase(url) {
10
10
  function credentialBase(url, id) {
11
11
  return `${storeBase(url)}/credential/${encodeURIComponent(id)}`;
12
12
  }
13
- async function credentialExists(cfg, id) {
13
+ /**
14
+ * 기존 credential 의 Java class. 없으면 null, 있는데 메타데이터를 못 읽으면 빈 문자열.
15
+ *
16
+ * boolean(존재 여부)만으로는 부족하다 — id 가 같고 **종류가 다른** credential 을
17
+ * upsert 하면 기존 값이 통째로 사라진다. 예: Secret text 로 앱 키를 넣어둔 id 에
18
+ * Play SA 파일을 올리면 앱 키가 소멸한다. `_class` 는 Jenkins 가 주는 Java 클래스명이라
19
+ * 표시 이름(typeName)과 달리 로케일에 흔들리지 않는다.
20
+ */
21
+ async function credentialClass(cfg, id) {
14
22
  const res = await fetchWithTimeout(`${credentialBase(cfg.url, id)}/api/json`, {
15
23
  headers: authHeaders(cfg),
16
24
  });
17
- return res.ok;
25
+ if (!res.ok)
26
+ return null;
27
+ try {
28
+ return (await res.json())._class ?? '';
29
+ }
30
+ catch {
31
+ return '';
32
+ }
33
+ }
34
+ /** id 가 이미 **다른 종류**로 쓰이고 있으면 덮어쓰지 않고 멈춘다. */
35
+ function assertSameKind(id, existing, wanted, label) {
36
+ if (existing === null || existing === '' || existing === wanted)
37
+ return;
38
+ throw new Error([
39
+ `Jenkins credential "${id}" 가 이미 다른 종류로 존재합니다.`,
40
+ ` 기존: ${existing}`,
41
+ ` 요청: ${label}`,
42
+ '',
43
+ '덮어쓰면 기존 값이 사라집니다. 다른 id 를 쓰거나, 정말 교체하려면 먼저 삭제하세요.',
44
+ 'jenkins_list_credentials 로 현재 목록을 확인할 수 있습니다.',
45
+ ].join('\n'));
18
46
  }
19
47
  export async function listCredentials(cfg) {
20
48
  const res = await fetchWithTimeout(`${storeBase(cfg.url)}/api/json?depth=1`, {
@@ -30,7 +58,9 @@ export async function listCredentials(cfg) {
30
58
  }));
31
59
  }
32
60
  export async function upsertSecretText(cfg, id, secret, description = '') {
33
- const exists = await credentialExists(cfg, id);
61
+ const existingClass = await credentialClass(cfg, id);
62
+ assertSameKind(id, existingClass, TEXT_CLASS, 'Secret text');
63
+ const exists = existingClass !== null;
34
64
  const payload = {
35
65
  credentials: {
36
66
  scope: 'GLOBAL',
@@ -64,7 +94,9 @@ export async function upsertSecretText(cfg, id, secret, description = '') {
64
94
  * json 본문이 "file": "<필드명>" 으로 참조한다 (secretBytes JSON 직접 입력은 불가).
65
95
  */
66
96
  export async function upsertSecretFile(cfg, id, fileBase64, fileName, description = '') {
67
- const exists = await credentialExists(cfg, id);
97
+ const existingClass = await credentialClass(cfg, id);
98
+ assertSameKind(id, existingClass, FILE_CLASS, 'Secret file');
99
+ const exists = existingClass !== null;
68
100
  const payload = {
69
101
  credentials: {
70
102
  scope: 'GLOBAL',
package/dist/lib/http.js CHANGED
@@ -37,6 +37,14 @@ const MAX_BACKOFF_MS = 20_000;
37
37
  const RETRY_WINDOW_MS = 30_000;
38
38
  /** 예산이 거의 소진돼도 최소한 이만큼은 준다 — 0초 타임아웃으로 즉시 죽는 것을 막는다. */
39
39
  const MIN_ATTEMPT_MS = 1_000;
40
+ /**
41
+ * 재시도 사이 최소 간격.
42
+ *
43
+ * `Retry-After: 0`(또는 이미 지난 HTTP-date)은 합법이지만 그대로 따르면 **지연 없이**
44
+ * 재요청하게 된다 — 방금 "속도 제한 중"이라고 답한 서버에 연타를 넣는 꼴이고, 대기가
45
+ * 0이면 벽시계가 흐르지 않아 총 예산 검사도 무력해진다.
46
+ */
47
+ const MIN_RETRY_DELAY_MS = 250;
40
48
  /**
41
49
  * 메서드가 재요청해도 안전한가(RFC 9110 idempotent).
42
50
  *
@@ -174,7 +182,7 @@ export async function fetchWithTimeout(input, init = {}, options = {}) {
174
182
  return response;
175
183
  if (!hasBudget(attempt + 1))
176
184
  return response;
177
- const wait = parseRetryAfter(response.headers.get('retry-after'), Date.now()) ?? backoffFor(attempt);
185
+ const wait = Math.max(parseRetryAfter(response.headers.get('retry-after'), Date.now()) ?? backoffFor(attempt), MIN_RETRY_DELAY_MS);
178
186
  // 재시도할 응답의 본문은 읽지 않고 버린다 — 소켓을 붙잡고 있지 않도록.
179
187
  await response.body?.cancel().catch(() => undefined);
180
188
  await sleep(cappedWait(wait, deadline));
@@ -90,7 +90,7 @@ export function registerAndroidTools(server) {
90
90
  ` 2. jenkins_create_credential(id="${prefix}-android-store-password", secret=...)`,
91
91
  ` 3. jenkins_create_credential(id="${prefix}-android-key-alias", secret=...)`,
92
92
  ` 4. jenkins_create_credential(id="${prefix}-android-key-password", secret=...)`,
93
- ` 5. jenkins_upload_playstore_sa(package_name="${package_name}", credential_id="${prefix}-app-key")`,
93
+ ` 5. jenkins_upload_playstore_sa(package_name="${package_name}", credential_id="${prefix}-playstore-sa")`,
94
94
  ` └ SA JSON이 없으면 먼저: setup_playstore_connection(packageName="${package_name}", projectId="...")`,
95
95
  ].join('\n'),
96
96
  }],
@@ -122,7 +122,7 @@ export function registerAndroidTools(server) {
122
122
  ? ` 6. setup_playstore_connection(packageName="${package_name}", projectId="${project_id}")`
123
123
  : ` 6. setup_playstore_connection(packageName="${package_name}", projectId="<GCP 프로젝트 ID>")`,
124
124
  ' └ GCP 프로젝트 ID를 모르면 사용자에게 확인하세요.',
125
- ` 7. jenkins_upload_playstore_sa(package_name="${package_name}", credential_id="${prefix}-app-key")`,
125
+ ` 7. jenkins_upload_playstore_sa(package_name="${package_name}", credential_id="${prefix}-playstore-sa")`,
126
126
  ' 8. Play Console에서 서비스 계정 초대 (수동, 1회)',
127
127
  ' → Play Console → 사용자 및 권한 → SA 이메일 → 릴리즈 관리자 권한 부여',
128
128
  ' 9. 첫 AAB 빌드 후 Play Console에 내부 테스트용으로 수동 업로드 (신규 앱 첫 번째만)',
@@ -203,9 +203,9 @@ export function registerAndroidTools(server) {
203
203
  package_name: z.string().describe('Android 패키지명 (예: com.example.app)'),
204
204
  // 기본값을 하드코딩 문자열에서 패키지명 파생으로 바꿨다 — 예전 기본값은 한 사설 앱
205
205
  // 이름이었고, 여러 앱을 쓰는 사용자는 모든 SA 가 그 이름 하나로 덮였다.
206
- credential_id: z.string().optional().describe('Jenkins Credential ID (생략 시 "<앱>-app-key")'),
206
+ credential_id: z.string().optional().describe('Jenkins Credential ID (생략 시 "<앱>-playstore-sa")'),
207
207
  }, async ({ package_name, credential_id: credentialIdInput }) => {
208
- const credential_id = credentialIdInput ?? `${credentialPrefix(package_name)}-app-key`;
208
+ const credential_id = credentialIdInput ?? `${credentialPrefix(package_name)}-playstore-sa`;
209
209
  const saPath = join(SA_DIR, `${package_name}.json`);
210
210
  if (!existsSync(saPath)) {
211
211
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Mimi Seed MCP server \u2014 Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {