@yoonion/mimi-seed-mcp 0.15.6 → 0.16.0
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/README.md +2 -1
- package/assets/agent-guide.md +1 -0
- package/dist/billing/tools.d.ts +50 -0
- package/dist/billing/tools.js +138 -0
- package/dist/lib/googleapis-lite.d.ts +4 -0
- package/dist/lib/googleapis-lite.js +4 -0
- package/dist/registers/billing.d.ts +2 -0
- package/dist/registers/billing.js +102 -0
- package/dist/server.js +2 -0
- package/package.json +1 -1
- package/tool-manifest.json +12 -1
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
-
## 제공 도구 (150+ 개 ·
|
|
78
|
+
## 제공 도구 (150+ 개 · 21개 영역)
|
|
79
79
|
|
|
80
80
|
| 영역 | 도구 수 | 주요 도구 |
|
|
81
81
|
|------|---------|-----------|
|
|
@@ -90,6 +90,7 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
90
90
|
| Google Ads | 6 | `googleads_list_campaigns` / `googleads_get_uac_report` / `googleads_get_campaign_report` |
|
|
91
91
|
| Facebook | 6 | `facebook_post_photo` / `facebook_post_multi_photo` / `facebook_list_pages` |
|
|
92
92
|
| Google Cloud IAM | 5 | `iam_create_service_account` / `iam_create_key` / `iam_add_iam_policy_binding` |
|
|
93
|
+
| GCP Billing | 4 | `gcp_get_billing_info` / `gcp_list_billing_projects` / `gcp_list_budgets` / `gcp_create_budget` |
|
|
93
94
|
| BigQuery | 5 | `bigquery_run_query` / `bigquery_list_datasets` / `bigquery_get_table_schema` |
|
|
94
95
|
| Threads | 7 | `threads_post` / `threads_post_video` / `threads_post_carousel` / `threads_refresh_token` |
|
|
95
96
|
| TikTok Business | 7 | `tiktok_business_plan_video_post` / `tiktok_business_publish_video` / `tiktok_business_get_publish_status` |
|
package/assets/agent-guide.md
CHANGED
|
@@ -69,6 +69,7 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
|
|
|
69
69
|
| Firebase apps + services (incl. web) | `select:firebase_list_android_apps,firebase_list_ios_apps,firebase_list_web_apps,firebase_create_web_app,firebase_get_web_config,firebase_enable_service,firebase_list_enabled_services,firebase_delete_android_app,firebase_delete_ios_app,firebase_delete_web_app` |
|
|
70
70
|
| Analytics wiring (Firebase ↔ GA4 ↔ BigQuery) | `select:firebase_link_analytics,firebase_get_analytics_details,ga4_list_account_summaries,ga4_list_properties,ga4_create_property,ga4_list_data_streams,ga4_create_data_stream,ga4_plan_bigquery_link,ga4_create_bigquery_link,ga4_run_report` |
|
|
71
71
|
| BigQuery | `select:bigquery_auth_status,bigquery_list_datasets,bigquery_list_tables,bigquery_get_table_schema,bigquery_run_query` |
|
|
72
|
+
| GCP billing (Blaze 여부 · 비용 범위 · 예산) | `select:gcp_get_billing_info,gcp_list_billing_projects,gcp_list_budgets,gcp_create_budget` |
|
|
72
73
|
| AdMob | `select:admob_list_accounts,admob_list_apps,admob_create_app,admob_create_ad_unit,admob_list_ad_units,admob_get_today_earnings,admob_get_report` |
|
|
73
74
|
| Google Ads (UAC) | `select:googleads_config_status,googleads_save_config,googleads_list_accessible_customers,googleads_list_campaigns,googleads_get_campaign_report,googleads_get_uac_report` |
|
|
74
75
|
| Search Console | `select:gsc_list_sites,gsc_list_sitemaps,gsc_get_sitemap,gsc_submit_sitemap,gsc_inspect_url,gsc_search_analytics` |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
/** `01F1F4-FD007B-2973A7` / `billingAccounts/01F1F4-...` 어느 형태로 줘도 정규화. */
|
|
3
|
+
export declare function normalizeBillingAccount(input: string): string;
|
|
4
|
+
export type BillingInfo = {
|
|
5
|
+
projectId: string;
|
|
6
|
+
billingEnabled: boolean;
|
|
7
|
+
billingAccountName: string | null;
|
|
8
|
+
/** 사람이 읽을 판정 — Blaze/Spark 오판을 막기 위해 문장으로 준다. */
|
|
9
|
+
verdict: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function getBillingInfo(auth: OAuth2Client, projectId: string): Promise<BillingInfo>;
|
|
12
|
+
/**
|
|
13
|
+
* 결제 계정에 붙은 프로젝트 목록 = **비용 범위 확인**.
|
|
14
|
+
* 여기서 프로젝트가 여러 개면 계정 전체 예산은 의미가 없고 프로젝트 필터를 써야 한다.
|
|
15
|
+
*/
|
|
16
|
+
export declare function listBillingProjects(auth: OAuth2Client, billingAccount: string, quotaProjectId?: string): Promise<{
|
|
17
|
+
billingAccount: string;
|
|
18
|
+
count: number;
|
|
19
|
+
shared: boolean;
|
|
20
|
+
projects: {
|
|
21
|
+
projectId: string | null | undefined;
|
|
22
|
+
billingEnabled: boolean;
|
|
23
|
+
}[];
|
|
24
|
+
}>;
|
|
25
|
+
export declare function listBudgets(auth: OAuth2Client, billingAccount: string, quotaProjectId?: string): Promise<{
|
|
26
|
+
name: string | null | undefined;
|
|
27
|
+
displayName: string | null | undefined;
|
|
28
|
+
amount: string;
|
|
29
|
+
projects: string[];
|
|
30
|
+
thresholds: (number | null | undefined)[];
|
|
31
|
+
}[]>;
|
|
32
|
+
export type CreateBudgetInput = {
|
|
33
|
+
billingAccount: string;
|
|
34
|
+
displayName: string;
|
|
35
|
+
/** 통화 단위 정수 금액 (KRW 는 소수 없음 — 10000 = ₩10,000). */
|
|
36
|
+
amountUnits: number;
|
|
37
|
+
currencyCode?: string;
|
|
38
|
+
/** 감시할 프로젝트 ID 목록. **비우면 결제 계정 전체** — 공용 계정이면 소음이 된다. */
|
|
39
|
+
projectIds?: string[];
|
|
40
|
+
/** 알림 임계 비율 (0~1). 기본 0.5 / 0.9 / 1.0. */
|
|
41
|
+
thresholds?: number[];
|
|
42
|
+
/** quota 주체 프로젝트 — Billing Budget API 를 켜 둔 프로젝트. quotaHeaders 주석 참고. */
|
|
43
|
+
quotaProjectId?: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function createBudget(auth: OAuth2Client, input: CreateBudgetInput): Promise<{
|
|
46
|
+
name: string | null | undefined;
|
|
47
|
+
displayName: string | null | undefined;
|
|
48
|
+
projects: string[];
|
|
49
|
+
thresholds: number[];
|
|
50
|
+
}>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { google } from '../lib/googleapis-lite.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cloud Billing + Billing Budgets 래퍼.
|
|
4
|
+
*
|
|
5
|
+
* 앱마다 전용 Firebase/GCP 프로젝트를 만드는 컨벤션이라, 새 프로젝트가 생길 때마다
|
|
6
|
+
* "이 프로젝트가 Blaze 인가 → 결제 계정이 공용인가 → 예산 알림을 걸었나"가 반복 작업이 된다.
|
|
7
|
+
*
|
|
8
|
+
* 실제로 겪은 함정 2개가 이 모듈의 존재 이유다:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Billing API 가 꺼져 있으면 조회 자체가 403** 이고, 그 403 을 "Spark 이라서"로
|
|
11
|
+
* 오해하기 쉽다. 실제로는 이미 Blaze 였다. `getBillingInfo` 는 이 구분을 명확히 한다
|
|
12
|
+
* (API 미활성 → firebase_enable_service 안내 / 활성인데 billingEnabled=false → 진짜 Spark).
|
|
13
|
+
*
|
|
14
|
+
* 2. **결제 계정이 회사 공용이면 계정 전체 예산은 쓸 수 없다.** 기존 지출(BigQuery export 등)만으로
|
|
15
|
+
* 즉시 임계를 넘겨 알림이 소음이 된다. 그래서 `createBudget` 은 `projects` 필터를 1급으로 받는다 —
|
|
16
|
+
* 프로젝트 범위 예산이 신규 앱 감시의 기본형이다.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ 예산은 **알림만 한다. 지출을 막지 않는다.** 하드 차단은 Pub/Sub → 결제 해제 함수뿐이고
|
|
19
|
+
* 그건 앱을 죽이는 조치다. 실질 방어는 각 서비스의 상한(예: functions maxInstances)이다.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ 권한은 **결제 계정 레벨 IAM** 이다(프로젝트 IAM 과 별개). 서비스 계정 키로는 대개 403 이고
|
|
22
|
+
* 사용자 OAuth(cloud-platform)로 호출해야 한다.
|
|
23
|
+
*/
|
|
24
|
+
const billing = () => google.cloudbilling('v1');
|
|
25
|
+
const budgets = () => google.billingbudgets('v1');
|
|
26
|
+
/**
|
|
27
|
+
* 🔴 quota 프로젝트 지정이 **필수**다.
|
|
28
|
+
*
|
|
29
|
+
* 사용자 OAuth 로 호출하면 GCP 는 quota/billing 을 **OAuth 클라이언트의 프로젝트**에 청구한다.
|
|
30
|
+
* mimi-seed 의 OAuth 클라이언트 프로젝트에는 Cloud Billing API 가 없으므로, 지정하지 않으면
|
|
31
|
+
* 대상 프로젝트가 아무리 정상이어도 항상 이렇게 실패한다 (2026-08-04 실측):
|
|
32
|
+
*
|
|
33
|
+
* "Cloud Billing API has not been used in project <OAuth 클라이언트 프로젝트 번호> before or it is disabled"
|
|
34
|
+
*
|
|
35
|
+
* 이 메시지의 프로젝트 번호는 **우리가 조회하려는 프로젝트가 아니라 OAuth 클라이언트 쪽**이다 —
|
|
36
|
+
* 그래서 "대상 프로젝트에서 API 를 켰는데도 왜 403 이냐"로 헤매게 된다.
|
|
37
|
+
* `x-goog-user-project` 로 우리가 통제하는(그리고 API 를 켜 둔) 프로젝트를 quota 주체로 넘긴다.
|
|
38
|
+
*/
|
|
39
|
+
function quotaHeaders(quotaProjectId) {
|
|
40
|
+
return quotaProjectId ? { headers: { 'x-goog-user-project': quotaProjectId } } : {};
|
|
41
|
+
}
|
|
42
|
+
/** `01F1F4-FD007B-2973A7` / `billingAccounts/01F1F4-...` 어느 형태로 줘도 정규화. */
|
|
43
|
+
export function normalizeBillingAccount(input) {
|
|
44
|
+
const trimmed = input.trim();
|
|
45
|
+
return trimmed.startsWith('billingAccounts/') ? trimmed : `billingAccounts/${trimmed}`;
|
|
46
|
+
}
|
|
47
|
+
export async function getBillingInfo(auth, projectId) {
|
|
48
|
+
// 조회 대상 프로젝트 자신을 quota 주체로 쓴다 — 거기 Billing API 가 켜져 있어야 한다.
|
|
49
|
+
const res = await billing().projects.getBillingInfo({
|
|
50
|
+
auth,
|
|
51
|
+
name: `projects/${projectId}`,
|
|
52
|
+
...quotaHeaders(projectId),
|
|
53
|
+
});
|
|
54
|
+
const enabled = res.data.billingEnabled ?? false;
|
|
55
|
+
const account = res.data.billingAccountName ?? null;
|
|
56
|
+
return {
|
|
57
|
+
projectId,
|
|
58
|
+
billingEnabled: enabled,
|
|
59
|
+
billingAccountName: account,
|
|
60
|
+
verdict: enabled
|
|
61
|
+
? `Blaze — 결제 계정 ${account} 연결됨. Cloud Functions/Run 배포 가능.`
|
|
62
|
+
: 'Spark — 결제 계정 미연결. Cloud Functions 등 유료 서비스 배포 불가.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 결제 계정에 붙은 프로젝트 목록 = **비용 범위 확인**.
|
|
67
|
+
* 여기서 프로젝트가 여러 개면 계정 전체 예산은 의미가 없고 프로젝트 필터를 써야 한다.
|
|
68
|
+
*/
|
|
69
|
+
export async function listBillingProjects(auth, billingAccount, quotaProjectId) {
|
|
70
|
+
const name = normalizeBillingAccount(billingAccount);
|
|
71
|
+
const res = await billing().billingAccounts.projects.list({
|
|
72
|
+
auth,
|
|
73
|
+
name,
|
|
74
|
+
pageSize: 200,
|
|
75
|
+
...quotaHeaders(quotaProjectId),
|
|
76
|
+
});
|
|
77
|
+
const projects = (res.data.projectBillingInfo ?? []).map((p) => ({
|
|
78
|
+
projectId: p.projectId,
|
|
79
|
+
billingEnabled: p.billingEnabled ?? false,
|
|
80
|
+
}));
|
|
81
|
+
return {
|
|
82
|
+
billingAccount: name,
|
|
83
|
+
count: projects.length,
|
|
84
|
+
shared: projects.length > 1,
|
|
85
|
+
projects,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function listBudgets(auth, billingAccount, quotaProjectId) {
|
|
89
|
+
const parent = normalizeBillingAccount(billingAccount);
|
|
90
|
+
const res = await budgets().billingAccounts.budgets.list({
|
|
91
|
+
auth,
|
|
92
|
+
parent,
|
|
93
|
+
pageSize: 200,
|
|
94
|
+
...quotaHeaders(quotaProjectId),
|
|
95
|
+
});
|
|
96
|
+
return (res.data.budgets ?? []).map((b) => ({
|
|
97
|
+
name: b.name,
|
|
98
|
+
displayName: b.displayName,
|
|
99
|
+
amount: b.amount?.specifiedAmount
|
|
100
|
+
? `${b.amount.specifiedAmount.units ?? '0'} ${b.amount.specifiedAmount.currencyCode ?? ''}`.trim()
|
|
101
|
+
: b.amount?.lastPeriodAmount
|
|
102
|
+
? '(직전 기간 금액)'
|
|
103
|
+
: '(미지정)',
|
|
104
|
+
projects: b.budgetFilter?.projects ?? [],
|
|
105
|
+
thresholds: (b.thresholdRules ?? []).map((t) => t.thresholdPercent),
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
export async function createBudget(auth, input) {
|
|
109
|
+
const parent = normalizeBillingAccount(input.billingAccount);
|
|
110
|
+
const thresholds = input.thresholds?.length ? input.thresholds : [0.5, 0.9, 1.0];
|
|
111
|
+
const res = await budgets().billingAccounts.budgets.create({
|
|
112
|
+
auth,
|
|
113
|
+
parent,
|
|
114
|
+
requestBody: {
|
|
115
|
+
displayName: input.displayName,
|
|
116
|
+
budgetFilter: {
|
|
117
|
+
// projects 는 `projects/<번호 또는 ID>` 형태를 받는다.
|
|
118
|
+
...(input.projectIds?.length
|
|
119
|
+
? { projects: input.projectIds.map((p) => (p.startsWith('projects/') ? p : `projects/${p}`)) }
|
|
120
|
+
: {}),
|
|
121
|
+
},
|
|
122
|
+
amount: {
|
|
123
|
+
specifiedAmount: {
|
|
124
|
+
currencyCode: input.currencyCode ?? 'KRW',
|
|
125
|
+
units: String(input.amountUnits),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
thresholdRules: thresholds.map((t) => ({ thresholdPercent: t })),
|
|
129
|
+
},
|
|
130
|
+
...quotaHeaders(input.quotaProjectId),
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
name: res.data.name,
|
|
134
|
+
displayName: res.data.displayName,
|
|
135
|
+
projects: res.data.budgetFilter?.projects ?? [],
|
|
136
|
+
thresholds,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -16,6 +16,8 @@ import { analyticsadmin } from 'googleapis/build/src/apis/analyticsadmin/index.j
|
|
|
16
16
|
import { analyticsdata } from 'googleapis/build/src/apis/analyticsdata/index.js';
|
|
17
17
|
import { androidpublisher } from 'googleapis/build/src/apis/androidpublisher/index.js';
|
|
18
18
|
import { bigquery } from 'googleapis/build/src/apis/bigquery/index.js';
|
|
19
|
+
import { billingbudgets } from 'googleapis/build/src/apis/billingbudgets/index.js';
|
|
20
|
+
import { cloudbilling } from 'googleapis/build/src/apis/cloudbilling/index.js';
|
|
19
21
|
import { cloudresourcemanager } from 'googleapis/build/src/apis/cloudresourcemanager/index.js';
|
|
20
22
|
import { firebase } from 'googleapis/build/src/apis/firebase/index.js';
|
|
21
23
|
import { iam } from 'googleapis/build/src/apis/iam/index.js';
|
|
@@ -30,6 +32,8 @@ export declare const google: {
|
|
|
30
32
|
analyticsdata: typeof analyticsdata;
|
|
31
33
|
androidpublisher: typeof androidpublisher;
|
|
32
34
|
bigquery: typeof bigquery;
|
|
35
|
+
billingbudgets: typeof billingbudgets;
|
|
36
|
+
cloudbilling: typeof cloudbilling;
|
|
33
37
|
cloudresourcemanager: typeof cloudresourcemanager;
|
|
34
38
|
firebase: typeof firebase;
|
|
35
39
|
iam: typeof iam;
|
|
@@ -16,6 +16,8 @@ import { analyticsadmin } from 'googleapis/build/src/apis/analyticsadmin/index.j
|
|
|
16
16
|
import { analyticsdata } from 'googleapis/build/src/apis/analyticsdata/index.js';
|
|
17
17
|
import { androidpublisher } from 'googleapis/build/src/apis/androidpublisher/index.js';
|
|
18
18
|
import { bigquery } from 'googleapis/build/src/apis/bigquery/index.js';
|
|
19
|
+
import { billingbudgets } from 'googleapis/build/src/apis/billingbudgets/index.js';
|
|
20
|
+
import { cloudbilling } from 'googleapis/build/src/apis/cloudbilling/index.js';
|
|
19
21
|
import { cloudresourcemanager } from 'googleapis/build/src/apis/cloudresourcemanager/index.js';
|
|
20
22
|
import { auth, firebase } from 'googleapis/build/src/apis/firebase/index.js';
|
|
21
23
|
import { iam } from 'googleapis/build/src/apis/iam/index.js';
|
|
@@ -29,6 +31,8 @@ export const google = {
|
|
|
29
31
|
analyticsdata,
|
|
30
32
|
androidpublisher,
|
|
31
33
|
bigquery,
|
|
34
|
+
billingbudgets,
|
|
35
|
+
cloudbilling,
|
|
32
36
|
cloudresourcemanager,
|
|
33
37
|
firebase,
|
|
34
38
|
iam,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as billing from '../billing/tools.js';
|
|
3
|
+
import { requireAuth } from '../helpers.js';
|
|
4
|
+
import { CLOUD_PLATFORM_SCOPE } from '../auth/scopes.js';
|
|
5
|
+
import { jsonResult } from '../lib/mcp-response.js';
|
|
6
|
+
export function registerBillingTools(server) {
|
|
7
|
+
server.tool('gcp_get_billing_info', [
|
|
8
|
+
'프로젝트의 결제 상태 조회 — Blaze(결제 계정 연결) 여부와 연결된 결제 계정 ID.',
|
|
9
|
+
'Cloud Functions/Run 배포 전 필수 확인.',
|
|
10
|
+
'⚠️ Cloud Billing API 가 꺼져 있으면 403 이 난다 — 그건 "Spark 이라서"가 아니라 조회 자체가 막힌 것이다.',
|
|
11
|
+
'그 경우 firebase_enable_service(projectId, "cloudbilling.googleapis.com") 로 먼저 켠 뒤 다시 호출.',
|
|
12
|
+
].join(' '), {
|
|
13
|
+
projectId: z.string().describe('GCP/Firebase 프로젝트 ID'),
|
|
14
|
+
}, async ({ projectId }) => {
|
|
15
|
+
const auth = await requireAuth(CLOUD_PLATFORM_SCOPE);
|
|
16
|
+
return jsonResult(await billing.getBillingInfo(auth, projectId));
|
|
17
|
+
});
|
|
18
|
+
server.tool('gcp_list_billing_projects', [
|
|
19
|
+
'결제 계정에 붙은 프로젝트 목록 = **비용 범위 확인**.',
|
|
20
|
+
'프로젝트가 2개 이상이면(shared=true) 그 계정은 공용이므로 **계정 전체 예산은 쓰지 말 것** —',
|
|
21
|
+
'기존 지출만으로 임계를 즉시 넘겨 알림이 소음이 된다. gcp_create_budget 에 projectIds 를 넘겨 범위를 좁힌다.',
|
|
22
|
+
].join(' '), {
|
|
23
|
+
billingAccount: z
|
|
24
|
+
.string()
|
|
25
|
+
.describe('결제 계정 ID (예: 01F1F4-FD007B-2973A7 또는 billingAccounts/01F1F4-...)'),
|
|
26
|
+
quotaProjectId: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('quota 주체 프로젝트 — Cloud Billing/Budget API 를 켜 둔 프로젝트 ID. 생략하면 OAuth 클라이언트 프로젝트로 quota 가 잡혀 403 이 난다(에러 메시지의 프로젝트 번호는 조회 대상이 아니라 OAuth 쪽이다).'),
|
|
30
|
+
}, async ({ billingAccount, quotaProjectId }) => {
|
|
31
|
+
const auth = await requireAuth(CLOUD_PLATFORM_SCOPE);
|
|
32
|
+
return jsonResult(await billing.listBillingProjects(auth, billingAccount, quotaProjectId));
|
|
33
|
+
});
|
|
34
|
+
server.tool('gcp_list_budgets', '결제 계정의 예산 목록 (금액·프로젝트 필터·알림 임계). 중복 생성 전 확인용.', {
|
|
35
|
+
billingAccount: z.string().describe('결제 계정 ID'),
|
|
36
|
+
quotaProjectId: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('quota 주체 프로젝트 — Cloud Billing/Budget API 를 켜 둔 프로젝트 ID. 생략하면 OAuth 클라이언트 프로젝트로 quota 가 잡혀 403 이 난다(에러 메시지의 프로젝트 번호는 조회 대상이 아니라 OAuth 쪽이다).'),
|
|
40
|
+
}, async ({ billingAccount, quotaProjectId }) => {
|
|
41
|
+
const auth = await requireAuth(CLOUD_PLATFORM_SCOPE);
|
|
42
|
+
return jsonResult(await billing.listBudgets(auth, billingAccount, quotaProjectId));
|
|
43
|
+
});
|
|
44
|
+
server.tool('gcp_create_budget', [
|
|
45
|
+
'예산 + 알림 임계 생성.',
|
|
46
|
+
'⚠️ **예산은 알림만 한다. 지출을 막지 않는다.** 하드 차단은 Pub/Sub→결제해제 함수뿐이고 그건 앱을 죽인다.',
|
|
47
|
+
'실질 방어는 각 서비스의 상한(예: Cloud Functions maxInstances)이고 예산은 트립와이어다.',
|
|
48
|
+
'⚠️ GCP 에 **일 예산은 없다** — 기간은 월(기본)/분기/연/사용자지정뿐. "하루 N원"을 원하면 월 환산하거나,',
|
|
49
|
+
'실사용이 적을 때는 작은 월 예산을 알람으로 쓰는 게 더 빨리 잡힌다.',
|
|
50
|
+
'projectIds 를 반드시 고려할 것 — 공용 결제 계정에서 생략하면 알림이 소음이 된다(gcp_list_billing_projects 로 먼저 확인).',
|
|
51
|
+
'권한은 결제 계정 레벨 IAM 이라 서비스 계정 키로는 대개 403 — 사용자 OAuth 로 호출된다.',
|
|
52
|
+
].join(' '), {
|
|
53
|
+
billingAccount: z.string().describe('결제 계정 ID'),
|
|
54
|
+
displayName: z.string().describe('예산 이름 (예: "my-app functions 감시")'),
|
|
55
|
+
amountUnits: z
|
|
56
|
+
.number()
|
|
57
|
+
.int()
|
|
58
|
+
.positive()
|
|
59
|
+
.describe('통화 단위 정수 금액. KRW 는 소수 없음 — 10000 = ₩10,000'),
|
|
60
|
+
currencyCode: z.string().optional().describe('통화 코드 (기본 KRW). 결제 계정 통화와 일치해야 한다'),
|
|
61
|
+
projectIds: z
|
|
62
|
+
.array(z.string())
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('감시할 프로젝트 ID 목록. 생략하면 결제 계정 전체 — 공용 계정이면 권장하지 않음'),
|
|
65
|
+
thresholds: z
|
|
66
|
+
.array(z.number().min(0).max(1))
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('알림 임계 비율 0~1 (기본 [0.5, 0.9, 1.0])'),
|
|
69
|
+
quotaProjectId: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('quota 주체 프로젝트 — Cloud Billing/Budget API 를 켜 둔 프로젝트 ID. 생략하면 OAuth 클라이언트 프로젝트로 quota 가 잡혀 403 이 난다(에러 메시지의 프로젝트 번호는 조회 대상이 아니라 OAuth 쪽이다).'),
|
|
73
|
+
}, async ({ billingAccount, displayName, amountUnits, currencyCode, projectIds, thresholds, quotaProjectId, }) => {
|
|
74
|
+
const auth = await requireAuth(CLOUD_PLATFORM_SCOPE);
|
|
75
|
+
const budget = await billing.createBudget(auth, {
|
|
76
|
+
billingAccount,
|
|
77
|
+
displayName,
|
|
78
|
+
amountUnits,
|
|
79
|
+
currencyCode,
|
|
80
|
+
projectIds,
|
|
81
|
+
thresholds,
|
|
82
|
+
quotaProjectId,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: [
|
|
89
|
+
'✓ 예산 생성 완료',
|
|
90
|
+
'',
|
|
91
|
+
`**name**: \`${budget.name}\``,
|
|
92
|
+
`**displayName**: ${budget.displayName}`,
|
|
93
|
+
`**범위**: ${budget.projects.length ? budget.projects.join(', ') : '결제 계정 전체'}`,
|
|
94
|
+
`**알림 임계**: ${budget.thresholds.map((t) => `${Math.round(t * 100)}%`).join(' / ')}`,
|
|
95
|
+
'',
|
|
96
|
+
'⚠️ 이 예산은 **알림만** 한다 — 임계를 넘어도 지출은 계속된다.',
|
|
97
|
+
].join('\n'),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { registerFirebaseTools } from './registers/firebase.js';
|
|
|
3
3
|
import { registerAdmobTools } from './registers/admob.js';
|
|
4
4
|
import { registerPlaystoreTools } from './registers/playstore.js';
|
|
5
5
|
import { registerIamTools } from './registers/iam.js';
|
|
6
|
+
import { registerBillingTools } from './registers/billing.js';
|
|
6
7
|
import { registerAppstoreTools } from './registers/appstore.js';
|
|
7
8
|
import { registerChecksTools } from './registers/checks.js';
|
|
8
9
|
import { registerAiTools } from './registers/ai.js';
|
|
@@ -38,6 +39,7 @@ export function buildServer(version) {
|
|
|
38
39
|
registerAdmobTools(server);
|
|
39
40
|
registerPlaystoreTools(server);
|
|
40
41
|
registerIamTools(server);
|
|
42
|
+
registerBillingTools(server);
|
|
41
43
|
registerAppstoreTools(server);
|
|
42
44
|
registerChecksTools(server);
|
|
43
45
|
registerAiTools(server);
|
package/package.json
CHANGED
package/tool-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "등록된 MCP 도구 + 도메인 메타데이터(label·credential·summary)의 SSOT. src/__tests__/tool-manifest.test.ts 가 실제 서버 등록 목록과 diff 하고 메타데이터 정합성을 검사한다. 도구 추가/삭제/개명 시 이 파일을 함께 갱신할 것 — 산문 문서에는 정확한 개수를 쓰지 말고 이 파일을 가리킬 것. mimi-seed://tools/catalog 리소스가 이 파일을 그대로 서빙한다.",
|
|
3
|
-
"total":
|
|
3
|
+
"total": 226,
|
|
4
4
|
"domains": {
|
|
5
5
|
"admob": {
|
|
6
6
|
"label": "AdMob",
|
|
@@ -126,6 +126,17 @@
|
|
|
126
126
|
"bigquery_auth_status"
|
|
127
127
|
]
|
|
128
128
|
},
|
|
129
|
+
"billing": {
|
|
130
|
+
"label": "GCP Billing",
|
|
131
|
+
"credential": "Google OAuth",
|
|
132
|
+
"summary": "결제 상태(Blaze 여부)·비용 범위(공용 계정 판별)·예산 알림",
|
|
133
|
+
"tools": [
|
|
134
|
+
"gcp_get_billing_info",
|
|
135
|
+
"gcp_list_billing_projects",
|
|
136
|
+
"gcp_list_budgets",
|
|
137
|
+
"gcp_create_budget"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
129
140
|
"checks": {
|
|
130
141
|
"label": "출시 점검",
|
|
131
142
|
"credential": "점검 대상 스토어의 자격증명",
|