express-genix 3.0.1 → 4.1.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 CHANGED
@@ -42,6 +42,13 @@ A production-grade CLI tool that generates Express.js applications with best-in-
42
42
  - Prometheus metrics (`/metrics` endpoint with prom-client)
43
43
  - Audit logging middleware with request tracking and sensitive field redaction
44
44
 
45
+ **AI & Agent Integration**
46
+ - MCP Server — exposes your API as tools for AI agents (Claude Desktop, Cursor, Copilot)
47
+ - LangChain / LangGraph AI service with chat, streaming (SSE), prompt chains, and ReAct agent
48
+ - Provider-agnostic: OpenAI, Anthropic, Google Gemini, Ollama (local)
49
+ - AI-powered CLI: `express-genix ai "describe your app"` generates a project from natural language
50
+ - **Coda VS Code extension** — in-editor AI chat for Express Genix projects (auto-prompted on AI/MCP projects)
51
+
45
52
  **Developer Experience**
46
53
  - Interactive prompts — pick language, database, features via checkbox
47
54
  - Winston or Pino logger (you choose)
@@ -84,7 +91,7 @@ You'll be prompted for:
84
91
  1. **Project name**
85
92
  2. **Language** — JavaScript or TypeScript
86
93
  3. **Database** — MongoDB, PostgreSQL (Sequelize), PostgreSQL (Prisma), or None
87
- 4. **Features** — Auth, Rate Limiting, Swagger, Redis, Docker, CI/CD, WebSocket, Request ID, Email, File Uploads, Soft Deletes, Audit Logging, Prometheus Metrics, API Versioning, Background Jobs, GraphQL
94
+ 4. **Features** — Auth, Rate Limiting, Swagger, Redis, Docker, CI/CD, WebSocket, Request ID, Email, File Uploads, Soft Deletes, Audit Logging, Prometheus Metrics, API Versioning, Background Jobs, GraphQL, MCP Server, AI/LLM Service
88
95
  5. **Logger** — Winston or Pino
89
96
 
90
97
  The CLI generates your project, installs dependencies, formats code, and creates an initial git commit.
@@ -111,6 +118,16 @@ express-genix add websocket # Adds Socket.io setup
111
118
  express-genix add prisma # Adds Prisma schema, client config, migrations
112
119
  ```
113
120
 
121
+ ### Generate from natural language (AI)
122
+
123
+ Requires `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in your environment.
124
+
125
+ ```bash
126
+ express-genix ai "a task manager API with auth, real-time updates, and email notifications"
127
+ ```
128
+
129
+ The CLI interprets your description, maps it to features, shows the config for confirmation, then generates the project.
130
+
114
131
  ## Generated Project Structure
115
132
 
116
133
  ```
@@ -120,6 +137,8 @@ my-express-app/
120
137
  │ ├── controllers/ # Route handlers (auth, user, admin, example)
121
138
  │ ├── graphql/ # GraphQL type definitions & resolvers (if selected)
122
139
  │ ├── jobs/ # BullMQ background workers (if selected)
140
+ │ ├── mcp/ # MCP server for AI agent tools (if selected)
141
+ │ ├── agents/ # LangGraph ReAct agent (if selected)
123
142
  │ ├── middleware/ # Auth, RBAC, validation, error handling, uploads, metrics, audit log
124
143
  │ ├── models/ # Database models (Mongoose/Sequelize/Prisma)
125
144
  │ ├── routes/ # API route definitions with Swagger annotations
@@ -132,6 +151,7 @@ my-express-app/
132
151
  ├── migrations/ # Sequelize migrations (if PostgreSQL + Sequelize)
133
152
  ├── seeders/ # Sequelize seeders (if PostgreSQL + Sequelize)
134
153
  ├── uploads/ # File upload directory (if selected)
154
+ ├── mcp.json # MCP client config for Claude Desktop (if selected)
135
155
  ├── .github/workflows/ # CI/CD pipeline (if selected)
136
156
  ├── .env # Auto-generated environment config
137
157
  ├── .env.example # Template for team sharing
@@ -189,6 +209,14 @@ my-express-app/
189
209
  | GET | `/metrics` | Prometheus metrics (prom-client) |
190
210
  | GET | `/graphql` | GraphQL Playground (Apollo Server) |
191
211
 
212
+ ### AI / LLM (LangChain)
213
+ | Method | Endpoint | Description |
214
+ |--------|----------|-------------|
215
+ | POST | `/api/ai/chat` | Chat with AI (complete response) |
216
+ | POST | `/api/ai/stream` | Stream AI response (SSE) |
217
+ | POST | `/api/ai/chain` | Run a prompt template chain |
218
+ | POST | `/api/ai/agent` | Run LangGraph ReAct agent with tools |
219
+
192
220
  ## Available Scripts
193
221
 
194
222
  ```bash
@@ -212,6 +240,7 @@ npm run prisma:studio # Open Prisma Studio
212
240
  express-genix init --skip-install # Skip npm install (useful for CI)
213
241
  express-genix init --skip-cleanup # Skip auto-formatting (for debugging)
214
242
  express-genix add <feature> # Add feature to existing project
243
+ express-genix ai "<description>" # Generate project from natural language
215
244
  ```
216
245
 
217
246
  ## Environment Variables
@@ -237,6 +266,14 @@ Generated `.env` includes auto-generated JWT secrets:
237
266
  | `EMAIL_FROM` | Default sender address | — |
238
267
  | `UPLOAD_MAX_SIZE` | Max file upload size in bytes | `5242880` (5 MB) |
239
268
  | `UPLOAD_DIR` | Upload destination directory | `uploads` |
269
+ | `MCP_SERVER_NAME` | MCP server display name | project name |
270
+ | `AI_PROVIDER` | LLM provider (`openai`, `anthropic`, `ollama`) | `openai` |
271
+ | `AI_MODEL` | Model name | `gpt-4o-mini` |
272
+ | `AI_TEMPERATURE` | Sampling temperature | `0.7` |
273
+ | `AI_MAX_TOKENS` | Max response tokens | `2048` |
274
+ | `OPENAI_API_KEY` | OpenAI API key (if using OpenAI) | — |
275
+ | `ANTHROPIC_API_KEY` | Anthropic API key (if using Anthropic) | — |
276
+ | `OLLAMA_BASE_URL` | Ollama server URL (if using Ollama) | `http://localhost:11434` |
240
277
 
241
278
  ## Docker
242
279
 
@@ -246,6 +283,42 @@ docker-compose up --build
246
283
 
247
284
  The generated `docker-compose.yml` includes services for your app and its dependencies (MongoDB/PostgreSQL, Redis) with health checks, volumes, and a shared network. The `Dockerfile` uses multi-stage builds, runs as a non-root user, and only copies production dependencies.
248
285
 
286
+ ## Coda VS Code Extension
287
+
288
+ When you select **AI** or **MCP** features, the CLI prompts you to install the **Coda** VS Code extension — an in-editor AI assistant that talks to your running project's AI endpoints.
289
+
290
+ ### What it does
291
+
292
+ - **Sidebar chat** — a chat panel in the VS Code activity bar that streams responses from your project's `/ai/chat` and `/ai/stream` endpoints
293
+ - **`@coda` in Copilot Chat** — type `@coda` in the native Copilot Chat to interact with your project's AI agent
294
+ - **Agent mode** — toggle between Chat (streaming) and Agent (LangGraph tool-calling) modes
295
+ - **Auto-detection** — activates automatically when it detects an Express Genix project
296
+ - **Code actions** — copy or insert code blocks from AI responses directly into your editor
297
+
298
+ ### Install manually
299
+
300
+ ```bash
301
+ # Download from GitHub Releases
302
+ curl -sLO https://github.com/LambdaAI001/coda/releases/latest/download/coda-ai-0.1.0.vsix
303
+
304
+ # Install
305
+ code --install-extension coda-ai-0.1.0.vsix
306
+ ```
307
+
308
+ Or grab it from [github.com/LambdaAI001/coda/releases](https://github.com/LambdaAI001/coda/releases).
309
+
310
+ ### Configuration
311
+
312
+ In VS Code Settings (search "Coda"):
313
+
314
+ | Setting | Default | Description |
315
+ |---------|---------|-------------|
316
+ | `coda.serverUrl` | `http://localhost:3000` | Your Express server URL |
317
+ | `coda.apiPrefix` | `/api/v1` | API prefix path |
318
+ | `coda.aiProvider` | `openai` | AI provider (openai, anthropic, gemini, ollama) |
319
+ | `coda.model` | *(server default)* | Model override |
320
+ | `coda.systemPrompt` | *Coding assistant prompt* | Default system prompt |
321
+
249
322
  ## Contributing
250
323
 
251
324
  Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
package/index.js CHANGED
@@ -14,6 +14,50 @@ const prompt = inquirer.createPromptModule();
14
14
 
15
15
  const generateSecret = (length = 64) => crypto.randomBytes(length).toString('hex');
16
16
 
17
+ const CODA_EXTENSION_ID = 'express-genix.coda-ai';
18
+ const CODA_VSIX_URL = 'https://github.com/LambdaAI001/coda/releases/download/v0.1.0/coda-ai-0.1.0.vsix';
19
+
20
+ async function promptCodaExtension() {
21
+ const inquirerPrompt = inquirer.createPromptModule();
22
+ console.log('');
23
+ console.log('🤖 Your project includes AI/MCP features.');
24
+ console.log(' The Coda VS Code extension lets you chat with your AI agent');
25
+ console.log(' directly from VS Code — no extra setup needed.');
26
+ console.log('');
27
+
28
+ const { installCoda } = await inquirerPrompt([
29
+ {
30
+ type: 'confirm',
31
+ name: 'installCoda',
32
+ message: 'Install the Coda AI VS Code extension?',
33
+ default: true,
34
+ },
35
+ ]);
36
+
37
+ if (!installCoda) return;
38
+
39
+ try {
40
+ // Check if VS Code CLI is available
41
+ execSync('code --version', { stdio: 'pipe' });
42
+
43
+ // Download .vsix from GitHub Releases and install
44
+ const os = require('os');
45
+ const tmpFile = path.join(os.tmpdir(), 'coda-ai.vsix');
46
+ console.log('\n📦 Downloading Coda AI extension...');
47
+ execSync(`curl -sL -o "${tmpFile}" "${CODA_VSIX_URL}"`, { stdio: 'pipe' });
48
+ console.log('📦 Installing...');
49
+ execSync(`code --install-extension "${tmpFile}"`, { stdio: 'pipe' });
50
+ // Clean up temp file
51
+ try { fs.unlinkSync(tmpFile); } catch {}
52
+ console.log('✅ Coda extension installed! Open your project in VS Code to use it.');
53
+ console.log(' Look for the Coda AI icon in the sidebar, or type @coda in Copilot Chat.');
54
+ } catch {
55
+ console.log('\n⚠️ Could not install automatically. Install it manually:');
56
+ console.log(' 1. Download from: https://github.com/LambdaAI001/coda/releases');
57
+ console.log(' 2. Then run: code --install-extension coda-ai-0.1.0.vsix');
58
+ }
59
+ }
60
+
17
61
  async function main() {
18
62
  program
19
63
  .name('express-genix')
@@ -83,6 +127,8 @@ async function main() {
83
127
  { name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
84
128
  { name: 'Background Jobs (BullMQ)', value: 'backgroundJobs' },
85
129
  { name: 'GraphQL (Apollo Server)', value: 'graphql' },
130
+ { name: 'MCP Server (AI Agent Tools)', value: 'mcp' },
131
+ { name: 'AI / LLM Service (LangChain)', value: 'ai' },
86
132
  ],
87
133
  when: (ans) => ans.db !== 'none',
88
134
  },
@@ -102,6 +148,8 @@ async function main() {
102
148
  { name: 'Prometheus Metrics', value: 'metrics' },
103
149
  { name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
104
150
  { name: 'GraphQL (Apollo Server)', value: 'graphql' },
151
+ { name: 'MCP Server (AI Agent Tools)', value: 'mcp' },
152
+ { name: 'AI / LLM Service (LangChain)', value: 'ai' },
105
153
  ],
106
154
  when: (ans) => ans.db === 'none',
107
155
  },
@@ -142,6 +190,8 @@ async function main() {
142
190
  hasApiVersioning: features.includes('apiVersioning'),
143
191
  hasBackgroundJobs: features.includes('backgroundJobs'),
144
192
  hasGraphQL: features.includes('graphql'),
193
+ hasMCP: features.includes('mcp'),
194
+ hasAI: features.includes('ai'),
145
195
  jwtSecret: generateSecret(),
146
196
  jwtRefreshSecret: generateSecret(),
147
197
  };
@@ -184,7 +234,7 @@ async function main() {
184
234
  To get started:
185
235
  cd ${config.projectName}
186
236
  npm run dev
187
- ${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}${config.hasGraphQL ? `\nGraphQL Playground: http://localhost:3000/graphql` : ''}${config.hasMetrics ? `\nPrometheus Metrics: http://localhost:3000/metrics` : ''}
237
+ ${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}${config.hasGraphQL ? `\nGraphQL Playground: http://localhost:3000/graphql` : ''}${config.hasMetrics ? `\nPrometheus Metrics: http://localhost:3000/metrics` : ''}${config.hasMCP ? `\nMCP Server: npm run mcp:start` : ''}${config.hasAI ? `\nAI Endpoints: http://localhost:3000/api${config.hasApiVersioning ? '/v1' : ''}/ai/chat` : ''}
188
238
  Health Check: http://localhost:3000/health
189
239
 
190
240
  Available scripts:
@@ -203,6 +253,11 @@ Configuration:
203
253
  Features: ${features.join(', ') || 'base'}
204
254
  `);
205
255
 
256
+ // Prompt to install Coda VS Code extension for AI/MCP projects
257
+ if (config.hasAI || config.hasMCP) {
258
+ await promptCodaExtension();
259
+ }
260
+
206
261
  } catch (error) {
207
262
  // Rollback: remove partial project directory on failure
208
263
  if (fs.existsSync(projectDir)) {
@@ -245,6 +300,110 @@ Configuration:
245
300
  }
246
301
  });
247
302
 
303
+ program
304
+ .command('ai <description>')
305
+ .description('Generate a project from a natural language description (requires OPENAI_API_KEY or ANTHROPIC_API_KEY)')
306
+ .option('--skip-install', 'Skip npm install')
307
+ .option('--skip-cleanup', 'Skip post-generation cleanup')
308
+ .action(async (description, options) => {
309
+ const { runAiCommand } = require('./lib/ai-cli');
310
+ const aiConfig = await runAiCommand(description);
311
+
312
+ const inquirerPrompt = inquirer.createPromptModule();
313
+ const { confirmed } = await inquirerPrompt([
314
+ {
315
+ type: 'confirm',
316
+ name: 'confirmed',
317
+ message: 'Generate project with this configuration?',
318
+ default: true,
319
+ },
320
+ ]);
321
+
322
+ if (!confirmed) {
323
+ console.log('Cancelled.');
324
+ process.exit(0);
325
+ }
326
+
327
+ const features = aiConfig.features || [];
328
+ const config = {
329
+ projectName: aiConfig.projectName,
330
+ language: aiConfig.language || 'javascript',
331
+ db: aiConfig.db || 'none',
332
+ logger: aiConfig.logger || 'winston',
333
+ hasDatabase: aiConfig.db !== 'none',
334
+ isNoDatabase: aiConfig.db === 'none',
335
+ isPrisma: aiConfig.db === 'prisma',
336
+ isTypescript: aiConfig.language === 'typescript',
337
+ hasAuth: features.includes('auth') && aiConfig.db !== 'none',
338
+ hasRateLimit: features.includes('rateLimit'),
339
+ hasSwagger: features.includes('swagger'),
340
+ hasDocker: features.includes('docker'),
341
+ hasCicd: features.includes('cicd'),
342
+ hasWebsocket: features.includes('websocket'),
343
+ hasRequestId: features.includes('requestId'),
344
+ hasRedis: features.includes('redis') && features.includes('auth') && aiConfig.db !== 'none',
345
+ hasEmail: features.includes('email') && aiConfig.db !== 'none',
346
+ hasFileUpload: features.includes('fileUpload'),
347
+ hasSoftDelete: features.includes('softDelete') && aiConfig.db !== 'none',
348
+ hasAuditLog: features.includes('auditLog'),
349
+ hasMetrics: features.includes('metrics'),
350
+ hasApiVersioning: features.includes('apiVersioning'),
351
+ hasBackgroundJobs: features.includes('backgroundJobs'),
352
+ hasGraphQL: features.includes('graphql'),
353
+ hasMCP: features.includes('mcp'),
354
+ hasAI: features.includes('ai'),
355
+ jwtSecret: generateSecret(),
356
+ jwtRefreshSecret: generateSecret(),
357
+ };
358
+
359
+ const projectDir = path.join(process.cwd(), config.projectName);
360
+
361
+ if (fs.existsSync(projectDir)) {
362
+ console.error(`\n❌ Directory "${config.projectName}" already exists!`);
363
+ process.exit(1);
364
+ }
365
+
366
+ const dbLabel = config.isNoDatabase ? 'no database' : config.db;
367
+ console.log(`\n🚀 Creating ${config.projectName} (${config.language}, ${dbLabel})...\n`);
368
+
369
+ try {
370
+ await generateProject(config, projectDir, options);
371
+
372
+ if (!options.skipCleanup) {
373
+ await runPostGenerationCleanup(projectDir, config);
374
+ }
375
+
376
+ try {
377
+ execSync('git init', { cwd: projectDir, stdio: 'pipe' });
378
+ execSync('git add -A', { cwd: projectDir, stdio: 'pipe' });
379
+ execSync('git commit -m "Initial commit from express-genix ai"', {
380
+ cwd: projectDir,
381
+ stdio: 'pipe',
382
+ env: { ...process.env, GIT_COMMITTER_NAME: 'express-genix', GIT_COMMITTER_EMAIL: 'cli@express-genix.dev', GIT_AUTHOR_NAME: 'express-genix', GIT_AUTHOR_EMAIL: 'cli@express-genix.dev' },
383
+ });
384
+ } catch {
385
+ // Git not available
386
+ }
387
+
388
+ console.log(`\n✅ Project ${config.projectName} created from AI description!\n`);
389
+ console.log(` cd ${config.projectName}`);
390
+ console.log(' npm run dev\n');
391
+
392
+ // Prompt to install Coda VS Code extension for AI/MCP projects
393
+ if (config.hasAI || config.hasMCP) {
394
+ await promptCodaExtension();
395
+ }
396
+
397
+ } catch (error) {
398
+ if (fs.existsSync(projectDir)) {
399
+ fs.rmSync(projectDir, { recursive: true, force: true });
400
+ console.error('\n🧹 Rolled back partial project directory.');
401
+ }
402
+ console.error(`\n❌ Failed to create project: ${error.message}`);
403
+ process.exit(1);
404
+ }
405
+ });
406
+
248
407
  await program.parseAsync(process.argv);
249
408
  }
250
409
 
package/lib/ai-cli.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * AI CLI handler — interprets natural language project descriptions
3
+ * and maps them to express-genix configuration flags.
4
+ *
5
+ * Requires @langchain/openai or @langchain/anthropic to be installed.
6
+ * These are NOT bundled with express-genix to keep the CLI lightweight.
7
+ *
8
+ * Install with: npm install -g @langchain/core @langchain/openai
9
+ */
10
+
11
+ const SYSTEM_PROMPT = `You are an expert Express.js project configurator for the express-genix CLI tool.
12
+ Given a natural language description of a web application, output a JSON configuration object.
13
+
14
+ Available options:
15
+ - projectName: string (lowercase, hyphens only, no spaces)
16
+ - language: "javascript" | "typescript"
17
+ - db: "mongodb" | "postgresql" | "prisma" | "none"
18
+ - logger: "winston" | "pino"
19
+ - features: array of feature strings from this list:
20
+ - "auth" (JWT authentication — requires a database)
21
+ - "rateLimit" (rate limiting)
22
+ - "swagger" (Swagger/OpenAPI docs)
23
+ - "redis" (Redis token blacklist — requires auth)
24
+ - "docker" (Docker & Docker Compose)
25
+ - "cicd" (GitHub Actions CI/CD)
26
+ - "websocket" (WebSocket via Socket.io)
27
+ - "requestId" (request ID / correlation tracking)
28
+ - "email" (Nodemailer email service — requires database)
29
+ - "fileUpload" (Multer file uploads)
30
+ - "softDelete" (soft deletes — requires database + auth)
31
+ - "auditLog" (audit logging middleware)
32
+ - "metrics" (Prometheus metrics)
33
+ - "apiVersioning" (API versioning /api/v1)
34
+ - "backgroundJobs" (BullMQ background jobs)
35
+ - "graphql" (GraphQL via Apollo Server)
36
+ - "mcp" (MCP Server — exposes API as AI-agent tools)
37
+ - "ai" (LangChain/LangGraph AI service)
38
+
39
+ Rules:
40
+ - Always include "auth", "rateLimit", "swagger", "requestId" unless explicitly unwanted
41
+ - Include "docker" for production-ready apps
42
+ - Include "ai" if the description mentions AI, chatbot, LLM, or intelligence
43
+ - Include "mcp" if the description mentions AI agents, tools, or MCP
44
+ - Choose "prisma" for modern stacks, "mongodb" for documents, "postgresql" for relational
45
+ - Infer a short kebab-case project name from the description
46
+ - Output ONLY valid JSON. No explanation, no markdown fences.`;
47
+
48
+ const runAiCommand = async (description) => {
49
+ // Check for API key
50
+ const hasOpenAI = !!process.env.OPENAI_API_KEY;
51
+ const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
52
+ const hasGemini = !!process.env.GOOGLE_API_KEY;
53
+
54
+ if (!hasOpenAI && !hasAnthropic && !hasGemini) {
55
+ console.error('❌ The AI command requires an API key.');
56
+ console.error(' Set one of these environment variables:');
57
+ console.error(' export OPENAI_API_KEY=sk-...');
58
+ console.error(' export ANTHROPIC_API_KEY=sk-ant-...');
59
+ console.error(' export GOOGLE_API_KEY=AI...');
60
+ process.exit(1);
61
+ }
62
+
63
+ // Try to load LangChain (not bundled — must be installed separately)
64
+ let ChatModel;
65
+ try {
66
+ if (hasGemini) {
67
+ const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
68
+ ChatModel = new ChatGoogleGenerativeAI({
69
+ model: 'gemini-2.0-flash',
70
+ maxOutputTokens: 1024,
71
+ apiKey: process.env.GOOGLE_API_KEY,
72
+ });
73
+ } else if (hasAnthropic) {
74
+ const { ChatAnthropic } = require('@langchain/anthropic');
75
+ ChatModel = new ChatAnthropic({
76
+ modelName: 'claude-sonnet-4-20250514',
77
+ maxTokens: 1024,
78
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
79
+ });
80
+ } else {
81
+ const { ChatOpenAI } = require('@langchain/openai');
82
+ ChatModel = new ChatOpenAI({
83
+ modelName: 'gpt-4o-mini',
84
+ maxTokens: 1024,
85
+ openAIApiKey: process.env.OPENAI_API_KEY,
86
+ });
87
+ }
88
+ } catch {
89
+ console.error('❌ LangChain packages not found.');
90
+ console.error(' Install them globally to use the AI command:');
91
+ console.error(' npm install -g @langchain/core @langchain/openai @langchain/anthropic @langchain/google-genai');
92
+ process.exit(1);
93
+ }
94
+
95
+ const { HumanMessage, SystemMessage } = require('@langchain/core/messages');
96
+
97
+ console.log('🤖 Analyzing your description...\n');
98
+
99
+ const response = await ChatModel.invoke([
100
+ new SystemMessage(SYSTEM_PROMPT),
101
+ new HumanMessage(description),
102
+ ]);
103
+
104
+ let config;
105
+ try {
106
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
107
+ if (!jsonMatch) throw new Error('No JSON found in response');
108
+ config = JSON.parse(jsonMatch[0]);
109
+ } catch {
110
+ console.error('❌ Could not parse AI response. Try a clearer description.');
111
+ console.error(' Raw output:', response.content);
112
+ process.exit(1);
113
+ }
114
+
115
+ // Validate required fields
116
+ if (!config.projectName || !config.language || !config.db) {
117
+ console.error('❌ AI response missing required fields. Try again.');
118
+ process.exit(1);
119
+ }
120
+
121
+ // Display interpreted config
122
+ console.log('📋 Interpreted configuration:\n');
123
+ console.log(` Project: ${config.projectName}`);
124
+ console.log(` Language: ${config.language}`);
125
+ console.log(` Database: ${config.db}`);
126
+ console.log(` Logger: ${config.logger || 'winston'}`);
127
+ console.log(` Features: ${(config.features || []).join(', ') || 'base'}`);
128
+ console.log('');
129
+
130
+ return config;
131
+ };
132
+
133
+ module.exports = { runAiCommand };
package/lib/features.js CHANGED
@@ -240,6 +240,8 @@ const inferConfig = (projectDir, pkg) => {
240
240
  hasApiVersioning: false,
241
241
  hasBackgroundJobs: !!(pkg.dependencies && pkg.dependencies.bullmq),
242
242
  hasGraphQL: !!(pkg.dependencies && pkg.dependencies['@apollo/server']),
243
+ hasMCP: !!(pkg.dependencies && pkg.dependencies['@modelcontextprotocol/sdk']),
244
+ hasAI: !!(pkg.dependencies && pkg.dependencies['@langchain/core']),
243
245
  logger: pkg.dependencies && pkg.dependencies.pino ? 'pino' : 'winston',
244
246
  };
245
247
  };
package/lib/generator.js CHANGED
@@ -61,6 +61,14 @@ const createDirectoryStructure = (projectDir, config) => {
61
61
  dirs.push('src/graphql');
62
62
  }
63
63
 
64
+ if (config.hasMCP) {
65
+ dirs.push('src/mcp');
66
+ }
67
+
68
+ if (config.hasAI) {
69
+ dirs.push('src/agents');
70
+ }
71
+
64
72
  fs.mkdirSync(projectDir);
65
73
  dirs.forEach((dir) => {
66
74
  fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
@@ -144,6 +152,10 @@ const generateFiles = async (config, projectDir) => {
144
152
  configFiles.push({ template: 'config/queue.js.ejs', output: `src/config/queue.${ext}` });
145
153
  }
146
154
 
155
+ if (config.hasAI) {
156
+ configFiles.push({ template: 'config/ai.js.ejs', output: `src/config/ai.${ext}` });
157
+ }
158
+
147
159
  // Route files
148
160
  const routeFiles = [
149
161
  { template: 'routes/index.js.ejs', output: `src/routes/index.${ext}` },
@@ -202,6 +214,18 @@ const generateFiles = async (config, projectDir) => {
202
214
  );
203
215
  }
204
216
 
217
+ if (config.hasAI) {
218
+ serviceFiles.push(
219
+ { template: 'services/aiService.js.ejs', output: `src/services/aiService.${ext}` }
220
+ );
221
+ controllerFiles.push(
222
+ { template: 'controllers/aiController.js.ejs', output: `src/controllers/aiController.${ext}` }
223
+ );
224
+ routeFiles.push(
225
+ { template: 'routes/aiRoutes.js.ejs', output: `src/routes/aiRoutes.${ext}` }
226
+ );
227
+ }
228
+
205
229
  if (config.hasFileUpload) {
206
230
  routeFiles.push(
207
231
  { template: 'routes/uploadRoutes.js.ejs', output: `src/routes/uploadRoutes.${ext}` }
@@ -214,6 +238,35 @@ const generateFiles = async (config, projectDir) => {
214
238
  );
215
239
  }
216
240
 
241
+ // GraphQL files
242
+ const graphqlFiles = [];
243
+
244
+ if (config.hasGraphQL) {
245
+ graphqlFiles.push(
246
+ { template: 'graphql/typeDefs.js.ejs', output: `src/graphql/typeDefs.${ext}` },
247
+ { template: 'graphql/resolvers.js.ejs', output: `src/graphql/resolvers.${ext}` }
248
+ );
249
+ }
250
+
251
+ // MCP files
252
+ const mcpFiles = [];
253
+
254
+ if (config.hasMCP) {
255
+ mcpFiles.push(
256
+ { template: 'mcp/server.js.ejs', output: `src/mcp/server.${ext}` },
257
+ { template: 'mcp/mcp-config.json.ejs', output: 'mcp.json' }
258
+ );
259
+ }
260
+
261
+ // Agent files
262
+ const agentFiles = [];
263
+
264
+ if (config.hasAI) {
265
+ agentFiles.push(
266
+ { template: 'agents/graph.js.ejs', output: `src/agents/graph.${ext}` }
267
+ );
268
+ }
269
+
217
270
  // Middleware files
218
271
  const middlewareFiles = [
219
272
  { template: 'middleware/errorHandler.js.ejs', output: `src/middleware/errorHandler.${ext}` },
@@ -303,15 +356,12 @@ const generateFiles = async (config, projectDir) => {
303
356
  ...serviceFiles,
304
357
  ...utilFiles,
305
358
  ...testFiles,
359
+ ...graphqlFiles,
360
+ ...mcpFiles,
361
+ ...agentFiles,
306
362
  ];
307
363
 
308
- // GraphQL files
309
- if (config.hasGraphQL) {
310
- allFiles.push(
311
- { template: 'graphql/typeDefs.js.ejs', output: `src/graphql/typeDefs.${ext}` },
312
- { template: 'graphql/resolvers.js.ejs', output: `src/graphql/resolvers.${ext}` }
313
- );
314
- }
364
+ // GraphQL files (legacy — now handled above)
315
365
 
316
366
  // Background job worker
317
367
  if (config.hasBackgroundJobs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-genix",
3
- "version": "3.0.1",
3
+ "version": "4.1.1",
4
4
  "description": "Production-grade CLI to generate Express apps with JWT, RBAC, GraphQL, TypeScript, Prisma, MongoDB, PostgreSQL, file uploads, email, background jobs, and more",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,85 @@
1
+ const { createReactAgent } = require('@langchain/langgraph/prebuilt');
2
+ const { tool } = require('@langchain/core/tools');
3
+ const { HumanMessage } = require('@langchain/core/messages');
4
+ const { z } = require('zod');
5
+ const { getModel } = require('../config/ai');
6
+
7
+ // --- Built-in Tools ---
8
+
9
+ const currentTimeTool = tool(
10
+ async () => new Date().toISOString(),
11
+ {
12
+ name: 'current_time',
13
+ description: 'Get the current date and time in ISO 8601 format',
14
+ schema: z.object({}),
15
+ }
16
+ );
17
+
18
+ const calculatorTool = tool(
19
+ async ({ expression }) => {
20
+ // Only allow digits and math operators — no code injection possible
21
+ const sanitized = expression.replace(/[^0-9+\-*/().%\s]/g, '');
22
+ if (sanitized !== expression.trim()) {
23
+ return 'Error: Expression contains invalid characters. Use only numbers and +, -, *, /, (, ), %';
24
+ }
25
+ try {
26
+ const result = new Function(`"use strict"; return (${sanitized})`)();
27
+ if (!isFinite(result)) return 'Error: Result is not a finite number';
28
+ return String(result);
29
+ } catch {
30
+ return 'Error: Could not evaluate expression';
31
+ }
32
+ },
33
+ {
34
+ name: 'calculator',
35
+ description: 'Evaluate a mathematical expression using standard operators: +, -, *, /, %',
36
+ schema: z.object({
37
+ expression: z.string().describe('Math expression, e.g. "2 + 3 * 4"'),
38
+ }),
39
+ }
40
+ );
41
+
42
+ /**
43
+ * Create a LangGraph ReAct agent with tool-calling capabilities.
44
+ *
45
+ * @param {Object} options
46
+ * @param {Array} options.tools - Additional @langchain/core tools to register
47
+ * @param {string} options.systemPrompt - System instruction for the agent
48
+ * @param {string} options.provider - AI provider (openai, anthropic, ollama)
49
+ * @param {string} options.model - Model name override
50
+ */
51
+ const createAgent = (options = {}) => {
52
+ const tools = [
53
+ currentTimeTool,
54
+ calculatorTool,
55
+ ...(options.tools || []),
56
+ ];
57
+
58
+ const model = getModel(options);
59
+
60
+ return createReactAgent({
61
+ llm: model,
62
+ tools,
63
+ messageModifier: options.systemPrompt || 'You are a helpful assistant. Use the provided tools when needed to answer questions accurately.',
64
+ });
65
+ };
66
+
67
+ /**
68
+ * Run the agent with a single message and return the final response.
69
+ */
70
+ const runAgent = async (message, options = {}) => {
71
+ const agent = createAgent(options);
72
+
73
+ const result = await agent.invoke({
74
+ messages: [new HumanMessage(message)],
75
+ });
76
+
77
+ const lastMessage = result.messages[result.messages.length - 1];
78
+
79
+ return {
80
+ content: lastMessage.content,
81
+ steps: result.messages.length,
82
+ };
83
+ };
84
+
85
+ module.exports = { createAgent, runAgent, currentTimeTool, calculatorTool };
@@ -0,0 +1,54 @@
1
+ const { ChatOpenAI } = require('@langchain/openai');
2
+ const { ChatAnthropic } = require('@langchain/anthropic');
3
+ const { ChatOllama } = require('@langchain/ollama');
4
+ const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
5
+
6
+ /**
7
+ * Create a LangChain chat model based on the configured provider.
8
+ * Supports OpenAI, Anthropic, Google Gemini, and Ollama (local).
9
+ */
10
+ const getModel = (options = {}) => {
11
+ const provider = (options.provider || process.env.AI_PROVIDER || 'openai').toLowerCase();
12
+ const temperature = options.temperature ?? parseFloat(process.env.AI_TEMPERATURE || '0.7');
13
+ const maxTokens = options.maxTokens ?? parseInt(process.env.AI_MAX_TOKENS || '2048', 10);
14
+
15
+ switch (provider) {
16
+ case 'openai':
17
+ return new ChatOpenAI({
18
+ modelName: options.model || process.env.AI_MODEL || 'gpt-4o-mini',
19
+ temperature,
20
+ maxTokens,
21
+ openAIApiKey: process.env.OPENAI_API_KEY,
22
+ });
23
+
24
+ case 'anthropic':
25
+ return new ChatAnthropic({
26
+ modelName: options.model || process.env.AI_MODEL || 'claude-sonnet-4-20250514',
27
+ temperature,
28
+ maxTokens,
29
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
30
+ });
31
+
32
+ case 'gemini':
33
+ return new ChatGoogleGenerativeAI({
34
+ model: options.model || process.env.AI_MODEL || 'gemini-2.0-flash',
35
+ temperature,
36
+ maxOutputTokens: maxTokens,
37
+ apiKey: process.env.GOOGLE_API_KEY,
38
+ });
39
+
40
+ case 'ollama':
41
+ return new ChatOllama({
42
+ model: options.model || process.env.AI_MODEL || 'llama3',
43
+ temperature,
44
+ baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
45
+ });
46
+
47
+ default:
48
+ throw new Error(
49
+ `Unsupported AI provider: "${provider}". Supported: openai, anthropic, gemini, ollama`
50
+ );
51
+ }
52
+ };
53
+
54
+ module.exports = { getModel };
@@ -0,0 +1,102 @@
1
+ const aiService = require('../services/aiService');
2
+ const { runAgent } = require('../agents/graph');
3
+ const { success, error } = require('../utils/response');
4
+
5
+ /**
6
+ * POST /ai/chat — Send a message, get a complete AI response.
7
+ */
8
+ const chatHandler = async (req, res, next) => {
9
+ try {
10
+ const { message, systemPrompt, history, provider, model, temperature, maxTokens } = req.body;
11
+
12
+ if (!message) {
13
+ return res.status(400).json(error('Message is required'));
14
+ }
15
+
16
+ const result = await aiService.chat(message, {
17
+ systemPrompt,
18
+ history,
19
+ provider,
20
+ model,
21
+ temperature,
22
+ maxTokens,
23
+ });
24
+
25
+ res.json(success(result));
26
+ } catch (err) {
27
+ next(err);
28
+ }
29
+ };
30
+
31
+ /**
32
+ * POST /ai/stream — Stream an AI response via Server-Sent Events.
33
+ */
34
+ const streamHandler = async (req, res, next) => {
35
+ try {
36
+ const { message, systemPrompt, history, provider, model, temperature, maxTokens } = req.body;
37
+
38
+ if (!message) {
39
+ return res.status(400).json(error('Message is required'));
40
+ }
41
+
42
+ res.setHeader('Content-Type', 'text/event-stream');
43
+ res.setHeader('Cache-Control', 'no-cache');
44
+ res.setHeader('Connection', 'keep-alive');
45
+
46
+ const generator = aiService.stream(message, {
47
+ systemPrompt,
48
+ history,
49
+ provider,
50
+ model,
51
+ temperature,
52
+ maxTokens,
53
+ });
54
+
55
+ for await (const chunk of generator) {
56
+ res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
57
+ }
58
+
59
+ res.write('data: [DONE]\n\n');
60
+ res.end();
61
+ } catch (err) {
62
+ next(err);
63
+ }
64
+ };
65
+
66
+ /**
67
+ * POST /ai/chain — Run a prompt template chain with variables.
68
+ */
69
+ const chainHandler = async (req, res, next) => {
70
+ try {
71
+ const { template, variables, provider, model } = req.body;
72
+
73
+ if (!template) {
74
+ return res.status(400).json(error('Template is required'));
75
+ }
76
+
77
+ const result = await aiService.chain(template, variables || {}, { provider, model });
78
+ res.json(success({ content: result }));
79
+ } catch (err) {
80
+ next(err);
81
+ }
82
+ };
83
+
84
+ /**
85
+ * POST /ai/agent — Run the LangGraph ReAct agent.
86
+ */
87
+ const agentHandler = async (req, res, next) => {
88
+ try {
89
+ const { message, systemPrompt, provider, model } = req.body;
90
+
91
+ if (!message) {
92
+ return res.status(400).json(error('Message is required'));
93
+ }
94
+
95
+ const result = await runAgent(message, { systemPrompt, provider, model });
96
+ res.json(success(result));
97
+ } catch (err) {
98
+ next(err);
99
+ }
100
+ };
101
+
102
+ module.exports = { chatHandler, streamHandler, chainHandler, agentHandler };
@@ -28,6 +28,19 @@ FRONTEND_URL=http://localhost:3000
28
28
  # File Uploads
29
29
  UPLOAD_DIR=uploads
30
30
  UPLOAD_MAX_SIZE=5242880
31
+ <% } %><% if (hasMCP) { %>
32
+ # MCP Server
33
+ MCP_SERVER_NAME=<%= projectName %>
34
+ <% } %><% if (hasAI) { %>
35
+ # AI Service (LangChain)
36
+ AI_PROVIDER=openai
37
+ AI_MODEL=gpt-4o-mini
38
+ AI_TEMPERATURE=0.7
39
+ AI_MAX_TOKENS=2048
40
+ OPENAI_API_KEY=
41
+ # ANTHROPIC_API_KEY=
42
+ # GOOGLE_API_KEY=
43
+ # OLLAMA_BASE_URL=http://localhost:11434
31
44
  <% } %><% if (db === 'mongodb') { %>
32
45
  # Database
33
46
  MONGO_URI=mongodb://localhost:27017/<%= projectName %>
@@ -28,6 +28,19 @@ FRONTEND_URL=http://localhost:3000
28
28
  # File Uploads
29
29
  UPLOAD_DIR=uploads
30
30
  UPLOAD_MAX_SIZE=5242880
31
+ <% } %><% if (hasMCP) { %>
32
+ # MCP Server
33
+ MCP_SERVER_NAME=<%= projectName %>
34
+ <% } %><% if (hasAI) { %>
35
+ # AI Service (LangChain)
36
+ AI_PROVIDER=openai
37
+ AI_MODEL=gpt-4o-mini
38
+ AI_TEMPERATURE=0.7
39
+ AI_MAX_TOKENS=2048
40
+ OPENAI_API_KEY=
41
+ # ANTHROPIC_API_KEY=
42
+ # GOOGLE_API_KEY=
43
+ # OLLAMA_BASE_URL=http://localhost:11434
31
44
  <% } %><% if (db === 'mongodb') { %>
32
45
  # Database
33
46
  MONGO_URI=mongodb://localhost:27017/<%= projectName %>
@@ -18,7 +18,8 @@
18
18
  "db:migrate": "npx sequelize-cli db:migrate",
19
19
  "db:migrate:undo": "npx sequelize-cli db:migrate:undo",
20
20
  "db:seed": "npx sequelize-cli db:seed:all",
21
- "db:create": "npx sequelize-cli db:create"<% } %>
21
+ "db:create": "npx sequelize-cli db:create"<% } %><% if (hasMCP) { %>,
22
+ "mcp:start": "node src/mcp/server.js"<% } %>
22
23
  },
23
24
  "dependencies": {
24
25
  "express": "^4.21.0",
@@ -31,7 +32,7 @@
31
32
  "swagger-ui-express": "^5.0.1"<% } %><% if (hasAuth) { %>,
32
33
  "jsonwebtoken": "^9.0.2",
33
34
  "bcryptjs": "^2.4.3",
34
- "validator": "^13.12.0",
35
+ "validator": "^13.12.0"<% } %><% if (hasAuth || hasMCP || hasAI) { %>,
35
36
  "zod": "^3.23.0"<% } %><% if (db === 'mongodb') { %>,
36
37
  "mongoose": "^8.8.0"<% } %><% if (db === 'postgresql') { %>,
37
38
  "pg": "^8.13.0",
@@ -46,7 +47,14 @@
46
47
  "@apollo/server": "^4.11.0",
47
48
  "graphql": "^16.9.0",
48
49
  "graphql-tag": "^2.12.6"<% } %><% if (hasBackgroundJobs) { %>,
49
- "bullmq": "^5.12.0"<% } %><% if (logger === 'winston') { %>,
50
+ "bullmq": "^5.12.0"<% } %><% if (hasMCP) { %>,
51
+ "@modelcontextprotocol/sdk": "^1.12.0"<% } %><% if (hasAI) { %>,
52
+ "@langchain/core": "^0.3.0",
53
+ "@langchain/openai": "^0.5.0",
54
+ "@langchain/anthropic": "^0.3.0",
55
+ "@langchain/google-genai": "^0.2.0",
56
+ "@langchain/ollama": "^0.1.0",
57
+ "@langchain/langgraph": "^0.2.0"<% } %><% if (logger === 'winston') { %>,
50
58
  "winston": "^3.15.0"<% } %><% if (logger === 'pino') { %>,
51
59
  "pino": "^9.5.0",
52
60
  "pino-pretty": "^13.0.0"<% } %>
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "<%= projectName %>": {
4
+ "command": "node",
5
+ "args": ["src/mcp/server.js"],
6
+ "cwd": "/path/to/<%= projectName %>"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,151 @@
1
+ require('dotenv').config();
2
+
3
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
4
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
5
+ const { z } = require('zod');
6
+ <% if (hasDatabase) { %>const db = require('../config/database');
7
+ <% } %><% if (hasAuth) { %>const userService = require('../services/userService');
8
+ <% } %><% if (!hasAuth && !isNoDatabase) { %>const exampleService = require('../services/exampleService');
9
+ <% } %><% if (hasEmail) { %>const emailService = require('../services/emailService');
10
+ <% } %>
11
+
12
+ const server = new McpServer({
13
+ name: process.env.MCP_SERVER_NAME || '<%= projectName %>',
14
+ version: '1.0.0',
15
+ });
16
+
17
+ <% if (hasAuth) { %>
18
+ // --- User Management Tools ---
19
+
20
+ server.tool(
21
+ 'list_users',
22
+ 'List all registered users with pagination',
23
+ {
24
+ page: z.number().optional().describe('Page number (default: 1)'),
25
+ limit: z.number().optional().describe('Items per page (default: 20)'),
26
+ },
27
+ async ({ page = 1, limit = 20 }) => {
28
+ const result = await userService.findAll({ page, limit });
29
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
30
+ }
31
+ );
32
+
33
+ server.tool(
34
+ 'get_user',
35
+ 'Get a user by their ID',
36
+ {
37
+ userId: z.string().describe('The user ID to look up'),
38
+ },
39
+ async ({ userId }) => {
40
+ const user = await userService.findById(userId);
41
+ if (!user) {
42
+ return { content: [{ type: 'text', text: 'User not found' }], isError: true };
43
+ }
44
+ const { password, ...safeUser } = user.toJSON ? user.toJSON() : user;
45
+ return { content: [{ type: 'text', text: JSON.stringify(safeUser, null, 2) }] };
46
+ }
47
+ );
48
+
49
+ server.tool(
50
+ 'register_user',
51
+ 'Register a new user account',
52
+ {
53
+ username: z.string().describe('Username for the new account'),
54
+ email: z.string().email().describe('Email address'),
55
+ password: z.string().min(8).describe('Password (min 8 characters)'),
56
+ },
57
+ async ({ username, email, password }) => {
58
+ const bcrypt = require('bcryptjs');
59
+ const existing = await userService.findByEmail(email);
60
+ if (existing) {
61
+ return { content: [{ type: 'text', text: 'A user with this email already exists' }], isError: true };
62
+ }
63
+ const hashedPassword = await bcrypt.hash(password, 12);
64
+ const user = await userService.create({ username, email, password: hashedPassword });
65
+ return {
66
+ content: [{
67
+ type: 'text',
68
+ text: JSON.stringify({ id: user.id, username: user.username, email: user.email, role: user.role || 'user' }, null, 2),
69
+ }],
70
+ };
71
+ }
72
+ );
73
+ <% } %>
74
+ <% if (!hasAuth) { %>
75
+ // --- Example CRUD Tools ---
76
+
77
+ server.tool(
78
+ 'list_examples',
79
+ 'List all example items with pagination',
80
+ {
81
+ page: z.number().optional().describe('Page number (default: 1)'),
82
+ limit: z.number().optional().describe('Items per page (default: 20)'),
83
+ },
84
+ async ({ page = 1, limit = 20 }) => {
85
+ const result = await exampleService.getAll({ page, limit });
86
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
87
+ }
88
+ );
89
+
90
+ server.tool(
91
+ 'create_example',
92
+ 'Create a new example item',
93
+ {
94
+ title: z.string().describe('Item title'),
95
+ description: z.string().optional().describe('Item description'),
96
+ },
97
+ async ({ title, description }) => {
98
+ const item = await exampleService.create({ title, description });
99
+ return { content: [{ type: 'text', text: JSON.stringify(item, null, 2) }] };
100
+ }
101
+ );
102
+ <% } %>
103
+ <% if (hasEmail) { %>
104
+ // --- Email Tools ---
105
+
106
+ server.tool(
107
+ 'send_email',
108
+ 'Send an email to a recipient',
109
+ {
110
+ to: z.string().email().describe('Recipient email address'),
111
+ subject: z.string().describe('Email subject line'),
112
+ html: z.string().describe('Email body in HTML format'),
113
+ },
114
+ async ({ to, subject, html }) => {
115
+ const info = await emailService.sendMail({ to, subject, html });
116
+ return { content: [{ type: 'text', text: `Email sent successfully (ID: ${info.messageId})` }] };
117
+ }
118
+ );
119
+ <% } %>
120
+
121
+ // --- Health Check Tool ---
122
+
123
+ server.tool(
124
+ 'health_check',
125
+ 'Check the application health and status',
126
+ {},
127
+ async () => {
128
+ const health = {
129
+ status: 'OK',
130
+ uptime: `${Math.floor(process.uptime())}s`,
131
+ memoryUsage: `${Math.round(process.memoryUsage().rss / 1024 / 1024)} MB`,
132
+ timestamp: new Date().toISOString(),
133
+ };
134
+ return { content: [{ type: 'text', text: JSON.stringify(health, null, 2) }] };
135
+ }
136
+ );
137
+
138
+ // --- Start MCP Server ---
139
+
140
+ const main = async () => {
141
+ <% if (hasDatabase) { %> await db.connect();
142
+ <% } %>
143
+ const transport = new StdioServerTransport();
144
+ await server.connect(transport);
145
+ console.error(`MCP server "${process.env.MCP_SERVER_NAME || '<%= projectName %>'}" running on stdio`);
146
+ };
147
+
148
+ main().catch((err) => {
149
+ console.error('Failed to start MCP server:', err);
150
+ process.exit(1);
151
+ });
@@ -1,4 +1,4 @@
1
- const logger = require('../utils/logger');
1
+ const { logger } = require('../utils/logger');
2
2
 
3
3
  /**
4
4
  * Audit Logging Middleware
@@ -47,10 +47,12 @@ const auditLog = (action) => (req, res, next) => {
47
47
  }
48
48
  }
49
49
 
50
+ const message = `${auditEntry.method} ${auditEntry.path} ${auditEntry.statusCode} ${auditEntry.duration}`;
51
+
50
52
  if (res.statusCode >= 400) {
51
- logger.warn(auditEntry);
53
+ logger.warn(message, auditEntry);
52
54
  } else {
53
- logger.info(auditEntry);
55
+ logger.info(message, auditEntry);
54
56
  }
55
57
 
56
58
  originalEnd.apply(res, args);
@@ -0,0 +1,126 @@
1
+ const express = require('express');
2
+ const { chatHandler, streamHandler, chainHandler, agentHandler } = require('../controllers/aiController');
3
+
4
+ const router = express.Router();
5
+
6
+ /**
7
+ * @swagger
8
+ * /api<% if (hasApiVersioning) { %>/v1<% } %>/ai/chat:
9
+ * post:
10
+ * summary: Send a chat message to the AI
11
+ * tags: [AI]
12
+ * requestBody:
13
+ * required: true
14
+ * content:
15
+ * application/json:
16
+ * schema:
17
+ * type: object
18
+ * required: [message]
19
+ * properties:
20
+ * message:
21
+ * type: string
22
+ * example: "What is Node.js?"
23
+ * systemPrompt:
24
+ * type: string
25
+ * example: "You are a helpful coding assistant."
26
+ * history:
27
+ * type: array
28
+ * items:
29
+ * type: object
30
+ * properties:
31
+ * role:
32
+ * type: string
33
+ * enum: [user, assistant]
34
+ * content:
35
+ * type: string
36
+ * provider:
37
+ * type: string
38
+ * enum: [openai, anthropic, gemini, ollama]
39
+ * model:
40
+ * type: string
41
+ * responses:
42
+ * 200:
43
+ * description: AI response
44
+ */
45
+ router.post('/chat', chatHandler);
46
+
47
+ /**
48
+ * @swagger
49
+ * /api<% if (hasApiVersioning) { %>/v1<% } %>/ai/stream:
50
+ * post:
51
+ * summary: Stream an AI response via Server-Sent Events
52
+ * tags: [AI]
53
+ * requestBody:
54
+ * required: true
55
+ * content:
56
+ * application/json:
57
+ * schema:
58
+ * type: object
59
+ * required: [message]
60
+ * properties:
61
+ * message:
62
+ * type: string
63
+ * responses:
64
+ * 200:
65
+ * description: SSE stream of AI response chunks
66
+ */
67
+ router.post('/stream', streamHandler);
68
+
69
+ /**
70
+ * @swagger
71
+ * /api<% if (hasApiVersioning) { %>/v1<% } %>/ai/chain:
72
+ * post:
73
+ * summary: Run a prompt template chain with variable substitution
74
+ * tags: [AI]
75
+ * requestBody:
76
+ * required: true
77
+ * content:
78
+ * application/json:
79
+ * schema:
80
+ * type: object
81
+ * required: [template]
82
+ * properties:
83
+ * template:
84
+ * type: string
85
+ * example: "Summarize the following text: {text}"
86
+ * variables:
87
+ * type: object
88
+ * example: { "text": "Node.js is a JavaScript runtime..." }
89
+ * provider:
90
+ * type: string
91
+ * enum: [openai, anthropic, gemini, ollama]
92
+ * responses:
93
+ * 200:
94
+ * description: Chain result
95
+ */
96
+ router.post('/chain', chainHandler);
97
+
98
+ /**
99
+ * @swagger
100
+ * /api<% if (hasApiVersioning) { %>/v1<% } %>/ai/agent:
101
+ * post:
102
+ * summary: Run the LangGraph ReAct agent with tool calling
103
+ * tags: [AI]
104
+ * requestBody:
105
+ * required: true
106
+ * content:
107
+ * application/json:
108
+ * schema:
109
+ * type: object
110
+ * required: [message]
111
+ * properties:
112
+ * message:
113
+ * type: string
114
+ * example: "What time is it right now?"
115
+ * systemPrompt:
116
+ * type: string
117
+ * provider:
118
+ * type: string
119
+ * enum: [openai, anthropic, gemini, ollama]
120
+ * responses:
121
+ * 200:
122
+ * description: Agent response with tool-use steps
123
+ */
124
+ router.post('/agent', agentHandler);
125
+
126
+ module.exports = router;
@@ -5,6 +5,7 @@ const adminRoutes = require('./adminRoutes');
5
5
  <% } else { %>const exampleRoutes = require('./exampleRoutes');
6
6
  <% } %><% if (hasFileUpload) { %>const uploadRoutes = require('./uploadRoutes');
7
7
  <% } %><% if (hasBackgroundJobs) { %>const jobRoutes = require('./jobRoutes');
8
+ <% } %><% if (hasAI) { %>const aiRoutes = require('./aiRoutes');
8
9
  <% } %>
9
10
 
10
11
  const router = express.Router();
@@ -15,6 +16,7 @@ router.use('/admin', adminRoutes);
15
16
  <% } else { %>router.use('/examples', exampleRoutes);
16
17
  <% } %><% if (hasFileUpload) { %>router.use('/uploads', uploadRoutes);
17
18
  <% } %><% if (hasBackgroundJobs) { %>router.use('/jobs', jobRoutes);
19
+ <% } %><% if (hasAI) { %>router.use('/ai', aiRoutes);
18
20
  <% } %>
19
21
 
20
22
  /**
@@ -0,0 +1,80 @@
1
+ const { getModel } = require('../config/ai');
2
+ const { HumanMessage, SystemMessage, AIMessage } = require('@langchain/core/messages');
3
+ const { StringOutputParser } = require('@langchain/core/output_parsers');
4
+ const { ChatPromptTemplate } = require('@langchain/core/prompts');
5
+ const logger = require('../utils/logger');
6
+
7
+ const DEFAULT_SYSTEM_PROMPT = 'You are a helpful AI assistant.';
8
+
9
+ /**
10
+ * Send a chat message and get a complete response.
11
+ */
12
+ const chat = async (message, options = {}) => {
13
+ const model = getModel(options);
14
+ const messages = [
15
+ new SystemMessage(options.systemPrompt || DEFAULT_SYSTEM_PROMPT),
16
+ ];
17
+
18
+ if (Array.isArray(options.history)) {
19
+ for (const msg of options.history) {
20
+ if (msg.role === 'user') messages.push(new HumanMessage(msg.content));
21
+ else if (msg.role === 'assistant') messages.push(new AIMessage(msg.content));
22
+ }
23
+ }
24
+
25
+ messages.push(new HumanMessage(message));
26
+
27
+ const response = await model.invoke(messages);
28
+
29
+ logger.info('AI chat completed', {
30
+ provider: options.provider || process.env.AI_PROVIDER || 'openai',
31
+ });
32
+
33
+ return {
34
+ content: response.content,
35
+ model: response.response_metadata?.model || 'unknown',
36
+ usage: response.usage_metadata || null,
37
+ };
38
+ };
39
+
40
+ /**
41
+ * Stream a chat response. Returns an async generator yielding text chunks.
42
+ */
43
+ const stream = async function* (message, options = {}) {
44
+ const model = getModel(options);
45
+ const messages = [
46
+ new SystemMessage(options.systemPrompt || DEFAULT_SYSTEM_PROMPT),
47
+ ];
48
+
49
+ if (Array.isArray(options.history)) {
50
+ for (const msg of options.history) {
51
+ if (msg.role === 'user') messages.push(new HumanMessage(msg.content));
52
+ else if (msg.role === 'assistant') messages.push(new AIMessage(msg.content));
53
+ }
54
+ }
55
+
56
+ messages.push(new HumanMessage(message));
57
+
58
+ const streamResponse = await model.stream(messages);
59
+
60
+ for await (const chunk of streamResponse) {
61
+ if (chunk.content) {
62
+ yield chunk.content;
63
+ }
64
+ }
65
+ };
66
+
67
+ /**
68
+ * Run a prompt template chain with variable substitution.
69
+ */
70
+ const chain = async (template, variables = {}, options = {}) => {
71
+ const model = getModel(options);
72
+ const prompt = ChatPromptTemplate.fromTemplate(template);
73
+ const outputParser = new StringOutputParser();
74
+ const pipeline = prompt.pipe(model).pipe(outputParser);
75
+
76
+ const result = await pipeline.invoke(variables);
77
+ return result;
78
+ };
79
+
80
+ module.exports = { chat, stream, chain };