@yoonion/mimi-seed-mcp 0.3.25 → 0.3.26
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/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/bigquery/tools.d.ts +7 -5
- package/dist/index.js +1 -0
- package/dist/registers/bigquery.js +106 -14
- package/package.json +3 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { JWT } from 'google-auth-library';
|
|
2
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
3
|
+
export interface ServiceAccountKey {
|
|
4
|
+
type?: string;
|
|
5
|
+
client_email?: string;
|
|
6
|
+
private_key?: string;
|
|
7
|
+
project_id?: string;
|
|
8
|
+
}
|
|
9
|
+
/** 서비스 계정 키 JSON 문자열을 검증 후 저장. 형식이 아니면 throw. */
|
|
10
|
+
export declare function saveBigQueryServiceAccountJson(json: string): ServiceAccountKey;
|
|
11
|
+
/** 저장된 BigQuery 서비스 계정 키. 없거나 형식 오류면 null. */
|
|
12
|
+
export declare function getBigQueryServiceAccountKey(): ServiceAccountKey | null;
|
|
13
|
+
/** 저장된 BigQuery 서비스 계정 키 삭제. 없으면 false. */
|
|
14
|
+
export declare function deleteBigQueryServiceAccountJson(): boolean;
|
|
15
|
+
export type BigQueryAuthSource = 'service-account' | 'user-oauth';
|
|
16
|
+
export interface BigQueryAuth {
|
|
17
|
+
client: OAuth2Client | JWT;
|
|
18
|
+
source: BigQueryAuthSource;
|
|
19
|
+
/** 서비스 계정이면 client_email, 사용자 OAuth면 null. */
|
|
20
|
+
clientEmail: string | null;
|
|
21
|
+
/** 서비스 계정 키의 project_id (있으면). */
|
|
22
|
+
projectId: string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* BigQuery 인증 클라이언트 해석. 우선순위:
|
|
26
|
+
* 1. BigQuery 서비스 계정 (~/.mimi-seed/bigquery-service-account.json)
|
|
27
|
+
* 2. 사용자 OAuth (~/.mimi-seed/tokens.json)
|
|
28
|
+
* 둘 다 없으면 null.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveBigQueryAuth(): BigQueryAuth | null;
|
|
31
|
+
/** BigQuery 인증을 강제. 없으면 안내 메시지와 함께 throw. */
|
|
32
|
+
export declare function requireBigQueryAuth(): BigQueryAuth;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { JWT } from 'google-auth-library';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { getAuthenticatedClient } from './google-auth.js';
|
|
6
|
+
// BigQuery 전용 서비스 계정 키 저장 위치.
|
|
7
|
+
// 서비스 계정 인증은 Google Workspace 의 재인증(reauth) 정책에서 면제되므로,
|
|
8
|
+
// 사용자 OAuth 가 `invalid_rapt` 로 갱신 거부되는 환경에서도 안정적으로 동작한다.
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
|
|
10
|
+
const BQ_SA_PATH = path.join(CONFIG_DIR, 'bigquery-service-account.json');
|
|
11
|
+
// jobs.create(쿼리 작업 생성) + 데이터셋 읽기에 필요한 최소 스코프.
|
|
12
|
+
const BQ_SCOPES = [
|
|
13
|
+
'https://www.googleapis.com/auth/bigquery.readonly',
|
|
14
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
15
|
+
];
|
|
16
|
+
function safeReadSa(p) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
19
|
+
if (parsed.type === 'service_account' && parsed.client_email && parsed.private_key) {
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** 서비스 계정 키 JSON 문자열을 검증 후 저장. 형식이 아니면 throw. */
|
|
29
|
+
export function saveBigQueryServiceAccountJson(json) {
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(json);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error('JSON 파싱 실패 — 올바른 서비스 계정 키 파일이 아닙니다.');
|
|
36
|
+
}
|
|
37
|
+
if (parsed.type !== 'service_account' || !parsed.client_email || !parsed.private_key) {
|
|
38
|
+
throw new Error('서비스 계정 키 형식이 아닙니다 (type="service_account", client_email, private_key 필요).');
|
|
39
|
+
}
|
|
40
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
41
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
42
|
+
fs.writeFileSync(BQ_SA_PATH, json, { mode: 0o600 });
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
/** 저장된 BigQuery 서비스 계정 키. 없거나 형식 오류면 null. */
|
|
46
|
+
export function getBigQueryServiceAccountKey() {
|
|
47
|
+
if (!fs.existsSync(BQ_SA_PATH))
|
|
48
|
+
return null;
|
|
49
|
+
return safeReadSa(BQ_SA_PATH);
|
|
50
|
+
}
|
|
51
|
+
/** 저장된 BigQuery 서비스 계정 키 삭제. 없으면 false. */
|
|
52
|
+
export function deleteBigQueryServiceAccountJson() {
|
|
53
|
+
if (!fs.existsSync(BQ_SA_PATH))
|
|
54
|
+
return false;
|
|
55
|
+
fs.unlinkSync(BQ_SA_PATH);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
function makeJwt(sa) {
|
|
59
|
+
return new JWT({ email: sa.client_email, key: sa.private_key, scopes: BQ_SCOPES });
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* BigQuery 인증 클라이언트 해석. 우선순위:
|
|
63
|
+
* 1. BigQuery 서비스 계정 (~/.mimi-seed/bigquery-service-account.json)
|
|
64
|
+
* 2. 사용자 OAuth (~/.mimi-seed/tokens.json)
|
|
65
|
+
* 둘 다 없으면 null.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveBigQueryAuth() {
|
|
68
|
+
const sa = getBigQueryServiceAccountKey();
|
|
69
|
+
if (sa) {
|
|
70
|
+
return {
|
|
71
|
+
client: makeJwt(sa),
|
|
72
|
+
source: 'service-account',
|
|
73
|
+
clientEmail: sa.client_email ?? null,
|
|
74
|
+
projectId: sa.project_id ?? null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const oauth = getAuthenticatedClient();
|
|
78
|
+
if (oauth) {
|
|
79
|
+
return { client: oauth, source: 'user-oauth', clientEmail: null, projectId: null };
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const BQ_AUTH_HINT = [
|
|
84
|
+
'❌ BigQuery 인증이 설정되지 않았어.',
|
|
85
|
+
'',
|
|
86
|
+
'방법 1 — 서비스 계정 (권장: Google Workspace 재인증 정책의 영향을 받지 않음):',
|
|
87
|
+
' npx -y @yoonion/mimi-seed-mcp mimi-seed-bigquery-auth',
|
|
88
|
+
'',
|
|
89
|
+
'방법 2 — Google 계정 OAuth:',
|
|
90
|
+
' npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
|
|
91
|
+
'',
|
|
92
|
+
'그 다음에 다시 물어봐줘.',
|
|
93
|
+
].join('\n');
|
|
94
|
+
/** BigQuery 인증을 강제. 없으면 안내 메시지와 함께 throw. */
|
|
95
|
+
export function requireBigQueryAuth() {
|
|
96
|
+
const auth = resolveBigQueryAuth();
|
|
97
|
+
if (!auth)
|
|
98
|
+
throw new Error(BQ_AUTH_HINT);
|
|
99
|
+
return auth;
|
|
100
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { saveBigQueryServiceAccountJson, getBigQueryServiceAccountKey, } from './bigquery-auth.js';
|
|
5
|
+
import * as bigquery from '../bigquery/tools.js';
|
|
6
|
+
import { JWT } from 'google-auth-library';
|
|
7
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
9
|
+
async function main() {
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log(' 🤖 Mimi Seed — BigQuery 서비스 계정 연결');
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log(' 서비스 계정 인증은 Google Workspace 의 재인증(reauth) 정책에서');
|
|
14
|
+
console.log(' 면제되므로, OAuth 가 invalid_rapt 로 막히는 환경에서도 동작해.');
|
|
15
|
+
console.log('');
|
|
16
|
+
const existing = getBigQueryServiceAccountKey();
|
|
17
|
+
if (existing) {
|
|
18
|
+
console.log(` ✅ 이미 연결됨 (${existing.client_email})`);
|
|
19
|
+
const answer = await ask(' 다시 설정할래? (y/N): ');
|
|
20
|
+
if (answer.toLowerCase() !== 'y') {
|
|
21
|
+
rl.close();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
console.log(' BigQuery 를 읽을 수 있는 서비스 계정 키 JSON 이 필요해:');
|
|
26
|
+
console.log(' 1. GCP Console → IAM & Admin → Service Accounts');
|
|
27
|
+
console.log(' 2. 서비스 계정 선택(또는 생성) → Keys → Add Key → JSON');
|
|
28
|
+
console.log(' 3. 그 서비스 계정에 프로젝트 IAM 역할 부여:');
|
|
29
|
+
console.log(' • roles/bigquery.jobUser (쿼리 작업 생성)');
|
|
30
|
+
console.log(' • roles/bigquery.dataViewer (데이터셋 읽기)');
|
|
31
|
+
console.log(' 4. 다운로드한 JSON 파일 경로를 입력해');
|
|
32
|
+
console.log('');
|
|
33
|
+
const jsonPath = await ask(' 서비스 계정 JSON 파일 경로: ');
|
|
34
|
+
const trimmedPath = jsonPath.trim().replace(/^["']|["']$/g, '');
|
|
35
|
+
if (!fs.existsSync(trimmedPath)) {
|
|
36
|
+
console.log(` ❌ 파일 없음: ${trimmedPath}`);
|
|
37
|
+
rl.close();
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const json = fs.readFileSync(trimmedPath, 'utf-8');
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = saveBigQueryServiceAccountJson(json);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
console.log(` ❌ ${e instanceof Error ? e.message : String(e)}`);
|
|
47
|
+
rl.close();
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(` ✅ 저장 완료! (${parsed.client_email})`);
|
|
52
|
+
if (parsed.project_id)
|
|
53
|
+
console.log(` 서비스 계정 프로젝트: ${parsed.project_id}`);
|
|
54
|
+
console.log('');
|
|
55
|
+
// 선택적 연결 테스트 — projectId 를 입력하면 datasets.list 로 권한 확인.
|
|
56
|
+
const testProject = await ask(` 연결 테스트할 GCP 프로젝트 ID (엔터 시 건너뜀${parsed.project_id ? `, 기본 ${parsed.project_id}` : ''}): `);
|
|
57
|
+
const projectId = testProject.trim() || parsed.project_id || '';
|
|
58
|
+
if (projectId) {
|
|
59
|
+
console.log(` 🔎 ${projectId} 데이터셋 조회 중...`);
|
|
60
|
+
try {
|
|
61
|
+
const jwt = new JWT({
|
|
62
|
+
email: parsed.client_email,
|
|
63
|
+
key: parsed.private_key,
|
|
64
|
+
scopes: [
|
|
65
|
+
'https://www.googleapis.com/auth/bigquery.readonly',
|
|
66
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
const datasets = await bigquery.listDatasets(jwt, projectId);
|
|
70
|
+
console.log(` ✅ 접근 OK — 데이터셋 ${datasets.length}개`);
|
|
71
|
+
for (const d of datasets.slice(0, 10))
|
|
72
|
+
console.log(` • ${d.datasetId} (${d.location})`);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
76
|
+
console.log(` ⚠️ 접근 실패: ${msg}`);
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(' IAM 역할이 없으면 아래를 실행해 (프로젝트 소유자 계정에서):');
|
|
79
|
+
console.log(` gcloud projects add-iam-policy-binding ${projectId} \\`);
|
|
80
|
+
console.log(` --member="serviceAccount:${parsed.client_email}" --role="roles/bigquery.jobUser"`);
|
|
81
|
+
console.log(` gcloud projects add-iam-policy-binding ${projectId} \\`);
|
|
82
|
+
console.log(` --member="serviceAccount:${parsed.client_email}" --role="roles/bigquery.dataViewer"`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(' 이제 Claude Code 에서:');
|
|
87
|
+
console.log(' "BigQuery 로 GA4 트래픽 분석해줘"');
|
|
88
|
+
console.log('');
|
|
89
|
+
rl.close();
|
|
90
|
+
}
|
|
91
|
+
main();
|
package/dist/auth/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AuthErrorCode = 'UNAUTHENTICATED' | 'NO_REFRESH_TOKEN' | 'INVALID_GRANT' | 'INVALID_CLIENT' | 'UNAUTHORIZED_CLIENT' | 'REFRESH_NETWORK_ERROR' | 'REFRESH_UNKNOWN' | 'CALLBACK_PORT_IN_USE' | 'CALLBACK_TIMEOUT' | 'BROWSER_OPEN_FAILED' | 'USER_DENIED' | 'CODE_EXCHANGE_FAILED' | 'TOKEN_RESPONSE_INVALID';
|
|
1
|
+
export type AuthErrorCode = 'UNAUTHENTICATED' | 'NO_REFRESH_TOKEN' | 'INVALID_GRANT' | 'RAPT_REQUIRED' | 'INVALID_CLIENT' | 'UNAUTHORIZED_CLIENT' | 'REFRESH_NETWORK_ERROR' | 'REFRESH_UNKNOWN' | 'CALLBACK_PORT_IN_USE' | 'CALLBACK_TIMEOUT' | 'BROWSER_OPEN_FAILED' | 'USER_DENIED' | 'CODE_EXCHANGE_FAILED' | 'TOKEN_RESPONSE_INVALID';
|
|
2
2
|
export interface AuthErrorPayload {
|
|
3
3
|
code: AuthErrorCode;
|
|
4
4
|
message: string;
|
package/dist/auth/errors.js
CHANGED
|
@@ -28,6 +28,17 @@ export function classifyError(e, ctx) {
|
|
|
28
28
|
cause: raw,
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
if (oauthCode === 'invalid_rapt' || oauthCode === 'rapt_required') {
|
|
32
|
+
return {
|
|
33
|
+
code: 'RAPT_REQUIRED',
|
|
34
|
+
message: 'Google Workspace 재인증(reauth) 정책으로 토큰 갱신이 거부되었습니다 (invalid_rapt).',
|
|
35
|
+
hint: 'mimi-seed-auth 로 재로그인하거나, 재인증 정책의 영향을 받지 않는 서비스 계정 인증을 사용하세요 ' +
|
|
36
|
+
'(BigQuery: mimi-seed-bigquery-auth).',
|
|
37
|
+
retriable: false,
|
|
38
|
+
needsReauth: true,
|
|
39
|
+
cause: raw,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
31
42
|
if (oauthCode === 'invalid_client') {
|
|
32
43
|
return {
|
|
33
44
|
code: 'INVALID_CLIENT',
|
|
@@ -128,7 +139,7 @@ function extractOAuthErrorCode(e, raw) {
|
|
|
128
139
|
return dataErr;
|
|
129
140
|
}
|
|
130
141
|
// message에 'invalid_grant' 같은 표준 코드가 박혀 있는 경우
|
|
131
|
-
const m = raw.match(/\b(invalid_grant|invalid_client|invalid_request|unauthorized_client|access_denied|unsupported_grant_type)\b/);
|
|
142
|
+
const m = raw.match(/\b(invalid_grant|invalid_client|invalid_request|unauthorized_client|access_denied|unsupported_grant_type|invalid_rapt|rapt_required)\b/);
|
|
132
143
|
return m?.[1];
|
|
133
144
|
}
|
|
134
145
|
function isNetworkError(e, raw) {
|
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];
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoonion/mimi-seed-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Cursor / any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mimi-seed-mcp": "dist/index.js",
|
|
8
8
|
"mimi-seed-auth": "dist/auth/cli.js",
|
|
9
9
|
"mimi-seed-appstore-auth": "dist/appstore/setup-cli.js",
|
|
10
|
-
"mimi-seed-playstore-auth": "dist/auth/playstore-setup-cli.js"
|
|
10
|
+
"mimi-seed-playstore-auth": "dist/auth/playstore-setup-cli.js",
|
|
11
|
+
"mimi-seed-bigquery-auth": "dist/auth/bigquery-setup-cli.js"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"dist",
|