@yoonion/mimi-seed-mcp 0.19.13 → 0.19.17

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.
Files changed (54) hide show
  1. package/README.md +2 -1
  2. package/assets/agent-guide.md +44 -7
  3. package/dist/appstore/tools.js +83 -29
  4. package/dist/auth/browser.d.ts +2 -0
  5. package/dist/auth/browser.js +4 -0
  6. package/dist/auth/cli.js +33 -8
  7. package/dist/auth/google-auth.d.ts +26 -5
  8. package/dist/auth/google-auth.js +133 -47
  9. package/dist/auth/scopes.d.ts +5 -0
  10. package/dist/auth/scopes.js +8 -0
  11. package/dist/auth/youtube-channel.d.ts +8 -0
  12. package/dist/auth/youtube-channel.js +26 -0
  13. package/dist/googleads/errors.d.ts +2 -0
  14. package/dist/googleads/errors.js +31 -0
  15. package/dist/googleads/tools.d.ts +7 -0
  16. package/dist/googleads/tools.js +89 -46
  17. package/dist/helpers.d.ts +1 -1
  18. package/dist/helpers.js +10 -10
  19. package/dist/instagram/browser-login.d.ts +13 -0
  20. package/dist/instagram/browser-login.js +125 -0
  21. package/dist/instagram/setup.d.ts +1 -1
  22. package/dist/instagram/setup.js +5 -2
  23. package/dist/lib/googleapis-lite.d.ts +2 -0
  24. package/dist/lib/googleapis-lite.js +3 -0
  25. package/dist/lib/store-create-result.d.ts +221 -0
  26. package/dist/lib/store-create-result.js +38 -0
  27. package/dist/playstore/tools.d.ts +2 -0
  28. package/dist/playstore/tools.js +23 -8
  29. package/dist/registers/appstore.js +3 -47
  30. package/dist/registers/auth.js +30 -6
  31. package/dist/registers/googleads.js +5 -2
  32. package/dist/registers/playstore.js +3 -16
  33. package/dist/registers/video.js +11 -5
  34. package/dist/registers/youtube.d.ts +2 -0
  35. package/dist/registers/youtube.js +89 -0
  36. package/dist/server.js +2 -0
  37. package/dist/social/setup-cli.js +66 -19
  38. package/dist/threads/browser-login.d.ts +13 -0
  39. package/dist/threads/browser-login.js +125 -0
  40. package/dist/threads/config.js +2 -4
  41. package/dist/threads/setup.d.ts +1 -1
  42. package/dist/threads/setup.js +7 -4
  43. package/dist/video/schemas.d.ts +12 -12
  44. package/dist/video/youtube-publish.d.ts +14 -0
  45. package/dist/video/youtube-publish.js +8 -0
  46. package/dist/youtube/comments.d.ts +156 -0
  47. package/dist/youtube/comments.js +158 -0
  48. package/dist/youtube/insights.d.ts +82 -0
  49. package/dist/youtube/insights.js +155 -0
  50. package/dist/youtube/tools.d.ts +174 -0
  51. package/dist/youtube/tools.js +292 -0
  52. package/package.json +5 -3
  53. package/scripts/googleads-report.mjs +32 -0
  54. package/tool-manifest.json +18 -1
@@ -0,0 +1,221 @@
1
+ interface AppleCreation {
2
+ success: boolean;
3
+ productId?: string;
4
+ internalId?: string;
5
+ error?: string;
6
+ priceSet?: boolean;
7
+ priceError?: string;
8
+ priceNearest?: Array<{
9
+ id: string;
10
+ price: string;
11
+ }>;
12
+ extraRegionsSet?: string[];
13
+ localizationAdded?: boolean;
14
+ }
15
+ /** Product creation is durable even when its price setup failed. Never advise recreating it. */
16
+ export declare function appleCreationResult(result: AppleCreation, appId: string, subscription: boolean): {
17
+ isError: boolean;
18
+ content: ({
19
+ type: "text";
20
+ text: string;
21
+ annotations?: {
22
+ audience?: ("user" | "assistant")[] | undefined;
23
+ priority?: number | undefined;
24
+ lastModified?: string | undefined;
25
+ } | undefined;
26
+ _meta?: {
27
+ [x: string]: unknown;
28
+ } | undefined;
29
+ } | {
30
+ type: "image";
31
+ data: string;
32
+ mimeType: string;
33
+ annotations?: {
34
+ audience?: ("user" | "assistant")[] | undefined;
35
+ priority?: number | undefined;
36
+ lastModified?: string | undefined;
37
+ } | undefined;
38
+ _meta?: {
39
+ [x: string]: unknown;
40
+ } | undefined;
41
+ } | {
42
+ type: "audio";
43
+ data: string;
44
+ mimeType: string;
45
+ annotations?: {
46
+ audience?: ("user" | "assistant")[] | undefined;
47
+ priority?: number | undefined;
48
+ lastModified?: string | undefined;
49
+ } | undefined;
50
+ _meta?: {
51
+ [x: string]: unknown;
52
+ } | undefined;
53
+ } | {
54
+ uri: string;
55
+ name: string;
56
+ type: "resource_link";
57
+ description?: string | undefined;
58
+ mimeType?: string | undefined;
59
+ size?: number | undefined;
60
+ annotations?: {
61
+ audience?: ("user" | "assistant")[] | undefined;
62
+ priority?: number | undefined;
63
+ lastModified?: string | undefined;
64
+ } | undefined;
65
+ _meta?: {
66
+ [x: string]: unknown;
67
+ } | undefined;
68
+ icons?: {
69
+ src: string;
70
+ mimeType?: string | undefined;
71
+ sizes?: string[] | undefined;
72
+ theme?: "light" | "dark" | undefined;
73
+ }[] | undefined;
74
+ title?: string | undefined;
75
+ } | {
76
+ type: "resource";
77
+ resource: {
78
+ uri: string;
79
+ text: string;
80
+ mimeType?: string | undefined;
81
+ _meta?: {
82
+ [x: string]: unknown;
83
+ } | undefined;
84
+ } | {
85
+ uri: string;
86
+ blob: string;
87
+ mimeType?: string | undefined;
88
+ _meta?: {
89
+ [x: string]: unknown;
90
+ } | undefined;
91
+ };
92
+ annotations?: {
93
+ audience?: ("user" | "assistant")[] | undefined;
94
+ priority?: number | undefined;
95
+ lastModified?: string | undefined;
96
+ } | undefined;
97
+ _meta?: {
98
+ [x: string]: unknown;
99
+ } | undefined;
100
+ })[];
101
+ _meta?: {
102
+ [x: string]: unknown;
103
+ progressToken?: string | number | undefined;
104
+ "io.modelcontextprotocol/related-task"?: {
105
+ taskId: string;
106
+ } | undefined;
107
+ } | undefined;
108
+ structuredContent?: {
109
+ [x: string]: unknown;
110
+ } | undefined;
111
+ };
112
+ interface GoogleCreation {
113
+ success: boolean;
114
+ productId?: string;
115
+ active?: boolean;
116
+ activationError?: string;
117
+ error?: string;
118
+ skippedRegions?: string[];
119
+ }
120
+ export declare function googleSubscriptionCreationResult(result: GoogleCreation, args: {
121
+ packageName: string;
122
+ price: number;
123
+ currency: string;
124
+ period: string;
125
+ }): {
126
+ isError: boolean;
127
+ content: ({
128
+ type: "text";
129
+ text: string;
130
+ annotations?: {
131
+ audience?: ("user" | "assistant")[] | undefined;
132
+ priority?: number | undefined;
133
+ lastModified?: string | undefined;
134
+ } | undefined;
135
+ _meta?: {
136
+ [x: string]: unknown;
137
+ } | undefined;
138
+ } | {
139
+ type: "image";
140
+ data: string;
141
+ mimeType: string;
142
+ annotations?: {
143
+ audience?: ("user" | "assistant")[] | undefined;
144
+ priority?: number | undefined;
145
+ lastModified?: string | undefined;
146
+ } | undefined;
147
+ _meta?: {
148
+ [x: string]: unknown;
149
+ } | undefined;
150
+ } | {
151
+ type: "audio";
152
+ data: string;
153
+ mimeType: string;
154
+ annotations?: {
155
+ audience?: ("user" | "assistant")[] | undefined;
156
+ priority?: number | undefined;
157
+ lastModified?: string | undefined;
158
+ } | undefined;
159
+ _meta?: {
160
+ [x: string]: unknown;
161
+ } | undefined;
162
+ } | {
163
+ uri: string;
164
+ name: string;
165
+ type: "resource_link";
166
+ description?: string | undefined;
167
+ mimeType?: string | undefined;
168
+ size?: number | undefined;
169
+ annotations?: {
170
+ audience?: ("user" | "assistant")[] | undefined;
171
+ priority?: number | undefined;
172
+ lastModified?: string | undefined;
173
+ } | undefined;
174
+ _meta?: {
175
+ [x: string]: unknown;
176
+ } | undefined;
177
+ icons?: {
178
+ src: string;
179
+ mimeType?: string | undefined;
180
+ sizes?: string[] | undefined;
181
+ theme?: "light" | "dark" | undefined;
182
+ }[] | undefined;
183
+ title?: string | undefined;
184
+ } | {
185
+ type: "resource";
186
+ resource: {
187
+ uri: string;
188
+ text: string;
189
+ mimeType?: string | undefined;
190
+ _meta?: {
191
+ [x: string]: unknown;
192
+ } | undefined;
193
+ } | {
194
+ uri: string;
195
+ blob: string;
196
+ mimeType?: string | undefined;
197
+ _meta?: {
198
+ [x: string]: unknown;
199
+ } | undefined;
200
+ };
201
+ annotations?: {
202
+ audience?: ("user" | "assistant")[] | undefined;
203
+ priority?: number | undefined;
204
+ lastModified?: string | undefined;
205
+ } | undefined;
206
+ _meta?: {
207
+ [x: string]: unknown;
208
+ } | undefined;
209
+ })[];
210
+ _meta?: {
211
+ [x: string]: unknown;
212
+ progressToken?: string | number | undefined;
213
+ "io.modelcontextprotocol/related-task"?: {
214
+ taskId: string;
215
+ } | undefined;
216
+ } | undefined;
217
+ structuredContent?: {
218
+ [x: string]: unknown;
219
+ } | undefined;
220
+ };
221
+ export {};
@@ -0,0 +1,38 @@
1
+ import { textResult } from './mcp-response.js';
2
+ /** Product creation is durable even when its price setup failed. Never advise recreating it. */
3
+ export function appleCreationResult(result, appId, subscription) {
4
+ if (!result.success)
5
+ return { ...textResult(`❌ 생성 실패: ${result.error ?? '알 수 없는 오류'}`), isError: true };
6
+ const complete = result.priceSet === true;
7
+ return {
8
+ ...textResult([
9
+ complete ? '✓ App Store 상품 생성·가격 설정 완료' : '⚠ App Store 상품 생성됨 — 가격 설정 미완료',
10
+ `productId: ${result.productId}`,
11
+ `internalId: ${result.internalId}`,
12
+ complete ? '✓ 가격 설정됨' : `가격 오류: ${result.priceError ?? '요청 가격을 설정하지 못했습니다.'}`,
13
+ !complete && result.priceNearest?.length ? `가장 가까운 가격: ${JSON.stringify(result.priceNearest)}` : '',
14
+ !complete ? '상품은 이미 생성됐습니다. 생성 도구를 재실행하지 말고 이 상품의 가격·판매 지역을 수정하세요.' : '',
15
+ result.extraRegionsSet?.length ? `✓ 추가 지역: ${result.extraRegionsSet.join(', ')}` : '',
16
+ result.localizationAdded ? '✓ KRW 한국어 로컬라이제이션 추가됨' : '',
17
+ '판매 전 현지화·심사용 스크린샷·리뷰 노트·심사 상태를 확인하세요.',
18
+ `https://appstoreconnect.apple.com/apps/${appId}/distribution/${subscription ? 'subscriptions' : 'iaps'}`,
19
+ ].filter(Boolean)),
20
+ isError: !complete,
21
+ };
22
+ }
23
+ export function googleSubscriptionCreationResult(result, args) {
24
+ if (!result.success)
25
+ return { ...textResult(`❌ 구독 생성 실패: ${result.error ?? '알 수 없는 오류'}`), isError: true };
26
+ return {
27
+ ...textResult([
28
+ result.active === true ? '✓ Play 구독 생성·활성화 완료' : '⚠ Play 구독 생성됨 — 활성화 확인 필요',
29
+ `productId: ${result.productId}`,
30
+ `price: ${args.price} ${args.currency} / ${args.period}`,
31
+ result.activationError ? `활성화 오류: ${result.activationError}` : '',
32
+ result.active !== true ? '생성 도구를 재실행하지 말고 기존 기본 요금제 상태를 조회한 뒤 활성화하세요.' : '',
33
+ result.skippedRegions?.length ? `⚠ 적용하지 못한 지역 통화: ${result.skippedRegions.join(', ')}` : '',
34
+ `https://play.google.com/console/u/0/developers/-/app/-/subscriptions?package=${encodeURIComponent(args.packageName)}`,
35
+ ].filter(Boolean)),
36
+ isError: result.active === false || !!result.activationError,
37
+ };
38
+ }
@@ -80,6 +80,8 @@ export declare function replaceImages(auth: OAuth2Client | JWT, packageName: str
80
80
  }[];
81
81
  }>;
82
82
  export declare function submitRelease(auth: OAuth2Client | JWT, packageName: string, track: string, versionCode: string, status?: 'completed' | 'draft' | 'inProgress' | 'halted'): Promise<{
83
+ changesNotSentForReview: boolean;
84
+ nextAction: string | undefined;
83
85
  track: string;
84
86
  versionCode: string;
85
87
  previousStatus: string | null | undefined;
@@ -362,7 +362,8 @@ export async function replaceImages(auth, packageName, language, imageType, file
362
362
  // halted → 일시 중단
363
363
  // 신중히 사용해야 함 — completed로 바꾸면 되돌리기 어려움.
364
364
  export async function submitRelease(auth, packageName, track, versionCode, status = 'completed') {
365
- return withEdit(auth, packageName, async (editId) => {
365
+ let commitInfo = { changesNotSentForReview: false };
366
+ const out = await withEdit(auth, packageName, async (editId) => {
366
367
  const current = await publisher().edits.tracks.get({
367
368
  auth, packageName, editId, track,
368
369
  });
@@ -380,10 +381,17 @@ export async function submitRelease(auth, packageName, track, versionCode, statu
380
381
  throw new Error(`versionCode "${versionCode}"를 ${track} 트랙에서 찾을 수 없어. 가능한 릴리스: ${JSON.stringify(available)}`);
381
382
  }
382
383
  const previousStatus = target.status;
383
- target.status = status;
384
+ const updatedTarget = { ...target, status };
385
+ // Full rollout replaces the previous active release in the same edit.
386
+ // userFraction is only valid for staged/paused rollouts.
387
+ if (status === 'completed')
388
+ delete updatedTarget.userFraction;
389
+ const updatedReleases = status === 'completed'
390
+ ? [updatedTarget]
391
+ : releases.map((release) => release === target ? updatedTarget : release);
384
392
  const updated = await publisher().edits.tracks.update({
385
393
  auth, packageName, editId, track,
386
- requestBody: { track, releases },
394
+ requestBody: { track, releases: updatedReleases },
387
395
  });
388
396
  return {
389
397
  track,
@@ -393,7 +401,14 @@ export async function submitRelease(auth, packageName, track, versionCode, statu
393
401
  committed: true,
394
402
  result: updated.data,
395
403
  };
396
- }, true);
404
+ }, true, (info) => { commitInfo = info; });
405
+ return {
406
+ ...out,
407
+ changesNotSentForReview: commitInfo.changesNotSentForReview,
408
+ nextAction: commitInfo.changesNotSentForReview
409
+ ? 'Play Console 에서 "변경사항 검토 후 게시"(심사를 위해 전송)를 눌러야 심사가 시작된다.'
410
+ : undefined,
411
+ };
397
412
  }
398
413
  export async function promoteRelease(auth, packageName, fromTrack, toTrack, versionCode, options = {}) {
399
414
  const { status = 'completed', userFraction, releaseName, releaseNotes, copyReleaseNotes = true, } = options;
@@ -455,13 +470,13 @@ export async function promoteRelease(auth, packageName, fromTrack, toTrack, vers
455
470
  const toReleases = toData.data.releases ?? [];
456
471
  const existingIdx = toReleases.findIndex((r) => (r.versionCodes ?? []).some((v) => String(v) === String(versionCode)));
457
472
  let mergedReleases;
458
- if (existingIdx >= 0) {
473
+ if (status === 'completed') {
474
+ mergedReleases = [newRelease];
475
+ }
476
+ else if (existingIdx >= 0) {
459
477
  mergedReleases = [...toReleases];
460
478
  mergedReleases[existingIdx] = newRelease;
461
479
  }
462
- else if (status === 'completed') {
463
- mergedReleases = [newRelease];
464
- }
465
480
  else {
466
481
  mergedReleases = [...toReleases, newRelease];
467
482
  }
@@ -14,6 +14,7 @@ import { requireAppStoreCreds } from '../helpers.js';
14
14
  import { buildAppStoreReleasePlan } from '../checks/plan.js';
15
15
  import { validateAppStoreWhatsNew, formatIssuesForUser } from '../lib/text-validators.js';
16
16
  import { jsonResult, textResult } from '../lib/mcp-response.js';
17
+ import { appleCreationResult } from '../lib/store-create-result.js';
17
18
  /** ASC 속성값을 한 줄로. 객체가 오면 String() 이 '[object Object]' 를 뱉으므로 JSON 으로. */
18
19
  function stringifyAttr(value) {
19
20
  if (value == null)
@@ -381,29 +382,7 @@ export function registerAppstoreTools(server) {
381
382
  issuerId: creds.issuerId,
382
383
  privateKey: creds.privateKey,
383
384
  });
384
- if (!result.success) {
385
- const hint = result.errorType === 'DUPLICATE'
386
- ? '\n이미 같은 productId가 존재해. App Store Connect에서 확인해줘.'
387
- : result.errorType === 'PRICE_NOT_FOUND'
388
- ? `\n가장 가까운 가격: ${JSON.stringify(result.priceNearest)}`
389
- : '';
390
- return textResult(`❌ IAP 생성 실패: ${result.error}${hint}`);
391
- }
392
- return {
393
- content: [{
394
- type: 'text',
395
- text: [
396
- `✓ App Store IAP 생성 완료`,
397
- `productId: ${result.productId}`,
398
- `internalId: ${result.internalId}`,
399
- result.priceSet ? `✓ 가격 설정됨` : `⚠ 가격 미설정 (가장 가까운 가격: ${JSON.stringify(result.priceNearest)})`,
400
- result.extraRegionsSet?.length ? `✓ 추가 지역: ${result.extraRegionsSet.join(', ')}` : '',
401
- '',
402
- '스크린샷·리뷰 노트를 추가한 후 App Store Connect에서 심사 제출:',
403
- `https://appstoreconnect.apple.com/apps/${args.appId}/distribution/iaps`,
404
- ].filter(Boolean).join('\n'),
405
- }],
406
- };
385
+ return appleCreationResult(result, args.appId, false);
407
386
  });
408
387
  server.tool('appstore_create_subscription', [
409
388
  'App Store에 자동 갱신 구독을 생성 — Subscription Group 자동 생성 포함.',
@@ -440,30 +419,7 @@ export function registerAppstoreTools(server) {
440
419
  issuerId: creds.issuerId,
441
420
  privateKey: creds.privateKey,
442
421
  });
443
- if (!result.success) {
444
- const hint = result.errorType === 'DUPLICATE'
445
- ? '\n이미 같은 productId가 존재해. App Store Connect에서 확인해줘.'
446
- : result.errorType === 'PRICE_NOT_FOUND'
447
- ? `\n가장 가까운 가격: ${JSON.stringify(result.priceNearest)}`
448
- : '';
449
- return textResult(`❌ 구독 생성 실패: ${result.error}${hint}`);
450
- }
451
- return {
452
- content: [{
453
- type: 'text',
454
- text: [
455
- `✓ App Store 구독 생성 완료`,
456
- `productId: ${result.productId}`,
457
- `internalId: ${result.internalId}`,
458
- result.priceSet ? `✓ 가격 설정됨` : `⚠ 가격 미설정 (가장 가까운 가격: ${JSON.stringify(result.priceNearest)})`,
459
- result.extraRegionsSet?.length ? `✓ 추가 지역: ${result.extraRegionsSet.join(', ')}` : '',
460
- result.localizationAdded ? '✓ KRW 한국어 로컬라이제이션 추가됨' : '',
461
- '',
462
- 'App Store Connect에서 심사 제출:',
463
- `https://appstoreconnect.apple.com/apps/${args.appId}/distribution/subscriptions`,
464
- ].filter(Boolean).join('\n'),
465
- }],
466
- };
422
+ return appleCreationResult(result, args.appId, true);
467
423
  });
468
424
  server.tool('appstore_list_products', 'App Store의 모든 IAP 상품(구독 + 일회성) 통합 조회. productId / internalId / name / status / type 반환.', {
469
425
  appId: z.string().describe('App Store 앱 ID (숫자형, appstore_list_apps 결과)'),
@@ -1,3 +1,5 @@
1
+ import { GOOGLE_PROFILE_ID, listGoogleProfiles, getAuthenticatedClient, getGoogleAuthAttempt } from '../auth/google-auth.js';
2
+ import { verifyYouTubeChannel, YOUTUBE_CHANNEL_ID } from '../auth/youtube-channel.js';
1
3
  import { z } from 'zod';
2
4
  import { getMcpOAuthClient } from '../auth/constants.js';
3
5
  import { classifyError } from '../auth/errors.js';
@@ -136,9 +138,12 @@ export function registerAuthTools(server) {
136
138
  else {
137
139
  lines.push('❌ Google OAuth — 미연결 → mimi_seed_auth_start');
138
140
  }
139
- const youtubeGranted = summarizeGrantedDomains(getStoredTokens()?.scope).granted.includes('youtube');
141
+ const grantedDomains = summarizeGrantedDomains(getStoredTokens()?.scope).granted;
142
+ const youtubeGranted = grantedDomains.includes('youtube') || grantedDomains.includes('youtube_analytics');
140
143
  if (youtubeGranted && (oauthResult.status === 'fresh' || oauthResult.status === 'refreshed')) {
141
- lines.push('✅ YouTube — 업로드·상태 관리 권한 연결됨');
144
+ lines.push(grantedDomains.includes('youtube')
145
+ ? '✅ YouTube — 업로드·상태 관리 권한 연결됨'
146
+ : '✅ YouTube — 채널·영상·성과 읽기 권한 연결됨');
142
147
  }
143
148
  else {
144
149
  lines.push('❌ YouTube — 미연결 → mimi_seed_auth_start(domains=["youtube"]) (선택)');
@@ -281,12 +286,14 @@ export function registerAuthTools(server) {
281
286
  'domains 로 필요한 권한 도메인만 골라 요청 가능 (미지정 시 전체). 재로그인 시 기존 부여 권한은 유지되고 새 권한만 추가됨(incremental).',
282
287
  '토큰 만료(invalid_rapt) / 재인증 필요 시 사용. 10분 내 완료해야 함.',
283
288
  ].join(' '), {
289
+ profile: z.string().regex(GOOGLE_PROFILE_ID).optional().describe('계정/채널별 Google 로그인 프로필. 생략하면 기존 기본 로그인.'),
290
+ expectedChannelId: z.string().regex(YOUTUBE_CHANNEL_ID).optional().describe('이 채널로 로그인했는지 검증한 뒤 저장. profile 필수. Google 로그인에서 해당 개인/브랜드 채널을 선택하세요.'),
284
291
  domains: z
285
292
  .array(z.enum(DOMAIN_IDS))
286
293
  .optional()
287
294
  .describe(`요청할 권한 도메인 서브셋 (미지정 시 전체). 가능한 값: ${DOMAIN_IDS.join(', ')}. ` +
288
295
  '예: ["ga4","googleads"] — 기존에 부여된 다른 도메인 권한은 유지된다.'),
289
- }, async ({ domains }) => {
296
+ }, async ({ domains, profile, expectedChannelId }) => {
290
297
  // 설정 조회 실패를 분류된 안내로 — raw throw 는 MCP 클라이언트에 마커 문자열만 노출된다.
291
298
  let clientId;
292
299
  let clientSecret;
@@ -297,7 +304,7 @@ export function registerAuthTools(server) {
297
304
  const p = classifyError(e, { phase: 'login' });
298
305
  return textResult(`❌ [${p.code}] ${p.message}${p.hint ? `\n→ ${p.hint}` : ''}`);
299
306
  }
300
- const { url, wait } = startAuth(clientId, clientSecret, { domains });
307
+ const { url, wait } = startAuth(clientId, clientSecret, { domains, profile, expectedChannelId });
301
308
  // fire-and-forget — 토큰은 콜백 서버가 자동 저장
302
309
  wait.then(() => { }, (err) => { console.error('[mimi-seed auth]', err.message); });
303
310
  const scopeLine = domains?.length
@@ -312,6 +319,8 @@ export function registerAuthTools(server) {
312
319
  url,
313
320
  '',
314
321
  scopeLine,
322
+ `로그인 프로필: ${profile ?? '(기본)'} / 대상 채널: ${expectedChannelId ?? '(로그인 후 확인)'}`,
323
+ 'YouTube는 Google 로그인에서 선택한 개인/브랜드 채널에 연결됩니다. 완료 후 mimi_seed_auth_status(profile=...)로 실제 채널을 확인하세요.',
315
324
  '',
316
325
  '이 URL을 브라우저에서 열고 Google 계정으로 승인해줘.',
317
326
  '완료되면 localhost:9876으로 자동 리다이렉트되고 토큰이 저장돼.',
@@ -320,10 +329,24 @@ export function registerAuthTools(server) {
320
329
  }],
321
330
  };
322
331
  });
323
- server.tool('mimi_seed_auth_status', 'Mimi Seed MCP 인증 상태 확인 (만료 시 refresh_token으로 자동 갱신 시도)', {}, async () => {
332
+ server.tool('mimi_seed_auth_status', 'Mimi Seed MCP 인증 상태 확인 (만료 시 refresh_token으로 자동 갱신 시도)', {
333
+ profile: z.string().regex(GOOGLE_PROFILE_ID).optional().describe('확인할 Google 로그인 프로필. 저장된 프로필 목록도 반환.'),
334
+ }, async ({ profile }) => {
335
+ const profiles = listGoogleProfiles();
336
+ const loginAttempt = getGoogleAuthAttempt(profile);
337
+ if (profile !== undefined) {
338
+ const result = await ensureFreshAccessToken(undefined, profile);
339
+ if (result.status === 'unauthenticated' || result.status === 'expired_refresh_failed') {
340
+ return textResult(JSON.stringify({ profile, status: result.status, error: result.error, loginAttempt, profiles }));
341
+ }
342
+ const client = getAuthenticatedClient(profile);
343
+ const channel = result.tokens.youtubeChannel && client
344
+ ? await verifyYouTubeChannel(client, result.tokens.youtubeChannel.id) : null;
345
+ return textResult(JSON.stringify({ profile, status: result.status, youtubeChannel: channel, loginAttempt, profiles }));
346
+ }
324
347
  const r = await ensureFreshAccessToken();
325
348
  const refreshHint = formatLastRefreshHint(getTokensLastRefreshMs());
326
- const refreshLine = ` 마지막 갱신: ${refreshHint.label}`;
349
+ const refreshLine = ` 마지막 갱신: ${refreshHint.label}\nGoogle profiles: ${JSON.stringify(profiles)}`;
327
350
  const recommendation = refreshHint.recommendation ? `\n\n${refreshHint.recommendation}` : '';
328
351
  // 도메인 선택형 로그인 이후 토큰은 전체 권한이 아닐 수 있다 — 부여 현황을 함께
329
352
  // 보여준다. 유효/갱신된 상태에서만 쓰므로 그 두 arm 에서만 계산한다(불필요한 토큰
@@ -349,6 +372,7 @@ export function registerAuthTools(server) {
349
372
  type: 'text',
350
373
  text: `❌ [${r.error.code}] ${r.error.message}\n` +
351
374
  (r.error.hint ? `→ ${r.error.hint}\n\n` : '\n') +
375
+ `Google profiles: ${JSON.stringify(profiles)}\n` +
352
376
  '터미널에서 실행:\n npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
353
377
  }],
354
378
  };
@@ -32,7 +32,7 @@ export function registerGoogleAdsTools(server) {
32
32
  const campaigns = await googleads.listCampaigns(auth, cfg);
33
33
  return textResult(JSON.stringify(campaigns, null, 2));
34
34
  });
35
- server.tool('googleads_get_campaign_report', '기간별 캠페인 성과 리포트 (클릭, 노출, 비용, 전환수, CPI, CTR)', {
35
+ server.tool('googleads_get_campaign_report', '기간별 캠페인 성과 리포트 (클릭, 노출, 계정 통화 비용, 전환수, 전환당 비용). 설치 CPI나 코호트 ROAS가 아님.', {
36
36
  startDate: z.string().describe('시작일 (YYYY-MM-DD)'),
37
37
  endDate: z.string().describe('종료일 (YYYY-MM-DD)'),
38
38
  }, async ({ startDate, endDate }) => {
@@ -48,6 +48,7 @@ export function registerGoogleAdsTools(server) {
48
48
  type: 'text',
49
49
  text: JSON.stringify({
50
50
  period: { startDate, endDate },
51
+ metricNote: googleads.REPORT_METRIC_NOTE,
51
52
  summary: {
52
53
  totalCost: Math.round(totalCost * 100) / 100,
53
54
  totalClicks,
@@ -60,7 +61,7 @@ export function registerGoogleAdsTools(server) {
60
61
  }],
61
62
  };
62
63
  });
63
- server.tool('googleads_get_uac_report', '앱 캠페인(UAC) 리포트 — 앱 설치 캠페인별 설치수, CPI, ROAS 집계', {
64
+ server.tool('googleads_get_uac_report', '앱 캠페인(UAC) 비용·전환 리포트. legacy installs/cpi 필드는 일반 전환 기반 별칭이며 설치수·설치 CPI·D7 ROAS로 단정하지 말 것.', {
64
65
  startDate: z.string().describe('시작일 (YYYY-MM-DD)'),
65
66
  endDate: z.string().describe('종료일 (YYYY-MM-DD)'),
66
67
  }, async ({ startDate, endDate }) => {
@@ -74,9 +75,11 @@ export function registerGoogleAdsTools(server) {
74
75
  type: 'text',
75
76
  text: JSON.stringify({
76
77
  period: { startDate, endDate },
78
+ metricNote: googleads.REPORT_METRIC_NOTE,
77
79
  summary: {
78
80
  totalCost: Math.round(totalCost * 100) / 100,
79
81
  totalInstalls,
82
+ totalConversions: totalInstalls,
80
83
  avgCpi: totalInstalls > 0 ? Math.round(totalCost / totalInstalls * 100) / 100 : null,
81
84
  campaignCount: report.length,
82
85
  },
@@ -12,6 +12,7 @@ import * as iam from '../iam/tools.js';
12
12
  import { buildPlayStoreReleasePlan } from '../checks/plan.js';
13
13
  import { validatePlayReleaseNotes, formatIssuesForUser } from '../lib/text-validators.js';
14
14
  import { jsonResult, textResult } from '../lib/mcp-response.js';
15
+ import { googleSubscriptionCreationResult } from '../lib/store-create-result.js';
15
16
  // 모든 playstore tools 호출을 친절 에러로 감싸는 프록시 — 403(권한)/404/edit 충돌/
16
17
  // invalid_grant 를 raw dump 대신 구체적 복구 안내로 변환. args[1] 이 packageName 규약.
17
18
  // 비-Promise 반환(publisher 등 sync factory)은 그대로 통과, 도메인 에러는 원본 보존.
@@ -343,22 +344,7 @@ export function registerPlaystoreTools(server) {
343
344
  ...(args.extraRegions && { extraRegions: args.extraRegions }),
344
345
  serviceAccountKey: json,
345
346
  });
346
- if (!result.success) {
347
- return textResult(`❌ 구독 생성 실패: ${result.error}`);
348
- }
349
- return {
350
- content: [{
351
- type: 'text',
352
- text: [
353
- `✓ Play 구독 생성 완료`,
354
- `productId: ${result.productId}`,
355
- `price: ${args.price} ${args.currency} / ${args.period}`,
356
- '',
357
- 'Play Console:',
358
- `https://play.google.com/console/u/0/developers/-/app/-/subscriptions?package=${encodeURIComponent(args.packageName)}`,
359
- ].join('\n'),
360
- }],
361
- };
347
+ return googleSubscriptionCreationResult(result, args);
362
348
  });
363
349
  server.tool('playstore_verify_service_account', [
364
350
  "서비스 계정 JSON이 주어진 packageName에 대해 Play Developer API 호출 가능한지 + 'View financial data' 권한까지 있는지 검증.",
@@ -542,6 +528,7 @@ export function registerPlaystoreTools(server) {
542
528
  });
543
529
  server.tool('playstore_submit_release', [
544
530
  'Google Play 트랙의 release status를 변경 — 일반적으로 draft → completed로 바꿔 검토/배포 큐에 진입시킬 때 사용.',
531
+ 'completed 전환은 해당 트랙의 릴리스 목록을 대상 릴리스 하나로 교체한다. changesNotSentForReview=true 응답이면 커밋만 완료된 것이며 Console에서 심사 전송이 필요하다.',
545
532
  '⚠️ status="completed"는 비가역에 가까움 (전체 출시 또는 Google 검토 시작). halted로 일시 중단은 가능하나 한 번 라이브된 release는 되돌리기 어려움.',
546
533
  'status 옵션: draft(검토 미시작) / inProgress(단계 출시) / completed(전체 출시) / halted(중단).',
547
534
  'playstore_check_submission_risks로 사전 점검 권장.',
@@ -1,3 +1,5 @@
1
+ import { GOOGLE_PROFILE_ID } from '../auth/google-auth.js';
2
+ import { YOUTUBE_CHANNEL_ID } from '../auth/youtube-channel.js';
1
3
  import { z } from 'zod';
2
4
  import { addLocalAsset, buildTimeline, loadProject, loadTimeline, planStory, savePlan, } from '../video/project.js';
3
5
  import { downloadStockAssets, generateImage, researchYouTube, searchStock, } from '../video/providers.js';
@@ -15,12 +17,14 @@ const httpUrl = z.string().url().refine((value) => {
15
17
  }, 'HTTP(S) URL만 허용합니다.');
16
18
  export function registerVideoTools(server) {
17
19
  server.tool('youtube_upload_video', [
18
- '로컬 영상 파일을 현재 Google OAuth 계정의 YouTube 채널에 업로드합니다.',
20
+ '로컬 영상 파일을 선택한 Google 로그인 프로필의 채널에 업로드합니다. expectedChannelId를 실제 인증 채널과 대조한 후 전송합니다.',
19
21
  '기본 공개 상태는 private이며 public/unlisted는 같은 턴의 명시 승인 후 confirmVisible=true가 필요합니다.',
20
22
  '업로드 후 youtube_get_video_status로 처리 완료 여부와 실제 공개 상태를 확인하세요.',
21
23
  '요청이 타임아웃되면 업로드가 뒤늦게 완료될 수 있으므로 YouTube Studio를 먼저 확인하고 중복 재시도하지 마세요.',
22
24
  'YouTube OAuth 권한이 없으면 mimi_seed_auth_start(domains=["youtube"])로 증분 연결하세요.',
23
25
  ].join(' '), {
26
+ profile: z.string().regex(GOOGLE_PROFILE_ID).optional().describe('Google 로그인 프로필. 지정한 프로필만 사용하며 기본 계정으로 대체하지 않습니다.'),
27
+ expectedChannelId: z.string().regex(YOUTUBE_CHANNEL_ID).describe('업로드 대상 채널 ID. 인증된 채널과 다르면 업로드 중단.'),
24
28
  filePath: absolutePath.describe('업로드할 .mp4/.mov/.webm 영상 절대경로'),
25
29
  title: z.string().min(1).max(100).describe('YouTube 영상 제목'),
26
30
  description: z.string().max(5_000).optional().describe('영상 설명과 음원 출처/라이선스 표기'),
@@ -33,24 +37,26 @@ export function registerVideoTools(server) {
33
37
  shortsOnly: z.boolean().default(false).describe('true면 세로/정사각형·180초 이하 쇼츠 규격을 강제'),
34
38
  confirmVisible: z.boolean().default(false).describe('public/unlisted 게시에 대한 명시 확인'),
35
39
  }, async (input) => {
36
- const auth = await requireAuth(YOUTUBE_SCOPE);
40
+ const auth = await requireAuth(YOUTUBE_SCOPE, input.profile);
37
41
  return text(await uploadYouTubeVideo(auth, input));
38
42
  });
39
43
  server.tool('youtube_get_video_status', '내 YouTube 영상의 업로드 처리 상태와 현재 공개 상태를 조회합니다.', {
44
+ profile: z.string().regex(GOOGLE_PROFILE_ID).optional().describe('영상 소유 채널의 Google 로그인 프로필'),
40
45
  videoId: z.string().min(1).max(64).describe('YouTube video ID'),
41
- }, async ({ videoId }) => {
42
- const auth = await requireAuth(YOUTUBE_SCOPE);
46
+ }, async ({ videoId, profile }) => {
47
+ const auth = await requireAuth(YOUTUBE_SCOPE, profile);
43
48
  return text(await getYouTubeVideoStatus(auth, videoId));
44
49
  });
45
50
  server.tool('youtube_update_video_privacy', [
46
51
  '내 YouTube 영상의 공개 상태를 변경하고 실제 반영 상태를 다시 조회합니다.',
47
52
  'public/unlisted 변경은 같은 턴의 명시 승인 후 confirmVisible=true가 필요합니다.',
48
53
  ].join(' '), {
54
+ profile: z.string().regex(GOOGLE_PROFILE_ID).optional().describe('영상 소유 채널의 Google 로그인 프로필'),
49
55
  videoId: z.string().min(1).max(64).describe('YouTube video ID'),
50
56
  privacyStatus: z.enum(['private', 'unlisted', 'public']),
51
57
  confirmVisible: z.boolean().default(false).describe('public/unlisted 변경에 대한 명시 확인'),
52
58
  }, async (input) => {
53
- const auth = await requireAuth(YOUTUBE_SCOPE);
59
+ const auth = await requireAuth(YOUTUBE_SCOPE, input.profile);
54
60
  return text(await updateYouTubeVideoPrivacy(auth, input));
55
61
  });
56
62
  server.tool('video_plan_from_story', [
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerYouTubeTools(server: McpServer): void;