@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,115 @@
|
|
|
1
|
+
import PoolManager from './PoolManager.js';
|
|
2
|
+
import Dialects from './Dialects.js'; // 쿼리 사전 임포트
|
|
3
|
+
|
|
4
|
+
class DatabaseManager {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.pool = null;
|
|
8
|
+
this.type = config.type?.toLowerCase();
|
|
9
|
+
this.dialect = Dialects[this.type];
|
|
10
|
+
|
|
11
|
+
if (!this.dialect) {
|
|
12
|
+
throw new Error(`Unsupported Dialect for: ${this.type}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* DatabaseFactory의 init() 역할을 수행합니다.
|
|
18
|
+
*/
|
|
19
|
+
// DatabaseManager.js 내부
|
|
20
|
+
async connect() {
|
|
21
|
+
try {
|
|
22
|
+
//console.info(this.config);
|
|
23
|
+
// 이제 createPool이 async이므로 await를 붙입니다.
|
|
24
|
+
this.pool = await PoolManager.createPool(this.config);
|
|
25
|
+
|
|
26
|
+
// 연결 확인
|
|
27
|
+
await this.query(this.dialect.testQuery || 'SELECT 1');
|
|
28
|
+
console.info(`[${this.type}] Database connection successful.`);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`[${this.type}] Connection failed:`, error.message);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* DB별 결과 포맷 차이를 여기서 해결합니다. (데이터 배열만 반환)
|
|
37
|
+
*/
|
|
38
|
+
// DatabaseManager.js 수정
|
|
39
|
+
async query(sql, params = []) {
|
|
40
|
+
if (!this.pool) throw new Error("Database not connected.");
|
|
41
|
+
|
|
42
|
+
const result = await this.pool.query(sql, params);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
if (this.type === 'postgres') {
|
|
46
|
+
return result.rows;
|
|
47
|
+
} else {
|
|
48
|
+
// 만약 result[0]이 또 배열이라면(mysql2 이중 배열), 0번 인덱스를 반환
|
|
49
|
+
// 아니라면 result를 그대로 반환
|
|
50
|
+
return (Array.isArray(result) && Array.isArray(result[0]))
|
|
51
|
+
? result[0]
|
|
52
|
+
: result;
|
|
53
|
+
} */
|
|
54
|
+
if (this.type === 'postgres' || this.type === 'postgresql') {
|
|
55
|
+
return result.rows;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MariaDB/MySQL 대응:
|
|
59
|
+
// DDL(INSERT/UPDATE) 결과와 DQL(SELECT) 결과를 구분해야 함
|
|
60
|
+
if (Array.isArray(result)) {
|
|
61
|
+
// mysql2 대응: rows가 0번에 담겨오는 경우
|
|
62
|
+
if (Array.isArray(result[0])) return result[0];
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* AI 가공을 위해 스키마 데이터를 구조화합니다.
|
|
71
|
+
* 결과 예시:
|
|
72
|
+
* {
|
|
73
|
+
* "orders": {
|
|
74
|
+
* "description": "주문 기본 정보 테이블",
|
|
75
|
+
* "columns": [{ "column": "id", "type": "int" }, ...]
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
*/
|
|
79
|
+
async getTableSchema() {
|
|
80
|
+
const rows = await this.query(this.dialect.schemaQuery);
|
|
81
|
+
|
|
82
|
+
// 1. 우선 객체 형태로 그룹화 (중복 방지 및 효율적 정리를 위해)
|
|
83
|
+
const grouped = rows.reduce((acc, row) => {
|
|
84
|
+
const { table_name, table_comment, column_name, data_type } = row;
|
|
85
|
+
|
|
86
|
+
if (!acc[table_name]) {
|
|
87
|
+
acc[table_name] = {
|
|
88
|
+
tableName: table_name, // 배열로 변환 시 식별을 위해 추가
|
|
89
|
+
description: table_comment || "",
|
|
90
|
+
columns: []
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
acc[table_name].columns.push({
|
|
95
|
+
column: column_name,
|
|
96
|
+
type: data_type
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return acc;
|
|
100
|
+
}, {});
|
|
101
|
+
|
|
102
|
+
// 2. 최종적으로 객체의 값들만 뽑아서 배열로 변환
|
|
103
|
+
return Object.values(grouped);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getTableSchemaSummary() {
|
|
107
|
+
const schema = await this.getTableSchema();
|
|
108
|
+
return schema.map(table => ({
|
|
109
|
+
tableName: table.tableName,
|
|
110
|
+
description: table.description
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default DatabaseManager;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const Dialects = {
|
|
2
|
+
// MySQL 추가 (MariaDB와 동일)
|
|
3
|
+
mysql: {
|
|
4
|
+
testQuery: 'SELECT 1',
|
|
5
|
+
schemaQuery: `
|
|
6
|
+
SELECT
|
|
7
|
+
c.TABLE_NAME as table_name,
|
|
8
|
+
t.TABLE_COMMENT as table_comment,
|
|
9
|
+
c.COLUMN_NAME as column_name,
|
|
10
|
+
c.DATA_TYPE as data_type
|
|
11
|
+
FROM information_schema.columns c
|
|
12
|
+
INNER JOIN information_schema.tables t
|
|
13
|
+
ON c.TABLE_NAME = t.TABLE_NAME
|
|
14
|
+
AND c.TABLE_SCHEMA = t.TABLE_SCHEMA
|
|
15
|
+
WHERE c.TABLE_SCHEMA = DATABASE()
|
|
16
|
+
ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`
|
|
17
|
+
},
|
|
18
|
+
mariadb: {
|
|
19
|
+
testQuery: 'SELECT 1',
|
|
20
|
+
schemaQuery: `
|
|
21
|
+
SELECT
|
|
22
|
+
c.TABLE_NAME as table_name,
|
|
23
|
+
t.TABLE_COMMENT as table_comment,
|
|
24
|
+
c.COLUMN_NAME as column_name,
|
|
25
|
+
c.DATA_TYPE as data_type
|
|
26
|
+
FROM information_schema.columns c
|
|
27
|
+
INNER JOIN information_schema.tables t
|
|
28
|
+
ON c.TABLE_NAME = t.TABLE_NAME
|
|
29
|
+
AND c.TABLE_SCHEMA = t.TABLE_SCHEMA
|
|
30
|
+
WHERE c.TABLE_SCHEMA = DATABASE()
|
|
31
|
+
ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`
|
|
32
|
+
},
|
|
33
|
+
postgres: {
|
|
34
|
+
testQuery: 'SELECT 1',
|
|
35
|
+
schemaQuery: `
|
|
36
|
+
SELECT
|
|
37
|
+
t.table_name,
|
|
38
|
+
(SELECT obj_description(pgc.oid, 'pg_class')
|
|
39
|
+
FROM pg_class pgc
|
|
40
|
+
WHERE pgc.relname = t.table_name
|
|
41
|
+
AND pgc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = t.table_schema)
|
|
42
|
+
) AS table_comment,
|
|
43
|
+
c.column_name,
|
|
44
|
+
c.data_type
|
|
45
|
+
FROM information_schema.tables t
|
|
46
|
+
JOIN information_schema.columns c ON t.table_name = c.table_name AND t.table_schema = c.table_schema
|
|
47
|
+
WHERE t.table_schema = CURRENT_SCHEMA() -- 현재 스키마 자동 대응
|
|
48
|
+
ORDER BY t.table_name, c.ordinal_position`
|
|
49
|
+
},
|
|
50
|
+
oracle: {
|
|
51
|
+
testQuery: 'SELECT 1 FROM DUAL',
|
|
52
|
+
schemaQuery: `
|
|
53
|
+
SELECT
|
|
54
|
+
t.TABLE_NAME as table_name,
|
|
55
|
+
tc.COMMENTS as table_comment,
|
|
56
|
+
c.COLUMN_NAME as column_name,
|
|
57
|
+
c.DATA_TYPE as data_type
|
|
58
|
+
FROM ALL_TABLES t
|
|
59
|
+
JOIN ALL_TAB_COLUMNS c ON t.TABLE_NAME = c.TABLE_NAME AND t.OWNER = c.OWNER
|
|
60
|
+
LEFT JOIN ALL_TAB_COMMENTS tc ON t.TABLE_NAME = tc.TABLE_NAME AND t.OWNER = tc.OWNER
|
|
61
|
+
WHERE t.OWNER = (SELECT USER FROM DUAL) -- 현재 접속한 스키마만 조회
|
|
62
|
+
ORDER BY t.TABLE_NAME, c.COLUMN_ID`
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default Dialects;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as mariadb from 'mariadb';
|
|
2
|
+
import pg from 'pg';
|
|
3
|
+
//import oracledb from 'oracledb';
|
|
4
|
+
|
|
5
|
+
class PoolManager {
|
|
6
|
+
/**
|
|
7
|
+
* DB 타입에 맞는 커넥션 풀을 생성하여 반환합니다.
|
|
8
|
+
*/
|
|
9
|
+
static createPool(config) {
|
|
10
|
+
const type = config?.type?.toLowerCase();
|
|
11
|
+
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'mariadb':
|
|
14
|
+
case 'mysql':
|
|
15
|
+
return this.#createMariaDBPool(config);
|
|
16
|
+
case 'postgres':
|
|
17
|
+
case 'postgresql':
|
|
18
|
+
return this.#createPostgresPool(config);
|
|
19
|
+
case 'oracle':
|
|
20
|
+
return this.#createOraclePool(config); // 추가
|
|
21
|
+
default:
|
|
22
|
+
throw new Error(`[PoolManager] Unsupported DB_TYPE: ${type}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static async #createOraclePool(config) {
|
|
27
|
+
try {
|
|
28
|
+
const { default: oracledb } = await import('oracledb');
|
|
29
|
+
|
|
30
|
+
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
|
|
31
|
+
oracledb.autoCommit = true;
|
|
32
|
+
|
|
33
|
+
const pool = await oracledb.createPool({
|
|
34
|
+
user: config.user,
|
|
35
|
+
password: config.password,
|
|
36
|
+
connectString: `${config.host}:${config.port}/${config.database}`,
|
|
37
|
+
poolMax: 10,
|
|
38
|
+
poolMin: 2,
|
|
39
|
+
poolIncrement: 1
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 다른 DB들과 사용법을 맞추기 위해 래핑(Wrapping)
|
|
43
|
+
return {
|
|
44
|
+
query: async (sql, params = []) => {
|
|
45
|
+
let conn;
|
|
46
|
+
try {
|
|
47
|
+
conn = await pool.getConnection();
|
|
48
|
+
const result = await conn.execute(sql, params);
|
|
49
|
+
return result.rows; // 객체 배열 반환
|
|
50
|
+
} finally {
|
|
51
|
+
if (conn) await conn.close();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
close: () => pool.close()
|
|
55
|
+
};
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error("Oracle Pool 생성 실패:", err.message);
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static #createMariaDBPool(config) {
|
|
63
|
+
return mariadb.createPool({
|
|
64
|
+
host: config.host,
|
|
65
|
+
port: config.port,
|
|
66
|
+
user: config.user,
|
|
67
|
+
password: config.password,
|
|
68
|
+
database: config.database,
|
|
69
|
+
connectionLimit: 10,
|
|
70
|
+
connectTimeout: 10000,
|
|
71
|
+
//ssl: config.ssl || undefined
|
|
72
|
+
acquireTimeout: 10000,
|
|
73
|
+
allowPublicKeyRetrieval: true
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static #createPostgresPool(config) {
|
|
78
|
+
const { Pool } = pg;
|
|
79
|
+
return new Pool({
|
|
80
|
+
host: config.host,
|
|
81
|
+
port: config.port,
|
|
82
|
+
user: config.user,
|
|
83
|
+
password: config.password,
|
|
84
|
+
database: config.database,
|
|
85
|
+
max: 10,
|
|
86
|
+
idleTimeoutMillis: 30000,
|
|
87
|
+
//ssl: config.ssl || undefined
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default PoolManager;
|
|
File without changes
|
package/src/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nine MCP Engine Entry Point
|
|
5
|
+
* mcp-server.js의 bootstrap을 실행하고 초기 설정을 관리합니다.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import 'dotenv/config'; // 환경 변수(.env) 로드
|
|
9
|
+
import { runInit } from './core/init.js'; // 초기 설정 명령어 처리
|
|
10
|
+
import { bootstrap } from './mcp/mcp-server.js'; // 실제 MCP 서버 로직
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 실행 시 'init' 인자가 있으면 설정을 진행하고,
|
|
16
|
+
* 없으면 MCP 서버를 가동합니다.
|
|
17
|
+
*/
|
|
18
|
+
async function main() {
|
|
19
|
+
try {
|
|
20
|
+
if (args.includes('init')) {
|
|
21
|
+
await runInit();
|
|
22
|
+
} else {
|
|
23
|
+
console.log("🚀 Nine MCP Engine 시동 중...");
|
|
24
|
+
// bootstrap이 Promise를 반환하므로 완전히 뜰 때까지 기다림
|
|
25
|
+
await bootstrap();
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("❌ 실행 중 치명적인 오류가 발생했습니다:");
|
|
29
|
+
// ⭐️ 에러의 전체 스택과 상세 정보를 출력
|
|
30
|
+
console.error(error);
|
|
31
|
+
if (error.stack) {
|
|
32
|
+
console.error(error.stack);
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 시동!
|
|
38
|
+
main();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 애플리케이션 프롬프트 마크다운 파일을 스캔하여 AI 체인에 등록합니다.
|
|
6
|
+
* @param {object} ai AIProcessor 인스턴스
|
|
7
|
+
* @param {string} dbType 현재 가동 중인 벤더명 (mysql, oracle, postgresql 등)
|
|
8
|
+
*/
|
|
9
|
+
export function loadApplicationPrompts(ai, dbType) {
|
|
10
|
+
const promptPath = path.join(process.cwd(), 'prompts');
|
|
11
|
+
const targetDb = (dbType || '').toLowerCase().trim();
|
|
12
|
+
const dbSpecificFolder = path.join(promptPath, targetDb);
|
|
13
|
+
|
|
14
|
+
const scanAndRegister = (dir) => {
|
|
15
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
16
|
+
|
|
17
|
+
items.forEach((item) => {
|
|
18
|
+
const fullPath = path.join(dir, item.name);
|
|
19
|
+
|
|
20
|
+
if (item.isDirectory()) {
|
|
21
|
+
// DB 예외 격리 타겟 폴더 자체는 중복 스캔을 방지하기 위해 완전히 패스합니다.
|
|
22
|
+
if (item.name.toLowerCase() === targetDb) return;
|
|
23
|
+
scanAndRegister(fullPath);
|
|
24
|
+
} else if (item.isFile() && path.extname(item.name).toLowerCase() === '.md') {
|
|
25
|
+
const pureName = path.parse(item.name).name;
|
|
26
|
+
|
|
27
|
+
let chainName = pureName;
|
|
28
|
+
|
|
29
|
+
// 파일 이름 자체에 마침표(.)가 낀 서브 소스 자산들은 자동 스킵합니다.
|
|
30
|
+
if (pureName.includes('.')) {
|
|
31
|
+
if (pureName.includes('.' + dbType)) {
|
|
32
|
+
chainName = pureName.split('.')[0];
|
|
33
|
+
} else {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dbSpecificPath = path.join(dbSpecificFolder, `${pureName}.md`);
|
|
39
|
+
|
|
40
|
+
// 전용 격리 폴더 우선 검사 후 텍스트 데이터 로드
|
|
41
|
+
const finalPath = fs.existsSync(dbSpecificPath) ? dbSpecificPath : fullPath;
|
|
42
|
+
const prompt = fs.readFileSync(finalPath, 'utf-8');
|
|
43
|
+
|
|
44
|
+
// 일관된 JSON 프로토콜 구조 파서 할당 등록
|
|
45
|
+
ai.registerChain(chainName, prompt, 'json');
|
|
46
|
+
|
|
47
|
+
const logTag = finalPath === dbSpecificPath ? `[${dbType.toUpperCase()} Dedicated]` : '[Default]';
|
|
48
|
+
console.log(` > [Chain Loaded] ${chainName} ➡️ ${logTag}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
if (fs.existsSync(promptPath)) {
|
|
55
|
+
scanAndRegister(promptPath);
|
|
56
|
+
console.log("✅ 모든 AI 프롬프트 체인이 메모리에 로드되었습니다.");
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error("❌ 프롬프트 로드 실패:", err.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { CustomWsTransport } from "../utils/CustomWsTransport.js";
|
|
6
|
+
|
|
7
|
+
import DatabaseManager from '../database/core/DatabaseManager.js';
|
|
8
|
+
import { AIProcessor } from '../ai/AIProcessor.js';
|
|
9
|
+
import { AIService } from '../ai/AIService.js';
|
|
10
|
+
import { SourceService } from '../services/SourceService.js';
|
|
11
|
+
|
|
12
|
+
// 📦 외부 모듈화 완료된 핵심 컴포넌트들 바인딩
|
|
13
|
+
import { generateSourceBrainTool } from './tools/generateSourceBrainTool.js';
|
|
14
|
+
import { modifySourceBrainTool } from './tools/modifySourceBrainTool.js';
|
|
15
|
+
import { systemBrainTool } from './tools/systemBrain.js';
|
|
16
|
+
import { getStaticToolsConfig } from './tools/staticTools.js';
|
|
17
|
+
import { loadApplicationPrompts } from './loaders/promptLoader.js'; // ★ 분리된 로더 임포트
|
|
18
|
+
import { safeExecute } from './utils/mcp-utils.js';
|
|
19
|
+
|
|
20
|
+
export async function bootstrap() {
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
try {
|
|
24
|
+
const result = {
|
|
25
|
+
full_source: "const reg = /\d+/;"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
console.log(JSON.stringify(result, null, 2));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error(e);
|
|
31
|
+
} */
|
|
32
|
+
|
|
33
|
+
if (!process.env.DB_TYPE) {
|
|
34
|
+
throw new Error(".env 파일의 DB_TYPE을 읽을 수 없습니다.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 1. 코어 인프라 자원 초기화
|
|
38
|
+
const db = new DatabaseManager({
|
|
39
|
+
type: process.env.DB_TYPE,
|
|
40
|
+
host: process.env.DB_HOST,
|
|
41
|
+
port: Number(process.env.DB_PORT),
|
|
42
|
+
user: process.env.DB_USER,
|
|
43
|
+
password: process.env.DB_PASS,
|
|
44
|
+
database: process.env.DB_NAME,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const ai = new AIProcessor(process.env.GEMINI_API_KEY, process.env.GEMINI_MODEL);
|
|
48
|
+
const aiService = new AIService(db, ai);
|
|
49
|
+
const sourceService = new SourceService(aiService);
|
|
50
|
+
|
|
51
|
+
// 📂 2. [분리 완료] 프롬프트 로더 유틸리티로 동적 스캔 및 가로채기 셋업 위임
|
|
52
|
+
loadApplicationPrompts(ai, db.type);
|
|
53
|
+
|
|
54
|
+
// 🌐 3. MCP 프로토콜 컨텍스트 선언
|
|
55
|
+
const server = new McpServer(
|
|
56
|
+
{ name: "nine-mcp", version: "1.0.0" },
|
|
57
|
+
{ capabilities: { tools: {}, logging: {} } }
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const generateSourceBrain = generateSourceBrainTool(db, ai);
|
|
61
|
+
const modifySourceBrain = modifySourceBrainTool(db, ai);
|
|
62
|
+
|
|
63
|
+
// 🔧 4. 일반 하위 실무 생성 도구 명세 로드
|
|
64
|
+
let nineTools = getStaticToolsConfig();
|
|
65
|
+
|
|
66
|
+
const coreTools = [
|
|
67
|
+
generateSourceBrain,
|
|
68
|
+
modifySourceBrain,
|
|
69
|
+
systemBrainTool(db, ai, nineTools, generateSourceBrain, modifySourceBrain)
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// 🧠 6. 관제탑 도구 MCP 등록
|
|
73
|
+
coreTools.forEach(tool => {
|
|
74
|
+
server.tool(tool.name, tool.description, tool.schema, async (params, context) => {
|
|
75
|
+
return await safeExecute(() => tool.handler(params, context));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 🔧 7. 일반 하위 실무 생성 도구 MCP 등록
|
|
80
|
+
nineTools.forEach(tool => {
|
|
81
|
+
server.tool(tool.name, tool.description, tool.schema, async (params) => {
|
|
82
|
+
return await safeExecute(async () => {
|
|
83
|
+
console.log(`🚀 [Tool Execution]: ${tool.name}`, params);
|
|
84
|
+
return await ai.runChain(tool.name, params);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 🗄️ 8. 데이터베이스 연결
|
|
90
|
+
try {
|
|
91
|
+
await db.connect();
|
|
92
|
+
console.log("데이터베이스 연결 성공");
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error("서버 기동 실패:", error.message);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 🌐 9. Express & WebSocket 인프라 기동 및 트랜스포트 바인딩
|
|
99
|
+
const app = express();
|
|
100
|
+
app.use(cors(), express.json());
|
|
101
|
+
|
|
102
|
+
const PORT = Number(process.env.SERVER_PORT) || 4001;
|
|
103
|
+
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
try {
|
|
106
|
+
const httpServer = app.listen(PORT, () => {
|
|
107
|
+
console.log(`🚀 Nine MCP Engine ON: ws://localhost:${PORT}`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
111
|
+
|
|
112
|
+
wss.on("connection", async (ws) => {
|
|
113
|
+
console.log("🔌 새로운 클라이언트 연결됨");
|
|
114
|
+
try {
|
|
115
|
+
global.rawSocket = ws;
|
|
116
|
+
const transport = new CustomWsTransport(ws);
|
|
117
|
+
await server.connect(transport);
|
|
118
|
+
console.log("✅ MCP 프로토콜 핸드셰이크 완료");
|
|
119
|
+
} catch (connErr) {
|
|
120
|
+
console.error("❌ MCP 연결 실패:", connErr);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
resolve(httpServer);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
reject(err);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const generateSourceBrainTool = (db, ai) => ({
|
|
4
|
+
name: "generate-source-brain",
|
|
5
|
+
description: "미매핑 라우터 정보를 전달받아, 해당 메뉴들과 연관된 데이터베이스 테이블 스키마를 정밀 분석한 후 MyBatis Mapper XML ➡️ 비즈니스 Service ➡️ 컨트롤러 API ➡️ React UI 컴포넌트까지 4대 영역의 소스코드를 순차적 의존성 주입 구조로 일괄 생성(EXECUTE_BATCH)하는 소스코드 빌드 전용 도구입니다.",
|
|
6
|
+
schema: {
|
|
7
|
+
user_input: z.string(),
|
|
8
|
+
base_package: z.string(),
|
|
9
|
+
unmapped_routes: z.any()
|
|
10
|
+
},
|
|
11
|
+
handler: async (params, context) => {
|
|
12
|
+
const schemaSummary = await db.getTableSchemaSummary();
|
|
13
|
+
const enrichedParams = { ...params, schema_summary: schemaSummary };
|
|
14
|
+
|
|
15
|
+
// 1. 코어 일괄 배치 타겟 분석 체인 구동
|
|
16
|
+
const rawToolResult = await ai.runChain("generate-source-brain", enrichedParams);
|
|
17
|
+
let decision = typeof rawToolResult === 'string' ? JSON.parse(rawToolResult) : rawToolResult;
|
|
18
|
+
|
|
19
|
+
if (decision.intent === "EXECUTE_BATCH") {
|
|
20
|
+
await context.sendNotification({
|
|
21
|
+
method: "notifications/logging/message",
|
|
22
|
+
params: { level: "info", logger: "generate-source-brain-start", message: decision.message }
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const batchList = decision.action?.batchList || [];
|
|
26
|
+
const schema = await db.getTableSchema();
|
|
27
|
+
const resultType = "com.ninelab.ai.util.CamelCaseMap";
|
|
28
|
+
|
|
29
|
+
console.log(batchList)
|
|
30
|
+
|
|
31
|
+
let i = 0;
|
|
32
|
+
for (let batch of batchList) {
|
|
33
|
+
|
|
34
|
+
i++;
|
|
35
|
+
//if (i < 10) continue;
|
|
36
|
+
//if (i > 1) break;
|
|
37
|
+
|
|
38
|
+
const filteredSchema = schema.filter(table => batch.tableIds.includes(table.tableName));
|
|
39
|
+
|
|
40
|
+
const formattedPath = (batch?.path || "").replaceAll("/", ".").replaceAll("-", "_");
|
|
41
|
+
let cleanPath = formattedPath;
|
|
42
|
+
if (cleanPath.startsWith(".")) cleanPath = cleanPath.substring(1);
|
|
43
|
+
if (cleanPath.endsWith(".")) cleanPath = cleanPath.substring(0, cleanPath.length - 1);
|
|
44
|
+
|
|
45
|
+
// ★ 만약 cleanPath가 빈 문자열(즉, "/" 경로)이라면 "main"으로 대체
|
|
46
|
+
if (!cleanPath) {
|
|
47
|
+
cleanPath = "main";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pathParts = (batch?.path || "").split("/");
|
|
51
|
+
const rawClassName = pathParts[pathParts.length - 1] || "Sample";
|
|
52
|
+
const baseClassName = rawClassName
|
|
53
|
+
.split("-")
|
|
54
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
55
|
+
.join("");
|
|
56
|
+
|
|
57
|
+
// 💡 [중요] 연쇄 주입을 위해 루프가 돌기 전, 생성된 소스들을 저장할 버퍼 임시 객체 선언
|
|
58
|
+
const generatedOutputs = {
|
|
59
|
+
mapperXml: "",
|
|
60
|
+
serviceCode: ""
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 2. ⚡ 레이어별 동적 파라미터 빌더 팩토리 (익명 함수가 아닌 런타임에 인자를 받도록 변경)
|
|
64
|
+
const targetChains = [
|
|
65
|
+
{
|
|
66
|
+
id: "generate-source-mapper",
|
|
67
|
+
label: "MyBatis 맵퍼",
|
|
68
|
+
buildParams: () => ({
|
|
69
|
+
...params,
|
|
70
|
+
//db_type: db.type,
|
|
71
|
+
asis_source: "",
|
|
72
|
+
menu_description: batch.description,
|
|
73
|
+
schema_detail: filteredSchema,
|
|
74
|
+
result_type: resultType,
|
|
75
|
+
namespace: `${params.base_package}.${cleanPath}.mapper`
|
|
76
|
+
}),
|
|
77
|
+
saveOutput: (source) => { generatedOutputs.mapperSource = source; } // 결과물 킵
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "generate-source-service",
|
|
81
|
+
label: "비즈니스 서비스",
|
|
82
|
+
buildParams: () => ({
|
|
83
|
+
...params,
|
|
84
|
+
asis_source: "",
|
|
85
|
+
menu_description: batch.description,
|
|
86
|
+
//schema_detail: filteredSchema,
|
|
87
|
+
service_package: `${params.base_package}.${cleanPath}.service`,
|
|
88
|
+
mapper_package: `${params.base_package}.${cleanPath}.mapper`,
|
|
89
|
+
// ★ [핵심 주입] 앞 단계에서 만들어진 MyBatis Mapper XML 소스를 통째로 프롬프트 컨텍스트에 제공!
|
|
90
|
+
mapper_source: generatedOutputs.mapperSource
|
|
91
|
+
}),
|
|
92
|
+
saveOutput: (source) => { generatedOutputs.serviceSource = source; } // 결과물 킵
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "generate-source-controller",
|
|
96
|
+
label: "컨트롤러 API",
|
|
97
|
+
buildParams: () => ({
|
|
98
|
+
...params,
|
|
99
|
+
asis_source: "",
|
|
100
|
+
menu_description: batch.description,
|
|
101
|
+
//schema_detail: filteredSchema,
|
|
102
|
+
api_base_url: batch.path,
|
|
103
|
+
controller_package: `${params.base_package}.${cleanPath}.controller`,
|
|
104
|
+
service_package: `${params.base_package}.${cleanPath}.service`,
|
|
105
|
+
// ★ [핵심 주입] 앞 단계에서 완성된 Service 인터페이스/클래스 명세를 보고 Controller를 조립할 수 있도록 제공!
|
|
106
|
+
service_source: generatedOutputs.serviceSource
|
|
107
|
+
}),
|
|
108
|
+
saveOutput: (source) => { generatedOutputs.controllerSource = source; }
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "generate-source-ui-react",
|
|
112
|
+
label: "리액트 UI 컴포넌트",
|
|
113
|
+
buildParams: () => ({
|
|
114
|
+
...params,
|
|
115
|
+
asis_source: "",
|
|
116
|
+
menu_description: batch.description,
|
|
117
|
+
api_base_url: batch.path,
|
|
118
|
+
menu_name: batch.description,
|
|
119
|
+
base_class: baseClassName,
|
|
120
|
+
schema_detail: filteredSchema, // 스키마 세부 사항 전달
|
|
121
|
+
mapper_source: generatedOutputs.mapperSource, // ★ 의존성 연쇄 주입
|
|
122
|
+
controller_source: generatedOutputs.controllerSource
|
|
123
|
+
}),
|
|
124
|
+
saveOutput: () => {} // UI 단은 최종 말단 레이어이므로 세이브 패스
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// 3. 순차 파라미터 격리 실행 루프
|
|
129
|
+
for (const chain of targetChains) {
|
|
130
|
+
|
|
131
|
+
const specificToolParams = chain.buildParams();
|
|
132
|
+
|
|
133
|
+
console.log(`🚀 [${batch.description}] ${chain.label} 실행 (연쇄 컨텍스트 주입 완료)`);
|
|
134
|
+
|
|
135
|
+
const rawSourceResult = await ai.runChain(chain.id, specificToolParams);
|
|
136
|
+
|
|
137
|
+
let resultObj = typeof rawSourceResult === 'string' ? JSON.parse(rawSourceResult) : rawSourceResult;
|
|
138
|
+
|
|
139
|
+
console.log(resultObj);
|
|
140
|
+
// 💡 현재 레이어에서 생성된 따끈따끈한 소스코드를 메모리 버퍼 객체에 세팅 (다음 레이어가 땡겨 쓸 수 있게)
|
|
141
|
+
chain.saveOutput(resultObj.full_source);
|
|
142
|
+
//console.log(resultObj.full_source);
|
|
143
|
+
|
|
144
|
+
// 화면 탭 분기를 위한 스트리밍 노티 발송
|
|
145
|
+
await context.sendNotification({
|
|
146
|
+
method: "notifications/logging/message",
|
|
147
|
+
params: {
|
|
148
|
+
level: "info",
|
|
149
|
+
logger: "generate-source-brain",
|
|
150
|
+
message: `${chain.label} 설계 완료`,
|
|
151
|
+
data: {
|
|
152
|
+
//status: "PROGRESS",
|
|
153
|
+
//menu: batch.description,
|
|
154
|
+
path: batch.path,
|
|
155
|
+
//layer: chain.id,
|
|
156
|
+
//mode: resultObj.mode,
|
|
157
|
+
//file_name: resultObj.file_name,
|
|
158
|
+
source: resultObj.full_source
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//i++;
|
|
166
|
+
//if (i > 5) break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await context.sendNotification({
|
|
170
|
+
method: "notifications/logging/message",
|
|
171
|
+
params: { level: "info", logger: "generate-source-brain-completed", message: "모든 작업이 완료되었습니다." }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return { success: true, intent: "NONE", message: null };
|
|
175
|
+
} else {
|
|
176
|
+
return { success: false, intent: "NONE", message: decision.message };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|