@yoonion/mimi-seed-mcp 0.3.26 → 0.3.28

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.
@@ -1,7 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { checkPlayStoreRisks, checkAppStoreRisks, formatRisks } from '../checks/risks.js';
3
3
  import { validateAppStoreScreenshots, validatePlayStoreScreenshots, formatValidationResults } from '../checks/screenshots.js';
4
- import { requireAuth } from '../helpers.js';
4
+ import { requireAuth, requirePlayStoreAuth } from '../helpers.js';
5
+ import * as appstore from '../appstore/tools.js';
6
+ import * as playstore from '../playstore/tools.js';
5
7
  export function registerChecksTools(server) {
6
8
  server.tool('playstore_check_submission_risks', [
7
9
  'Google Play 제출 전 위험 요소를 자동으로 점검합니다.',
@@ -53,4 +55,90 @@ export function registerChecksTools(server) {
53
55
  return { content: [{ type: 'text', text }] };
54
56
  }
55
57
  });
58
+ server.tool('release_status', [
59
+ '양 스토어의 동일 버전 상태를 한 번에 조회 — 1.4.x 배포 시 "Play 는 production?", "ASC 는 심사중?" 을',
60
+ 'list_versions + list_tracks 2회 호출 + grep 으로 합치던 멘탈 부담을 단일 응답으로 줄임.',
61
+ 'App Store: appId 가 있을 때 listVersions 에서 version 일치 항목의 state/releaseType/attached build/createdDate.',
62
+ 'Play Store: packageName 이 있을 때 listTracks 에서 동일 versionName 또는 versionCode 가 있는 모든 트랙의 status/versionCodes.',
63
+ 'appId 또는 packageName 둘 다 비어 있으면 에러. 둘 중 하나만 줘도 그 스토어 상태만 조회 가능.',
64
+ ].join(' '), {
65
+ version: z.string().describe('조회할 버전명 (예: "1.4.9"). App Store versionString + Play release.name 매칭에 사용.'),
66
+ appId: z.string().optional().describe('App Store appId (appstore_list_apps 결과). 없으면 App Store 영역 skip.'),
67
+ packageName: z.string().optional().describe('Play 패키지명 (예: gg.pryzm.coffee). 없으면 Play 영역 skip.'),
68
+ }, async ({ version, appId, packageName }) => {
69
+ if (!appId && !packageName) {
70
+ throw new Error('appId 또는 packageName 중 최소 하나는 지정해야 해요.');
71
+ }
72
+ let appStoreSection = null;
73
+ if (appId) {
74
+ try {
75
+ const versions = (await appstore.listVersions(appId));
76
+ const match = versions.find((v) => v.version === version);
77
+ if (match) {
78
+ // attached build 조회 — listBuilds 까지 가지 않고 단일 versionId 의 build 만.
79
+ // appstore.tools.ts 의 attachBuildToVersion 인접 헬퍼는 외부 노출 X — 여기서는
80
+ // listBuilds(appId) 의 최근 10개에서 매칭 추정. 일치하는 buildNumber 가 없어도 무방.
81
+ const builds = (await appstore.listBuilds(appId).catch(() => []));
82
+ appStoreSection = {
83
+ versionId: match.id,
84
+ version: match.version,
85
+ state: match.state,
86
+ releaseType: match.releaseType,
87
+ createdDate: match.createdDate,
88
+ recentBuilds: builds.slice(0, 3).map((b) => ({
89
+ id: b.id,
90
+ buildNumber: b.version,
91
+ processingState: b.processingState,
92
+ })),
93
+ };
94
+ }
95
+ else {
96
+ appStoreSection = {
97
+ found: false,
98
+ available: versions.slice(0, 10).map((v) => ({ version: v.version, state: v.state })),
99
+ };
100
+ }
101
+ }
102
+ catch (e) {
103
+ appStoreSection = { error: e instanceof Error ? e.message : String(e) };
104
+ }
105
+ }
106
+ // ── Play Store ────────────────────────────────────────
107
+ let playStoreSection = null;
108
+ if (packageName) {
109
+ try {
110
+ const auth = requirePlayStoreAuth(packageName);
111
+ const tracks = await playstore.listTracks(auth, packageName);
112
+ // 트랙별로 version 이 매칭되는 release 찾기.
113
+ // match 기준: release.name === version (보통 "1.4.9" 또는 "1.4.9 (373)")
114
+ // 완전 일치가 없으면 prefix 매칭으로 폴백 (releaseName 표기 변동성 흡수).
115
+ const byTrack = {};
116
+ for (const t of tracks) {
117
+ const exact = t.releases.find((r) => r.name === version);
118
+ const prefix = exact ?? t.releases.find((r) => typeof r.name === 'string' && r.name?.startsWith(`${version} `));
119
+ if (prefix) {
120
+ byTrack[t.track ?? 'unknown'] = {
121
+ releaseName: prefix.name,
122
+ status: prefix.status,
123
+ versionCodes: prefix.versionCodes,
124
+ };
125
+ }
126
+ }
127
+ playStoreSection = Object.keys(byTrack).length > 0
128
+ ? byTrack
129
+ : {
130
+ found: false,
131
+ available: tracks.map((t) => ({
132
+ track: t.track,
133
+ releases: t.releases.map((r) => ({ name: r.name, status: r.status })),
134
+ })),
135
+ };
136
+ }
137
+ catch (e) {
138
+ playStoreSection = { error: e instanceof Error ? e.message : String(e) };
139
+ }
140
+ }
141
+ const result = { version, appStore: appStoreSection, playStore: playStoreSection };
142
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
143
+ });
56
144
  }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerGoogleAdsTools(server: McpServer): void;
@@ -0,0 +1,120 @@
1
+ import { z } from 'zod';
2
+ import { requireAuth } from '../helpers.js';
3
+ import { saveConfig, loadConfig, requireConfig } from '../googleads/config.js';
4
+ import * as googleads from '../googleads/tools.js';
5
+ export function registerGoogleAdsTools(server) {
6
+ server.tool('googleads_save_config', 'Google Ads API 설정 저장 (Developer Token + 계정 ID). 최초 1회만 필요.', {
7
+ developerToken: z.string().describe('Google Ads 콘솔 → 관리자 → API 센터에서 발급한 Developer Token'),
8
+ customerId: z.string().describe('Google Ads 계정 ID (예: 123-456-7890)'),
9
+ loginCustomerId: z.string().optional().describe('MCC(관리자) 계정 ID — 하위 계정 접근 시 필요'),
10
+ }, async ({ developerToken, customerId, loginCustomerId }) => {
11
+ saveConfig({ developerToken, customerId, loginCustomerId });
12
+ return {
13
+ content: [{
14
+ type: 'text',
15
+ text: [
16
+ '✅ Google Ads 설정 저장 완료.',
17
+ ` customerId: ${customerId}`,
18
+ loginCustomerId ? ` loginCustomerId: ${loginCustomerId}` : '',
19
+ '',
20
+ '이제 googleads_list_campaigns 나 googleads_get_uac_report 를 사용할 수 있어.',
21
+ '',
22
+ '⚠️ Google Ads API는 adwords OAuth 스코프가 필요해.',
23
+ '기존 토큰에 스코프가 없으면 npx -y @yoonion/mimi-seed-mcp mimi-seed-auth 로 재인증해줘.',
24
+ ].filter(Boolean).join('\n'),
25
+ }],
26
+ };
27
+ });
28
+ server.tool('googleads_list_campaigns', 'Google Ads 캠페인 목록 조회 (상태, 채널 타입, 일일 예산 포함)', {}, async () => {
29
+ const auth = requireAuth();
30
+ const cfg = requireConfig();
31
+ const campaigns = await googleads.listCampaigns(auth, cfg);
32
+ return {
33
+ content: [{
34
+ type: 'text',
35
+ text: JSON.stringify(campaigns, null, 2),
36
+ }],
37
+ };
38
+ });
39
+ server.tool('googleads_get_campaign_report', '기간별 캠페인 성과 리포트 (클릭, 노출, 비용, 전환수, CPI, CTR)', {
40
+ startDate: z.string().describe('시작일 (YYYY-MM-DD)'),
41
+ endDate: z.string().describe('종료일 (YYYY-MM-DD)'),
42
+ }, async ({ startDate, endDate }) => {
43
+ const auth = requireAuth();
44
+ const cfg = requireConfig();
45
+ const report = await googleads.getCampaignReport(auth, cfg, { startDate, endDate });
46
+ const totalCost = report.reduce((s, r) => s + r.cost, 0);
47
+ const totalClicks = report.reduce((s, r) => s + r.clicks, 0);
48
+ const totalImpressions = report.reduce((s, r) => s + r.impressions, 0);
49
+ const totalConversions = report.reduce((s, r) => s + r.conversions, 0);
50
+ return {
51
+ content: [{
52
+ type: 'text',
53
+ text: JSON.stringify({
54
+ period: { startDate, endDate },
55
+ summary: {
56
+ totalCost: Math.round(totalCost * 100) / 100,
57
+ totalClicks,
58
+ totalImpressions,
59
+ totalConversions,
60
+ avgCpi: totalConversions > 0 ? Math.round(totalCost / totalConversions * 100) / 100 : null,
61
+ },
62
+ campaigns: report,
63
+ }, null, 2),
64
+ }],
65
+ };
66
+ });
67
+ server.tool('googleads_get_uac_report', '앱 캠페인(UAC) 리포트 — 앱 설치 캠페인별 설치수, CPI, ROAS 집계', {
68
+ startDate: z.string().describe('시작일 (YYYY-MM-DD)'),
69
+ endDate: z.string().describe('종료일 (YYYY-MM-DD)'),
70
+ }, async ({ startDate, endDate }) => {
71
+ const auth = requireAuth();
72
+ const cfg = requireConfig();
73
+ const report = await googleads.getUacReport(auth, cfg, { startDate, endDate });
74
+ const totalCost = report.reduce((s, r) => s + r.cost, 0);
75
+ const totalInstalls = report.reduce((s, r) => s + r.installs, 0);
76
+ return {
77
+ content: [{
78
+ type: 'text',
79
+ text: JSON.stringify({
80
+ period: { startDate, endDate },
81
+ summary: {
82
+ totalCost: Math.round(totalCost * 100) / 100,
83
+ totalInstalls,
84
+ avgCpi: totalInstalls > 0 ? Math.round(totalCost / totalInstalls * 100) / 100 : null,
85
+ campaignCount: report.length,
86
+ },
87
+ campaigns: report,
88
+ }, null, 2),
89
+ }],
90
+ };
91
+ });
92
+ server.tool('googleads_list_accessible_customers', 'OAuth 토큰으로 접근 가능한 Google Ads 계정 목록 (API 연결 확인용)', {}, async () => {
93
+ const auth = requireAuth();
94
+ const cfg = requireConfig();
95
+ const result = await googleads.listAccessibleCustomers(auth, cfg);
96
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
97
+ });
98
+ server.tool('googleads_config_status', 'Google Ads 연동 설정 현황 확인', {}, async () => {
99
+ const cfg = loadConfig();
100
+ if (!cfg) {
101
+ return {
102
+ content: [{
103
+ type: 'text',
104
+ text: '❌ Google Ads 설정 없음. googleads_save_config 로 먼저 설정해.',
105
+ }],
106
+ };
107
+ }
108
+ return {
109
+ content: [{
110
+ type: 'text',
111
+ text: JSON.stringify({
112
+ status: 'configured',
113
+ customerId: cfg.customerId,
114
+ loginCustomerId: cfg.loginCustomerId ?? null,
115
+ hasDeveloperToken: !!cfg.developerToken,
116
+ }, null, 2),
117
+ }],
118
+ };
119
+ });
120
+ }
@@ -4,6 +4,7 @@ import { saveServiceAccountJsonForPackage, listRegisteredServiceAccounts, delete
4
4
  import { createGoogleOneTimePurchase, createGoogleSubscription, updateGoogleProduct, deleteGoogleProduct, listGoogleProducts, } from '@onesub/providers';
5
5
  import { requirePlayStoreAuth, requireServiceAccountJson } from '../helpers.js';
6
6
  import { buildPlayStoreReleasePlan } from '../checks/plan.js';
7
+ import { validatePlayReleaseNotes, formatIssuesForUser } from '../lib/text-validators.js';
7
8
  export function registerPlaystoreTools(server) {
8
9
  server.tool('playstore_get_app', 'Google Play 앱 상세 정보 조회', { packageName: z.string().describe('패키지명 (예: com.findthem.app)') }, async ({ packageName }) => {
9
10
  const auth = requirePlayStoreAuth(packageName);
@@ -36,6 +37,45 @@ export function registerPlaystoreTools(server) {
36
37
  const tracks = await playstore.listTracks(auth, packageName);
37
38
  return { content: [{ type: 'text', text: JSON.stringify(tracks, null, 2) }] };
38
39
  });
40
+ server.tool('playstore_get_statistics', 'Google Play Developer Reporting API / Android vitals 통계 조회. ANR/Crash/Error count를 기간·버전·기기·국가 등으로 분해해 확인', {
41
+ packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
42
+ metricSet: z.enum(['anrRate', 'crashRate', 'errorCount'])
43
+ .default('anrRate')
44
+ .describe('조회할 Vitals metric set. 기본: anrRate'),
45
+ startDate: z.string().describe('시작일 YYYY-MM-DD (포함)'),
46
+ endDate: z.string().describe('종료일 YYYY-MM-DD (보통 exclusive처럼 다음날 지정 권장)'),
47
+ aggregationPeriod: z.enum(['DAILY', 'HOURLY']).default('DAILY').describe('집계 단위'),
48
+ dimensions: z.array(z.enum([
49
+ 'apiLevel',
50
+ 'versionCode',
51
+ 'deviceModel',
52
+ 'deviceBrand',
53
+ 'deviceType',
54
+ 'countryCode',
55
+ 'deviceRamBucket',
56
+ 'deviceSocMake',
57
+ 'deviceSocModel',
58
+ 'deviceCpuMake',
59
+ 'deviceCpuModel',
60
+ 'deviceGpuMake',
61
+ 'deviceGpuModel',
62
+ 'deviceGpuVersion',
63
+ 'deviceVulkanVersion',
64
+ 'deviceGlEsVersion',
65
+ 'deviceScreenSize',
66
+ 'deviceScreenDpi',
67
+ ])).optional().describe('분해 차원. 기본: ["versionCode"]'),
68
+ metrics: z.array(z.string()).optional().describe('조회 metric. 기본은 metricSet별 핵심 metric + distinctUsers'),
69
+ filter: z.string().optional().describe('AIP-160 필터. 예: versionCode = 42'),
70
+ pageSize: z.number().int().min(1).max(100000).optional().describe('최대 행 수'),
71
+ pageToken: z.string().optional().describe('다음 페이지 토큰'),
72
+ userCohort: z.enum(['OS_PUBLIC', 'APP_TESTERS', 'OS_BETA']).optional().describe('사용자 cohort'),
73
+ timeZone: z.string().optional().describe('날짜 기준 timezone. 기본: America/Los_Angeles (Play Reporting API 샘플/지원 기준)'),
74
+ }, async (args) => {
75
+ const auth = requirePlayStoreAuth(args.packageName);
76
+ const result = await playstore.getStatistics(auth, args.packageName, args);
77
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
78
+ });
39
79
  server.tool('playstore_list_images', 'Google Play 리스팅 이미지 목록 조회 (imageType별)', {
40
80
  packageName: z.string().describe('패키지명'),
41
81
  language: z.string().describe('언어 코드 (예: ko-KR)'),
@@ -81,19 +121,73 @@ export function registerPlaystoreTools(server) {
81
121
  language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
82
122
  text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
83
123
  }, async ({ packageName, track, versionCode, language, text }) => {
124
+ // ── 사전 lint — 500자 / HTML / 역슬래시 가격(\5000원) round-trip 차단.
125
+ const validation = validatePlayReleaseNotes(text);
126
+ if (!validation.ok) {
127
+ return {
128
+ content: [{
129
+ type: 'text',
130
+ text: `❌ 릴리스 노트 사전 검증 실패 — API 호출 안 함\n\n${formatIssuesForUser(validation.issues)}\n\n수정 후 다시 호출해주세요.`,
131
+ }],
132
+ isError: true,
133
+ };
134
+ }
84
135
  const auth = requirePlayStoreAuth(packageName);
85
136
  const result = await playstore.updateReleaseNotes(auth, packageName, track, versionCode, language, text);
86
137
  return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode} ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
87
138
  });
88
- server.tool('playstore_update_latest_release_notes', "Google Play 트랙의 최신 릴리스(versionCode 최대) '최근 변경사항' 업데이트 — versionCode를 모를 때 편의용", {
139
+ server.tool('playstore_update_latest_release_notes', [
140
+ "Google Play 트랙의 최신 릴리스(versionCode 최대) '최근 변경사항' 업데이트 — versionCode를 모를 때 편의용.",
141
+ '⚠️ 지정한 단일 트랙에만 적용 — 다른 트랙에는 자동 복사되지 않음 (Google Play 정책: promote_release 시점에 노트 캐리됨).',
142
+ '동일 노트를 여러 트랙에 즉시 반영하려면 syncTracks 옵션 사용 — 지정 트랙들에 대해 순차로 같은 노트 적용.',
143
+ ].join(' '), {
89
144
  packageName: z.string().describe('패키지명'),
90
- track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('릴리스 트랙'),
145
+ track: z.enum(['production', 'beta', 'alpha', 'internal']).describe('1차 적용 트랙'),
91
146
  language: z.string().describe('언어 코드 (예: ko-KR)'),
92
147
  text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
93
- }, async ({ packageName, track, language, text }) => {
148
+ syncTracks: z.array(z.enum(['production', 'beta', 'alpha', 'internal']))
149
+ .optional()
150
+ .describe('추가로 동일 노트 적용할 트랙 배열 (예: ["production"]). 지정 시 1차 track 반영 후 순차 동기화.'),
151
+ }, async ({ packageName, track, language, text, syncTracks }) => {
152
+ const validation = validatePlayReleaseNotes(text);
153
+ if (!validation.ok) {
154
+ return {
155
+ content: [{
156
+ type: 'text',
157
+ text: `❌ 릴리스 노트 사전 검증 실패 — API 호출 안 함\n\n${formatIssuesForUser(validation.issues)}\n\n수정 후 다시 호출해주세요.`,
158
+ }],
159
+ isError: true,
160
+ };
161
+ }
94
162
  const auth = requirePlayStoreAuth(packageName);
95
- const result = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
96
- return { content: [{ type: 'text', text: `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(result.updatedVersionCodes)}) ${language} 노트 반영\n\n${JSON.stringify(result, null, 2)}` }] };
163
+ // 1차 적용 + 결과 누적.
164
+ const primaryResult = await playstore.updateLatestReleaseNotes(auth, packageName, track, language, text);
165
+ const lines = [
166
+ `✅ ${packageName} ${track} (versionCodes=${JSON.stringify(primaryResult.updatedVersionCodes)}) ${language} 노트 반영`,
167
+ ];
168
+ const allResults = { [track]: primaryResult };
169
+ // 추가 트랙 — 1차와 중복은 skip. 한 트랙 실패해도 나머지 트랙은 계속 시도.
170
+ if (syncTracks && syncTracks.length > 0) {
171
+ const targets = syncTracks.filter((t) => t !== track);
172
+ for (const t of targets) {
173
+ try {
174
+ const r = await playstore.updateLatestReleaseNotes(auth, packageName, t, language, text);
175
+ allResults[t] = r;
176
+ lines.push(` ↳ sync ${t} (versionCodes=${JSON.stringify(r.updatedVersionCodes)}) 반영`);
177
+ }
178
+ catch (e) {
179
+ const msg = e instanceof Error ? e.message : String(e);
180
+ allResults[t] = { error: msg };
181
+ lines.push(` ↳ sync ${t} 실패: ${msg}`);
182
+ }
183
+ }
184
+ }
185
+ return {
186
+ content: [{
187
+ type: 'text',
188
+ text: `${lines.join('\n')}\n\n${JSON.stringify(allResults, null, 2)}`,
189
+ }],
190
+ };
97
191
  });
98
192
  server.tool('playstore_list_reviews', 'Google Play 리뷰 목록 조회', { packageName: z.string().describe('패키지명') }, async ({ packageName }) => {
99
193
  const auth = requirePlayStoreAuth(packageName);
package/dist/resources.js CHANGED
@@ -25,7 +25,7 @@ export function registerResources(server) {
25
25
  '# Mimi Seed — 앱 출시·운영 Agent',
26
26
  '',
27
27
  '당신은 Mimi Seed MCP를 통해 인디 개발자의 앱 출시와 운영을 돕는 에이전트입니다.',
28
- 'Google Play · App Store · Firebase · AdMob 직접 제어하는 65+ 도구를 사용할 수 있습니다.',
28
+ 'Google Play · App Store · Firebase · AdMob · CI/CD · BigQuery를 직접 제어하는 110+ 도구를 사용할 수 있습니다.',
29
29
  '',
30
30
  '## 출시 요청 처리 순서',
31
31
  '',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.26",
4
- "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Cursor / any MCP client.",
3
+ "version": "0.3.28",
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": {
7
7
  "mimi-seed-mcp": "dist/index.js",
@@ -27,6 +27,7 @@
27
27
  "mcp-server",
28
28
  "model-context-protocol",
29
29
  "claude",
30
+ "codex",
30
31
  "firebase",
31
32
  "admob",
32
33
  "google-play",