@yoonion/mimi-seed-mcp 0.14.0 → 0.14.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/dist/admob/cli.js CHANGED
@@ -27,7 +27,8 @@ const HELP = `
27
27
  인증: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth
28
28
  `;
29
29
  const AD_FORMATS = ['BANNER', 'INTERSTITIAL', 'REWARDED', 'REWARDED_INTERSTITIAL', 'APP_OPEN', 'NATIVE'];
30
- runDomainCli({
30
+ // runDomainCli 는 내부에서 모든 예외를 잡고 항상 process.exit 한다.
31
+ void runDomainCli({
31
32
  binName: 'mimi-seed-admob',
32
33
  argv: process.argv.slice(2),
33
34
  help: HELP,
@@ -47,7 +47,7 @@ export async function generateToken(creds) {
47
47
  key = await importPKCS8(normalizedKey, 'ES256');
48
48
  }
49
49
  catch (err) {
50
- throw new Error(`App Store 개인 키 파싱 실패 — ~/.mimi-seed/appstore.json의 privateKey 형식 확인 필요.\n원인: ${err.message}`);
50
+ throw new Error(`App Store 개인 키 파싱 실패 — ~/.mimi-seed/appstore.json의 privateKey 형식 확인 필요.\n원인: ${err.message}`, { cause: err });
51
51
  }
52
52
  // Subtract 60s from iat to tolerate local clock running slightly ahead of Apple servers.
53
53
  const iat = Math.floor(Date.now() / 1000) - 60;
@@ -85,4 +85,7 @@ async function main() {
85
85
  console.log('');
86
86
  rl.close();
87
87
  }
88
- main();
88
+ main().catch((e) => {
89
+ console.error(`\n ❌ ${e instanceof Error ? e.message : String(e)}\n`);
90
+ process.exit(1);
91
+ });
@@ -141,4 +141,7 @@ async function main() {
141
141
  console.log('');
142
142
  rl.close();
143
143
  }
144
- main();
144
+ main().catch((e) => {
145
+ console.error(`\n ❌ ${e instanceof Error ? e.message : String(e)}\n`);
146
+ process.exit(1);
147
+ });
@@ -25,7 +25,7 @@ export async function getMcpOAuthClient() {
25
25
  }
26
26
  catch (e) {
27
27
  const detail = e instanceof Error ? e.message : String(e);
28
- throw new Error(`mcp-auth-config fetch failed (${WEB_BASE} 접속 불가: ${detail})`);
28
+ throw new Error(`mcp-auth-config fetch failed (${WEB_BASE} 접속 불가: ${detail})`, { cause: e });
29
29
  }
30
30
  if (!res.ok)
31
31
  throw new Error(`mcp-auth-config fetch failed (${res.status})`);
@@ -37,7 +37,7 @@ export async function getMcpOAuthClient() {
37
37
  }
38
38
  catch (e) {
39
39
  const detail = e instanceof Error ? e.message : String(e);
40
- throw new Error(`mcp-auth-config fetch failed (응답이 JSON 이 아님: ${detail})`);
40
+ throw new Error(`mcp-auth-config fetch failed (응답이 JSON 이 아님: ${detail})`, { cause: e });
41
41
  }
42
42
  // 빈 값 200 응답(서버 env 미설정)을 캐싱하면 프로세스 수명 내내 invalid_client 로 오진된다.
43
43
  if (!data || typeof data !== 'object' || !data.clientId || !data.clientSecret) {
@@ -17,9 +17,6 @@ const LEGACY_TOKEN_DIR = path.join(os.homedir(), '.preseed');
17
17
  const TOKEN_PATH = path.join(TOKEN_DIR, 'tokens.json');
18
18
  const LEGACY_TOKEN_PATH = path.join(LEGACY_TOKEN_DIR, 'tokens.json');
19
19
  const CREDENTIALS_PATH = path.join(TOKEN_DIR, 'credentials.json');
20
- // Default OAuth client for development — users should replace with their own
21
- const DEFAULT_CLIENT_ID = ''; // Will be set during auth setup
22
- const DEFAULT_CLIENT_SECRET = '';
23
20
  export function getStoredCredentials() {
24
21
  if (!fs.existsSync(CREDENTIALS_PATH))
25
22
  return null;
@@ -142,6 +139,9 @@ export function startAuth(clientId, clientSecret, options = {}) {
142
139
  });
143
140
  const wait = new Promise((resolve, reject) => {
144
141
  const rejectAuth = (e) => reject(new AuthError(classifyError(e, { phase: 'login' })));
142
+ // 핸들러 전체가 try/catch 로 감싸여 있고 모든 경로가 응답 후 rejectAuth 로 끝난다 —
143
+ // createServer 가 void 를 기대하지만 여기서 새어 나갈 rejection 은 없다.
144
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
145
145
  const server = http.createServer(async (req, res) => {
146
146
  try {
147
147
  const url = new URL(req.url, `http://localhost:9876`);
@@ -112,4 +112,7 @@ async function main() {
112
112
  console.log('');
113
113
  rl.close();
114
114
  }
115
- main();
115
+ main().catch((e) => {
116
+ console.error(`\n ❌ ${e instanceof Error ? e.message : String(e)}\n`);
117
+ process.exit(1);
118
+ });
@@ -73,13 +73,13 @@ export async function buildPlayStoreReleasePlan(opts) {
73
73
  let trackInfo = {
74
74
  hasTargetVersion: false,
75
75
  };
76
- let listingPresent = false;
77
76
  try {
78
77
  await withEdit(auth, packageName, async (editId) => {
79
- const [tracks, listing] = await Promise.all([
80
- publisher().edits.tracks.list({ auth, packageName, editId }).catch(() => null),
81
- publisher().edits.listings.get({ auth, packageName, editId, language }).catch(() => null),
82
- ]);
78
+ // 리스팅 존재 여부는 checkPlayStoreRisks 가 따로 본다. 예전엔 여기서도 한 번 더
79
+ // 받아 listingPresent 담았지만 값을 읽는 곳이 없어 edit 세션마다 낭비였다.
80
+ const tracks = await publisher()
81
+ .edits.tracks.list({ auth, packageName, editId })
82
+ .catch(() => null);
83
83
  const targetTrack = tracks?.data?.tracks?.find((t) => t.track === track);
84
84
  const release = targetTrack?.releases?.[0];
85
85
  const codes = release?.versionCodes ?? [];
@@ -88,7 +88,6 @@ export async function buildPlayStoreReleasePlan(opts) {
88
88
  latestVersionCode: codes[codes.length - 1],
89
89
  releaseStatus: release?.status ?? undefined,
90
90
  };
91
- listingPresent = !!listing?.data?.title;
92
91
  return null;
93
92
  });
94
93
  steps.push({ state: 'done', title: 'Google Play 인증 + 트랙 조회 성공' });
@@ -62,7 +62,8 @@ async function getConfig(projectId, appId, platform) {
62
62
  return firebase.getWebConfig(auth, projectId, appId);
63
63
  throw new CliUsageError('--platform 은 android | ios | web 중 하나여야 합니다.');
64
64
  }
65
- runDomainCli({
65
+ // runDomainCli 는 내부에서 모든 예외를 잡고 항상 process.exit 한다.
66
+ void runDomainCli({
66
67
  binName: 'mimi-seed-firebase',
67
68
  argv: process.argv.slice(2),
68
69
  help: HELP,
package/dist/ga4/cli.js CHANGED
@@ -32,7 +32,8 @@ const HELP = `
32
32
  ⚠️ analytics.edit 스코프 필요 — 처음이면 재로그인:
33
33
  npx -y @yoonion/mimi-seed-mcp mimi-seed-auth
34
34
  `;
35
- runDomainCli({
35
+ // runDomainCli 는 내부에서 모든 예외를 잡고 항상 process.exit 한다.
36
+ void runDomainCli({
36
37
  binName: 'mimi-seed-ga4',
37
38
  argv: process.argv.slice(2),
38
39
  help: HELP,
@@ -39,8 +39,10 @@ export function googleErrorDetail(e) {
39
39
  const any = e;
40
40
  const apiErr = any.response?.data?.error;
41
41
  const parts = [];
42
- if (apiErr?.message)
43
- parts.push(String(apiErr.message));
42
+ // Google 이 message 에 객체를 넣어 보내는 경우가 있다 — String() 이면 '[object Object]'.
43
+ if (apiErr?.message) {
44
+ parts.push(typeof apiErr.message === 'string' ? apiErr.message : JSON.stringify(apiErr.message));
45
+ }
44
46
  const reasons = apiErr?.errors ?? any.errors;
45
47
  if (Array.isArray(reasons)) {
46
48
  for (const r of reasons) {
@@ -0,0 +1,10 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ /** 구조화된 값을 그대로 JSON 으로 반환한다. */
3
+ export declare function jsonResult(value: unknown): CallToolResult;
4
+ /**
5
+ * 사람이 읽을 텍스트 응답.
6
+ *
7
+ * 배열을 주면 개행으로 합친다. 빈 문자열은 **빈 줄로 남는다** — 조건부 줄을
8
+ * 없애고 싶으면 호출부에서 `.filter(Boolean)` 을 명시할 것.
9
+ */
10
+ export declare function textResult(text: string | readonly string[]): CallToolResult;
@@ -0,0 +1,26 @@
1
+ // 도구 핸들러 응답 래퍼.
2
+ //
3
+ // register 파일들은 `{ content: [{ type: 'text', text: … }] }` 를 250번 되풀이하고
4
+ // 있었다 — 그중 66곳은 `JSON.stringify(x, null, 2)` 한 줄로 완전히 동일하다.
5
+ // 그 껍데기가 register 를 두껍게 만들어 "register 는 얇게" 규칙이 지켜지는지
6
+ // 눈으로 확인할 수 없게 했다.
7
+ //
8
+ // 의도적으로 두 개만 둔다. 줄 배열을 어떻게 합칠지(빈 줄을 남길지 `.filter(Boolean)`
9
+ // 으로 버릴지)는 **호출부의 의미**다 — 헬퍼가 대신 정하면 빈 줄을 의도한 곳과
10
+ // 조건부 줄을 버리려는 곳이 조용히 뒤바뀐다. 그래서 합치는 것까지만 돕고
11
+ // 필터링은 호출부에 남긴다.
12
+ /** 구조화된 값을 그대로 JSON 으로 반환한다. */
13
+ export function jsonResult(value) {
14
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
15
+ }
16
+ /**
17
+ * 사람이 읽을 텍스트 응답.
18
+ *
19
+ * 배열을 주면 개행으로 합친다. 빈 문자열은 **빈 줄로 남는다** — 조건부 줄을
20
+ * 없애고 싶으면 호출부에서 `.filter(Boolean)` 을 명시할 것.
21
+ */
22
+ export function textResult(text) {
23
+ return {
24
+ content: [{ type: 'text', text: typeof text === 'string' ? text : text.join('\n') }],
25
+ };
26
+ }
@@ -1,26 +1,27 @@
1
1
  import { z } from 'zod';
2
2
  import * as admob from '../admob/tools.js';
3
3
  import { requireAuth } from '../helpers.js';
4
+ import { jsonResult } from '../lib/mcp-response.js';
4
5
  export function registerAdmobTools(server) {
5
6
  server.tool('admob_list_accounts', 'AdMob 계정 목록 조회', {}, async () => {
6
7
  const auth = await requireAuth();
7
8
  const accounts = await admob.listAccounts(auth);
8
- return { content: [{ type: 'text', text: JSON.stringify(accounts, null, 2) }] };
9
+ return jsonResult(accounts);
9
10
  });
10
11
  server.tool('admob_list_apps', 'AdMob에 등록된 앱 목록', { accountId: z.string().describe('AdMob 계정 ID (예: accounts/pub-XXXX)') }, async ({ accountId }) => {
11
12
  const auth = await requireAuth();
12
13
  const apps = await admob.listApps(auth, accountId);
13
- return { content: [{ type: 'text', text: JSON.stringify(apps, null, 2) }] };
14
+ return jsonResult(apps);
14
15
  });
15
16
  server.tool('admob_list_ad_units', 'AdMob 광고 단위 목록', { accountId: z.string().describe('AdMob 계정 ID') }, async ({ accountId }) => {
16
17
  const auth = await requireAuth();
17
18
  const units = await admob.listAdUnits(auth, accountId);
18
- return { content: [{ type: 'text', text: JSON.stringify(units, null, 2) }] };
19
+ return jsonResult(units);
19
20
  });
20
21
  server.tool('admob_get_today_earnings', '오늘 AdMob 수익 요약', { accountId: z.string().describe('AdMob 계정 ID') }, async ({ accountId }) => {
21
22
  const auth = await requireAuth();
22
23
  const report = await admob.getTodayEarnings(auth, accountId);
23
- return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
24
+ return jsonResult(report);
24
25
  });
25
26
  server.tool('admob_get_report', 'AdMob 수익 리포트 (기간 지정)', {
26
27
  accountId: z.string().describe('AdMob 계정 ID'),
@@ -33,7 +34,7 @@ export function registerAdmobTools(server) {
33
34
  }, async ({ accountId, startYear, startMonth, startDay, endYear, endMonth, endDay }) => {
34
35
  const auth = await requireAuth();
35
36
  const report = await admob.getNetworkReport(auth, accountId, { year: startYear, month: startMonth, day: startDay }, { year: endYear, month: endMonth, day: endDay });
36
- return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
37
+ return jsonResult(report);
37
38
  });
38
39
  server.tool('admob_create_app', 'AdMob에 새 앱 등록 (v1beta — Limited Access). appStoreId 를 주면 스토어 게시 앱과 링크(Android=패키지명, iOS=App Store 숫자 ID), 없으면 manual(unlinked) 앱.', {
39
40
  accountId: z.string().describe('AdMob 계정 ID'),
@@ -44,7 +45,7 @@ export function registerAdmobTools(server) {
44
45
  const auth = await requireAuth();
45
46
  try {
46
47
  const result = await admob.createApp(auth, accountId, platform, displayName, appStoreId);
47
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
48
+ return jsonResult(result);
48
49
  }
49
50
  catch (err) {
50
51
  if (err.code === 403) {
@@ -75,7 +76,7 @@ export function registerAdmobTools(server) {
75
76
  const auth = await requireAuth();
76
77
  try {
77
78
  const result = await admob.createAdUnit(auth, accountId, appId, displayName, adFormat);
78
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
79
+ return jsonResult(result);
79
80
  }
80
81
  catch (err) {
81
82
  if (err.code === 403) {