@yoonion/mimi-seed-mcp 0.3.25 → 0.3.27
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/appstore/errors.d.ts +45 -0
- package/dist/appstore/errors.js +88 -0
- package/dist/appstore/tools.d.ts +48 -0
- package/dist/appstore/tools.js +95 -3
- package/dist/auth/bigquery-auth.d.ts +32 -0
- package/dist/auth/bigquery-auth.js +100 -0
- package/dist/auth/bigquery-setup-cli.d.ts +2 -0
- package/dist/auth/bigquery-setup-cli.js +91 -0
- package/dist/auth/errors.d.ts +1 -1
- package/dist/auth/errors.js +12 -1
- package/dist/auth/google-auth.d.ts +12 -0
- package/dist/auth/google-auth.js +34 -8
- package/dist/bigquery/tools.d.ts +7 -5
- package/dist/index.js +1 -0
- package/dist/lib/text-validators.d.ts +34 -0
- package/dist/lib/text-validators.js +106 -0
- package/dist/registers/appstore.js +78 -1
- package/dist/registers/auth.js +38 -3
- package/dist/registers/bigquery.js +106 -14
- package/dist/registers/checks.js +89 -1
- package/dist/registers/playstore.js +60 -5
- package/package.json +3 -2
package/dist/auth/google-auth.js
CHANGED
|
@@ -60,6 +60,27 @@ export function getStoredTokens() {
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* tokens.json mtime — 마지막 refresh 시각의 근사값.
|
|
65
|
+
* (saveTokens 가 매번 writeFileSync 으로 갱신하므로 mtime ≈ 마지막 갱신/저장.)
|
|
66
|
+
* Google refresh_token 은 7일(미인증 앱) ~ 6개월(인증 앱) 미사용 시 revoke 됨.
|
|
67
|
+
* auth_status 응답 enrichment 에 사용.
|
|
68
|
+
*/
|
|
69
|
+
export function getTokensLastRefreshMs() {
|
|
70
|
+
const pathToRead = fs.existsSync(TOKEN_PATH)
|
|
71
|
+
? TOKEN_PATH
|
|
72
|
+
: fs.existsSync(LEGACY_TOKEN_PATH)
|
|
73
|
+
? LEGACY_TOKEN_PATH
|
|
74
|
+
: null;
|
|
75
|
+
if (!pathToRead)
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
return fs.statSync(pathToRead).mtimeMs;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
63
84
|
function saveTokens(tokens) {
|
|
64
85
|
ensureDir();
|
|
65
86
|
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
@@ -189,13 +210,13 @@ export function startAuth(clientId, clientSecret, options = {}) {
|
|
|
189
210
|
};
|
|
190
211
|
saveTokens(stored);
|
|
191
212
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
192
|
-
res.end(`
|
|
193
|
-
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
|
|
194
|
-
<div style="text-align:center">
|
|
195
|
-
<h1>✅ Mimi Seed 인증 완료!</h1>
|
|
196
|
-
<p>이 창을 닫고 Claude Code로 돌아가세요.</p>
|
|
197
|
-
</div>
|
|
198
|
-
</body></html>
|
|
213
|
+
res.end(`
|
|
214
|
+
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
|
|
215
|
+
<div style="text-align:center">
|
|
216
|
+
<h1>✅ Mimi Seed 인증 완료!</h1>
|
|
217
|
+
<p>이 창을 닫고 Claude Code로 돌아가세요.</p>
|
|
218
|
+
</div>
|
|
219
|
+
</body></html>
|
|
199
220
|
`);
|
|
200
221
|
server.close();
|
|
201
222
|
activeAuthServer = null;
|
|
@@ -253,7 +274,12 @@ export async function login(clientId, clientSecret) {
|
|
|
253
274
|
*
|
|
254
275
|
* MCP 도구와 CLI 양쪽에서 공유.
|
|
255
276
|
*/
|
|
256
|
-
|
|
277
|
+
/**
|
|
278
|
+
* 사전 갱신 마진. 기존 60_000(1분)에서 300_000(5분)으로 상향 — Google OAuth access_token 의
|
|
279
|
+
* 통상 lifetime 이 1h 이므로, 5분 마진으로 매 도구 호출 시 만료 임박 시 사전 갱신해
|
|
280
|
+
* "토큰 만료 → 도구 fail → 재호출" 의 단절 마찰 제거. 5분 마진은 평균 도구 작업 시간을 흡수.
|
|
281
|
+
*/
|
|
282
|
+
export async function ensureFreshAccessToken(marginMs = 300_000) {
|
|
257
283
|
const tokens = getStoredTokens();
|
|
258
284
|
if (!tokens) {
|
|
259
285
|
return {
|
package/dist/bigquery/tools.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { OAuth2Client } from 'google-auth-library';
|
|
2
|
-
|
|
1
|
+
import type { OAuth2Client, JWT } from 'google-auth-library';
|
|
2
|
+
/** BigQuery 호출에 쓰이는 인증 클라이언트 — 사용자 OAuth 또는 서비스 계정 JWT. */
|
|
3
|
+
export type BigQueryAuthClient = OAuth2Client | JWT;
|
|
4
|
+
export declare function runQuery(auth: BigQueryAuthClient, projectId: string, query: string, maxResults?: number): Promise<{
|
|
3
5
|
jobComplete: boolean | null | undefined;
|
|
4
6
|
totalRows: string | null | undefined;
|
|
5
7
|
schema: {
|
|
@@ -10,15 +12,15 @@ export declare function runQuery(auth: OAuth2Client, projectId: string, query: s
|
|
|
10
12
|
[k: string]: any;
|
|
11
13
|
}[];
|
|
12
14
|
}>;
|
|
13
|
-
export declare function listDatasets(auth:
|
|
15
|
+
export declare function listDatasets(auth: BigQueryAuthClient, projectId: string): Promise<{
|
|
14
16
|
datasetId: string | null | undefined;
|
|
15
17
|
location: string | undefined;
|
|
16
18
|
}[]>;
|
|
17
|
-
export declare function listTables(auth:
|
|
19
|
+
export declare function listTables(auth: BigQueryAuthClient, projectId: string, datasetId: string): Promise<{
|
|
18
20
|
tableId: string | null | undefined;
|
|
19
21
|
type: string | undefined;
|
|
20
22
|
}[]>;
|
|
21
|
-
export declare function getTableSchema(auth:
|
|
23
|
+
export declare function getTableSchema(auth: BigQueryAuthClient, projectId: string, datasetId: string, tableId: string): Promise<{
|
|
22
24
|
tableId: string;
|
|
23
25
|
schema: {
|
|
24
26
|
name: string | null | undefined;
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ const SUBCOMMANDS = {
|
|
|
41
41
|
'mimi-seed-auth': () => import('./auth/cli.js'),
|
|
42
42
|
'mimi-seed-playstore-auth': () => import('./auth/playstore-setup-cli.js'),
|
|
43
43
|
'mimi-seed-appstore-auth': () => import('./appstore/setup-cli.js'),
|
|
44
|
+
'mimi-seed-bigquery-auth': () => import('./auth/bigquery-setup-cli.js'),
|
|
44
45
|
};
|
|
45
46
|
async function main() {
|
|
46
47
|
const sub = process.argv[2];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 릴리스 노트 / What's New 텍스트 사전 검증.
|
|
3
|
+
*
|
|
4
|
+
* 스토어 측 거부 패턴(HTML 마크업, 길이 초과, 백슬래시 가격 표기 등)을
|
|
5
|
+
* API 호출 전에 잡아 round-trip 낭비와 무지한 재시도를 차단.
|
|
6
|
+
*
|
|
7
|
+
* 모든 detection 은 정규식 기반 단순 휴리스틱 — false positive 최소화를
|
|
8
|
+
* 위해 실측으로 거부된 패턴만 포함한다.
|
|
9
|
+
*/
|
|
10
|
+
export type ValidationCode = "HTML_TAG" | "LENGTH_EXCEEDED" | "BACKSLASH_PRICE";
|
|
11
|
+
export interface ValidationIssue {
|
|
12
|
+
code: ValidationCode;
|
|
13
|
+
message: string;
|
|
14
|
+
/** 0-based 문자 인덱스. LENGTH_EXCEEDED 에는 없음. */
|
|
15
|
+
position?: number;
|
|
16
|
+
/** 문제 구간 ±10자 잘라낸 문자열 (HTML/BACKSLASH 위치 표시용). */
|
|
17
|
+
excerpt?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ValidationResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
issues: ValidationIssue[];
|
|
22
|
+
}
|
|
23
|
+
/** App Store What's New 검증 — 길이(≤4000) + HTML 태그 거부. */
|
|
24
|
+
export declare function validateAppStoreWhatsNew(text: string): ValidationResult;
|
|
25
|
+
/** Play release notes 검증 — 길이(≤500) + HTML + 백슬래시 가격. */
|
|
26
|
+
export declare function validatePlayReleaseNotes(text: string): ValidationResult;
|
|
27
|
+
/** 사용자 친화 메시지로 포맷. MCP 도구가 거부 응답에 그대로 노출. */
|
|
28
|
+
export declare function formatIssuesForUser(issues: ValidationIssue[]): string;
|
|
29
|
+
export declare const __testing: {
|
|
30
|
+
MAX_APPSTORE_WHATSNEW: number;
|
|
31
|
+
MAX_PLAY_RELEASE_NOTES: number;
|
|
32
|
+
HTML_TAG_PATTERN: RegExp;
|
|
33
|
+
BACKSLASH_PRICE_PATTERN: RegExp;
|
|
34
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 릴리스 노트 / What's New 텍스트 사전 검증.
|
|
3
|
+
*
|
|
4
|
+
* 스토어 측 거부 패턴(HTML 마크업, 길이 초과, 백슬래시 가격 표기 등)을
|
|
5
|
+
* API 호출 전에 잡아 round-trip 낭비와 무지한 재시도를 차단.
|
|
6
|
+
*
|
|
7
|
+
* 모든 detection 은 정규식 기반 단순 휴리스틱 — false positive 최소화를
|
|
8
|
+
* 위해 실측으로 거부된 패턴만 포함한다.
|
|
9
|
+
*/
|
|
10
|
+
// ── 한계값 (스토어 정책 SSOT) ─────────────────────────────────────
|
|
11
|
+
const MAX_APPSTORE_WHATSNEW = 4000;
|
|
12
|
+
const MAX_PLAY_RELEASE_NOTES = 500;
|
|
13
|
+
// ── 패턴 ──────────────────────────────────────────────────────────
|
|
14
|
+
// HTML 태그: <br>, </tag>, <a href="…"> 모두 매칭. Apple/Play 둘 다 마크업 거부.
|
|
15
|
+
const HTML_TAG_PATTERN = /<\/?[a-zA-Z][^>]*>/g;
|
|
16
|
+
// Play Store 가 홍보 정책으로 거부한 실측 사례: 본문에 '\5000원' 같이
|
|
17
|
+
// 역슬래시 + 숫자 + (선택적 통화 단위) 패턴 (memory: reference_appstore_whatsnew_blacklist).
|
|
18
|
+
const BACKSLASH_PRICE_PATTERN = /\\\d[\d,]*(?:원|won|krw)?/gi;
|
|
19
|
+
// ── 헬퍼 ──────────────────────────────────────────────────────────
|
|
20
|
+
function makeExcerpt(text, pos, radius = 10) {
|
|
21
|
+
const start = Math.max(0, pos - radius);
|
|
22
|
+
const end = Math.min(text.length, pos + radius);
|
|
23
|
+
const left = start > 0 ? "…" : "";
|
|
24
|
+
const right = end < text.length ? "…" : "";
|
|
25
|
+
return `${left}${text.slice(start, end)}${right}`;
|
|
26
|
+
}
|
|
27
|
+
function lintHtmlTags(text) {
|
|
28
|
+
const issues = [];
|
|
29
|
+
HTML_TAG_PATTERN.lastIndex = 0;
|
|
30
|
+
let m;
|
|
31
|
+
while ((m = HTML_TAG_PATTERN.exec(text)) !== null) {
|
|
32
|
+
issues.push({
|
|
33
|
+
code: "HTML_TAG",
|
|
34
|
+
message: `HTML 태그 '${m[0]}' 가 포함됐어요 — App Store / Play 모두 마크업 거부`,
|
|
35
|
+
position: m.index,
|
|
36
|
+
excerpt: makeExcerpt(text, m.index),
|
|
37
|
+
});
|
|
38
|
+
// 무한루프 방어 (제로폭 매치는 사실상 발생 안 하지만 안전망)
|
|
39
|
+
if (m.index === HTML_TAG_PATTERN.lastIndex)
|
|
40
|
+
HTML_TAG_PATTERN.lastIndex++;
|
|
41
|
+
}
|
|
42
|
+
return issues;
|
|
43
|
+
}
|
|
44
|
+
function lintLength(text, limit, label) {
|
|
45
|
+
if (text.length <= limit)
|
|
46
|
+
return [];
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
code: "LENGTH_EXCEEDED",
|
|
50
|
+
message: `${label} 길이 ${text.length}자 — ${limit}자 이하로 줄여주세요`,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
function lintBackslashPrice(text) {
|
|
55
|
+
const issues = [];
|
|
56
|
+
BACKSLASH_PRICE_PATTERN.lastIndex = 0;
|
|
57
|
+
let m;
|
|
58
|
+
while ((m = BACKSLASH_PRICE_PATTERN.exec(text)) !== null) {
|
|
59
|
+
issues.push({
|
|
60
|
+
code: "BACKSLASH_PRICE",
|
|
61
|
+
message: `'${m[0]}' 처럼 역슬래시 + 숫자(가격) 표기는 Play Store 가 홍보 정책으로 거부할 수 있어요`,
|
|
62
|
+
position: m.index,
|
|
63
|
+
excerpt: makeExcerpt(text, m.index),
|
|
64
|
+
});
|
|
65
|
+
if (m.index === BACKSLASH_PRICE_PATTERN.lastIndex)
|
|
66
|
+
BACKSLASH_PRICE_PATTERN.lastIndex++;
|
|
67
|
+
}
|
|
68
|
+
return issues;
|
|
69
|
+
}
|
|
70
|
+
// ── public API ────────────────────────────────────────────────────
|
|
71
|
+
/** App Store What's New 검증 — 길이(≤4000) + HTML 태그 거부. */
|
|
72
|
+
export function validateAppStoreWhatsNew(text) {
|
|
73
|
+
const issues = [
|
|
74
|
+
...lintLength(text, MAX_APPSTORE_WHATSNEW, "App Store What's New"),
|
|
75
|
+
...lintHtmlTags(text),
|
|
76
|
+
];
|
|
77
|
+
return { ok: issues.length === 0, issues };
|
|
78
|
+
}
|
|
79
|
+
/** Play release notes 검증 — 길이(≤500) + HTML + 백슬래시 가격. */
|
|
80
|
+
export function validatePlayReleaseNotes(text) {
|
|
81
|
+
const issues = [
|
|
82
|
+
...lintLength(text, MAX_PLAY_RELEASE_NOTES, "Play release notes"),
|
|
83
|
+
...lintHtmlTags(text),
|
|
84
|
+
...lintBackslashPrice(text),
|
|
85
|
+
];
|
|
86
|
+
return { ok: issues.length === 0, issues };
|
|
87
|
+
}
|
|
88
|
+
/** 사용자 친화 메시지로 포맷. MCP 도구가 거부 응답에 그대로 노출. */
|
|
89
|
+
export function formatIssuesForUser(issues) {
|
|
90
|
+
if (issues.length === 0)
|
|
91
|
+
return "검증 통과";
|
|
92
|
+
return issues
|
|
93
|
+
.map((i) => {
|
|
94
|
+
const loc = i.position !== undefined ? ` (pos ${i.position})` : "";
|
|
95
|
+
const ex = i.excerpt ? ` — 근처: ${JSON.stringify(i.excerpt)}` : "";
|
|
96
|
+
return `[${i.code}] ${i.message}${loc}${ex}`;
|
|
97
|
+
})
|
|
98
|
+
.join("\n");
|
|
99
|
+
}
|
|
100
|
+
// 테스트용 export (한계값 단언)
|
|
101
|
+
export const __testing = {
|
|
102
|
+
MAX_APPSTORE_WHATSNEW,
|
|
103
|
+
MAX_PLAY_RELEASE_NOTES,
|
|
104
|
+
HTML_TAG_PATTERN,
|
|
105
|
+
BACKSLASH_PRICE_PATTERN,
|
|
106
|
+
};
|
|
@@ -4,6 +4,7 @@ import * as appstoreScreenshots from '../appstore/screenshots.js';
|
|
|
4
4
|
import { createAppleOneTimePurchase, createAppleSubscription, updateAppleProduct, deleteAppleProduct, listAppleProducts, } from '@onesub/providers';
|
|
5
5
|
import { requireAppStoreCreds } from '../helpers.js';
|
|
6
6
|
import { buildAppStoreReleasePlan } from '../checks/plan.js';
|
|
7
|
+
import { validateAppStoreWhatsNew, formatIssuesForUser } from '../lib/text-validators.js';
|
|
7
8
|
export function registerAppstoreTools(server) {
|
|
8
9
|
server.tool('appstore_list_apps', 'App Store Connect 앱 목록 조회', {}, async () => {
|
|
9
10
|
const apps = await appstore.listApps();
|
|
@@ -80,6 +81,25 @@ export function registerAppstoreTools(server) {
|
|
|
80
81
|
],
|
|
81
82
|
};
|
|
82
83
|
});
|
|
84
|
+
server.tool('appstore_attach_latest_build', [
|
|
85
|
+
'App Store 버전에 최신 VALID 빌드를 자동으로 attach — list_builds → 필터 → attach 의 3-step 을 1회로 단축.',
|
|
86
|
+
'내부: versionId 로 appId 역추적 → listBuilds 에서 processingState=VALID 만 필터 → buildNumber 숫자 최대값 선택 → attach.',
|
|
87
|
+
'PROCESSING 중인 빌드를 실수로 attach 해서 심사 제출 시 깨지는 케이스를 차단.',
|
|
88
|
+
'minBuildNumber 옵션으로 floor 지정 가능 (예: 1.4.x 빌드만 attach).',
|
|
89
|
+
].join(' '), {
|
|
90
|
+
versionId: z.string().describe('App Store 버전 ID (appstore_create_version 또는 list_versions 결과)'),
|
|
91
|
+
minBuildNumber: z.number().int().optional().describe('attach 후보 최소 buildNumber (예: 186 — 이전 빌드 무시)'),
|
|
92
|
+
}, async ({ versionId, minBuildNumber }) => {
|
|
93
|
+
const result = await appstore.attachLatestValidBuild(versionId, { minBuildNumber });
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: `✅ 최신 VALID 빌드 #${result.buildNumber} (id=${result.attachedBuildId}) 가 버전 ${versionId} 에 연결됐어.\n\n${JSON.stringify(result, null, 2)}`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
});
|
|
83
103
|
server.tool('appstore_get_metadata', 'App Store 버전 메타데이터 (설명문, 키워드, What\'s New)', { versionId: z.string().describe('버전 ID') }, async ({ versionId }) => {
|
|
84
104
|
const localizations = await appstore.getVersionLocalizations(versionId);
|
|
85
105
|
return { content: [{ type: 'text', text: JSON.stringify(localizations, null, 2) }] };
|
|
@@ -97,6 +117,19 @@ export function registerAppstoreTools(server) {
|
|
|
97
117
|
if (Object.keys(cleaned).length === 0) {
|
|
98
118
|
throw new Error('수정할 필드를 하나 이상 지정해줘 (whatsNew, description, keywords, promotionalText, supportUrl, marketingUrl).');
|
|
99
119
|
}
|
|
120
|
+
// whatsNew 가 포함될 때만 사전 lint — 다른 필드(description/keywords)는 별도 정책.
|
|
121
|
+
if (typeof cleaned.whatsNew === 'string') {
|
|
122
|
+
const validation = validateAppStoreWhatsNew(cleaned.whatsNew);
|
|
123
|
+
if (!validation.ok) {
|
|
124
|
+
return {
|
|
125
|
+
content: [{
|
|
126
|
+
type: 'text',
|
|
127
|
+
text: `❌ whatsNew 사전 검증 실패 — API 호출 안 함\n\n${formatIssuesForUser(validation.issues)}\n\n수정 후 다시 호출해주세요.`,
|
|
128
|
+
}],
|
|
129
|
+
isError: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
100
133
|
const result = await appstore.updateVersionLocalization(localizationId, cleaned);
|
|
101
134
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
102
135
|
});
|
|
@@ -139,6 +172,17 @@ export function registerAppstoreTools(server) {
|
|
|
139
172
|
locale: z.string().describe('로캘 (예: ko, en-US, ja)'),
|
|
140
173
|
whatsNew: z.string().describe("'이 버전의 새로운 기능' 텍스트 (4000자 이내)"),
|
|
141
174
|
}, async ({ versionId, locale, whatsNew }) => {
|
|
175
|
+
// ── 사전 lint — Apple 409 INVALID_CHARACTERS 등 round-trip 낭비 차단.
|
|
176
|
+
const validation = validateAppStoreWhatsNew(whatsNew);
|
|
177
|
+
if (!validation.ok) {
|
|
178
|
+
return {
|
|
179
|
+
content: [{
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: `❌ What's New 사전 검증 실패 — API 호출 안 함\n\n${formatIssuesForUser(validation.issues)}\n\n수정 후 다시 호출해주세요.`,
|
|
182
|
+
}],
|
|
183
|
+
isError: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
142
186
|
const result = await appstore.updateVersionWhatsNew(versionId, locale, { whatsNew });
|
|
143
187
|
return { content: [{ type: 'text', text: `✅ ${locale} 로캘의 What's New가 업데이트됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
144
188
|
});
|
|
@@ -431,11 +475,44 @@ export function registerAppstoreTools(server) {
|
|
|
431
475
|
'내부 흐름: POST /reviewSubmissions(또는 CREATED 상태 재사용) → POST /reviewSubmissionItems(version attach) → PATCH submitted=true.',
|
|
432
476
|
'appId와 platform은 versionId에서 자동 조회 — 별도 입력 불필요.',
|
|
433
477
|
'⚠️ 비가역 작업: 제출 후엔 Apple 심사가 시작되며, 메타데이터/스크린샷/빌드를 더 못 바꿈 (REJECTED/METADATA_REJECTED 시 다시 편집 가능).',
|
|
478
|
+
'안전 가드: confirm 생략/false 시 dry-run preview 만 반환 (versionString·빌드·whatsNew 발췌). 실제 제출은 confirm: true 로 재호출.',
|
|
434
479
|
'사전 조건: 버전이 PREPARE_FOR_SUBMISSION 또는 DEVELOPER_REJECTED 상태, 빌드 attached, 모든 필수 메타데이터 채워짐.',
|
|
435
480
|
'appstore_check_submission_risks로 사전 점검 권장.',
|
|
436
481
|
].join(' '), {
|
|
437
482
|
versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
|
|
438
|
-
|
|
483
|
+
confirm: z.boolean().optional().describe('true 명시 시에만 실제 심사 제출. 생략/false 면 dry-run preview 만 반환 (비가역 사고 차단).'),
|
|
484
|
+
}, async ({ versionId, confirm }) => {
|
|
485
|
+
if (!confirm) {
|
|
486
|
+
// ── dry-run preview — versionString·빌드·whatsNew 발췌를 사용자에게 보여주고 재호출 유도.
|
|
487
|
+
const preview = await appstore.buildSubmitForReviewPreview(versionId);
|
|
488
|
+
const lines = [];
|
|
489
|
+
lines.push('🛑 심사 제출 dry-run — 아직 실제 제출 안 함.');
|
|
490
|
+
lines.push('');
|
|
491
|
+
lines.push(` versionId : ${preview.versionId}`);
|
|
492
|
+
lines.push(` versionString: ${preview.versionString ?? '(조회 실패)'}`);
|
|
493
|
+
lines.push(` state : ${preview.state ?? '(조회 실패)'}`);
|
|
494
|
+
lines.push(` appId : ${preview.appId}`);
|
|
495
|
+
lines.push(` platform : ${preview.platform}`);
|
|
496
|
+
if (preview.attachedBuild) {
|
|
497
|
+
lines.push(` attachedBuild: #${preview.attachedBuild.buildNumber ?? '?'} (id=${preview.attachedBuild.id}, state=${preview.attachedBuild.processingState ?? '?'})`);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
lines.push(` attachedBuild: ⚠️ 미연결 — appstore_attach_latest_build 필요`);
|
|
501
|
+
}
|
|
502
|
+
if (preview.whatsNewByLocale.length === 0) {
|
|
503
|
+
lines.push(` whatsNew : ⚠️ 등록된 로컬라이제이션 없음`);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
lines.push(` whatsNew :`);
|
|
507
|
+
for (const wn of preview.whatsNewByLocale) {
|
|
508
|
+
lines.push(` [${wn.locale}] (${wn.length}자) "${wn.excerpt}"`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
lines.push('');
|
|
512
|
+
lines.push('실제 제출하려면 같은 versionId 로 `confirm: true` 옵션을 추가해 재호출하세요.');
|
|
513
|
+
lines.push('⚠️ 제출 후엔 cancel_review 가 큐 진입(WAITING_FOR_REVIEW) 시점에 막힐 수 있어요 (실측: 1.4.2→3, 1.4.5→6).');
|
|
514
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
515
|
+
}
|
|
439
516
|
const result = await appstore.submitVersionForReview(versionId);
|
|
440
517
|
return { content: [{ type: 'text', text: `✅ 버전 ${versionId} 심사 제출 완료 (state: ${result.state}). App Store Connect에서 진행 상태 확인 가능.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
441
518
|
});
|
package/dist/registers/auth.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { getMcpOAuthClient } from '../auth/constants.js';
|
|
2
|
-
import { startAuth, ensureFreshAccessToken } from '../auth/google-auth.js';
|
|
2
|
+
import { startAuth, ensureFreshAccessToken, getTokensLastRefreshMs } from '../auth/google-auth.js';
|
|
3
|
+
/**
|
|
4
|
+
* tokens.json mtime → "Nd Hh ago" 형식 문자열 + 재인증 권고.
|
|
5
|
+
* Google 정책: 7일 미사용 시 미인증 앱 refresh_token revoke 가능.
|
|
6
|
+
* 14일 초과 시 강한 권고, 7일~14일 부드러운 안내.
|
|
7
|
+
*/
|
|
8
|
+
function formatLastRefreshHint(lastMs) {
|
|
9
|
+
if (lastMs === null)
|
|
10
|
+
return { label: '(파일 mtime 조회 실패)' };
|
|
11
|
+
const ageMs = Date.now() - lastMs;
|
|
12
|
+
const days = Math.floor(ageMs / 86_400_000);
|
|
13
|
+
const hours = Math.floor((ageMs % 86_400_000) / 3_600_000);
|
|
14
|
+
const label = days > 0 ? `${days}d ${hours}h 전 갱신` : `${hours}h 전 갱신`;
|
|
15
|
+
if (days >= 14) {
|
|
16
|
+
return {
|
|
17
|
+
label,
|
|
18
|
+
recommendation: `⚠️ 14일 이상 미사용 — Google 정책상 refresh_token revoke 위험. 안전을 위해 'mimi_seed_auth_start' 로 재인증 권장.`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (days >= 7) {
|
|
22
|
+
return {
|
|
23
|
+
label,
|
|
24
|
+
recommendation: `ℹ️ 7일 이상 미사용 — 한 번 더 갱신하지 않으면 곧 revoke 가능. 다음 도구 호출이 자동 갱신해주지만, 장기 미사용 예정이면 미리 인증 갱신 권장.`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { label };
|
|
28
|
+
}
|
|
3
29
|
export function registerAuthTools(server) {
|
|
4
30
|
server.tool('mimi_seed_auth_start', [
|
|
5
31
|
'Google OAuth 로그인 링크를 발급하고 백그라운드 콜백 서버를 시작.',
|
|
@@ -28,6 +54,9 @@ export function registerAuthTools(server) {
|
|
|
28
54
|
});
|
|
29
55
|
server.tool('mimi_seed_auth_status', 'Mimi Seed MCP 인증 상태 확인 (만료 시 refresh_token으로 자동 갱신 시도)', {}, async () => {
|
|
30
56
|
const r = await ensureFreshAccessToken();
|
|
57
|
+
const refreshHint = formatLastRefreshHint(getTokensLastRefreshMs());
|
|
58
|
+
const refreshLine = ` 마지막 갱신: ${refreshHint.label}`;
|
|
59
|
+
const recommendation = refreshHint.recommendation ? `\n\n${refreshHint.recommendation}` : '';
|
|
31
60
|
switch (r.status) {
|
|
32
61
|
case 'unauthenticated':
|
|
33
62
|
return {
|
|
@@ -40,14 +69,19 @@ export function registerAuthTools(server) {
|
|
|
40
69
|
};
|
|
41
70
|
case 'fresh': {
|
|
42
71
|
const min = Math.round(r.msUntilExpiry / 60000);
|
|
43
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
content: [{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: `✅ 인증 유효 (${min}분 남음).\n${refreshLine}${recommendation}`,
|
|
76
|
+
}],
|
|
77
|
+
};
|
|
44
78
|
}
|
|
45
79
|
case 'refreshed': {
|
|
46
80
|
const min = Math.round(r.msUntilExpiry / 60000);
|
|
47
81
|
return {
|
|
48
82
|
content: [{
|
|
49
83
|
type: 'text',
|
|
50
|
-
text: `✅ 토큰 만료 → refresh_token으로 자동 갱신 완료 (${min}분 남음)
|
|
84
|
+
text: `✅ 토큰 만료 → refresh_token으로 자동 갱신 완료 (${min}분 남음).\n${refreshLine}${recommendation}`,
|
|
51
85
|
}],
|
|
52
86
|
};
|
|
53
87
|
}
|
|
@@ -59,6 +93,7 @@ export function registerAuthTools(server) {
|
|
|
59
93
|
` 코드: ${r.error.code}\n` +
|
|
60
94
|
` ${r.error.message}\n` +
|
|
61
95
|
(r.error.hint ? ` → ${r.error.hint}\n` : '') +
|
|
96
|
+
`${refreshLine}\n` +
|
|
62
97
|
'\n터미널에서 재로그인:\n npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
|
|
63
98
|
}],
|
|
64
99
|
};
|
|
@@ -1,38 +1,130 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import * as bigquery from '../bigquery/tools.js';
|
|
3
|
-
import {
|
|
3
|
+
import { requireBigQueryAuth, resolveBigQueryAuth } from '../auth/bigquery-auth.js';
|
|
4
|
+
/**
|
|
5
|
+
* BigQuery 에러를 사람이 읽을 수 있게 가공.
|
|
6
|
+
* 403(accessDenied)은 인증 자체는 됐으나 IAM 역할이 없는 경우 — 어떤 역할을
|
|
7
|
+
* 어디에 부여해야 하는지 정확히 안내한다.
|
|
8
|
+
*/
|
|
9
|
+
function describeBqError(e, auth, projectId) {
|
|
10
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
11
|
+
const obj = (e && typeof e === 'object' ? e : {});
|
|
12
|
+
const status = (obj.code ?? obj.status);
|
|
13
|
+
const is403 = status === 403 || status === '403' || /accessDenied|does not have|permission/i.test(msg);
|
|
14
|
+
if (is403) {
|
|
15
|
+
const who = auth.clientEmail
|
|
16
|
+
? `서비스 계정 ${auth.clientEmail}`
|
|
17
|
+
: '현재 OAuth 사용자 계정';
|
|
18
|
+
const lines = [
|
|
19
|
+
`❌ BigQuery 접근 거부 (project: ${projectId})`,
|
|
20
|
+
` ${msg}`,
|
|
21
|
+
'',
|
|
22
|
+
`${who}에 다음 IAM 역할이 필요해 — 프로젝트 ${projectId}에 부여:`,
|
|
23
|
+
' • roles/bigquery.jobUser (쿼리 작업 생성)',
|
|
24
|
+
' • roles/bigquery.dataViewer (데이터셋 읽기)',
|
|
25
|
+
];
|
|
26
|
+
if (auth.clientEmail) {
|
|
27
|
+
lines.push('', 'gcloud 로 부여 (프로젝트 소유자 계정에서 1회):', ` gcloud projects add-iam-policy-binding ${projectId} \\`, ` --member="serviceAccount:${auth.clientEmail}" --role="roles/bigquery.jobUser"`, ` gcloud projects add-iam-policy-binding ${projectId} \\`, ` --member="serviceAccount:${auth.clientEmail}" --role="roles/bigquery.dataViewer"`);
|
|
28
|
+
}
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
// 토큰 만료/재인증 류 — 서비스 계정 등록 권유
|
|
32
|
+
if (/invalid_rapt|rapt_required|invalid_grant|reauth/i.test(msg)) {
|
|
33
|
+
return [
|
|
34
|
+
`❌ BigQuery 인증 갱신 실패: ${msg}`,
|
|
35
|
+
'',
|
|
36
|
+
'Google Workspace 재인증 정책으로 OAuth 토큰 갱신이 막힌 상태일 수 있어.',
|
|
37
|
+
'서비스 계정은 이 정책의 영향을 받지 않아 — 등록 권장:',
|
|
38
|
+
' npx -y @yoonion/mimi-seed-mcp mimi-seed-bigquery-auth',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
return `❌ BigQuery 오류: ${msg}`;
|
|
42
|
+
}
|
|
43
|
+
function errResult(text) {
|
|
44
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
45
|
+
}
|
|
4
46
|
export function registerBigqueryTools(server) {
|
|
5
|
-
server.tool('bigquery_run_query', 'BigQuery SQL 쿼리 실행 (SELECT). GA4 analytics_* 테이블 분석에 사용.'
|
|
47
|
+
server.tool('bigquery_run_query', 'BigQuery SQL 쿼리 실행 (SELECT). GA4 analytics_* 테이블 분석에 사용. ' +
|
|
48
|
+
'서비스 계정(권장) 또는 사용자 OAuth 로 인증.', {
|
|
6
49
|
projectId: z.string().describe('GCP 프로젝트 ID (예: ads-coffee)'),
|
|
7
50
|
query: z.string().describe('실행할 StandardSQL 쿼리'),
|
|
8
51
|
maxResults: z.number().optional().describe('최대 행 수 (기본 1000)'),
|
|
9
52
|
}, async ({ projectId, query, maxResults }) => {
|
|
10
|
-
const auth =
|
|
11
|
-
|
|
12
|
-
|
|
53
|
+
const auth = requireBigQueryAuth();
|
|
54
|
+
try {
|
|
55
|
+
const result = await bigquery.runQuery(auth.client, projectId, query, maxResults ?? 1000);
|
|
56
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return errResult(describeBqError(e, auth, projectId));
|
|
60
|
+
}
|
|
13
61
|
});
|
|
14
62
|
server.tool('bigquery_list_datasets', 'BigQuery 프로젝트의 데이터셋 목록 조회', {
|
|
15
63
|
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
16
64
|
}, async ({ projectId }) => {
|
|
17
|
-
const auth =
|
|
18
|
-
|
|
19
|
-
|
|
65
|
+
const auth = requireBigQueryAuth();
|
|
66
|
+
try {
|
|
67
|
+
const datasets = await bigquery.listDatasets(auth.client, projectId);
|
|
68
|
+
return { content: [{ type: 'text', text: JSON.stringify(datasets, null, 2) }] };
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
return errResult(describeBqError(e, auth, projectId));
|
|
72
|
+
}
|
|
20
73
|
});
|
|
21
74
|
server.tool('bigquery_list_tables', 'BigQuery 데이터셋의 테이블 목록 조회', {
|
|
22
75
|
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
23
76
|
datasetId: z.string().describe('데이터셋 ID (예: analytics_530080532)'),
|
|
24
77
|
}, async ({ projectId, datasetId }) => {
|
|
25
|
-
const auth =
|
|
26
|
-
|
|
27
|
-
|
|
78
|
+
const auth = requireBigQueryAuth();
|
|
79
|
+
try {
|
|
80
|
+
const tables = await bigquery.listTables(auth.client, projectId, datasetId);
|
|
81
|
+
return { content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }] };
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return errResult(describeBqError(e, auth, projectId));
|
|
85
|
+
}
|
|
28
86
|
});
|
|
29
87
|
server.tool('bigquery_get_table_schema', 'BigQuery 테이블 스키마(컬럼 정보) 조회', {
|
|
30
88
|
projectId: z.string().describe('GCP 프로젝트 ID'),
|
|
31
89
|
datasetId: z.string().describe('데이터셋 ID'),
|
|
32
90
|
tableId: z.string().describe('테이블 ID'),
|
|
33
91
|
}, async ({ projectId, datasetId, tableId }) => {
|
|
34
|
-
const auth =
|
|
35
|
-
|
|
36
|
-
|
|
92
|
+
const auth = requireBigQueryAuth();
|
|
93
|
+
try {
|
|
94
|
+
const schema = await bigquery.getTableSchema(auth.client, projectId, datasetId, tableId);
|
|
95
|
+
return { content: [{ type: 'text', text: JSON.stringify(schema, null, 2) }] };
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return errResult(describeBqError(e, auth, projectId));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
server.tool('bigquery_auth_status', '현재 BigQuery 인증 상태 조회 — 어떤 인증(서비스 계정/OAuth)이 사용되는지 확인', {}, async () => {
|
|
102
|
+
const auth = resolveBigQueryAuth();
|
|
103
|
+
if (!auth) {
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: JSON.stringify({
|
|
109
|
+
authenticated: false,
|
|
110
|
+
hint: 'mimi-seed-bigquery-auth (서비스 계정) 또는 mimi-seed-auth (OAuth) 로 인증 필요',
|
|
111
|
+
}, null, 2),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: 'text',
|
|
120
|
+
text: JSON.stringify({
|
|
121
|
+
authenticated: true,
|
|
122
|
+
source: auth.source,
|
|
123
|
+
clientEmail: auth.clientEmail,
|
|
124
|
+
serviceAccountProjectId: auth.projectId,
|
|
125
|
+
}, null, 2),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
37
129
|
});
|
|
38
130
|
}
|