@openaisdk/billing-mcp 0.2.1
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 +64 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +418 -0
- package/dist/megaretro-catalog-seed.d.ts +32 -0
- package/dist/megaretro-catalog-seed.js +444 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @openaisdk/billing-mcp
|
|
2
|
+
|
|
3
|
+
MCP-сервер для **управления каталогом** в Billing API: проекты, **планы**, **цены** (REST `v1/projects/.../plans`, `.../prices`).
|
|
4
|
+
|
|
5
|
+
## Переменные окружения
|
|
6
|
+
|
|
7
|
+
| Переменная | Обязательно | Описание |
|
|
8
|
+
|------------|-------------|----------|
|
|
9
|
+
| `BILLING_API_KEY` | да | Project-scoped integration key (получить через Admin UI → Integration) |
|
|
10
|
+
| `BILLING_PROJECT_ID` | да | UUID проекта (Admin UI → Settings или `pnpm db:seed` output) |
|
|
11
|
+
| `BILLING_API_BASE_URL` | нет | По умолчанию `http://127.0.0.1:4001` |
|
|
12
|
+
|
|
13
|
+
Ключ передаётся как `Authorization: Bearer <key>`. `BILLING_TENANT_ID` / `DEV_TENANT_ID` больше не нужны.
|
|
14
|
+
|
|
15
|
+
## Cursor
|
|
16
|
+
|
|
17
|
+
В `.cursor/mcp.json` добавьте сервер (после `pnpm install` и `pnpm --filter @openaisdk/billing-mcp run build`):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"billing-catalog": {
|
|
23
|
+
"command": "node",
|
|
24
|
+
"args": ["packages/billing-catalog-mcp/dist/index.js"],
|
|
25
|
+
"cwd": "${workspaceFolder}",
|
|
26
|
+
"env": {
|
|
27
|
+
"BILLING_API_BASE_URL": "http://127.0.0.1:4001",
|
|
28
|
+
"BILLING_API_KEY": "<project-scoped integration key>",
|
|
29
|
+
"BILLING_PROJECT_ID": "<project-uuid>"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Для разработки без сборки можно `tsx`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
"command": "pnpm",
|
|
40
|
+
"args": ["exec", "tsx", "packages/billing-catalog-mcp/index.ts"],
|
|
41
|
+
"cwd": "${workspaceFolder}"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Инструменты (tools)
|
|
45
|
+
|
|
46
|
+
- `billing_list_projects` — проекты тенанта
|
|
47
|
+
- `billing_list_plans` / `billing_get_plan` / `billing_create_plan` / `billing_update_plan`
|
|
48
|
+
- `billing_list_prices` / `billing_get_price` / `billing_create_price` / `billing_update_price`
|
|
49
|
+
|
|
50
|
+
`billing_create_price`: укажите `planId` **или** `planCode` (например `team`), плюс `code`, `amountMinor` (копейки), `interval` (`month` | `year`).
|
|
51
|
+
|
|
52
|
+
## Сборка
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pnpm --filter @openaisdk/billing-mcp run build
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Локальный запуск (проверка)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
BILLING_API_KEY=<key> BILLING_PROJECT_ID=<uuid> pnpm --filter @openaisdk/billing-mcp run dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Stdio ждёт MCP-клиент; для ручной проверки используйте Cursor или тестовый MCP-клиент.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP server: Billing Catalog — планы и цены (REST Billing API).
|
|
4
|
+
*/
|
|
5
|
+
import 'dotenv/config';
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { seedMegaretroCatalog as runMegaretroCatalogSeed, upsertMegaretroFeaturesAndLimits, } from './megaretro-catalog-seed.js';
|
|
10
|
+
function getBaseUrl() {
|
|
11
|
+
return (process.env.BILLING_API_BASE_URL ?? 'http://127.0.0.1:4001').replace(/\/$/, '');
|
|
12
|
+
}
|
|
13
|
+
function buildHeaders(extra) {
|
|
14
|
+
const h = { ...extra };
|
|
15
|
+
// Auth: project-scoped integration key (ADR-010).
|
|
16
|
+
// Tenant и project резолвятся из ключа на стороне API — x-tenant-id не нужен.
|
|
17
|
+
const key = process.env.BILLING_API_KEY?.trim();
|
|
18
|
+
if (!key) {
|
|
19
|
+
throw new Error('Задайте BILLING_API_KEY в .env / env MCP (project-scoped integration key, создаётся в Admin UI → Интеграция)');
|
|
20
|
+
}
|
|
21
|
+
h['Authorization'] = `Bearer ${key}`;
|
|
22
|
+
return h;
|
|
23
|
+
}
|
|
24
|
+
async function billingFetch(path, init = {}) {
|
|
25
|
+
const url = `${getBaseUrl()}${path.startsWith('/') ? path : `/${path}`}`;
|
|
26
|
+
const headers = new Headers(buildHeaders());
|
|
27
|
+
if (init.body) {
|
|
28
|
+
headers.set('Content-Type', 'application/json');
|
|
29
|
+
}
|
|
30
|
+
if (init.headers) {
|
|
31
|
+
const extra = new Headers(init.headers);
|
|
32
|
+
extra.forEach((v, k) => headers.set(k, v));
|
|
33
|
+
}
|
|
34
|
+
const res = await fetch(url, { ...init, headers });
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = text ? JSON.parse(text) : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
body = text;
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const msg = typeof body === 'string' ? body : JSON.stringify(body);
|
|
45
|
+
throw new Error(`HTTP ${res.status}: ${msg}`);
|
|
46
|
+
}
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
async function resolvePlanId(projectId, planCode) {
|
|
50
|
+
const plans = (await billingFetch(`/v1/projects/${projectId}/plans`));
|
|
51
|
+
const p = plans.find((x) => x.code === planCode);
|
|
52
|
+
if (!p) {
|
|
53
|
+
throw new Error(`План с code="${planCode}" не найден в проекте ${projectId}`);
|
|
54
|
+
}
|
|
55
|
+
return p.id;
|
|
56
|
+
}
|
|
57
|
+
async function resolvePlanIdFromArgs(projectId, args) {
|
|
58
|
+
let planId = args.planId;
|
|
59
|
+
if (!planId && args.planCode) {
|
|
60
|
+
planId = await resolvePlanId(projectId, String(args.planCode));
|
|
61
|
+
}
|
|
62
|
+
if (!planId) {
|
|
63
|
+
throw new Error('Укажите planId или planCode');
|
|
64
|
+
}
|
|
65
|
+
return planId;
|
|
66
|
+
}
|
|
67
|
+
const tools = [
|
|
68
|
+
{
|
|
69
|
+
name: 'billing_list_projects',
|
|
70
|
+
description: 'Список проектов тенанта (GET /v1/projects)',
|
|
71
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'billing_list_plans',
|
|
75
|
+
description: 'Список планов проекта',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
projectId: { type: 'string', description: 'UUID проекта' },
|
|
80
|
+
},
|
|
81
|
+
required: ['projectId'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'billing_get_plan',
|
|
86
|
+
description: 'План по ID',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
projectId: { type: 'string' },
|
|
91
|
+
planId: { type: 'string' },
|
|
92
|
+
},
|
|
93
|
+
required: ['projectId', 'planId'],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'billing_create_plan',
|
|
98
|
+
description: 'Создать план (POST). code уникален в проекте.',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
projectId: { type: 'string' },
|
|
103
|
+
code: { type: 'string', description: 'slug, напр. team, business' },
|
|
104
|
+
name: { type: 'string' },
|
|
105
|
+
description: { type: 'string', description: 'опционально' },
|
|
106
|
+
isPublic: { type: 'boolean', description: 'по умолчанию true' },
|
|
107
|
+
isActive: { type: 'boolean', description: 'по умолчанию true' },
|
|
108
|
+
sortOrder: { type: 'number', description: 'по умолчанию 0' },
|
|
109
|
+
},
|
|
110
|
+
required: ['projectId', 'code', 'name'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'billing_update_plan',
|
|
115
|
+
description: 'Обновить план (PATCH). code менять нельзя.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
projectId: { type: 'string' },
|
|
120
|
+
planId: { type: 'string' },
|
|
121
|
+
name: { type: 'string' },
|
|
122
|
+
description: { type: 'string' },
|
|
123
|
+
isPublic: { type: 'boolean' },
|
|
124
|
+
isActive: { type: 'boolean' },
|
|
125
|
+
sortOrder: { type: 'number' },
|
|
126
|
+
},
|
|
127
|
+
required: ['projectId', 'planId'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'billing_list_prices',
|
|
132
|
+
description: 'Список цен проекта',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
projectId: { type: 'string' },
|
|
137
|
+
},
|
|
138
|
+
required: ['projectId'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'billing_get_price',
|
|
143
|
+
description: 'Цена по ID',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
projectId: { type: 'string' },
|
|
148
|
+
priceId: { type: 'string' },
|
|
149
|
+
},
|
|
150
|
+
required: ['projectId', 'priceId'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'billing_create_price',
|
|
155
|
+
description: 'Создать цену для плана. amountMinor — копейки (RUB). interval: month | year. Укажите planId ИЛИ planCode.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
projectId: { type: 'string' },
|
|
160
|
+
planId: { type: 'string', description: 'UUID плана (если нет planCode)' },
|
|
161
|
+
planCode: { type: 'string', description: 'код плана, напр. team' },
|
|
162
|
+
code: { type: 'string', description: 'уникальный код цены в проекте, напр. team_monthly' },
|
|
163
|
+
amountMinor: { type: 'number', description: 'сумма в минорных единицах' },
|
|
164
|
+
currency: { type: 'string', description: 'по умолчанию RUB' },
|
|
165
|
+
interval: { type: 'string', enum: ['month', 'year'] },
|
|
166
|
+
intervalCount: { type: 'number', description: 'по умолчанию 1' },
|
|
167
|
+
trialDays: { type: 'number' },
|
|
168
|
+
isActive: { type: 'boolean', description: 'по умолчанию true' },
|
|
169
|
+
},
|
|
170
|
+
required: ['projectId', 'code', 'amountMinor', 'interval'],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'billing_update_price',
|
|
175
|
+
description: 'Обновить цену (PATCH). planId менять нельзя.',
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
projectId: { type: 'string' },
|
|
180
|
+
priceId: { type: 'string' },
|
|
181
|
+
code: { type: 'string' },
|
|
182
|
+
amountMinor: { type: 'number' },
|
|
183
|
+
currency: { type: 'string' },
|
|
184
|
+
interval: { type: 'string' },
|
|
185
|
+
intervalCount: { type: 'number' },
|
|
186
|
+
trialDays: { type: 'number' },
|
|
187
|
+
isActive: { type: 'boolean' },
|
|
188
|
+
},
|
|
189
|
+
required: ['projectId', 'priceId'],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'billing_reset_project_catalog',
|
|
194
|
+
description: '[DEV] Удалить каталог проекта (plans/prices/…) и связанные подписки/инвойсы/платежи. Только NODE_ENV !== production. POST /v1/dev/projects/:id/reset-catalog',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
projectId: { type: 'string', description: 'UUID проекта (напр. megaretro)' },
|
|
199
|
+
},
|
|
200
|
+
required: ['projectId'],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'billing_reset_and_seed_megaretro_catalog',
|
|
205
|
+
description: 'Сброс проекта, затем полный каталог MegaRetro по mega-retro/docs/product/pricing-and-capabilities.md: планы/цены (trial 14 дн. на team_monthly и business_monthly), фичи/лимиты, add-on доп. команда Business (+490₽/мес, год −20%), ориентиры Enterprise.',
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {
|
|
209
|
+
projectId: { type: 'string', description: 'UUID проекта megaretro' },
|
|
210
|
+
},
|
|
211
|
+
required: ['projectId'],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'billing_upsert_megaretro_features_and_limits',
|
|
216
|
+
description: 'Без сброса: создать недостающие фичи и привязки к планам free/team/business/enterprise с лимитами по pricing-and-capabilities.md (идемпотентно; уже существующие привязки пропускаются).',
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
properties: {
|
|
220
|
+
projectId: { type: 'string', description: 'UUID проекта' },
|
|
221
|
+
},
|
|
222
|
+
required: ['projectId'],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'billing_list_features',
|
|
227
|
+
description: 'Список фич проекта (GET /v1/projects/:id/features)',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: { projectId: { type: 'string' } },
|
|
231
|
+
required: ['projectId'],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'billing_create_feature',
|
|
236
|
+
description: 'Создать фичу: kind boolean | limit',
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
projectId: { type: 'string' },
|
|
241
|
+
code: { type: 'string' },
|
|
242
|
+
name: { type: 'string' },
|
|
243
|
+
kind: { type: 'string', enum: ['boolean', 'limit'] },
|
|
244
|
+
},
|
|
245
|
+
required: ['projectId', 'code', 'name', 'kind'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'billing_list_plan_features',
|
|
250
|
+
description: 'Привязки фич к плану. Укажите planId или planCode.',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
projectId: { type: 'string' },
|
|
255
|
+
planId: { type: 'string' },
|
|
256
|
+
planCode: { type: 'string', description: 'напр. team, business' },
|
|
257
|
+
},
|
|
258
|
+
required: ['projectId'],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'billing_create_plan_feature',
|
|
263
|
+
description: 'Привязать фичу к плану (лимит или boolean). Укажите planId или planCode; для limit передайте limitValue (null = безлимит).',
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
projectId: { type: 'string' },
|
|
268
|
+
planId: { type: 'string' },
|
|
269
|
+
planCode: { type: 'string' },
|
|
270
|
+
featureId: { type: 'string' },
|
|
271
|
+
enabled: { type: 'boolean' },
|
|
272
|
+
limitValue: {
|
|
273
|
+
type: 'number',
|
|
274
|
+
description: 'для kind=limit; для безлимита передайте null через сырой JSON аргумента',
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
required: ['projectId', 'featureId'],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
async function seedMegaretroCatalog(projectId) {
|
|
282
|
+
return runMegaretroCatalogSeed(billingFetch, projectId);
|
|
283
|
+
}
|
|
284
|
+
function pickBody(args, keys) {
|
|
285
|
+
const out = {};
|
|
286
|
+
for (const k of keys) {
|
|
287
|
+
if (args[k] !== undefined && args[k] !== null) {
|
|
288
|
+
out[k] = args[k];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
async function handleTool(name, args) {
|
|
294
|
+
switch (name) {
|
|
295
|
+
case 'billing_list_projects':
|
|
296
|
+
return billingFetch('/v1/projects');
|
|
297
|
+
case 'billing_list_plans':
|
|
298
|
+
return billingFetch(`/v1/projects/${args.projectId}/plans`);
|
|
299
|
+
case 'billing_get_plan':
|
|
300
|
+
return billingFetch(`/v1/projects/${args.projectId}/plans/${args.planId}`);
|
|
301
|
+
case 'billing_create_plan':
|
|
302
|
+
return billingFetch(`/v1/projects/${args.projectId}/plans`, {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
body: JSON.stringify(pickBody(args, ['code', 'name', 'description', 'isPublic', 'isActive', 'sortOrder'])),
|
|
305
|
+
});
|
|
306
|
+
case 'billing_update_plan':
|
|
307
|
+
return billingFetch(`/v1/projects/${args.projectId}/plans/${args.planId}`, {
|
|
308
|
+
method: 'PATCH',
|
|
309
|
+
body: JSON.stringify(pickBody(args, ['name', 'description', 'isPublic', 'isActive', 'sortOrder'])),
|
|
310
|
+
});
|
|
311
|
+
case 'billing_list_prices':
|
|
312
|
+
return billingFetch(`/v1/projects/${args.projectId}/prices`);
|
|
313
|
+
case 'billing_get_price':
|
|
314
|
+
return billingFetch(`/v1/projects/${args.projectId}/prices/${args.priceId}`);
|
|
315
|
+
case 'billing_create_price': {
|
|
316
|
+
let planId = args.planId;
|
|
317
|
+
const planCode = args.planCode;
|
|
318
|
+
if (!planId && planCode) {
|
|
319
|
+
planId = await resolvePlanId(String(args.projectId), planCode);
|
|
320
|
+
}
|
|
321
|
+
if (!planId) {
|
|
322
|
+
throw new Error('Укажите planId или planCode');
|
|
323
|
+
}
|
|
324
|
+
const body = {
|
|
325
|
+
planId,
|
|
326
|
+
code: args.code,
|
|
327
|
+
amountMinor: args.amountMinor,
|
|
328
|
+
currency: args.currency ?? 'RUB',
|
|
329
|
+
interval: args.interval,
|
|
330
|
+
intervalCount: args.intervalCount ?? 1,
|
|
331
|
+
trialDays: args.trialDays,
|
|
332
|
+
isActive: args.isActive ?? true,
|
|
333
|
+
};
|
|
334
|
+
return billingFetch(`/v1/projects/${args.projectId}/prices`, {
|
|
335
|
+
method: 'POST',
|
|
336
|
+
body: JSON.stringify(body),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
case 'billing_update_price':
|
|
340
|
+
return billingFetch(`/v1/projects/${args.projectId}/prices/${args.priceId}`, {
|
|
341
|
+
method: 'PATCH',
|
|
342
|
+
body: JSON.stringify(pickBody(args, [
|
|
343
|
+
'code',
|
|
344
|
+
'amountMinor',
|
|
345
|
+
'currency',
|
|
346
|
+
'interval',
|
|
347
|
+
'intervalCount',
|
|
348
|
+
'trialDays',
|
|
349
|
+
'isActive',
|
|
350
|
+
])),
|
|
351
|
+
});
|
|
352
|
+
case 'billing_reset_project_catalog':
|
|
353
|
+
return billingFetch(`/v1/dev/projects/${args.projectId}/reset-catalog`, {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
});
|
|
356
|
+
case 'billing_reset_and_seed_megaretro_catalog':
|
|
357
|
+
return seedMegaretroCatalog(String(args.projectId));
|
|
358
|
+
case 'billing_upsert_megaretro_features_and_limits':
|
|
359
|
+
return upsertMegaretroFeaturesAndLimits(billingFetch, String(args.projectId));
|
|
360
|
+
case 'billing_list_features':
|
|
361
|
+
return billingFetch(`/v1/projects/${args.projectId}/features`);
|
|
362
|
+
case 'billing_create_feature':
|
|
363
|
+
return billingFetch(`/v1/projects/${args.projectId}/features`, {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
body: JSON.stringify(pickBody(args, ['code', 'name', 'kind'])),
|
|
366
|
+
});
|
|
367
|
+
case 'billing_list_plan_features': {
|
|
368
|
+
const planId = await resolvePlanIdFromArgs(String(args.projectId), args);
|
|
369
|
+
return billingFetch(`/v1/projects/${args.projectId}/plans/${planId}/plan-features`);
|
|
370
|
+
}
|
|
371
|
+
case 'billing_create_plan_feature': {
|
|
372
|
+
const planId = await resolvePlanIdFromArgs(String(args.projectId), args);
|
|
373
|
+
const body = { featureId: args.featureId };
|
|
374
|
+
if (args.enabled !== undefined) {
|
|
375
|
+
body.enabled = args.enabled;
|
|
376
|
+
}
|
|
377
|
+
if (Object.prototype.hasOwnProperty.call(args, 'limitValue')) {
|
|
378
|
+
body.limitValue = args.limitValue;
|
|
379
|
+
}
|
|
380
|
+
return billingFetch(`/v1/projects/${args.projectId}/plans/${planId}/plan-features`, {
|
|
381
|
+
method: 'POST',
|
|
382
|
+
body: JSON.stringify(body),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
default:
|
|
386
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const server = new Server({ name: 'billing-catalog', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
390
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
391
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
392
|
+
const { name, arguments: raw } = request.params;
|
|
393
|
+
const args = raw || {};
|
|
394
|
+
try {
|
|
395
|
+
const result = await handleTool(name, args);
|
|
396
|
+
return {
|
|
397
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
404
|
+
isError: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
async function main() {
|
|
409
|
+
// Проверка наличия BILLING_API_KEY при старте (buildHeaders() бросит если ключ отсутствует)
|
|
410
|
+
buildHeaders();
|
|
411
|
+
const transport = new StdioServerTransport();
|
|
412
|
+
await server.connect(transport);
|
|
413
|
+
console.error('Billing Catalog MCP on stdio (project-scoped key configured)');
|
|
414
|
+
}
|
|
415
|
+
main().catch((e) => {
|
|
416
|
+
console.error(e);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Каталог MegaRetro по канону:
|
|
3
|
+
* mega-retro/docs/product/pricing-and-capabilities.md (2026-03-22)
|
|
4
|
+
*
|
|
5
|
+
* Планы, цены, фичи (limits/boolean), add-on «доп. команда» для Business (+490 ₽/мес, год −20%).
|
|
6
|
+
*/
|
|
7
|
+
export type BillingFetch = (path: string, init?: RequestInit) => Promise<unknown>;
|
|
8
|
+
type PlanCode = 'free' | 'team' | 'business' | 'enterprise';
|
|
9
|
+
/** Помесячный trial для Team/Business — см. mega-retro/docs/product/pricing-and-capabilities.md */
|
|
10
|
+
export declare const MEGARETRO_TRIAL_DAYS_PAID_MONTHLY = 14;
|
|
11
|
+
/** Создаёт отсутствующие фичи проекта по канону MegaRetro (идемпотентно). */
|
|
12
|
+
export declare function ensureMegaretroProjectFeatures(billingFetch: BillingFetch, projectId: string): Promise<Record<string, string>>;
|
|
13
|
+
/** Привязки фич к планам по BINDINGS; существующие пары plan+feature пропускает. */
|
|
14
|
+
export declare function applyMegaretroPlanFeatureBindings(billingFetch: BillingFetch, projectId: string, planIds: Record<PlanCode, string>, featureIds: Record<string, string>): Promise<{
|
|
15
|
+
items: unknown[];
|
|
16
|
+
created: number;
|
|
17
|
+
skipped: number;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Без сброса каталога: убедиться, что есть планы free/team/business/enterprise,
|
|
21
|
+
* завести недостающие фичи и привязки с лимитами по pricing-and-capabilities.md.
|
|
22
|
+
*/
|
|
23
|
+
export declare function upsertMegaretroFeaturesAndLimits(billingFetch: BillingFetch, projectId: string): Promise<{
|
|
24
|
+
sourceDoc: string;
|
|
25
|
+
planIds: Record<PlanCode, string>;
|
|
26
|
+
featureCodes: string[];
|
|
27
|
+
featureIds: Record<string, string>;
|
|
28
|
+
planFeatureBindingsCreated: number;
|
|
29
|
+
planFeatureBindingsSkipped: number;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function seedMegaretroCatalog(billingFetch: BillingFetch, projectId: string): Promise<unknown>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Каталог MegaRetro по канону:
|
|
3
|
+
* mega-retro/docs/product/pricing-and-capabilities.md (2026-03-22)
|
|
4
|
+
*
|
|
5
|
+
* Планы, цены, фичи (limits/boolean), add-on «доп. команда» для Business (+490 ₽/мес, год −20%).
|
|
6
|
+
*/
|
|
7
|
+
const PLAN_ORDER = ['free', 'team', 'business', 'enterprise'];
|
|
8
|
+
/** Описание фичи в проекте */
|
|
9
|
+
const FEATURE_DEFS = [
|
|
10
|
+
{ code: 'workspaces_in_subscription', name: 'Команд (workspaces) в подписке', kind: 'limit' },
|
|
11
|
+
{ code: 'members_per_workspace', name: 'Участников на команду (null = безлимит)', kind: 'limit' },
|
|
12
|
+
{
|
|
13
|
+
code: 'concurrent_active_retro_boards',
|
|
14
|
+
name: 'Активных ретро-досок одновременно (null = безлимит)',
|
|
15
|
+
kind: 'limit',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
code: 'retro_history_retention_days',
|
|
19
|
+
name: 'История ретро: дней хранения контента (null = безлимит)',
|
|
20
|
+
kind: 'limit',
|
|
21
|
+
},
|
|
22
|
+
{ code: 'guest_board_access', name: 'Гостевой вход по ссылке на доску', kind: 'boolean' },
|
|
23
|
+
{ code: 'board_templates_basic', name: 'Базовые шаблоны досок', kind: 'boolean' },
|
|
24
|
+
{ code: 'board_templates_custom', name: 'Все шаблоны + кастомные шаблоны', kind: 'boolean' },
|
|
25
|
+
{ code: 'corporate_template_library', name: 'Корпоративная библиотека шаблонов', kind: 'boolean' },
|
|
26
|
+
{ code: 'export_full', name: 'Расширенный экспорт (сверх базового)', kind: 'boolean' },
|
|
27
|
+
{ code: 'integration_slack', name: 'Интеграция Slack', kind: 'boolean' },
|
|
28
|
+
{ code: 'integration_jira', name: 'Интеграция Jira', kind: 'boolean' },
|
|
29
|
+
{ code: 'integration_webhooks', name: 'Webhooks', kind: 'boolean' },
|
|
30
|
+
{ code: 'integration_public_api', name: 'Публичный / партнёрский API', kind: 'boolean' },
|
|
31
|
+
{ code: 'integration_custom_contract', name: 'Кастомные интеграции (договор)', kind: 'boolean' },
|
|
32
|
+
{ code: 'analytics_team_basic', name: 'Базовая аналитика по команде', kind: 'boolean' },
|
|
33
|
+
{ code: 'analytics_cross_workspace', name: 'Расширенная аналитика и кросс-командные срезы', kind: 'boolean' },
|
|
34
|
+
{ code: 'analytics_enterprise_reports', name: 'Корпоративные отчёты и выгрузки', kind: 'boolean' },
|
|
35
|
+
{ code: 'data_isolation_multi_tenant', name: 'Базовая изоляция данных (multi-tenant)', kind: 'boolean' },
|
|
36
|
+
{ code: 'security_sso_saml', name: 'SSO / SAML', kind: 'boolean' },
|
|
37
|
+
{ code: 'security_rbac_extended', name: 'RBAC, расширенные роли', kind: 'boolean' },
|
|
38
|
+
{ code: 'security_audit_extended', name: 'Журнал аудита расширенный', kind: 'boolean' },
|
|
39
|
+
{ code: 'security_contractual_data', name: 'Условия хранения и обработки данных по договору', kind: 'boolean' },
|
|
40
|
+
{ code: 'support_priority_email', name: 'Приоритетная поддержка (email)', kind: 'boolean' },
|
|
41
|
+
{ code: 'support_chat_priority_247', name: 'Поддержка чат / приоритет 24/7', kind: 'boolean' },
|
|
42
|
+
{ code: 'support_dedicated_account_manager', name: 'Выделенный аккаунт-менеджер', kind: 'boolean' },
|
|
43
|
+
{ code: 'support_sla_availability', name: 'SLA по доступности сервиса', kind: 'boolean' },
|
|
44
|
+
{ code: 'support_onboarding_training', name: 'Онбординг и обучение', kind: 'boolean' },
|
|
45
|
+
{ code: 'ai_retro_summary', name: 'AI-саммари ретро', kind: 'boolean' },
|
|
46
|
+
{ code: 'ai_card_rewrite', name: 'AI-rewrite / улучшение текста карточки', kind: 'boolean' },
|
|
47
|
+
{ code: 'ai_card_clustering', name: 'AI-кластеризация карточек', kind: 'boolean' },
|
|
48
|
+
{ code: 'ai_session_insights', name: 'AI-инсайты (одна сессия)', kind: 'boolean' },
|
|
49
|
+
{ code: 'ai_action_items', name: 'AI-генерация action items', kind: 'boolean' },
|
|
50
|
+
{ code: 'ai_cross_retro_patterns', name: 'Кросс-ретро тренды и паттерны', kind: 'boolean' },
|
|
51
|
+
{ code: 'ai_priority_models', name: 'Приоритетные AI-модели и очередь', kind: 'boolean' },
|
|
52
|
+
{ code: 'ai_custom_prompts', name: 'Кастомные промпты (корп. стандарты)', kind: 'boolean' },
|
|
53
|
+
{ code: 'ai_opt_out_workspace', name: 'Opt-out AI на уровне workspace', kind: 'boolean' },
|
|
54
|
+
{
|
|
55
|
+
code: 'ai_requests_tier',
|
|
56
|
+
name: 'Уровень лимита AI-запросов (черновик: 0=нет, 1=умеренный, 2=выше, 3=наивысший)',
|
|
57
|
+
kind: 'limit',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
/** Для каждой фичи — значения по планам (нет ключа = фича выключена / не применима для плана) */
|
|
61
|
+
const BINDINGS = {
|
|
62
|
+
workspaces_in_subscription: {
|
|
63
|
+
free: { limit: 1 },
|
|
64
|
+
team: { limit: 1 },
|
|
65
|
+
business: { limit: 5 },
|
|
66
|
+
enterprise: { limit: null },
|
|
67
|
+
},
|
|
68
|
+
members_per_workspace: {
|
|
69
|
+
free: { limit: 10 },
|
|
70
|
+
team: { limit: 30 },
|
|
71
|
+
business: { limit: null },
|
|
72
|
+
enterprise: { limit: null },
|
|
73
|
+
},
|
|
74
|
+
concurrent_active_retro_boards: {
|
|
75
|
+
free: { limit: 3 },
|
|
76
|
+
team: { limit: null },
|
|
77
|
+
business: { limit: null },
|
|
78
|
+
enterprise: { limit: null },
|
|
79
|
+
},
|
|
80
|
+
retro_history_retention_days: {
|
|
81
|
+
free: { limit: 30 },
|
|
82
|
+
team: { limit: null },
|
|
83
|
+
business: { limit: null },
|
|
84
|
+
enterprise: { limit: null },
|
|
85
|
+
},
|
|
86
|
+
guest_board_access: {
|
|
87
|
+
free: { enabled: true },
|
|
88
|
+
team: { enabled: true },
|
|
89
|
+
business: { enabled: true },
|
|
90
|
+
enterprise: { enabled: true },
|
|
91
|
+
},
|
|
92
|
+
board_templates_basic: {
|
|
93
|
+
free: { enabled: true },
|
|
94
|
+
team: { enabled: true },
|
|
95
|
+
business: { enabled: true },
|
|
96
|
+
enterprise: { enabled: true },
|
|
97
|
+
},
|
|
98
|
+
board_templates_custom: {
|
|
99
|
+
team: { enabled: true },
|
|
100
|
+
business: { enabled: true },
|
|
101
|
+
enterprise: { enabled: true },
|
|
102
|
+
},
|
|
103
|
+
corporate_template_library: { enterprise: { enabled: true } },
|
|
104
|
+
export_full: {
|
|
105
|
+
team: { enabled: true },
|
|
106
|
+
business: { enabled: true },
|
|
107
|
+
enterprise: { enabled: true },
|
|
108
|
+
},
|
|
109
|
+
integration_slack: {
|
|
110
|
+
team: { enabled: true },
|
|
111
|
+
business: { enabled: true },
|
|
112
|
+
enterprise: { enabled: true },
|
|
113
|
+
},
|
|
114
|
+
integration_jira: {
|
|
115
|
+
business: { enabled: true },
|
|
116
|
+
enterprise: { enabled: true },
|
|
117
|
+
},
|
|
118
|
+
integration_webhooks: {
|
|
119
|
+
business: { enabled: true },
|
|
120
|
+
enterprise: { enabled: true },
|
|
121
|
+
},
|
|
122
|
+
integration_public_api: {
|
|
123
|
+
business: { enabled: true },
|
|
124
|
+
enterprise: { enabled: true },
|
|
125
|
+
},
|
|
126
|
+
integration_custom_contract: { enterprise: { enabled: true } },
|
|
127
|
+
analytics_team_basic: {
|
|
128
|
+
team: { enabled: true },
|
|
129
|
+
business: { enabled: true },
|
|
130
|
+
enterprise: { enabled: true },
|
|
131
|
+
},
|
|
132
|
+
analytics_cross_workspace: {
|
|
133
|
+
business: { enabled: true },
|
|
134
|
+
enterprise: { enabled: true },
|
|
135
|
+
},
|
|
136
|
+
analytics_enterprise_reports: { enterprise: { enabled: true } },
|
|
137
|
+
data_isolation_multi_tenant: {
|
|
138
|
+
free: { enabled: true },
|
|
139
|
+
team: { enabled: true },
|
|
140
|
+
business: { enabled: true },
|
|
141
|
+
enterprise: { enabled: true },
|
|
142
|
+
},
|
|
143
|
+
security_sso_saml: { enterprise: { enabled: true } },
|
|
144
|
+
security_rbac_extended: { enterprise: { enabled: true } },
|
|
145
|
+
security_audit_extended: { enterprise: { enabled: true } },
|
|
146
|
+
security_contractual_data: { enterprise: { enabled: true } },
|
|
147
|
+
support_priority_email: {
|
|
148
|
+
team: { enabled: true },
|
|
149
|
+
business: { enabled: true },
|
|
150
|
+
enterprise: { enabled: true },
|
|
151
|
+
},
|
|
152
|
+
support_chat_priority_247: {
|
|
153
|
+
business: { enabled: true },
|
|
154
|
+
enterprise: { enabled: true },
|
|
155
|
+
},
|
|
156
|
+
support_dedicated_account_manager: { enterprise: { enabled: true } },
|
|
157
|
+
support_sla_availability: { enterprise: { enabled: true } },
|
|
158
|
+
support_onboarding_training: { enterprise: { enabled: true } },
|
|
159
|
+
ai_retro_summary: {
|
|
160
|
+
team: { enabled: true },
|
|
161
|
+
business: { enabled: true },
|
|
162
|
+
enterprise: { enabled: true },
|
|
163
|
+
},
|
|
164
|
+
ai_card_rewrite: {
|
|
165
|
+
team: { enabled: true },
|
|
166
|
+
business: { enabled: true },
|
|
167
|
+
enterprise: { enabled: true },
|
|
168
|
+
},
|
|
169
|
+
ai_card_clustering: {
|
|
170
|
+
business: { enabled: true },
|
|
171
|
+
enterprise: { enabled: true },
|
|
172
|
+
},
|
|
173
|
+
ai_session_insights: {
|
|
174
|
+
business: { enabled: true },
|
|
175
|
+
enterprise: { enabled: true },
|
|
176
|
+
},
|
|
177
|
+
ai_action_items: {
|
|
178
|
+
business: { enabled: true },
|
|
179
|
+
enterprise: { enabled: true },
|
|
180
|
+
},
|
|
181
|
+
ai_cross_retro_patterns: {
|
|
182
|
+
business: { enabled: true },
|
|
183
|
+
enterprise: { enabled: true },
|
|
184
|
+
},
|
|
185
|
+
ai_priority_models: { enterprise: { enabled: true } },
|
|
186
|
+
ai_custom_prompts: { enterprise: { enabled: true } },
|
|
187
|
+
ai_opt_out_workspace: {
|
|
188
|
+
free: { enabled: true },
|
|
189
|
+
team: { enabled: true },
|
|
190
|
+
business: { enabled: true },
|
|
191
|
+
enterprise: { enabled: true },
|
|
192
|
+
},
|
|
193
|
+
ai_requests_tier: {
|
|
194
|
+
free: { limit: 0 },
|
|
195
|
+
team: { limit: 1 },
|
|
196
|
+
business: { limit: 2 },
|
|
197
|
+
enterprise: { limit: 3 },
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
const PLAN_DEFS = [
|
|
201
|
+
{
|
|
202
|
+
code: 'free',
|
|
203
|
+
name: 'Free',
|
|
204
|
+
description: 'Freemium: 1 workspace, лимиты по mega-retro/docs/product/pricing-and-capabilities.md. Базовый экспорт, без платных AI.',
|
|
205
|
+
sortOrder: 0,
|
|
206
|
+
isPublic: true,
|
|
207
|
+
isActive: true,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
code: 'team',
|
|
211
|
+
name: 'Team',
|
|
212
|
+
description: '1 workspace, 1 290 ₽/мес. AI: саммари и rewrite. Годовая оплата −20% от суммы 12 месяцев.',
|
|
213
|
+
sortOrder: 1,
|
|
214
|
+
isPublic: true,
|
|
215
|
+
isActive: true,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
code: 'business',
|
|
219
|
+
name: 'Business',
|
|
220
|
+
description: 'До 5 команд в базе; +490 ₽/мес за каждую команду сверх (add-on). 2 990 ₽/мес база. Jira, webhooks, API, расширенная AI.',
|
|
221
|
+
sortOrder: 2,
|
|
222
|
+
isPublic: true,
|
|
223
|
+
isActive: true,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
code: 'enterprise',
|
|
227
|
+
name: 'Enterprise',
|
|
228
|
+
description: 'По запросу, договор. Цены в биллинге — ориентиры для кассы/офферов; фактические условия в договоре.',
|
|
229
|
+
sortOrder: 3,
|
|
230
|
+
isPublic: false,
|
|
231
|
+
isActive: true,
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
/** Помесячный trial для Team/Business — см. mega-retro/docs/product/pricing-and-capabilities.md */
|
|
235
|
+
export const MEGARETRO_TRIAL_DAYS_PAID_MONTHLY = 14;
|
|
236
|
+
const PRICE_DEFS = [
|
|
237
|
+
{ planCode: 'free', code: 'free_monthly', amountMinor: 0, interval: 'month' },
|
|
238
|
+
{ planCode: 'free', code: 'free_yearly', amountMinor: 0, interval: 'year' },
|
|
239
|
+
{
|
|
240
|
+
planCode: 'team',
|
|
241
|
+
code: 'team_monthly',
|
|
242
|
+
amountMinor: 129_000,
|
|
243
|
+
interval: 'month',
|
|
244
|
+
trialDays: MEGARETRO_TRIAL_DAYS_PAID_MONTHLY,
|
|
245
|
+
},
|
|
246
|
+
{ planCode: 'team', code: 'team_yearly', amountMinor: 1_238_400, interval: 'year' },
|
|
247
|
+
{
|
|
248
|
+
planCode: 'business',
|
|
249
|
+
code: 'business_monthly',
|
|
250
|
+
amountMinor: 299_000,
|
|
251
|
+
interval: 'month',
|
|
252
|
+
trialDays: MEGARETRO_TRIAL_DAYS_PAID_MONTHLY,
|
|
253
|
+
},
|
|
254
|
+
{ planCode: 'business', code: 'business_yearly', amountMinor: 2_870_400, interval: 'year' },
|
|
255
|
+
{
|
|
256
|
+
planCode: 'enterprise',
|
|
257
|
+
code: 'enterprise_monthly_from',
|
|
258
|
+
amountMinor: 2_700_000,
|
|
259
|
+
interval: 'month',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
planCode: 'enterprise',
|
|
263
|
+
code: 'enterprise_yearly_from',
|
|
264
|
+
amountMinor: 25_920_000,
|
|
265
|
+
interval: 'year',
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
/** 490 ₽ × 12 × 0,8 в копейках */
|
|
269
|
+
const BUSINESS_EXTRA_WORKSPACE_YEARLY_MINOR = 490 * 100 * 12 * 0.8;
|
|
270
|
+
function buildPlanFeatureRequestBody(f, featureId, b) {
|
|
271
|
+
const body = { featureId };
|
|
272
|
+
if (f.kind === 'boolean') {
|
|
273
|
+
body.enabled = b.enabled ?? true;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
body.enabled = true;
|
|
277
|
+
if (b.limit !== undefined) {
|
|
278
|
+
body.limitValue = b.limit;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return body;
|
|
282
|
+
}
|
|
283
|
+
/** Создаёт отсутствующие фичи проекта по канону MegaRetro (идемпотентно). */
|
|
284
|
+
export async function ensureMegaretroProjectFeatures(billingFetch, projectId) {
|
|
285
|
+
const existing = (await billingFetch(`/v1/projects/${projectId}/features`));
|
|
286
|
+
const byCode = new Map(existing.map((x) => [x.code, x.id]));
|
|
287
|
+
const featureIds = {};
|
|
288
|
+
for (const f of FEATURE_DEFS) {
|
|
289
|
+
let id = byCode.get(f.code);
|
|
290
|
+
if (!id) {
|
|
291
|
+
const row = (await billingFetch(`/v1/projects/${projectId}/features`, {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
body: JSON.stringify({ code: f.code, name: f.name, kind: f.kind }),
|
|
294
|
+
}));
|
|
295
|
+
id = row.id;
|
|
296
|
+
byCode.set(f.code, id);
|
|
297
|
+
}
|
|
298
|
+
featureIds[f.code] = id;
|
|
299
|
+
}
|
|
300
|
+
return featureIds;
|
|
301
|
+
}
|
|
302
|
+
/** Привязки фич к планам по BINDINGS; существующие пары plan+feature пропускает. */
|
|
303
|
+
export async function applyMegaretroPlanFeatureBindings(billingFetch, projectId, planIds, featureIds) {
|
|
304
|
+
const items = [];
|
|
305
|
+
let created = 0;
|
|
306
|
+
let skipped = 0;
|
|
307
|
+
for (const planCode of PLAN_ORDER) {
|
|
308
|
+
const planId = planIds[planCode];
|
|
309
|
+
const existingList = (await billingFetch(`/v1/projects/${projectId}/plans/${planId}/plan-features`));
|
|
310
|
+
const have = new Set(existingList.map((x) => x.featureId));
|
|
311
|
+
for (const f of FEATURE_DEFS) {
|
|
312
|
+
const b = BINDINGS[f.code]?.[planCode];
|
|
313
|
+
if (!b) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const fid = featureIds[f.code];
|
|
317
|
+
if (have.has(fid)) {
|
|
318
|
+
skipped++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const body = buildPlanFeatureRequestBody(f, fid, b);
|
|
322
|
+
const pf = await billingFetch(`/v1/projects/${projectId}/plans/${planId}/plan-features`, { method: 'POST', body: JSON.stringify(body) });
|
|
323
|
+
items.push(pf);
|
|
324
|
+
have.add(fid);
|
|
325
|
+
created++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { items, created, skipped };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Без сброса каталога: убедиться, что есть планы free/team/business/enterprise,
|
|
332
|
+
* завести недостающие фичи и привязки с лимитами по pricing-and-capabilities.md.
|
|
333
|
+
*/
|
|
334
|
+
export async function upsertMegaretroFeaturesAndLimits(billingFetch, projectId) {
|
|
335
|
+
const plans = (await billingFetch(`/v1/projects/${projectId}/plans`));
|
|
336
|
+
const planIds = {};
|
|
337
|
+
for (const code of PLAN_ORDER) {
|
|
338
|
+
const p = plans.find((x) => x.code === code);
|
|
339
|
+
if (!p) {
|
|
340
|
+
throw new Error(`В проекте нет плана с code="${code}". Создайте планы или выполните billing_reset_and_seed_megaretro_catalog.`);
|
|
341
|
+
}
|
|
342
|
+
planIds[code] = p.id;
|
|
343
|
+
}
|
|
344
|
+
const featureIds = await ensureMegaretroProjectFeatures(billingFetch, projectId);
|
|
345
|
+
const { created, skipped } = await applyMegaretroPlanFeatureBindings(billingFetch, projectId, planIds, featureIds);
|
|
346
|
+
return {
|
|
347
|
+
sourceDoc: 'mega-retro/docs/product/pricing-and-capabilities.md',
|
|
348
|
+
planIds,
|
|
349
|
+
featureCodes: FEATURE_DEFS.map((x) => x.code),
|
|
350
|
+
featureIds,
|
|
351
|
+
planFeatureBindingsCreated: created,
|
|
352
|
+
planFeatureBindingsSkipped: skipped,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
export async function seedMegaretroCatalog(billingFetch, projectId) {
|
|
356
|
+
await billingFetch(`/v1/dev/projects/${projectId}/reset-catalog`, { method: 'POST' });
|
|
357
|
+
const planIds = {};
|
|
358
|
+
for (const p of PLAN_DEFS) {
|
|
359
|
+
const res = (await billingFetch(`/v1/projects/${projectId}/plans`, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
body: JSON.stringify(p),
|
|
362
|
+
}));
|
|
363
|
+
planIds[p.code] = res.id;
|
|
364
|
+
}
|
|
365
|
+
const createdPrices = [];
|
|
366
|
+
for (const pr of PRICE_DEFS) {
|
|
367
|
+
const payload = {
|
|
368
|
+
planId: planIds[pr.planCode],
|
|
369
|
+
code: pr.code,
|
|
370
|
+
amountMinor: pr.amountMinor,
|
|
371
|
+
currency: 'RUB',
|
|
372
|
+
interval: pr.interval,
|
|
373
|
+
intervalCount: 1,
|
|
374
|
+
isActive: true,
|
|
375
|
+
};
|
|
376
|
+
if (pr.trialDays != null) {
|
|
377
|
+
payload.trialDays = pr.trialDays;
|
|
378
|
+
}
|
|
379
|
+
const r = await billingFetch(`/v1/projects/${projectId}/prices`, {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
body: JSON.stringify(payload),
|
|
382
|
+
});
|
|
383
|
+
createdPrices.push(r);
|
|
384
|
+
}
|
|
385
|
+
const extraMonthly = (await billingFetch(`/v1/projects/${projectId}/prices`, {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
body: JSON.stringify({
|
|
388
|
+
planId: planIds.business,
|
|
389
|
+
code: 'business_extra_workspace_monthly',
|
|
390
|
+
amountMinor: 490_000,
|
|
391
|
+
currency: 'RUB',
|
|
392
|
+
interval: 'month',
|
|
393
|
+
intervalCount: 1,
|
|
394
|
+
isActive: true,
|
|
395
|
+
}),
|
|
396
|
+
}));
|
|
397
|
+
const extraYearly = (await billingFetch(`/v1/projects/${projectId}/prices`, {
|
|
398
|
+
method: 'POST',
|
|
399
|
+
body: JSON.stringify({
|
|
400
|
+
planId: planIds.business,
|
|
401
|
+
code: 'business_extra_workspace_yearly',
|
|
402
|
+
amountMinor: BUSINESS_EXTRA_WORKSPACE_YEARLY_MINOR,
|
|
403
|
+
currency: 'RUB',
|
|
404
|
+
interval: 'year',
|
|
405
|
+
intervalCount: 1,
|
|
406
|
+
isActive: true,
|
|
407
|
+
}),
|
|
408
|
+
}));
|
|
409
|
+
const featureIds = await ensureMegaretroProjectFeatures(billingFetch, projectId);
|
|
410
|
+
const { items: planFeatures, created: pfCreated, skipped: pfSkipped } = await applyMegaretroPlanFeatureBindings(billingFetch, projectId, planIds, featureIds);
|
|
411
|
+
const addonMonthly = await billingFetch(`/v1/admin/projects/${projectId}/addons`, {
|
|
412
|
+
method: 'POST',
|
|
413
|
+
body: JSON.stringify({
|
|
414
|
+
code: 'business_extra_workspace',
|
|
415
|
+
name: 'Дополнительная команда (workspace)',
|
|
416
|
+
description: '+490 ₽/мес за каждую команду сверх 5 (Business). pricing-and-capabilities.md',
|
|
417
|
+
priceId: extraMonthly.id,
|
|
418
|
+
billingType: 'recurring',
|
|
419
|
+
compatiblePlanIds: [planIds.business],
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
const addonYearly = await billingFetch(`/v1/admin/projects/${projectId}/addons`, {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
code: 'business_extra_workspace_yearly',
|
|
426
|
+
name: 'Дополнительная команда — годовая',
|
|
427
|
+
description: 'Годовая оплата за одну доп. команду: 12×490₽×0,8',
|
|
428
|
+
priceId: extraYearly.id,
|
|
429
|
+
billingType: 'recurring',
|
|
430
|
+
compatiblePlanIds: [planIds.business],
|
|
431
|
+
}),
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
sourceDoc: 'mega-retro/docs/product/pricing-and-capabilities.md',
|
|
435
|
+
plans: planIds,
|
|
436
|
+
prices: createdPrices,
|
|
437
|
+
extraWorkspacePrices: { monthly: extraMonthly, yearly: extraYearly },
|
|
438
|
+
addons: { addonMonthly, addonYearly },
|
|
439
|
+
featureCount: FEATURE_DEFS.length,
|
|
440
|
+
planFeatureBindingsCreated: pfCreated,
|
|
441
|
+
planFeatureBindingsSkipped: pfSkipped,
|
|
442
|
+
planFeatures,
|
|
443
|
+
};
|
|
444
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openaisdk/billing-mcp",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "MCP server: управление планами и ценами Billing API (catalog)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"billing-catalog-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"dev": "tsx index.ts",
|
|
19
|
+
"lint": "eslint .",
|
|
20
|
+
"clean": "rm -rf dist"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
24
|
+
"dotenv": "^16.4.7"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@saas-billing/eslint-config": "workspace:*",
|
|
28
|
+
"@saas-billing/typescript-config": "workspace:*",
|
|
29
|
+
"@types/node": "^20",
|
|
30
|
+
"tsx": "^4.21.0",
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"mcp",
|
|
35
|
+
"mcp-server",
|
|
36
|
+
"model-context-protocol",
|
|
37
|
+
"billing",
|
|
38
|
+
"saas-billing",
|
|
39
|
+
"catalog",
|
|
40
|
+
"cursor"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "Anatoliy Tukov <openaisdk@gmail.com>"
|
|
44
|
+
}
|