express-genix 3.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -11
- package/index.js +160 -1
- package/lib/ai-cli.js +133 -0
- package/lib/features.js +2 -0
- package/lib/generator.js +57 -7
- package/package.json +1 -1
- package/templates/agents/graph.js.ejs +85 -0
- package/templates/config/ai.js.ejs +54 -0
- package/templates/controllers/aiController.js.ejs +102 -0
- package/templates/core/env.ejs +13 -0
- package/templates/core/env.example.ejs +13 -0
- package/templates/core/package.json.ejs +11 -3
- package/templates/mcp/mcp-config.json.ejs +9 -0
- package/templates/mcp/server.js.ejs +151 -0
- package/templates/middleware/auditLog.js.ejs +5 -3
- package/templates/routes/aiRoutes.js.ejs +126 -0
- package/templates/routes/index.js.ejs +2 -0
- package/templates/services/aiService.js.ejs +80 -0
|
@@ -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 };
|
package/templates/core/env.ejs
CHANGED
|
@@ -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 (
|
|
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,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
|
/**
|