@nestcorehq/mcp-server 1.6.0 → 1.8.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/.mcp.json +2 -2
- package/README.md +510 -501
- package/dist/sdk/endpoints.d.ts +7 -0
- package/dist/sdk/endpoints.d.ts.map +1 -1
- package/dist/sdk/endpoints.js +8 -0
- package/dist/sdk/endpoints.js.map +1 -1
- package/dist/sdk/nestcore.client.d.ts +28 -1
- package/dist/sdk/nestcore.client.d.ts.map +1 -1
- package/dist/sdk/nestcore.client.js +55 -3
- package/dist/sdk/nestcore.client.js.map +1 -1
- package/dist/tools/branches.tools.d.ts +10 -0
- package/dist/tools/branches.tools.d.ts.map +1 -0
- package/dist/tools/branches.tools.js +114 -0
- package/dist/tools/branches.tools.js.map +1 -0
- package/dist/tools/credits.tools.d.ts +9 -0
- package/dist/tools/credits.tools.d.ts.map +1 -0
- package/dist/tools/credits.tools.js +63 -0
- package/dist/tools/credits.tools.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/project.tools.d.ts.map +1 -1
- package/dist/tools/project.tools.js +60 -9
- package/dist/tools/project.tools.js.map +1 -1
- package/dist/utils/errors.d.ts +10 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +34 -2
- package/dist/utils/errors.js.map +1 -1
- package/package.json +54 -54
- package/src/sdk/endpoints.ts +9 -0
- package/src/sdk/nestcore.client.ts +65 -4
- package/src/tools/branches.tools.ts +124 -0
- package/src/tools/credits.tools.ts +68 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/project.tools.ts +64 -9
- package/src/utils/errors.ts +34 -2
|
@@ -157,7 +157,24 @@ export class NestCoreClient {
|
|
|
157
157
|
'are limited to the user who created the project. Browsing and row edits work for every workspace member; ' +
|
|
158
158
|
'ask the project owner to run this operation.';
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
const body = errorBody as Record<string, unknown>;
|
|
161
|
+
const code = typeof body.code === 'string' ? body.code : undefined;
|
|
162
|
+
if (code === 'AGENT_PROJECT_RESTRICTED') {
|
|
163
|
+
text += '. This is an Agent project (one PC running NestCore Agent); it only holds the agent schema, agent-* buckets and the agent functions. Use an App project for app data.';
|
|
164
|
+
} else if (code === 'PROJECT_PAUSED') {
|
|
165
|
+
text += '. The project is paused (plan limit or owner): reads work in Studio, everything else waits until it is resumed or the plan is upgraded.';
|
|
166
|
+
} else if (code === 'CREDITS_EXHAUSTED') {
|
|
167
|
+
text += body.reason === 'spend_cap'
|
|
168
|
+
? ". The workspace's monthly AI credit cap is reached: raise or clear it (nestcore_set_credit_cap), add the project's own provider key, or wait for next month."
|
|
169
|
+
: '. The workspace has no AI credit left: buy a pack (nestcore_buy_credits gives a checkout link for a human to pay) or add the project\'s own provider key.';
|
|
170
|
+
if (typeof body.buyUrl === 'string') text += ` Buy credit: ${body.buyUrl}`;
|
|
171
|
+
text += ' Do not retry the call until one of these is done.';
|
|
172
|
+
} else if (code === 'QUOTA_EXCEEDED' && (body.reason === 'ai_collections' || body.quota === 'ai_collections')) {
|
|
173
|
+
text += '. The project has as many AI collections as its plan allows (Free 3, Starter 20, Pro 100): delete one or upgrade the plan.';
|
|
174
|
+
if (typeof body.upgradeUrl === 'string') text += ` Upgrade: ${body.upgradeUrl}`;
|
|
175
|
+
}
|
|
176
|
+
// Keep the API's own code and structured fields (e.g. QUOTA_EXCEEDED's kind/reason/upgradeUrl).
|
|
177
|
+
throw new ApiError(text, response.status, code, body);
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
/**
|
|
@@ -307,18 +324,62 @@ export class NestCoreClient {
|
|
|
307
324
|
|
|
308
325
|
// ─── Projects ───
|
|
309
326
|
|
|
310
|
-
async createProject(data: {
|
|
327
|
+
async createProject(data: {
|
|
328
|
+
name: string;
|
|
329
|
+
description?: string;
|
|
330
|
+
kind?: 'app' | 'agent';
|
|
331
|
+
agent?: { deviceName: string; os: string; agentVersion?: string; createdByClient?: string };
|
|
332
|
+
}) {
|
|
311
333
|
return this.request<Record<string, unknown>>('POST', EP.PROJECTS.CREATE, data);
|
|
312
334
|
}
|
|
313
335
|
|
|
314
|
-
async listProjects() {
|
|
315
|
-
return this.request<unknown[]>('GET', EP.PROJECTS.LIST);
|
|
336
|
+
async listProjects(kind?: 'app' | 'agent') {
|
|
337
|
+
return this.request<unknown[]>('GET', EP.PROJECTS.LIST, undefined, kind ? { query: { kind } } : undefined);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Can this workspace create one more project of a kind? Carries the upgrade link when not. */
|
|
341
|
+
async getProjectEligibility(kind: 'app' | 'agent' = 'app') {
|
|
342
|
+
return this.request<Record<string, unknown>>('GET', EP.PROJECTS.ELIGIBILITY, undefined, { query: { kind } });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Pause a project, or resume it (resuming is checked against the plan). */
|
|
346
|
+
async setProjectStatus(projectId: string, status: 'active' | 'paused') {
|
|
347
|
+
return this.request<Record<string, unknown>>('POST', EP.PROJECTS.STATUS(projectId), { status });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Workspace AI credit ───
|
|
351
|
+
|
|
352
|
+
/** Balance, monthly cap, this month's spend, availability, packs, margin and the recent ledger. */
|
|
353
|
+
async getCredits() {
|
|
354
|
+
return this.request<Record<string, unknown>>('GET', EP.BILLING.CREDITS);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** A Stripe Checkout URL (one-time payment) for a credit pack; nothing is charged until a human pays it. */
|
|
358
|
+
async createCreditCheckout(pack: string, ref?: string) {
|
|
359
|
+
return this.request<{ url: string; pack: Record<string, unknown>; ref: string | null }>(
|
|
360
|
+
'POST', EP.BILLING.CREDITS_CHECKOUT, ref ? { pack, ref } : { pack },
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Set the workspace's monthly cap on credit spend (USD), or clear it with null. Returns the new summary. */
|
|
365
|
+
async setCreditCap(monthlyCapUsd: number | null) {
|
|
366
|
+
return this.request<Record<string, unknown>>('PUT', EP.BILLING.CREDITS_CAP, { monthlyCapUsd });
|
|
316
367
|
}
|
|
317
368
|
|
|
318
369
|
async getProject(projectId: string) {
|
|
319
370
|
return this.request<Record<string, unknown>>('GET', EP.PROJECTS.GET(projectId));
|
|
320
371
|
}
|
|
321
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Project branches (Production + Development). `projectId` may be the
|
|
375
|
+
* Production project or any of its branches; `sub` is appended to
|
|
376
|
+
* `/projects/:id/branches` (e.g. `/development/promote`).
|
|
377
|
+
*/
|
|
378
|
+
async branches<T = Record<string, unknown>>(method: 'GET' | 'POST' | 'DELETE', projectId: string, sub = '', body?: unknown): Promise<T> {
|
|
379
|
+
const res = await this.request<{ data?: T } & Record<string, unknown>>(method, `/projects/${encodeURIComponent(projectId)}/branches${sub}`, body);
|
|
380
|
+
return (res && 'data' in res ? res.data : res) as T;
|
|
381
|
+
}
|
|
382
|
+
|
|
322
383
|
async updateProject(projectId: string, data: Record<string, unknown>) {
|
|
323
384
|
return this.request<Record<string, unknown>>('PATCH', EP.PROJECTS.UPDATE(projectId), data);
|
|
324
385
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ToolContext } from './index.js';
|
|
4
|
+
import { formatErrorResponse } from '../utils/errors.js';
|
|
5
|
+
import { requireProjectId } from '../utils/validators.js';
|
|
6
|
+
|
|
7
|
+
const json = (data: unknown) => ({ content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] });
|
|
8
|
+
|
|
9
|
+
type Branch = { id: string; branch: 'production' | 'development'; slug: string; provisioning_status: string; pending_changes: number };
|
|
10
|
+
type BranchList = { enabled: boolean; production_id: string; branches: Branch[] };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Project branches. Every App project has Production (the live project, its
|
|
14
|
+
* existing id) and Development (its own project id, URL, keys and database:
|
|
15
|
+
* Production's schema and config, none of its data). Work on Development by
|
|
16
|
+
* making its id the active project; promote reviewed changes to Production.
|
|
17
|
+
*/
|
|
18
|
+
export function registerBranchTools(server: McpServer, ctx: ToolContext): void {
|
|
19
|
+
const projectOf = (projectId?: string) => requireProjectId(projectId, ctx.session.getSession().currentProjectId);
|
|
20
|
+
|
|
21
|
+
server.registerTool('nestcore_list_branches', {
|
|
22
|
+
description:
|
|
23
|
+
"A project's branches: Production (the live app) and Development (safe copy: same schema and config, its own data, users, URL and keys). " +
|
|
24
|
+
'Returns each branch id, slug, setup status, database/file size and the number of schema changes waiting to be promoted. ' +
|
|
25
|
+
'To work on Development, call nestcore_set_active_project with the development branch id: every other tool then targets it. ' +
|
|
26
|
+
'Prefer Development for schema experiments; never test destructive SQL on Production.',
|
|
27
|
+
inputSchema: { projectId: z.string().optional().describe('Project or branch id (default: active project)') },
|
|
28
|
+
}, async ({ projectId }) => {
|
|
29
|
+
try {
|
|
30
|
+
await ctx.session.ensureAuthenticated();
|
|
31
|
+
return json(await ctx.apiClient.branches<BranchList>('GET', projectOf(projectId)));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return formatErrorResponse(error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.registerTool('nestcore_create_branch', {
|
|
38
|
+
description:
|
|
39
|
+
'Create the Development branch of a project if it has none (idempotent). Copies schema and config from Production (tables, RLS, functions, ' +
|
|
40
|
+
'buckets, auth settings, env vars with their values); never copies rows, end users or files. Jobs and webhooks start disabled. Takes a few seconds.',
|
|
41
|
+
inputSchema: { projectId: z.string().optional() },
|
|
42
|
+
}, async ({ projectId }) => {
|
|
43
|
+
try {
|
|
44
|
+
await ctx.session.ensureAuthenticated();
|
|
45
|
+
return json(await ctx.apiClient.branches('POST', projectOf(projectId), '/development'));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return formatErrorResponse(error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
server.registerTool('nestcore_reset_branch', {
|
|
52
|
+
description:
|
|
53
|
+
"Rebuild Development from Production: drops Development's database and re-clones Production's schema; ALL Development rows, end users and files " +
|
|
54
|
+
'are deleted and unpromoted changes discarded. URL, keys, env vars and functions are kept. Production is not touched. ' +
|
|
55
|
+
'Destructive for Development: only with the user\'s explicit request. Pass confirm: true.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
projectId: z.string().optional(),
|
|
58
|
+
confirm: z.literal(true).describe('Must be true: reset deletes all Development data'),
|
|
59
|
+
},
|
|
60
|
+
}, async ({ projectId }) => {
|
|
61
|
+
try {
|
|
62
|
+
await ctx.session.ensureAuthenticated();
|
|
63
|
+
const res = await ctx.apiClient.branches<Branch>('POST', projectOf(projectId), '/development/reset', { confirm: true });
|
|
64
|
+
ctx.schemaService.invalidate(res.id);
|
|
65
|
+
return json(res);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return formatErrorResponse(error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
server.registerTool('nestcore_branch_changes', {
|
|
72
|
+
description:
|
|
73
|
+
'Schema changes recorded on Development and not yet promoted (DDL from the SQL editor, table editor, RLS, realtime, CLI or MCP), in order. ' +
|
|
74
|
+
'`destructive: true` marks DROP / RENAME / type changes / RLS off. Data changes (INSERT/UPDATE/DELETE) are never recorded or promoted.',
|
|
75
|
+
inputSchema: { projectId: z.string().optional() },
|
|
76
|
+
}, async ({ projectId }) => {
|
|
77
|
+
try {
|
|
78
|
+
await ctx.session.ensureAuthenticated();
|
|
79
|
+
return json(await ctx.apiClient.branches('GET', projectOf(projectId), '/development/changes'));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return formatErrorResponse(error);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
server.registerTool('nestcore_promote_branch', {
|
|
86
|
+
description:
|
|
87
|
+
'Promote Development to Production. Without apply: true it only PREVIEWS (pending schema changes, functions, buckets, jobs, env vars missing on ' +
|
|
88
|
+
'Production, auth-setting differences, and whether Production drifted). With apply: true (workspace admins) the schema changes run on Production in ONE ' +
|
|
89
|
+
'transaction (any failure rolls all back), then functions, new buckets and new jobs (created disabled) are copied. Env vars and auth settings are only ' +
|
|
90
|
+
'copied when asked. Production data is never touched. ALWAYS show the preview to the user and get their explicit go-ahead before apply: true; ' +
|
|
91
|
+
'destructive changes also need confirmDestructive and drift needs acknowledgeDrift, both only with the user\'s say-so.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
projectId: z.string().optional(),
|
|
94
|
+
apply: z.boolean().optional().describe('false/omitted = preview only'),
|
|
95
|
+
changeIds: z.array(z.string()).optional().describe('Subset of pending change ids (default: all)'),
|
|
96
|
+
includeEnvVars: z.boolean().optional().describe('Also add env vars missing on Production (Development values)'),
|
|
97
|
+
includeAuthSettings: z.boolean().optional().describe("Replace Production's auth settings with Development's"),
|
|
98
|
+
confirmDestructive: z.boolean().optional(),
|
|
99
|
+
acknowledgeDrift: z.boolean().optional(),
|
|
100
|
+
},
|
|
101
|
+
}, async ({ projectId, apply, changeIds, includeEnvVars, includeAuthSettings, confirmDestructive, acknowledgeDrift }) => {
|
|
102
|
+
try {
|
|
103
|
+
await ctx.session.ensureAuthenticated();
|
|
104
|
+
const id = projectOf(projectId);
|
|
105
|
+
const preview = await ctx.apiClient.branches<any>('GET', id, '/development/promote');
|
|
106
|
+
if (!apply) return json({ preview: true, ...preview });
|
|
107
|
+
const cfg = preview.config ?? {};
|
|
108
|
+
const result = await ctx.apiClient.branches<any>('POST', id, '/development/promote', {
|
|
109
|
+
change_ids: changeIds ?? (preview.changes ?? []).map((c: { id: string }) => c.id),
|
|
110
|
+
functions: (cfg.functions ?? []).map((f: { name: string }) => f.name),
|
|
111
|
+
buckets: (cfg.buckets ?? []).map((b: { name: string }) => b.name),
|
|
112
|
+
jobs: (cfg.jobs ?? []).filter((j: { status: string }) => j.status === 'new').map((j: { name: string }) => j.name),
|
|
113
|
+
env_vars: includeEnvVars ? (cfg.env_vars ?? []).map((e: { name: string }) => e.name) : [],
|
|
114
|
+
auth_settings: !!includeAuthSettings,
|
|
115
|
+
confirm_destructive: !!confirmDestructive,
|
|
116
|
+
acknowledge_drift: !!acknowledgeDrift,
|
|
117
|
+
});
|
|
118
|
+
ctx.schemaService.invalidate(preview.production_id);
|
|
119
|
+
return json(result);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return formatErrorResponse(error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ToolContext } from './index.js';
|
|
4
|
+
import { formatErrorResponse } from '../utils/errors.js';
|
|
5
|
+
|
|
6
|
+
const json = (data: unknown) => ({ content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] });
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Workspace AI credit (prepaid). A project's own provider key always wins; without one,
|
|
10
|
+
* the workspace's credit pays for AI calls at the platform's key plus the margin.
|
|
11
|
+
* Credit is per workspace, shared by all its projects.
|
|
12
|
+
*/
|
|
13
|
+
export function registerCreditTools(server: McpServer, ctx: ToolContext): void {
|
|
14
|
+
server.registerTool('nestcore_get_credits', {
|
|
15
|
+
description:
|
|
16
|
+
"The workspace's AI credit: balance (USD), monthly cap (null = none), this month's spend, available (credit can pay for a call now), " +
|
|
17
|
+
'active (credit was ever bought; a workspace that never bought credit is never charged), margin %, the credit packs for sale and the recent ledger. ' +
|
|
18
|
+
"Credit only pays for AI calls of projects without their own provider key.",
|
|
19
|
+
inputSchema: {},
|
|
20
|
+
}, async () => {
|
|
21
|
+
try {
|
|
22
|
+
await ctx.session.ensureAuthenticated();
|
|
23
|
+
return json(await ctx.apiClient.getCredits());
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return formatErrorResponse(error);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
server.registerTool('nestcore_set_credit_cap', {
|
|
30
|
+
description:
|
|
31
|
+
"Set the workspace's monthly cap on AI credit spend in USD (0 to 100000), or pass null to remove the cap. " +
|
|
32
|
+
'Calls paid from credit fail with CREDITS_EXHAUSTED (reason spend_cap) once the month reaches it. Needs the workspace admin role. Returns the new summary.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
monthlyCapUsd: z.number().min(0).max(100_000).nullable().describe('USD per month, or null for no cap'),
|
|
35
|
+
},
|
|
36
|
+
}, async ({ monthlyCapUsd }) => {
|
|
37
|
+
try {
|
|
38
|
+
await ctx.session.ensureAuthenticated();
|
|
39
|
+
return json(await ctx.apiClient.setCreditCap(monthlyCapUsd));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return formatErrorResponse(error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
server.registerTool('nestcore_buy_credits', {
|
|
46
|
+
description:
|
|
47
|
+
'Create a Stripe Checkout link for an AI credit pack (one-time payment) for a HUMAN to open and pay in a browser. ' +
|
|
48
|
+
'This does NOT charge anything or add credit by itself: the credit lands only after the person pays and Stripe confirms. ' +
|
|
49
|
+
'Never tell the user the purchase is done; give them the URL and check nestcore_get_credits afterwards. ' +
|
|
50
|
+
'Pack ids come from nestcore_get_credits (e.g. credits_5, credits_10, credits_25). Needs the workspace admin role.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
pack: z.string().regex(/^[a-z0-9_-]{1,40}$/).describe('Credit pack id from nestcore_get_credits'),
|
|
53
|
+
},
|
|
54
|
+
}, async ({ pack }) => {
|
|
55
|
+
try {
|
|
56
|
+
await ctx.session.ensureAuthenticated();
|
|
57
|
+
const res = await ctx.apiClient.createCreditCheckout(pack, 'mcp');
|
|
58
|
+
return json({
|
|
59
|
+
checkoutUrl: res.url,
|
|
60
|
+
pack: res.pack,
|
|
61
|
+
paid: false,
|
|
62
|
+
next: 'Give this URL to the user to open and pay in a browser. No credit is added until the payment completes; nestcore_get_credits shows the new balance then.',
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return formatErrorResponse(error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -27,11 +27,13 @@ import { registerLoggingTools } from './logging.tools.js';
|
|
|
27
27
|
import { registerAdminTools } from './admin.tools.js';
|
|
28
28
|
import { registerSmtpTools } from './smtp.tools.js';
|
|
29
29
|
import { registerIntegrationTools } from './integrations.tools.js';
|
|
30
|
+
import { registerCreditTools } from './credits.tools.js';
|
|
30
31
|
import { registerDashboardTools } from './dashboard.tools.js';
|
|
31
32
|
import { registerBackgroundJobsTools } from './background-jobs.tools.js';
|
|
32
33
|
import { registerRealtimeTools } from './realtime.tools.js';
|
|
33
34
|
import { registerAnalyticsTools } from './analytics.tools.js';
|
|
34
35
|
import { registerMigrationTools } from './migration.tools.js';
|
|
36
|
+
import { registerBranchTools } from './branches.tools.js';
|
|
35
37
|
import { registerProjectIntelligenceTools } from './project-intelligence.tools.js';
|
|
36
38
|
import { registerProjectIntelligencePrompts } from '../prompts/project-intelligence.prompts.js';
|
|
37
39
|
|
|
@@ -47,10 +49,12 @@ export function registerAllTools(server: McpServer, ctx: ToolContext): void {
|
|
|
47
49
|
registerAdminTools(server, ctx);
|
|
48
50
|
registerSmtpTools(server, ctx);
|
|
49
51
|
registerIntegrationTools(server, ctx);
|
|
52
|
+
registerCreditTools(server, ctx);
|
|
50
53
|
registerDashboardTools(server, ctx);
|
|
51
54
|
registerRealtimeTools(server, ctx);
|
|
52
55
|
registerAnalyticsTools(server, ctx);
|
|
53
56
|
registerMigrationTools(server, ctx);
|
|
57
|
+
registerBranchTools(server, ctx);
|
|
54
58
|
registerProjectIntelligenceTools(server, ctx);
|
|
55
59
|
registerProjectIntelligencePrompts(server, ctx);
|
|
56
60
|
}
|
|
@@ -1,19 +1,53 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { ToolContext } from './index.js';
|
|
4
|
-
import { formatErrorResponse } from '../utils/errors.js';
|
|
4
|
+
import { formatErrorResponse, ValidationError } from '../utils/errors.js';
|
|
5
5
|
import { requireProjectId } from '../utils/validators.js';
|
|
6
6
|
|
|
7
7
|
export function registerProjectTools(server: McpServer, ctx: ToolContext): void {
|
|
8
8
|
|
|
9
9
|
// ─── Create Project ───
|
|
10
10
|
server.registerTool('nestcore_create_project', {
|
|
11
|
-
description:
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
description:
|
|
12
|
+
'Create a new NestCore project with a dedicated database. Returns project details including API keys. ' +
|
|
13
|
+
'kind "app" (default) is a backend for an app; kind "agent" is the private backend of ONE PC running NestCore Agent ' +
|
|
14
|
+
'(needs agent.deviceName and agent.os) and only ever holds the agent schema, agent-* buckets and the agent functions. ' +
|
|
15
|
+
'The kind cannot be changed later. Check nestcore_get_project_eligibility first: on the free plan the one project is an app OR an agent.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
name: z.string().min(3).max(120),
|
|
18
|
+
description: z.string().max(500).optional(),
|
|
19
|
+
kind: z.enum(['app', 'agent']).optional(),
|
|
20
|
+
agent: z.object({
|
|
21
|
+
deviceName: z.string().min(1).max(80),
|
|
22
|
+
os: z.string().min(1).max(40),
|
|
23
|
+
agentVersion: z.string().max(32).optional(),
|
|
24
|
+
}).optional(),
|
|
25
|
+
},
|
|
26
|
+
}, async ({ name, description, kind, agent }) => {
|
|
27
|
+
try {
|
|
28
|
+
await ctx.session.ensureAuthenticated();
|
|
29
|
+
if (kind === 'agent' && !agent) {
|
|
30
|
+
throw new ValidationError('An agent project needs agent: { deviceName, os } (the PC it belongs to).');
|
|
31
|
+
}
|
|
32
|
+
const result = await ctx.apiClient.createProject({ name, description, kind, ...(kind === 'agent' && agent ? { agent } : {}) });
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
35
|
+
};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return formatErrorResponse(error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ─── Project Eligibility ───
|
|
42
|
+
server.registerTool('nestcore_get_project_eligibility', {
|
|
43
|
+
description:
|
|
44
|
+
'Can this workspace create one more project of a kind ("app" or "agent")? Returns { allowed, plan, used, limit, reason?, upgradeUrl }. ' +
|
|
45
|
+
'When allowed is false, give the user the upgradeUrl instead of trying to create the project.',
|
|
46
|
+
inputSchema: { kind: z.enum(['app', 'agent']).optional() },
|
|
47
|
+
}, async ({ kind }) => {
|
|
14
48
|
try {
|
|
15
49
|
await ctx.session.ensureAuthenticated();
|
|
16
|
-
const result = await ctx.apiClient.
|
|
50
|
+
const result = await ctx.apiClient.getProjectEligibility(kind ?? 'app');
|
|
17
51
|
return {
|
|
18
52
|
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
19
53
|
};
|
|
@@ -24,12 +58,33 @@ export function registerProjectTools(server: McpServer, ctx: ToolContext): void
|
|
|
24
58
|
|
|
25
59
|
// ─── List Projects ───
|
|
26
60
|
server.registerTool('nestcore_list_projects', {
|
|
27
|
-
description:
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
description:
|
|
62
|
+
'List all projects for the authenticated admin user. Each project has kind ("app" | "agent") and status ' +
|
|
63
|
+
'("active" | "paused" | "compromised"; paused projects reject writes and data-plane calls with 423 PROJECT_PAUSED). Optionally filter by kind.',
|
|
64
|
+
inputSchema: { kind: z.enum(['app', 'agent']).optional() },
|
|
65
|
+
}, async ({ kind }) => {
|
|
66
|
+
try {
|
|
67
|
+
await ctx.session.ensureAuthenticated();
|
|
68
|
+
const result = await ctx.apiClient.listProjects(kind);
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return formatErrorResponse(error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ─── Pause / Resume Project ───
|
|
78
|
+
server.registerTool('nestcore_set_project_status', {
|
|
79
|
+
description:
|
|
80
|
+
'Pause a project (API, realtime and relay go off; nothing is deleted) or resume it. Resuming counts against the plan\'s project limit ' +
|
|
81
|
+
'and returns QUOTA_EXCEEDED with an upgradeUrl when the plan is full. A "compromised" project (kill switch) cannot be resumed here.',
|
|
82
|
+
inputSchema: { projectId: z.string().optional(), status: z.enum(['active', 'paused']) },
|
|
83
|
+
}, async ({ projectId, status }) => {
|
|
30
84
|
try {
|
|
31
85
|
await ctx.session.ensureAuthenticated();
|
|
32
|
-
const
|
|
86
|
+
const id = requireProjectId(projectId, ctx.session.getSession().currentProjectId);
|
|
87
|
+
const result = await ctx.apiClient.setProjectStatus(id, status);
|
|
33
88
|
return {
|
|
34
89
|
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
35
90
|
};
|
package/src/utils/errors.ts
CHANGED
|
@@ -34,14 +34,30 @@ export class SafetyError extends McpError {
|
|
|
34
34
|
|
|
35
35
|
export class ApiError extends McpError {
|
|
36
36
|
public readonly statusCode: number;
|
|
37
|
+
/**
|
|
38
|
+
* The API's error body (QUOTA_EXCEEDED carries quota/plan/limit/usage/kind/reason/upgradeUrl;
|
|
39
|
+
* CREDITS_EXHAUSTED carries reason/balanceUsd/monthlyCapUsd/spentThisMonthUsd/buyUrl).
|
|
40
|
+
*/
|
|
41
|
+
public readonly details?: Record<string, unknown>;
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
/**
|
|
44
|
+
* `apiCode` is the platform's own code when it sent one, e.g. QUOTA_EXCEEDED,
|
|
45
|
+
* CREDITS_EXHAUSTED, AGENT_PROJECT_RESTRICTED, NOT_AN_AGENT_PROJECT or PROJECT_PAUSED.
|
|
46
|
+
*/
|
|
47
|
+
constructor(message: string, statusCode: number, apiCode?: string, details?: Record<string, unknown>) {
|
|
48
|
+
super(message, apiCode ?? 'API_ERROR');
|
|
40
49
|
this.name = 'ApiError';
|
|
41
50
|
this.statusCode = statusCode;
|
|
51
|
+
this.details = details;
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
/** Fields of an API error body worth handing to the model (never the raw body). */
|
|
56
|
+
const API_DETAIL_FIELDS = [
|
|
57
|
+
'quota', 'plan', 'limit', 'usage', 'kind', 'reason', 'upgradeUrl', 'status',
|
|
58
|
+
'balanceUsd', 'monthlyCapUsd', 'spentThisMonthUsd', 'buyUrl',
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
45
61
|
export class ProjectContextError extends McpError {
|
|
46
62
|
constructor(message: string = 'No active project. Use nestcore_set_active_project first or provide projectId.') {
|
|
47
63
|
super(message, 'PROJECT_CONTEXT_ERROR');
|
|
@@ -63,6 +79,22 @@ export function formatErrorResponse(error: unknown): { content: Array<{ type: 't
|
|
|
63
79
|
};
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
if (error instanceof ApiError) {
|
|
83
|
+
const details: Record<string, unknown> = {};
|
|
84
|
+
for (const k of API_DETAIL_FIELDS) if (error.details?.[k] !== undefined) details[k] = error.details[k];
|
|
85
|
+
return {
|
|
86
|
+
content: [{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: JSON.stringify({
|
|
89
|
+
error: error.message,
|
|
90
|
+
code: error.code,
|
|
91
|
+
statusCode: error.statusCode,
|
|
92
|
+
...(Object.keys(details).length ? { details } : {}),
|
|
93
|
+
}, null, 2),
|
|
94
|
+
}],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
if (error instanceof McpError) {
|
|
67
99
|
return {
|
|
68
100
|
content: [{
|