@launchframe/mcp 1.0.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/dist/content/auth/overview.md +64 -0
- package/dist/content/content/auth/overview.md +64 -0
- package/dist/content/content/credits/deduction.md +27 -0
- package/dist/content/content/credits/strategies.md +25 -0
- package/dist/content/content/crons/pattern.md +51 -0
- package/dist/content/content/entities/conventions.md +97 -0
- package/dist/content/content/env/conventions.md +123 -0
- package/dist/content/content/feature-gates/overview.md +74 -0
- package/dist/content/content/modules/structure.md +44 -0
- package/dist/content/content/queues/names.md +18 -0
- package/dist/content/content/variants/overview.md +67 -0
- package/dist/content/content/webhooks/architecture.md +53 -0
- package/dist/content/credits/deduction.md +27 -0
- package/dist/content/credits/strategies.md +25 -0
- package/dist/content/crons/pattern.md +51 -0
- package/dist/content/entities/conventions.md +97 -0
- package/dist/content/env/conventions.md +123 -0
- package/dist/content/feature-gates/overview.md +74 -0
- package/dist/content/modules/structure.md +44 -0
- package/dist/content/queues/names.md +18 -0
- package/dist/content/variants/overview.md +67 -0
- package/dist/content/webhooks/architecture.md +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/lib/content.d.ts +5 -0
- package/dist/lib/content.js +11 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +27 -0
- package/dist/tools/auth.d.ts +2 -0
- package/dist/tools/auth.js +108 -0
- package/dist/tools/credits.d.ts +2 -0
- package/dist/tools/credits.js +43 -0
- package/dist/tools/crons.d.ts +2 -0
- package/dist/tools/crons.js +76 -0
- package/dist/tools/entities.d.ts +2 -0
- package/dist/tools/entities.js +94 -0
- package/dist/tools/env.d.ts +2 -0
- package/dist/tools/env.js +6 -0
- package/dist/tools/feature-gates.d.ts +2 -0
- package/dist/tools/feature-gates.js +59 -0
- package/dist/tools/modules.d.ts +2 -0
- package/dist/tools/modules.js +144 -0
- package/dist/tools/queues.d.ts +2 -0
- package/dist/tools/queues.js +81 -0
- package/dist/tools/variants.d.ts +2 -0
- package/dist/tools/variants.js +6 -0
- package/dist/tools/webhooks.d.ts +2 -0
- package/dist/tools/webhooks.js +121 -0
- package/package.json +23 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { registerAuthTools } from './tools/auth.js';
|
|
3
|
+
import { registerFeatureGatesTools } from './tools/feature-gates.js';
|
|
4
|
+
import { registerCreditsTools } from './tools/credits.js';
|
|
5
|
+
import { registerQueueTools } from './tools/queues.js';
|
|
6
|
+
import { registerWebhookTools } from './tools/webhooks.js';
|
|
7
|
+
import { registerCronTools } from './tools/crons.js';
|
|
8
|
+
import { registerModuleTools } from './tools/modules.js';
|
|
9
|
+
import { registerEntityTools } from './tools/entities.js';
|
|
10
|
+
import { registerEnvTools } from './tools/env.js';
|
|
11
|
+
import { registerVariantTools } from './tools/variants.js';
|
|
12
|
+
// Phase 2: import { registerCliTools } from './tools/cli.js';
|
|
13
|
+
export function createServer() {
|
|
14
|
+
const server = new McpServer({ name: 'launchframe-mcp', version: '1.0.0' });
|
|
15
|
+
registerAuthTools(server);
|
|
16
|
+
registerFeatureGatesTools(server);
|
|
17
|
+
registerCreditsTools(server);
|
|
18
|
+
registerQueueTools(server);
|
|
19
|
+
registerWebhookTools(server);
|
|
20
|
+
registerCronTools(server);
|
|
21
|
+
registerModuleTools(server);
|
|
22
|
+
registerEntityTools(server);
|
|
23
|
+
registerEnvTools(server);
|
|
24
|
+
registerVariantTools(server);
|
|
25
|
+
// Phase 2: registerCliTools(server);
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadContent } from '../lib/content.js';
|
|
3
|
+
export function registerAuthTools(server) {
|
|
4
|
+
server.tool('auth_get_overview', 'Get full auth system overview: guard hierarchy, session flow, Better Auth setup, roles, and decorator system.', {}, async () => ({
|
|
5
|
+
content: [{ type: 'text', text: loadContent('auth/overview.md') }],
|
|
6
|
+
}));
|
|
7
|
+
server.tool('auth_get_decorator_usage', 'Get the exact decorator and import for a specific auth need.', {
|
|
8
|
+
need: z.enum([
|
|
9
|
+
'public',
|
|
10
|
+
'optional',
|
|
11
|
+
'customer_portal',
|
|
12
|
+
'admin_only',
|
|
13
|
+
'current_user',
|
|
14
|
+
'full_session',
|
|
15
|
+
]).describe('The auth need for the route'),
|
|
16
|
+
}, async ({ need }) => {
|
|
17
|
+
const map = {
|
|
18
|
+
public: `// No authentication required
|
|
19
|
+
import { AllowAnonymous } from '../auth/auth.decorator';
|
|
20
|
+
|
|
21
|
+
@AllowAnonymous()
|
|
22
|
+
@Get('route')
|
|
23
|
+
handler() { ... }`,
|
|
24
|
+
optional: `// Auth is checked but not required — user may be undefined
|
|
25
|
+
import { OptionalAuth, UserSession } from '../auth/auth.decorator';
|
|
26
|
+
import { User } from '../users/user.entity';
|
|
27
|
+
|
|
28
|
+
@OptionalAuth()
|
|
29
|
+
@Get('route')
|
|
30
|
+
handler(@UserSession() user?: User) { ... }`,
|
|
31
|
+
customer_portal: `// Accessible by regular_user role (B2B2C variant only)
|
|
32
|
+
// Without this decorator, regular_user gets 401
|
|
33
|
+
import { CustomerPortal, UserSession } from '../auth/auth.decorator';
|
|
34
|
+
import { User } from '../users/user.entity';
|
|
35
|
+
|
|
36
|
+
@CustomerPortal()
|
|
37
|
+
@Get('route')
|
|
38
|
+
handler(@UserSession() user: User) { ... }`,
|
|
39
|
+
admin_only: `// Admin-only access: superadmin role only.
|
|
40
|
+
// Admin routes are separated by the /admin/* prefix with AdminGuard (applied globally via RouterModule).
|
|
41
|
+
// For a superadmin check on a non-admin route, guard manually in the handler:
|
|
42
|
+
import { UserSession } from '../auth/auth.decorator';
|
|
43
|
+
import { User } from '../users/user.entity';
|
|
44
|
+
import { ForbiddenException } from '@nestjs/common';
|
|
45
|
+
|
|
46
|
+
@Get('route')
|
|
47
|
+
handler(@UserSession() user: User) {
|
|
48
|
+
if (user.role !== 'superadmin') throw new ForbiddenException();
|
|
49
|
+
...
|
|
50
|
+
}`,
|
|
51
|
+
current_user: `// Inject the current authenticated user
|
|
52
|
+
import { UserSession } from '../auth/auth.decorator';
|
|
53
|
+
import { User } from '../users/user.entity';
|
|
54
|
+
|
|
55
|
+
@Get('route')
|
|
56
|
+
handler(@UserSession() user: User) {
|
|
57
|
+
// user.id, user.email, user.role, etc.
|
|
58
|
+
}`,
|
|
59
|
+
full_session: `// Inject the full Better Auth session (user + session metadata)
|
|
60
|
+
import { Session } from '../auth/auth.decorator';
|
|
61
|
+
|
|
62
|
+
@Get('route')
|
|
63
|
+
handler(@Session() session: { user: any; session: any }) {
|
|
64
|
+
const { user, session: sessionData } = session;
|
|
65
|
+
}`,
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: map[need] ?? `Unknown need: ${need}` }],
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
server.tool('auth_get_guard_usage', 'Get the guard class, import path, and decorator combo for a specific guard type.', {
|
|
72
|
+
guard: z.enum(['admin', 'business_user', 'credits']).describe('The guard to look up'),
|
|
73
|
+
}, async ({ guard }) => {
|
|
74
|
+
const map = {
|
|
75
|
+
admin: `// AdminGuard — applied globally to all /admin/* routes via RouterModule.
|
|
76
|
+
// Source: src/modules/admin/guards/admin.guard.ts
|
|
77
|
+
// You do NOT need to add @UseGuards(AdminGuard) manually on admin controllers.
|
|
78
|
+
// The admin router config registers it globally for the admin route prefix.
|
|
79
|
+
// Checking inside: verifies user.role === 'superadmin'`,
|
|
80
|
+
business_user: `// BetterAuthGuard — the global default guard (all routes).
|
|
81
|
+
// Source: src/modules/auth/better-auth.guard.ts
|
|
82
|
+
// Applied globally in app.module.ts as APP_GUARD.
|
|
83
|
+
// Allows: business_user, superadmin
|
|
84
|
+
// Blocks: unauthenticated, regular_user (unless @CustomerPortal())
|
|
85
|
+
// You never need to add this manually.`,
|
|
86
|
+
credits: `// CreditsGuard — deducts credits per request based on @DeductCredits(n).
|
|
87
|
+
// Source: src/modules/credits/guards/credits.guard.ts
|
|
88
|
+
// MUST be combined with @DeductCredits(n) decorator.
|
|
89
|
+
import { UseGuards } from '@nestjs/common';
|
|
90
|
+
import { CreditsGuard } from '../credits/guards/credits.guard';
|
|
91
|
+
import { DeductCredits } from '../credits/decorators/deduct-credits.decorator';
|
|
92
|
+
|
|
93
|
+
@DeductCredits(10)
|
|
94
|
+
@UseGuards(CreditsGuard)
|
|
95
|
+
@Post('ai-operation')
|
|
96
|
+
handler() { ... }
|
|
97
|
+
|
|
98
|
+
// Strategy behaviour (read from AdminSettings, cached 24h in Redis):
|
|
99
|
+
// - free: bypass (no deduction)
|
|
100
|
+
// - subscription: bypass (use feature gates instead)
|
|
101
|
+
// - credits: deduct from balance
|
|
102
|
+
// - hybrid: deduct from monthly allowance first, then Polar overage`,
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: map[guard] ?? `Unknown guard: ${guard}` }],
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadContent } from '../lib/content.js';
|
|
3
|
+
export function registerCreditsTools(server) {
|
|
4
|
+
server.tool('credits_get_deduction_pattern', 'Get the decorator + guard pattern for deducting credits on a route.', {}, async () => ({
|
|
5
|
+
content: [{ type: 'text', text: loadContent('credits/deduction.md') }],
|
|
6
|
+
}));
|
|
7
|
+
server.tool('credits_get_add_pattern', 'Get the code snippet for programmatically adding credits to a user with a specific transaction type.', {
|
|
8
|
+
transactionType: z
|
|
9
|
+
.enum(['INITIAL', 'PURCHASE', 'USAGE', 'REFUND', 'BONUS', 'EXPIRY', 'REDEMPTION'])
|
|
10
|
+
.describe('The CreditTransactionType to use'),
|
|
11
|
+
}, async ({ transactionType }) => {
|
|
12
|
+
const descriptions = {
|
|
13
|
+
INITIAL: 'Grant starting credits to a new user',
|
|
14
|
+
PURCHASE: 'Credit top-up purchased by the user',
|
|
15
|
+
USAGE: 'Manual usage deduction (prefer CreditsGuard for route-level deduction)',
|
|
16
|
+
REFUND: 'Refund credits after a failed or cancelled operation',
|
|
17
|
+
BONUS: 'Promotional or reward credits',
|
|
18
|
+
EXPIRY: 'Expire/remove unused credits',
|
|
19
|
+
REDEMPTION: 'Redeem credits (e.g. voucher, referral)',
|
|
20
|
+
};
|
|
21
|
+
const snippet = `// ${descriptions[transactionType]}
|
|
22
|
+
import { CreditsService } from '../credits/credits.service';
|
|
23
|
+
import { CreditTransactionType } from '../credits/entities/credit-transaction.entity';
|
|
24
|
+
|
|
25
|
+
// Inject in constructor:
|
|
26
|
+
constructor(private readonly creditsService: CreditsService) {}
|
|
27
|
+
|
|
28
|
+
// Call:
|
|
29
|
+
await this.creditsService.addCredits(
|
|
30
|
+
user, // User entity
|
|
31
|
+
100, // amount (positive to add, negative to deduct)
|
|
32
|
+
CreditTransactionType.${transactionType},
|
|
33
|
+
'Optional description', // optional
|
|
34
|
+
'optional-ref-id', // optional — external reference (e.g. Polar order ID)
|
|
35
|
+
);`;
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: snippet }],
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
server.tool('credits_get_monetization_strategies', 'Get an overview of all monetization strategies (free, subscription, credits, hybrid) and when to use each.', {}, async () => ({
|
|
41
|
+
content: [{ type: 'text', text: loadContent('credits/strategies.md') }],
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadContent } from '../lib/content.js';
|
|
3
|
+
const CRON_EXPRESSIONS = [
|
|
4
|
+
'EVERY_MINUTE',
|
|
5
|
+
'EVERY_30_MINUTES',
|
|
6
|
+
'EVERY_HOUR',
|
|
7
|
+
'EVERY_DAY_AT_MIDNIGHT',
|
|
8
|
+
'EVERY_WEEK',
|
|
9
|
+
];
|
|
10
|
+
export function registerCronTools(server) {
|
|
11
|
+
server.tool('cron_get_pattern', 'Get the LaunchFrame cron job pattern: where jobs live, available CronExpression presets, and module registration rules.', {}, async () => ({
|
|
12
|
+
content: [{ type: 'text', text: loadContent('crons/pattern.md') }],
|
|
13
|
+
}));
|
|
14
|
+
server.tool('cron_scaffold_job', 'Scaffold a new cron method to add to CronService (src/jobs/cron.service.ts).', {
|
|
15
|
+
methodName: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('The name of the cron method, camelCase (e.g. "syncUserStats")'),
|
|
18
|
+
schedule: z
|
|
19
|
+
.enum(CRON_EXPRESSIONS)
|
|
20
|
+
.describe('The CronExpression preset to use'),
|
|
21
|
+
queueName: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Optional Bull queue name to enqueue work into (e.g. "api"). Omit for jobs that do lightweight direct work.'),
|
|
25
|
+
}, async ({ methodName, schedule, queueName }) => {
|
|
26
|
+
const queueInjection = queueName
|
|
27
|
+
? `\n @InjectQueue('${queueName}') private readonly ${toCamelCase(queueName)}Queue: Queue,`
|
|
28
|
+
: '';
|
|
29
|
+
const queueImports = queueName
|
|
30
|
+
? `\nimport { InjectQueue } from '@nestjs/bull';\nimport { Queue } from 'bull';`
|
|
31
|
+
: '';
|
|
32
|
+
const methodBody = queueName
|
|
33
|
+
? ` this.logger.log('Starting ${methodName}...');
|
|
34
|
+
try {
|
|
35
|
+
// TODO: fetch items to process
|
|
36
|
+
// const items = await this.repo.find({ where: { ... } });
|
|
37
|
+
// for (const item of items) {
|
|
38
|
+
// await this.${toCamelCase(queueName)}Queue.add({ id: item.id }, {
|
|
39
|
+
// attempts: 3,
|
|
40
|
+
// backoff: { type: 'exponential', delay: 2000 },
|
|
41
|
+
// });
|
|
42
|
+
// }
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.logger.error('Error in ${methodName}:', error);
|
|
45
|
+
}`
|
|
46
|
+
: ` this.logger.log('Starting ${methodName}...');
|
|
47
|
+
try {
|
|
48
|
+
// TODO: implement job logic
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.logger.error('Error in ${methodName}:', error);
|
|
51
|
+
}`;
|
|
52
|
+
const snippet = `// Add this method to src/jobs/cron.service.ts
|
|
53
|
+
// ─── Additional imports needed ───
|
|
54
|
+
import { Cron, CronExpression } from '@nestjs/schedule';${queueImports}
|
|
55
|
+
|
|
56
|
+
// ─── Add to CronService constructor params ───
|
|
57
|
+
constructor(
|
|
58
|
+
// ... existing params ...${queueInjection}
|
|
59
|
+
) {}
|
|
60
|
+
|
|
61
|
+
// ─── New cron method ───
|
|
62
|
+
@Cron(CronExpression.${schedule})
|
|
63
|
+
async ${methodName}() {
|
|
64
|
+
${methodBody}
|
|
65
|
+
}`;
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text: snippet }],
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function toCamelCase(str) {
|
|
72
|
+
return str
|
|
73
|
+
.split('-')
|
|
74
|
+
.map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
75
|
+
.join('');
|
|
76
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadContent } from '../lib/content.js';
|
|
3
|
+
export function registerEntityTools(server) {
|
|
4
|
+
server.tool('entity_get_conventions', 'Get TypeORM entity conventions for LaunchFrame: required decorators, naming strategy, column types, relations, and multi-tenancy.', {}, async () => ({
|
|
5
|
+
content: [{ type: 'text', text: loadContent('entities/conventions.md') }],
|
|
6
|
+
}));
|
|
7
|
+
server.tool('entity_scaffold_typeorm', 'Generate a TypeORM entity file following LaunchFrame conventions.', {
|
|
8
|
+
entityName: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe('Entity class name in PascalCase (e.g. "FeedbackEntry", "AiSummary")'),
|
|
11
|
+
tableName: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe('Database table name in snake_case (e.g. "feedback_entries", "ai_summaries")'),
|
|
14
|
+
primaryKeyType: z
|
|
15
|
+
.enum(['int', 'uuid'])
|
|
16
|
+
.default('int')
|
|
17
|
+
.describe('Primary key type: "int" (auto-increment) or "uuid"'),
|
|
18
|
+
multiTenant: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.default(false)
|
|
21
|
+
.describe('Add projectId column for multi-tenant variant'),
|
|
22
|
+
withEnum: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('Optional: define a status enum. Provide enum name in PascalCase (e.g. "EntryStatus"). Values will be a placeholder — edit as needed.'),
|
|
26
|
+
}, async ({ entityName, tableName, primaryKeyType, multiTenant, withEnum, }) => {
|
|
27
|
+
const pkDeclarator = primaryKeyType === 'uuid'
|
|
28
|
+
? `@PrimaryGeneratedColumn('uuid')\n id: string;`
|
|
29
|
+
: `@PrimaryGeneratedColumn()\n id: number;`;
|
|
30
|
+
const enumBlock = withEnum
|
|
31
|
+
? `export enum ${withEnum} {
|
|
32
|
+
ACTIVE = 'active',
|
|
33
|
+
INACTIVE = 'inactive',
|
|
34
|
+
// TODO: add values
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
`
|
|
38
|
+
: '';
|
|
39
|
+
const enumImportEntry = withEnum ? `, Column` : `, Column`;
|
|
40
|
+
const multiTenantBlock = multiTenant
|
|
41
|
+
? ` // MULTI_TENANT_FIELDS_START
|
|
42
|
+
@Column() projectId: number;
|
|
43
|
+
// MULTI_TENANT_FIELDS_END
|
|
44
|
+
|
|
45
|
+
`
|
|
46
|
+
: ` // MULTI_TENANT_FIELDS_START
|
|
47
|
+
// @Column() projectId: number;
|
|
48
|
+
// MULTI_TENANT_FIELDS_END
|
|
49
|
+
|
|
50
|
+
`;
|
|
51
|
+
const enumColumnBlock = withEnum
|
|
52
|
+
? `
|
|
53
|
+
@Column({ type: 'enum', enum: ${withEnum} })
|
|
54
|
+
status: ${withEnum};
|
|
55
|
+
`
|
|
56
|
+
: '';
|
|
57
|
+
const imports = [
|
|
58
|
+
'Entity',
|
|
59
|
+
'PrimaryGeneratedColumn',
|
|
60
|
+
'Column',
|
|
61
|
+
'CreateDateColumn',
|
|
62
|
+
'UpdateDateColumn',
|
|
63
|
+
];
|
|
64
|
+
const entityFile = `import { ${imports.join(', ')} } from 'typeorm';
|
|
65
|
+
|
|
66
|
+
${enumBlock}@Entity('${tableName}')
|
|
67
|
+
export class ${entityName} {
|
|
68
|
+
${pkDeclarator}
|
|
69
|
+
|
|
70
|
+
${multiTenantBlock} // TODO: add domain columns here
|
|
71
|
+
// @Column() name: string;
|
|
72
|
+
// @Column({ type: 'text', nullable: true }) description: string;
|
|
73
|
+
${enumColumnBlock}
|
|
74
|
+
@CreateDateColumn()
|
|
75
|
+
createdAt: Date;
|
|
76
|
+
|
|
77
|
+
@UpdateDateColumn()
|
|
78
|
+
updatedAt: Date;
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
const registerNote = `// === Register in your module ===
|
|
82
|
+
// In src/modules/<domain>/<domain>.module.ts:
|
|
83
|
+
// imports: [TypeOrmModule.forFeature([${entityName}])]
|
|
84
|
+
`;
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: `// === entities/${tableName}.entity.ts ===\n${entityFile}\n${registerNote}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { loadContent } from '../lib/content.js';
|
|
2
|
+
export function registerEnvTools(server) {
|
|
3
|
+
server.tool('env_get_conventions', 'Get environment variable conventions for LaunchFrame: single centralized .env location, variable naming rules, full key variable reference, and how to add new variables.', {}, async () => ({
|
|
4
|
+
content: [{ type: 'text', text: loadContent('env/conventions.md') }],
|
|
5
|
+
}));
|
|
6
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadContent } from '../lib/content.js';
|
|
3
|
+
export function registerFeatureGatesTools(server) {
|
|
4
|
+
server.tool('feature_gates_get_overview', 'Get the feature gate system overview: how features are stored, how to query them, and the check pattern.', {}, async () => ({
|
|
5
|
+
content: [{ type: 'text', text: loadContent('feature-gates/overview.md') }],
|
|
6
|
+
}));
|
|
7
|
+
server.tool('feature_gates_get_check_pattern', 'Get a copy-paste TypeScript snippet for checking a feature gate by code and type.', {
|
|
8
|
+
featureCode: z.string().describe('The feature code to check (as defined in the database)'),
|
|
9
|
+
featureType: z.enum(['boolean', 'numeric']).describe('Whether to generate a boolean or numeric (with limit) check'),
|
|
10
|
+
}, async ({ featureCode, featureType }) => {
|
|
11
|
+
const label = featureCode.replace(/_/g, ' ');
|
|
12
|
+
const camel = featureCode.split('_').map(w => w[0].toUpperCase() + w.slice(1)).join('');
|
|
13
|
+
const checkSnippet = featureType === 'boolean'
|
|
14
|
+
? `const features = await this.userSubscriptionService.getCurrentFeatures(userId);
|
|
15
|
+
|
|
16
|
+
const hasAccess = features['${featureCode}'] === true;
|
|
17
|
+
if (!hasAccess) {
|
|
18
|
+
throw new ForbiddenException('Your plan does not include ${label}');
|
|
19
|
+
}`
|
|
20
|
+
: `const features = await this.userSubscriptionService.getCurrentFeatures(userId);
|
|
21
|
+
|
|
22
|
+
const limit = features['${featureCode}'] as number ?? 0;
|
|
23
|
+
const isUnlimited = limit === -1;
|
|
24
|
+
const currentCount = await this.get${camel}Count(userId);
|
|
25
|
+
if (!isUnlimited && currentCount >= limit) {
|
|
26
|
+
throw new ForbiddenException(\`Plan limit reached for ${label} (\${limit})\`);
|
|
27
|
+
}`;
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: 'text',
|
|
31
|
+
text: `# Feature Gate Check: \`${featureCode}\` (${featureType})
|
|
32
|
+
|
|
33
|
+
## Imports
|
|
34
|
+
|
|
35
|
+
\`\`\`typescript
|
|
36
|
+
import { Injectable, ForbiddenException } from '@nestjs/common';
|
|
37
|
+
import { UserSubscriptionService } from '../subscriptions/services/user-subscription.service';
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
## Constructor Injection
|
|
41
|
+
|
|
42
|
+
\`\`\`typescript
|
|
43
|
+
constructor(
|
|
44
|
+
private readonly userSubscriptionService: UserSubscriptionService,
|
|
45
|
+
) {}
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
## Check
|
|
49
|
+
|
|
50
|
+
\`\`\`typescript
|
|
51
|
+
${checkSnippet}
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
> Feature code \`${featureCode}\` must exist in the \`subscription_plan_features\` table and have values configured per plan.
|
|
55
|
+
`,
|
|
56
|
+
}],
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadContent } from '../lib/content.js';
|
|
3
|
+
export function registerModuleTools(server) {
|
|
4
|
+
server.tool('module_get_structure', 'Get the NestJS module folder structure, conventions, and rules used in LaunchFrame.', {}, async () => ({
|
|
5
|
+
content: [{ type: 'text', text: loadContent('modules/structure.md') }],
|
|
6
|
+
}));
|
|
7
|
+
server.tool('module_scaffold_nestjs', 'Generate a NestJS module scaffold (module + service + optional controller + optional entity) following LaunchFrame conventions.', {
|
|
8
|
+
moduleName: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe('Domain name in kebab-case (e.g. "projects", "ai-summaries")'),
|
|
11
|
+
withController: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.default(true)
|
|
14
|
+
.describe('Include a controller with basic CRUD routes'),
|
|
15
|
+
withEntity: z
|
|
16
|
+
.boolean()
|
|
17
|
+
.default(true)
|
|
18
|
+
.describe('Include a TypeORM entity and register it via TypeOrmModule.forFeature'),
|
|
19
|
+
}, async ({ moduleName, withController, withEntity, }) => {
|
|
20
|
+
const pascal = toPascalCase(moduleName);
|
|
21
|
+
const camel = toCamelCase(moduleName);
|
|
22
|
+
const snake = toSnakeCase(moduleName);
|
|
23
|
+
const entityImport = withEntity
|
|
24
|
+
? `import { TypeOrmModule } from '@nestjs/typeorm';\nimport { ${pascal} } from './entities/${moduleName}.entity';`
|
|
25
|
+
: '';
|
|
26
|
+
const entityFeature = withEntity
|
|
27
|
+
? `\n TypeOrmModule.forFeature([${pascal}]),`
|
|
28
|
+
: '';
|
|
29
|
+
const controllerImport = withController
|
|
30
|
+
? `import { ${pascal}Controller } from './${moduleName}.controller';`
|
|
31
|
+
: '';
|
|
32
|
+
const controllerDecl = withController ? `\n controllers: [${pascal}Controller],` : '';
|
|
33
|
+
const moduleFile = `import { Module } from '@nestjs/common';
|
|
34
|
+
${entityImport}
|
|
35
|
+
import { ${pascal}Service } from './${moduleName}.service';
|
|
36
|
+
${controllerImport}
|
|
37
|
+
|
|
38
|
+
@Module({
|
|
39
|
+
imports: [${entityFeature}
|
|
40
|
+
// MULTI_TENANT_MODULE_IMPORTS_START
|
|
41
|
+
// MULTI_TENANT_MODULE_IMPORTS_END
|
|
42
|
+
],
|
|
43
|
+
providers: [
|
|
44
|
+
${pascal}Service,
|
|
45
|
+
// MULTI_TENANT_PROVIDERS_START
|
|
46
|
+
// MULTI_TENANT_PROVIDERS_END
|
|
47
|
+
],${controllerDecl}
|
|
48
|
+
exports: [${pascal}Service],
|
|
49
|
+
})
|
|
50
|
+
export class ${pascal}Module {}`;
|
|
51
|
+
const serviceFile = `import { Injectable } from '@nestjs/common';
|
|
52
|
+
${withEntity ? `import { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { ${pascal} } from './entities/${moduleName}.entity';` : ''}
|
|
53
|
+
|
|
54
|
+
@Injectable()
|
|
55
|
+
export class ${pascal}Service {
|
|
56
|
+
${withEntity ? ` constructor(
|
|
57
|
+
@InjectRepository(${pascal})
|
|
58
|
+
private readonly ${camel}Repository: Repository<${pascal}>,
|
|
59
|
+
) {}` : ''}
|
|
60
|
+
// TODO: implement service methods
|
|
61
|
+
}`;
|
|
62
|
+
const controllerFile = withController
|
|
63
|
+
? `import { Controller, Get, Post, Body, Param, Delete, Put } from '@nestjs/common';
|
|
64
|
+
import { ${pascal}Service } from './${moduleName}.service';
|
|
65
|
+
import { CurrentUser } from '@/modules/auth/auth.decorator';
|
|
66
|
+
import { User } from '@/modules/users/user.entity';
|
|
67
|
+
|
|
68
|
+
@Controller('${moduleName}')
|
|
69
|
+
export class ${pascal}Controller {
|
|
70
|
+
constructor(private readonly ${camel}Service: ${pascal}Service) {}
|
|
71
|
+
|
|
72
|
+
@Get()
|
|
73
|
+
findAll(@CurrentUser() user: User) {
|
|
74
|
+
// TODO: implement
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Get(':id')
|
|
78
|
+
findOne(@Param('id') id: string, @CurrentUser() user: User) {
|
|
79
|
+
// TODO: implement
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Post()
|
|
83
|
+
create(@Body() body: Record<string, unknown>, @CurrentUser() user: User) {
|
|
84
|
+
// TODO: implement
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Put(':id')
|
|
88
|
+
update(@Param('id') id: string, @Body() body: Record<string, unknown>, @CurrentUser() user: User) {
|
|
89
|
+
// TODO: implement
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Delete(':id')
|
|
93
|
+
remove(@Param('id') id: string, @CurrentUser() user: User) {
|
|
94
|
+
// TODO: implement
|
|
95
|
+
}
|
|
96
|
+
}`
|
|
97
|
+
: null;
|
|
98
|
+
const entityFile = withEntity
|
|
99
|
+
? `import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
|
100
|
+
|
|
101
|
+
@Entity('${snake}')
|
|
102
|
+
export class ${pascal} {
|
|
103
|
+
@PrimaryGeneratedColumn()
|
|
104
|
+
id: number;
|
|
105
|
+
|
|
106
|
+
// MULTI_TENANT_FIELDS_START
|
|
107
|
+
// @Column() projectId: number;
|
|
108
|
+
// MULTI_TENANT_FIELDS_END
|
|
109
|
+
|
|
110
|
+
// TODO: add domain columns here
|
|
111
|
+
|
|
112
|
+
@CreateDateColumn()
|
|
113
|
+
createdAt: Date;
|
|
114
|
+
|
|
115
|
+
@UpdateDateColumn()
|
|
116
|
+
updatedAt: Date;
|
|
117
|
+
}`
|
|
118
|
+
: null;
|
|
119
|
+
const sections = [
|
|
120
|
+
`// === ${moduleName}.module.ts ===\n${moduleFile}`,
|
|
121
|
+
`\n// === ${moduleName}.service.ts ===\n${serviceFile}`,
|
|
122
|
+
];
|
|
123
|
+
if (controllerFile)
|
|
124
|
+
sections.push(`\n// === ${moduleName}.controller.ts ===\n${controllerFile}`);
|
|
125
|
+
if (entityFile)
|
|
126
|
+
sections.push(`\n// === entities/${moduleName}.entity.ts ===\n${entityFile}`);
|
|
127
|
+
sections.push(`
|
|
128
|
+
// === Register in app.module.ts ===
|
|
129
|
+
// Add ${pascal}Module to the imports array in src/modules/app/app.module.ts`);
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: 'text', text: sections.join('\n') }],
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function toCamelCase(str) {
|
|
136
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
137
|
+
}
|
|
138
|
+
function toPascalCase(str) {
|
|
139
|
+
const camel = toCamelCase(str);
|
|
140
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
141
|
+
}
|
|
142
|
+
function toSnakeCase(str) {
|
|
143
|
+
return str.replace(/-/g, '_');
|
|
144
|
+
}
|