@ninebone/mcp 0.1.26
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.kr.md +71 -0
- package/README.md +72 -0
- package/bak/generate-query.md +28 -0
- package/bak/generator-source-mapper.md +123 -0
- package/bak/mcp-server.js +498 -0
- package/bak/nomenu-navigator.md +16 -0
- package/bak/system-brain.md +62 -0
- package/bak/table-filter.md +21 -0
- package/package.json +33 -0
- package/prompts/menu/generate-menu.md +89 -0
- package/prompts/source/generate-source-controller.md +120 -0
- package/prompts/source/generate-source-mapper.mysql.md +97 -0
- package/prompts/source/generate-source-mapper.oracle.md +90 -0
- package/prompts/source/generate-source-mapper.postgre.md +89 -0
- package/prompts/source/generate-source-service.md +116 -0
- package/prompts/source/generate-source-ui-react.md +174 -0
- package/prompts/system/generate-source-brain.md +57 -0
- package/prompts/system/modify-source-brain.md +50 -0
- package/prompts/system/system-brain.md +88 -0
- package/src/ai/AIProcessor.js +85 -0
- package/src/ai/AIService.js +24 -0
- package/src/core/init.js +116 -0
- package/src/database/config/database.js +42 -0
- package/src/database/core/DatabaseManager.js +115 -0
- package/src/database/core/Dialects.js +66 -0
- package/src/database/core/PoolManager.js +92 -0
- package/src/drivers/mysql.js +0 -0
- package/src/index.js +38 -0
- package/src/mcp/loaders/promptLoader.js +62 -0
- package/src/mcp/mcp-server.js +129 -0
- package/src/mcp/tools/generateSourceBrainTool.js +179 -0
- package/src/mcp/tools/modifySourceBrainTool.js +283 -0
- package/src/mcp/tools/staticTools.js +29 -0
- package/src/mcp/tools/systemBrain.js +182 -0
- package/src/mcp/utils/mcp-utils.js +30 -0
- package/src/mcp-handler.js +131 -0
- package/src/services/NoMenuService.js +43 -0
- package/src/services/QueryService.js +26 -0
- package/src/services/SourceService.js +32 -0
- package/src/utils/CustomWsTransport.js +52 -0
- package/src/utils/asyncHandler.js +13 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { WebSocketServer } from "ws";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { CustomWsTransport } from "../utils/CustomWsTransport.js";
|
|
9
|
+
|
|
10
|
+
import DatabaseManager from '../database/core/DatabaseManager.js';
|
|
11
|
+
import { AIProcessor } from '../ai/AIProcessor.js';
|
|
12
|
+
import { AIService } from '../ai/AIService.js';
|
|
13
|
+
import { SourceService } from '../services/SourceService.js';
|
|
14
|
+
import { generateSourceBrainTool } from './tools/generateSourceBrainTool.js';
|
|
15
|
+
import { systemBrainTool } from './mcp/tools/systemBrain.js';
|
|
16
|
+
|
|
17
|
+
export async function bootstrap() {
|
|
18
|
+
if (!process.env.DB_TYPE) {
|
|
19
|
+
throw new Error(".env 파일의 DB_TYPE을 읽을 수 없습니다.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const db = new DatabaseManager({
|
|
23
|
+
type: process.env.DB_TYPE,
|
|
24
|
+
host: process.env.DB_HOST,
|
|
25
|
+
port: Number(process.env.DB_PORT),
|
|
26
|
+
user: process.env.DB_USER,
|
|
27
|
+
password: process.env.DB_PASS,
|
|
28
|
+
database: process.env.DB_NAME,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const ai = new AIProcessor(process.env.GEMINI_API_KEY, process.env.GEMINI_MODEL);
|
|
32
|
+
const aiService = new AIService(db, ai);
|
|
33
|
+
const sourceService = new SourceService(aiService);
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const initPrompt = (ai) => {
|
|
37
|
+
const promptPath = path.join(process.cwd(), 'prompts');
|
|
38
|
+
|
|
39
|
+
// 하위 폴더를 재귀적으로 탐색하되, 파일명만 사용하는 내부 함수
|
|
40
|
+
const scanAndRegister = (dir) => {
|
|
41
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
42
|
+
|
|
43
|
+
items.forEach((item) => {
|
|
44
|
+
const fullPath = path.join(dir, item.name);
|
|
45
|
+
|
|
46
|
+
if (item.isDirectory()) {
|
|
47
|
+
// 폴더면 안으로 더 들어감 (이름은 전달하지 않음)
|
|
48
|
+
scanAndRegister(fullPath);
|
|
49
|
+
} else if (item.isFile() && path.extname(item.name).toLowerCase() === '.md') {
|
|
50
|
+
// 파일이면 확장자를 뺀 순수 파일명만 추출
|
|
51
|
+
const chainName = path.parse(item.name).name;
|
|
52
|
+
const prompt = fs.readFileSync(fullPath, 'utf-8');
|
|
53
|
+
|
|
54
|
+
if (chainName.includes('.') && !chainName.includes('.' + db.type)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//ai.registerChain(chainName, prompt);
|
|
59
|
+
//const parentDirName = path.basename(dir);
|
|
60
|
+
|
|
61
|
+
ai.registerChain(chainName, prompt, 'json');
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
if (parentDirName === 'source') {
|
|
65
|
+
// 상위 폴더가 'source'라면 MyBatis, Service 등 순수 소스 코드 추출용이므로 'text'로 등록
|
|
66
|
+
ai.registerChain(chainName, prompt, 'text');
|
|
67
|
+
console.log(` > [Source Chain Loaded (TEXT)]: ${chainName}`);
|
|
68
|
+
} else {
|
|
69
|
+
// 그 외의 일반 관제탑 분석용 프롬프트는 기존대로 'json'으로 등록
|
|
70
|
+
ai.registerChain(chainName, prompt, 'json');
|
|
71
|
+
console.log(` > [Standard Chain Loaded (JSON)]: ${chainName}`);
|
|
72
|
+
}
|
|
73
|
+
//console.log(` > [Chain Loaded]: ${chainName}`); */
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(promptPath)) {
|
|
80
|
+
scanAndRegister(promptPath);
|
|
81
|
+
console.log("✅ 모든 AI 프롬프트 체인이 로드되었습니다.");
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error("❌ 프롬프트 로드 실패:", err.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// AI 체인 등록
|
|
90
|
+
initPrompt(ai);
|
|
91
|
+
|
|
92
|
+
const server = new McpServer(
|
|
93
|
+
{
|
|
94
|
+
name: "nine-mcp",
|
|
95
|
+
version: "1.0.0"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
capabilities: {
|
|
99
|
+
tools: {},
|
|
100
|
+
logging: {} // 이 스펙이 켜져 있으면, 클라이언트의 setLevel 요청을 받아 로그 파이프를 열어줍니다.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* [Wrapper] 모든 도구 실행 시 결과를 MCP 규격 객체({ content: [...] })로 감싸주는 헬퍼 함수
|
|
107
|
+
*/
|
|
108
|
+
const safeExecute = async (fn) => {
|
|
109
|
+
try {
|
|
110
|
+
const result = await fn();
|
|
111
|
+
|
|
112
|
+
// 최종 리턴값을 문자열로 안전하게 변환 (객체나 배열이면 stringify)
|
|
113
|
+
const textResult = typeof result === 'string'
|
|
114
|
+
? result
|
|
115
|
+
: JSON.stringify(result, null, 2);
|
|
116
|
+
|
|
117
|
+
// 💡 [핵심] MCP 표준 응답 객체 규격에 맞춰 감싸서 반환합니다.
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: textResult
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error("Tool Error:", error);
|
|
128
|
+
|
|
129
|
+
// 에러가 났을 때도 MCP 규격에 맞춰 반환하되, isError 플래그 지정 가능
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: JSON.stringify({ success: false, error: error.message })
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
isError: true
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// bootstrap.js 또는 tool 등록 루프 내부
|
|
143
|
+
server.tool(
|
|
144
|
+
"system-brain",
|
|
145
|
+
"의도 분석 및 자동 실행 관제",
|
|
146
|
+
{
|
|
147
|
+
chat_history: z.string(),
|
|
148
|
+
user_input: z.string(),
|
|
149
|
+
current_path: z.string(),
|
|
150
|
+
routes: z.any(), // 메뉴 지도는 클라이언트가 관리하는 경우 전달 받음
|
|
151
|
+
},
|
|
152
|
+
async (params, context) => {
|
|
153
|
+
return await safeExecute(async () => {
|
|
154
|
+
|
|
155
|
+
const schemaSummary = await db.getTableSchemaSummary();
|
|
156
|
+
//const { selected_tables } = await this.ai.filterTables(question, schemaSummary);
|
|
157
|
+
|
|
158
|
+
console.log(schemaSummary);
|
|
159
|
+
|
|
160
|
+
const enrichedParams = {
|
|
161
|
+
...params,
|
|
162
|
+
schema_summary: schemaSummary,
|
|
163
|
+
tools: nineTools.map(t => ({
|
|
164
|
+
name: t.name,
|
|
165
|
+
description: t.description,
|
|
166
|
+
schema: t.schema
|
|
167
|
+
}))
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// 1. 관제탑(system-brain)에게 먼저 물어봄
|
|
171
|
+
const brainResult = await ai.runChain("system-brain", enrichedParams);
|
|
172
|
+
const decision = typeof brainResult === 'string' ? JSON.parse(brainResult) : brainResult;
|
|
173
|
+
|
|
174
|
+
console.log(decision);
|
|
175
|
+
// 💡 전송 함수를 실행하기 전에, 서버 객체가 이 세션을 진짜 열어둔 게 맞는지 눈으로 확인
|
|
176
|
+
|
|
177
|
+
// 2. [내부 루프] 정보가 충분하고 다른 툴을 실행해야 하는 경우
|
|
178
|
+
if (decision.intent === "EXECUTE_TOOL" && decision.action?.selected_tool) {
|
|
179
|
+
|
|
180
|
+
await context.sendNotification({
|
|
181
|
+
method: "notifications/logging/message",
|
|
182
|
+
params: {
|
|
183
|
+
level: "info",
|
|
184
|
+
logger: "system-brain",
|
|
185
|
+
message: decision.message,
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
console.log(`🔄 [Auto-Chain]: ${decision.action.selected_tool} 실행`);
|
|
190
|
+
|
|
191
|
+
// 🛠️ 안전한 하위 툴 체이닝 파라미터 전달 방식
|
|
192
|
+
const toolParams = {
|
|
193
|
+
...enrichedParams, // 원본 컨텍스트(routes, schema_summary 등)를 기본으로 깔고
|
|
194
|
+
...decision.action.params // AI가 가공하거나 수정한 파라미터로 덮어쓰기
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const rawToolResult = await ai.runChain(decision.action.selected_tool, toolParams);
|
|
198
|
+
|
|
199
|
+
console.log(rawToolResult);
|
|
200
|
+
|
|
201
|
+
let processedResult = rawToolResult;
|
|
202
|
+
if (typeof rawToolResult === 'string') {
|
|
203
|
+
try {
|
|
204
|
+
processedResult = JSON.parse(rawToolResult);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
processedResult = { message: rawToolResult, data: null };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 💡 [핵심] 하위 툴이 정성껏 작성한 진짜 분석 이유(message)와 결과(data)를 그대로 살려서 반환합니다!
|
|
211
|
+
return {
|
|
212
|
+
intent: "EXECUTE_TOOL",
|
|
213
|
+
selected_tool: decision.action.selected_tool,
|
|
214
|
+
action: {
|
|
215
|
+
//selected_tool: decision.action.selected_tool,
|
|
216
|
+
//params: decision.action.params
|
|
217
|
+
},
|
|
218
|
+
message: processedResult.message, // 👈 AI 아키텍트가 작성한 진짜 분석 근거 멘트가 들어감
|
|
219
|
+
data: processedResult.data // 👈 신규 통합 라우트 리스트 데이터가 들어감
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
return decision;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
server.tool(
|
|
230
|
+
"generate-source-brain",
|
|
231
|
+
"의도 분석 및 자동 실행 관제",
|
|
232
|
+
{
|
|
233
|
+
user_input: z.string(),
|
|
234
|
+
//asis_source: z.string(),
|
|
235
|
+
routes: z.any(), // 메뉴 지도는 클라이언트가 관리하는 경우 전달 받음
|
|
236
|
+
},
|
|
237
|
+
async (params, context) => {
|
|
238
|
+
return await safeExecute(async () => {
|
|
239
|
+
|
|
240
|
+
const schemaSummary = await db.getTableSchemaSummary();
|
|
241
|
+
//const { selected_tables } = await this.ai.filterTables(question, schemaSummary);
|
|
242
|
+
|
|
243
|
+
console.log(schemaSummary);
|
|
244
|
+
|
|
245
|
+
const enrichedParams = {
|
|
246
|
+
...params,
|
|
247
|
+
schema_summary: schemaSummary
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// 3. 초경량화된 source-generator-brain 체인 구동
|
|
251
|
+
const rawToolResult = await ai.runChain("generate-source-brain", enrichedParams);
|
|
252
|
+
|
|
253
|
+
let processedResult = rawToolResult;
|
|
254
|
+
if (typeof rawToolResult === 'string') {
|
|
255
|
+
try {
|
|
256
|
+
processedResult = JSON.parse(rawToolResult);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
processedResult = { message: rawToolResult, data: null };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log("=== [3] AI 원시 응답 수신 ===");
|
|
263
|
+
console.log(processedResult);
|
|
264
|
+
|
|
265
|
+
// 4. JSON 파싱 및 결과 구조 검증 테스트
|
|
266
|
+
//try {
|
|
267
|
+
const decision = processedResult;
|
|
268
|
+
|
|
269
|
+
console.log("=== [4] JSON 파싱 성공 및 구조 분석 ===");
|
|
270
|
+
console.log(`▶ Intent 확인: ${decision}`);
|
|
271
|
+
|
|
272
|
+
if (decision.intent === "EXECUTE_BATCH") {
|
|
273
|
+
|
|
274
|
+
await context.sendNotification({
|
|
275
|
+
method: "notifications/logging/message",
|
|
276
|
+
params: {
|
|
277
|
+
level: "info",
|
|
278
|
+
logger: "generate-source-brain",
|
|
279
|
+
message: decision.message,
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const batchList = decision.action?.batchList || [];
|
|
284
|
+
console.log(`▶ 매핑 완료된 메뉴 개수: ${batchList.length}개`);
|
|
285
|
+
// console.table(batchList); // 테이블 형태로 콘솔에 예쁘게 출력
|
|
286
|
+
|
|
287
|
+
const schema = await db.getTableSchema();
|
|
288
|
+
console.log(schema);
|
|
289
|
+
|
|
290
|
+
const resultType = "com.ninelab.ai.util.CamelCaseMap";
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
let i = 0;
|
|
294
|
+
|
|
295
|
+
for (let batch of batchList) {
|
|
296
|
+
const filteredSchema = schema.filter(table => batch.tableIds.includes(table.tableName));
|
|
297
|
+
|
|
298
|
+
console.log(batch);
|
|
299
|
+
const formattedPath = (batch?.path || "")
|
|
300
|
+
.replaceAll("/", ".") // 슬래시를 점으로 변경
|
|
301
|
+
.replaceAll("-", "_"); // ★ 하이픈(-)을 언더바(_)로 강제 치환하여 자바/MyBatis 문법 에러 방지
|
|
302
|
+
|
|
303
|
+
const namespace = "ninelab" + (formattedPath.startsWith(".") ? formattedPath : "." + formattedPath);
|
|
304
|
+
|
|
305
|
+
const toolParams = {
|
|
306
|
+
...params,
|
|
307
|
+
db_type: db.type,
|
|
308
|
+
asis_source: "",
|
|
309
|
+
menu_description: batch.description,
|
|
310
|
+
"schema_detail": filteredSchema,
|
|
311
|
+
"result_type": resultType,
|
|
312
|
+
"namespace": namespace
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
console.log(toolParams);
|
|
316
|
+
|
|
317
|
+
const rawToolResult = await ai.runChain("generate-source-mapper." + db.type, toolParams);
|
|
318
|
+
console.log(rawToolResult.full_source);
|
|
319
|
+
|
|
320
|
+
await context.sendNotification({
|
|
321
|
+
method: "notifications/logging/message",
|
|
322
|
+
params: {
|
|
323
|
+
level: "info",
|
|
324
|
+
logger: "generate-source-brain",
|
|
325
|
+
|
|
326
|
+
// 1. 💬 화면 로그 컴포넌트에 깔끔하게 찍힐 텍스트
|
|
327
|
+
message: `▶ [${batch.description}] 소스 일괄 생성 완료 (${rawToolResult.file_name})`,
|
|
328
|
+
|
|
329
|
+
// 2. 📦 프론트엔드 에디터(TO-BE)나 탭 매니저가 가로채서 처리할 실제 소스 데이터
|
|
330
|
+
data: {
|
|
331
|
+
status: "PROGRESS",
|
|
332
|
+
menu: batch.description,
|
|
333
|
+
path: batch.path,
|
|
334
|
+
mode: rawToolResult.mode,
|
|
335
|
+
file_name: rawToolResult.file_name,
|
|
336
|
+
source: rawToolResult.full_source
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (i++ > 1) break;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
const toolParams = {
|
|
345
|
+
...enrichedParams, // 원본 컨텍스트(routes, schema_summary 등)를 기본으로 깔고
|
|
346
|
+
...decision.action.params // AI가 가공하거나 수정한 파라미터로 덮어쓰기
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const rawToolResult = await ai.runChain(decision.action.selected_tool, toolParams);
|
|
350
|
+
|
|
351
|
+
console.log(rawToolResult);
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
let processedResult = rawToolResult;
|
|
355
|
+
if (typeof rawToolResult === 'string') {
|
|
356
|
+
try {
|
|
357
|
+
processedResult = JSON.parse(rawToolResult);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
processedResult = { message: rawToolResult, data: null };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
*/
|
|
363
|
+
|
|
364
|
+
// 💡 [핵심] 하위 툴이 정성껏 작성한 진짜 분석 이유(message)와 결과(data)를 그대로 살려서 반환합니다!
|
|
365
|
+
return {
|
|
366
|
+
success: true,
|
|
367
|
+
intent: "NONE",
|
|
368
|
+
message: decision.message
|
|
369
|
+
};
|
|
370
|
+
} else {
|
|
371
|
+
console.log(`⚠ 매핑 실패 또는 일반 대화 상태입니다. Message: ${brainJson.message}`);
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
intent: "NONE",
|
|
375
|
+
message: decision.message
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// } catch (parseError) {
|
|
380
|
+
// console.error("❌ AI가 올바른 JSON 포맷을 반환하지 않았습니다. 프롬프트 Output Protocol을 점검하세요.");
|
|
381
|
+
// return {
|
|
382
|
+
// success: false,
|
|
383
|
+
// error: "JSON_PARSE_ERROR",
|
|
384
|
+
// raw: parseError
|
|
385
|
+
// };
|
|
386
|
+
// }
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
const nineTools = [
|
|
395
|
+
{
|
|
396
|
+
name: "generate-menu",
|
|
397
|
+
description: [
|
|
398
|
+
"기존 라우트(Route) 설정과 DB 스키마를 분석하여 미구현 소스 목록을 도출합니다.",
|
|
399
|
+
"또한, 사용자의 자연어 지시(메뉴나 라우터 생성/수정/삭제)를 반영하여",
|
|
400
|
+
"메뉴 트리 구조 및 라우터 설정을 재설계(Refactoring)하고 최종 통합 Route 리스트(JSON)를 새로 작성합니다."
|
|
401
|
+
].join(" "),
|
|
402
|
+
schema: { user_input: z.string(), routes: z.any(), schema_summary: z.any() }
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: "generate-query",
|
|
406
|
+
description: "자연어 기반 SQL 생성 및 분석",
|
|
407
|
+
schema: { user_input: z.string(), routes: z.any() }
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: "generate-source",
|
|
411
|
+
description: "테이블 정보를 바탕으로 소스 코드를 생성합니다.",
|
|
412
|
+
schema: { user_input: z.string(), routes: z.any() }
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: "generate-source-controller",
|
|
416
|
+
description: "테이블 정보를 바탕으로 소스 코드를 생성합니다.",
|
|
417
|
+
schema: { user_input: z.string(), routes: z.any() }
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "generate-source-service",
|
|
421
|
+
description: "테이블 정보를 바탕으로 소스 코드를 생성합니다.",
|
|
422
|
+
schema: { user_input: z.string(), routes: z.any() }
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "generate-source-mapper." + db.type,
|
|
426
|
+
description: "테이블 정보를 바탕으로 소스 코드를 생성합니다.",
|
|
427
|
+
schema: { user_input: z.string(), routes: z.any() }
|
|
428
|
+
}
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
// ai.getChain(name) 같은 메서드가 있다는 가정하에
|
|
432
|
+
nineTools.forEach(tool => {
|
|
433
|
+
server.tool(
|
|
434
|
+
tool.name,
|
|
435
|
+
tool.description,
|
|
436
|
+
tool.schema,
|
|
437
|
+
async (params) => {
|
|
438
|
+
return await safeExecute(async () => {
|
|
439
|
+
// tool.name 변수를 그대로 사용하므로 하드코딩이 사라집니다!
|
|
440
|
+
console.log(`🚀 [Tool Execution]: ${tool.name}`, params);
|
|
441
|
+
|
|
442
|
+
const result = await ai.runChain(tool.name, params);
|
|
443
|
+
|
|
444
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
// 6. DB 연결 및 서버 기동
|
|
452
|
+
try {
|
|
453
|
+
await db.connect();
|
|
454
|
+
console.log("데이터베이스 연결 성공");
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error("서버 기동 실패:", error.message);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 서버 기동
|
|
461
|
+
const app = express();
|
|
462
|
+
app.use(cors());
|
|
463
|
+
app.use(express.json());
|
|
464
|
+
|
|
465
|
+
const PORT = Number(process.env.SERVER_PORT) || 4001;
|
|
466
|
+
|
|
467
|
+
return new Promise((resolve, reject) => {
|
|
468
|
+
try {
|
|
469
|
+
const httpServer = app.listen(PORT, () => {
|
|
470
|
+
console.log(`🚀 Nine MCP Engine ON: ws://localhost:${PORT}`);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
474
|
+
|
|
475
|
+
wss.on("connection", async (ws) => {
|
|
476
|
+
console.log("🔌 새로운 클라이언트 연결됨");
|
|
477
|
+
//const transport = new CustomWsTransport(ws);
|
|
478
|
+
try {
|
|
479
|
+
//global.activeWs = ws;
|
|
480
|
+
global.rawSocket = ws; // 👈 MCP 규격 안 거치는 생 소켓 창고
|
|
481
|
+
|
|
482
|
+
const transport = new CustomWsTransport(ws);
|
|
483
|
+
|
|
484
|
+
await server.connect(transport);
|
|
485
|
+
console.log("✅ MCP 프로토콜 핸드셰이크 완료");
|
|
486
|
+
} catch (connErr) {
|
|
487
|
+
console.error("❌ MCP 연결 실패:", connErr);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
resolve(httpServer);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
reject(err);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Role
|
|
2
|
+
너는 사용자의 의도를 분석하여 가장 적절한 시스템 경로(Path)를 찾아주는 내비게이션 전문가야.
|
|
3
|
+
|
|
4
|
+
# Task
|
|
5
|
+
제공된 [Route 정보]를 바탕으로 사용자의 질문에 가장 부합하는 경로를 선택해줘.
|
|
6
|
+
|
|
7
|
+
# Constraints
|
|
8
|
+
1. 반드시 결과는 JSON 형식으로만 응답해.
|
|
9
|
+
2. 선택한 경로가 리스트에 없다면 path를 null로 리턴해.
|
|
10
|
+
3. 'reason' 필드에는 왜 이 경로를 선택했는지 짧게 설명해줘.
|
|
11
|
+
|
|
12
|
+
# Route 정보
|
|
13
|
+
{route_context}
|
|
14
|
+
|
|
15
|
+
# User Question
|
|
16
|
+
{question}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Role: nine-mu Strategic Orchestrator
|
|
2
|
+
당신은 시스템의 관제탑입니다. 사용자의 요청을 분석하여 적절한 **내부 툴 실행(EXECUTE_TOOL)**, 부족한 데이터에 대한 **데이터 요청(DATA_REQUEST)**, 또는 일반 답변(**NONE**)을 결정합니다.
|
|
3
|
+
|
|
4
|
+
# Context
|
|
5
|
+
- Maps: {routes}
|
|
6
|
+
- Tools: {tools}
|
|
7
|
+
- Path: {current_path}
|
|
8
|
+
- Data: {schema_summary}
|
|
9
|
+
|
|
10
|
+
# Chat History (이전 대화 기록)
|
|
11
|
+
{chat_history}
|
|
12
|
+
|
|
13
|
+
# Execution
|
|
14
|
+
사용자의 실시간 요청: "{user_input}"
|
|
15
|
+
|
|
16
|
+
# Data Vocabulary (required_args 표준)
|
|
17
|
+
- **SOURCE**, **DDL**, **QUERY_RESULT**, **API_SPEC**
|
|
18
|
+
|
|
19
|
+
# Strategic Reasoning Logic
|
|
20
|
+
1. **툴 매칭 우선**: 사용자의 입력이 제공된 `Tools` 중 특정 툴의 목적/설명과 명확히 부합하는 경우에만 해당 툴의 실행을 검토하십시오. 매칭되는 툴이 없다면 즉시 `intent`를 `"NONE"`으로 결정하십시오.
|
|
21
|
+
2. **파라미터 검증**: 1번에서 매칭된 툴이 실행되기 위해 필요한 **필수 파라미터 데이터**가 현재 `Context`에 누락되어 있다면, 절대 실행하지 말고 `intent`를 `"DATA_REQUEST"`로 설정하십시오.
|
|
22
|
+
3. **메시지 생성 규칙 (중요)**: `message`는 예시 문장을 그대로 베끼지 말고, **현재 사용자의 질문과 상황(Path, Data 상태)에 맞춰 매번 동적으로 자연스럽게 작성**하십시오.
|
|
23
|
+
- `DATA_REQUEST`인 경우: 필요한 데이터(예: DDL, SOURCE 등)가 무엇인지 명확히 짚어주며 요청하십시오.
|
|
24
|
+
- `NONE`인 경우: 사용자의 친근한 대화에 위트 있고 자연스럽게 응답하십시오.
|
|
25
|
+
|
|
26
|
+
# Output Protocol (JSON Only)
|
|
27
|
+
상황에 맞는 최적의 JSON 구조 하나만 선택하여 응답하십시오. 다른 텍스트는 절대 포함하지 마십시오.
|
|
28
|
+
|
|
29
|
+
### Case 1: 일반 대화 및 매칭 실패 (NONE)
|
|
30
|
+
{{
|
|
31
|
+
"intent": "NONE",
|
|
32
|
+
"current_path": "{current_path}",
|
|
33
|
+
"target_path": "",
|
|
34
|
+
"action": null,
|
|
35
|
+
"message": "[동적 생성: 사용자의 컨텍스트와 입력에 맞춘 자연스러운 답변 및 인사말]"
|
|
36
|
+
}}
|
|
37
|
+
|
|
38
|
+
### Case 2: 즉시 실행 (EXECUTE_TOOL)
|
|
39
|
+
{{
|
|
40
|
+
"intent": "EXECUTE_TOOL",
|
|
41
|
+
"current_path": "{current_path}",
|
|
42
|
+
"target_path": "[동적 생성: 대상 경로]",
|
|
43
|
+
"action": {{
|
|
44
|
+
"selected_tool": "[동적 생성: 선택된 툴 이름]",
|
|
45
|
+
"params": {{ "[필수 인자명]": "[현재 Context의 실제 데이터]" }},
|
|
46
|
+
"required_args": []
|
|
47
|
+
}},
|
|
48
|
+
"message": "[동적 생성: 실행할 작업에 대한 명확한 안내 문구]"
|
|
49
|
+
}}
|
|
50
|
+
|
|
51
|
+
### Case 3: 데이터 부족 (DATA_REQUEST)
|
|
52
|
+
{{
|
|
53
|
+
"intent": "DATA_REQUEST",
|
|
54
|
+
"current_path": "{current_path}",
|
|
55
|
+
"target_path": "[동적 생성: 대상 경로]",
|
|
56
|
+
"action": {{
|
|
57
|
+
"selected_tool": "[동적 생성: 선택된 툴 이름]",
|
|
58
|
+
"params": {{ "context": "[동적 생성: 현재 작업 맥락]" }},
|
|
59
|
+
"required_args": ["[요구할 데이터 키워드]"]
|
|
60
|
+
}},
|
|
61
|
+
"message": "[동적 생성: 어떤 데이터가 왜 필요한지 설명하고 전송을 유도하는 자연스러운 문구]"
|
|
62
|
+
}}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
### TABLE_FILTER_PROMPT
|
|
2
|
+
당신은 SQL 전문가입니다. 사용자의 질문을 분석하여 데이터베이스에서 조회해야 할 필수 테이블들만 선정하세요.
|
|
3
|
+
|
|
4
|
+
[전체 테이블 목록]
|
|
5
|
+
{schema_summary}
|
|
6
|
+
|
|
7
|
+
[사용자 질문]
|
|
8
|
+
"{question}"
|
|
9
|
+
|
|
10
|
+
[응답 규칙]
|
|
11
|
+
1. 반드시 JSON 형식으로만 응답하세요.
|
|
12
|
+
2. 질문을 해결하기 위해 반드시 참조해야 하는 테이블 이름만 'selected_tables' 배열에 넣으세요.
|
|
13
|
+
3. 만약 질문이 데이터베이스 조회와 관련이 없거나, 제공된 테이블 정보로 답할 수 없다면 'is_executable'을 false로 설정하세요.
|
|
14
|
+
4. 'reasoning' 필드에는 왜 해당 테이블들을 선택했는지 간략하게 설명하세요.
|
|
15
|
+
|
|
16
|
+
[응답 포맷]
|
|
17
|
+
{{
|
|
18
|
+
"reasoning": "선정 이유를 여기에 작성",
|
|
19
|
+
"selected_tables": ["table1", "table2"],
|
|
20
|
+
"is_executable": true
|
|
21
|
+
}}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ninebone/mcp",
|
|
3
|
+
"version": "0.1.26",
|
|
4
|
+
"description": "NineQuery AI Connector for Database",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nine-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"dev": "node --watch src/index.js",
|
|
13
|
+
"build": "echo '빌드가 필요한 프로젝트라면 여기에 빌드 명령 작성'",
|
|
14
|
+
"release": "npm version patch && npm run build && npm publish"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@langchain/core": "^1.1.44",
|
|
18
|
+
"@langchain/google-genai": "^2.1.30",
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
|
+
"@nine-lab/nine-util": "^0.9.35",
|
|
21
|
+
"cors": "^2.8.6",
|
|
22
|
+
"dotenv": "^16.4.5",
|
|
23
|
+
"express": "^4.22.1",
|
|
24
|
+
"mariadb": "^3.5.2",
|
|
25
|
+
"mysql2": "^3.9.7",
|
|
26
|
+
"oracledb": "^6.10.0",
|
|
27
|
+
"pg": "^8.11.5",
|
|
28
|
+
"ws": "^8.20.1"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|