@ninebone/mcp 0.1.26 → 0.1.28

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/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@ninebone/mcp",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "NineQuery AI Connector for Database",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "nine-mcp": "src/index.js"
9
9
  },
10
+ "files": [
11
+ "src",
12
+ "prompts"
13
+ ],
10
14
  "scripts": {
11
15
  "start": "node src/index.js",
12
16
  "dev": "node --watch src/index.js",
package/src/core/init.js CHANGED
@@ -13,7 +13,7 @@ const SOURCE_PROMPT_DIR = path.resolve(__dirname, '../../prompts');
13
13
  */
14
14
  async function createEnvFile(rl) {
15
15
  const questions = [
16
- { key: 'SERVER_PORT', label: '1. 커넥터 서버 포트', default: '3000' },
16
+ { key: 'SERVER_PORT', label: '1. 커넥터 서버 포트', default: '4001' },
17
17
  { key: 'GEMINI_API_KEY', label: '2. Gemini API Key', default: '' },
18
18
  { key: 'GEMINI_MODEL', label: '3. 사용할 모델', default: 'gemini-2.5-flash' },
19
19
  { key: 'DB_TYPE', label: '4. DB 종류 (mysql, mariadb, postgres, oracle)', default: 'mysql' },
@@ -57,30 +57,45 @@ async function createEnvFile(rl) {
57
57
  * 2. 프롬프트 파일(.md) 자동 복사 함수 (개선됨)
58
58
  */
59
59
  function createPromptFiles() {
60
+ // 사용자가 명령어를 실행한 현재 터미널 위치(작업 디렉토리) 아래에 prompts 폴더를 만듭니다.
60
61
  const targetDir = path.join(process.cwd(), 'prompts');
61
62
 
62
63
  if (!fs.existsSync(targetDir)) {
63
64
  fs.mkdirSync(targetDir, { recursive: true });
64
65
  }
65
66
 
67
+ // 디버깅을 위해 로그를 살짝 추가해두면 어디서 고장 났는지 알기 쉽습니다.
68
+ console.log(`\n🔍 원본 프롬프트 탐색 경로: ${SOURCE_PROMPT_DIR}`);
69
+ console.log(`📂 복사될 대상 경로: ${targetDir}`);
70
+
66
71
  try {
67
72
  if (fs.existsSync(SOURCE_PROMPT_DIR)) {
68
- // 원본 폴더의 모든 파일을 읽어서 자동으로 복사합니다.
69
73
  const files = fs.readdirSync(SOURCE_PROMPT_DIR);
70
74
 
71
- files.filter(f => f.endsWith('.md')).forEach((filename) => {
75
+ // .md 파일 필터링 및 복사
76
+ const mdFiles = files.filter(f => f.endsWith('.md'));
77
+
78
+ if (mdFiles.length === 0) {
79
+ console.log('⚠️ 원본 폴더에 마크다운(.md) 파일이 존재하지 않습니다.');
80
+ return;
81
+ }
82
+
83
+ mdFiles.forEach((filename) => {
72
84
  const sourcePath = path.join(SOURCE_PROMPT_DIR, filename);
73
85
  const targetPath = path.join(targetDir, filename);
74
86
 
75
87
  const content = fs.readFileSync(sourcePath, 'utf-8');
76
88
  fs.writeFileSync(targetPath, content.trim() + '\n', 'utf-8');
89
+ console.log(` └📄 ${filename} 복사 완료`);
77
90
  });
78
- console.log('프롬프트 파일(prompts/*.md) 복사가 완료되었습니다.');
91
+ console.log('프롬프트 파일(prompts/*.md) 배포가 성공적으로 완료되었습니다.');
79
92
  } else {
80
- console.error(`원본 템플릿 경로를 찾을 없습니다: ${SOURCE_PROMPT_DIR}`);
93
+ // 여기가 실행된다면 package.json의 files 항목에 prompts 폴더가 담겼거나 경로 depth가 틀린 것입니다.
94
+ console.error(`❌ [오류] 원본 템플릿 경로를 찾을 수 없습니다.`);
95
+ console.error(` 현재 배포된 패키지 내부 구조를 확인하세요.`);
81
96
  }
82
97
  } catch (err) {
83
- console.error(`프롬프트 생성 중 오류 발생:`, err.message);
98
+ console.error(`❌ 프롬프트 생성 중 오류 발생:`, err.message);
84
99
  }
85
100
  }
86
101
 
package/src/index.js CHANGED
@@ -20,12 +20,12 @@ async function main() {
20
20
  if (args.includes('init')) {
21
21
  await runInit();
22
22
  } else {
23
- console.log("🚀 Nine MCP Engine 시동 중...");
23
+ console.log("Nine MCP Engine 시동 중...");
24
24
  // bootstrap이 Promise를 반환하므로 완전히 뜰 때까지 기다림
25
25
  await bootstrap();
26
26
  }
27
27
  } catch (error) {
28
- console.error("실행 중 치명적인 오류가 발생했습니다:");
28
+ console.error("실행 중 치명적인 오류가 발생했습니다:");
29
29
  // ⭐️ 에러의 전체 스택과 상세 정보를 출력
30
30
  console.error(error);
31
31
  if (error.stack) {
@@ -1,28 +0,0 @@
1
- ### SQL_GENERATE_PROMPT
2
- 당신은 SQL 생성기입니다. 제공된 테이블 정보를 바탕으로 사용자의 질문에 답하는 최적의 SQL 쿼리를 작성하세요.
3
-
4
- [선택된 테이블 정보]
5
- {detailed_schema}
6
-
7
- [사용자 질문]
8
- "{question}"
9
-
10
- [데이터베이스 타입]
11
- {db_type}
12
-
13
- [기본 응답 규칙]
14
- 1. 반드시 JSON 형식으로만 응답하세요.
15
- 2. SQL은 {db_type} 문법에 완벽하게 맞아야 하며, 즉시 실행 가능해야 합니다.
16
- 3. 쿼리문 안에 마크다운 코드 블록(```sql)을 포함하지 마세요.
17
-
18
- [고객사 전용 비즈니스 로직 및 보안 규칙]
19
- - 사용자가 특정 단어(예: 지명, 상호명, 기술명)를 언급하며 조회를 요청할 경우,
20
- 고정값 비교(=) 대신 반드시 'LIKE %검색어%' 패턴을 사용하세요.
21
- -
22
- (여기에 "특정 테이블 조회 금지"나 "기본 정렬 기준" 등 커스텀 규칙을 작성하세요.)
23
-
24
- [응답 포맷]
25
- {{
26
- "sql": "SELECT ... FROM ... WHERE ...",
27
- "explanation": "이 쿼리는 어떤 데이터를 어떻게 조회하는지에 대한 설명입니다."
28
- }}
@@ -1,123 +0,0 @@
1
- 너는 Spring 기반 백엔드에서 MyBatis SQL Mapper XML을 작성하는 AI입니다.
2
-
3
- 지금부터 사용자로부터 받은 **기능 설명**과 **테이블 스키마 정보**를 바탕으로, 아래 조건을 **모두 만족**하는 **MyBatis SQL Mapper XML 파일(.xml)**을 작성해주세요.
4
-
5
- ---
6
-
7
- **[사용자 요청 기능 설명]**
8
-
9
-
10
- **[테이블 스키마 정보 (JSON)]**
11
- {schema_detail}
12
-
13
- ---
14
-
15
- **[XML 작성 필수 조건]**
16
-
17
- 1. **XML 기본 구조:**
18
- * `<?xml version="1.0" encoding="UTF-8"?>` 및 `<!DOCTYPE mapper ...>` 선언을 **반드시 포함**해야 합니다.
19
- * 전체 XML 코드는 `<mapper namespace="{namespace}" >` 태그로 감싸야 합니다.
20
- * `namespace` 값은 `{namespace}` 플레이스홀더에 전달된 값으로 정확하게 채워야 합니다.
21
- * **인터페이스 파일은 가정하지 않으며, XML 단독으로 동작하도록 합니다.**
22
-
23
- 2. **필수 쿼리 구성:**
24
- * **`selectList`**: 목록 조회 (필수: `ORDER BY` 절 포함)
25
- * **`selectOne`**: 단일 건 조회 (주요 컬럼을 WHERE 조건에 포함)
26
- * **`insert`**: 데이터 삽입
27
- * **`update`**: 데이터 업데이트 (주요 컬럼을 WHERE 조건에 포함)
28
- * **`delete`**: 데이터 삭제 (주요 컬럼을 WHERE 조건에 포함)
29
-
30
- 3. **SQL 작성 규칙:**
31
- * SQL은 제공된 **테이블 스키마(컬럼명, 데이터 타입)**를 기반으로 작성되어야 합니다.
32
- * **자주 사용될 것으로 예상되는 컬럼** (예: ID, 이름, 상태 등)은 `selectOne`, `update`, `delete` 쿼리의 `WHERE` 조건에 포함해야 합니다.
33
- * `result_type`은 **반드시 `{result_type}`으로 처리**합니다.
34
- * `selectList` 쿼리에는 동적 WHERE 조건 처리를 위해 **반드시 아래 구문을 주입**해야 합니다:
35
- ```xml
36
- <where>
37
- <if test="_whereClause != null"> AND ${{_whereClause}} </if>
38
- </where>
39
- ```
40
- * `selectList` 쿼리에는 **반드시 `ORDER BY` 절을 명시**하여 정렬 기준을 제공합니다.
41
-
42
- 4. **출력 형식:**
43
- * 결과는 **오직 완성된 XML 코드만 텍스트로 출력**해야 합니다. (설명 텍스트나 주석 없이)
44
- * **마크다운 코드 블록(```xml`)은 절대 사용하지 마세요.**
45
-
46
- ---
47
-
48
- **[출력 예시]** (AI는 이 형식에 맞춰 XML 코드만 출력해야 합니다.)
49
-
50
- ```xml
51
- <?xml version="1.0" encoding="UTF-8"?>
52
- <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
53
- <mapper namespace="com.example.mapper.BoardMapper">
54
-
55
- <select id="selectList" resultType="`{result_type}`">
56
- SELECT
57
- BOARD_ID,
58
- TITLE,
59
- CONTENT,
60
- REG_DATE
61
- FROM
62
- TB_BOARD
63
- <where>
64
- <if test="_whereClause != null"> AND ${{_whereClause}} </if>
65
- </where>
66
- <where>
67
- <if test="borderId != null"> BOARD_ID = ${{borderId}} </if>
68
- </where>
69
- <where>
70
- <if test="title != null"> TITLE = ${{title}} </if>
71
- </where>
72
- <where>
73
- <if test="content != null"> CONTENT = ${{content}} </if>
74
- </where>
75
- <where>
76
- <if test="regDate != null"> REG_DATE = ${{regDate}} </if>
77
- </where>
78
- ORDER BY
79
- BOARD_ID DESC
80
- </select>
81
-
82
- <select id="selectOne" resultType="java.util.Map">
83
- SELECT
84
- BOARD_ID,
85
- TITLE,
86
- CONTENT,
87
- REG_DATE
88
- FROM
89
- TB_BOARD
90
- WHERE
91
- BOARD_ID = #{{boardId}}
92
- </select>
93
-
94
- <insert id="insert" parameterType="java.util.Map">
95
- INSERT INTO TB_BOARD (
96
- BOARD_ID,
97
- TITLE,
98
- CONTENT,
99
- REG_DATE
100
- ) VALUES (
101
- #{{boardId}},
102
- #{{title}},
103
- #{{content}},
104
- NOW()
105
- )
106
- </insert>
107
-
108
- <update id="update" parameterType="java.util.Map">
109
- UPDATE TB_BOARD
110
- SET
111
- TITLE = #{{title}},
112
- CONTENT = #{{content}}
113
- WHERE
114
- BOARD_ID = #{{boardId}}
115
- </update>
116
-
117
- <delete id="delete" parameterType="java.util.Map">
118
- DELETE FROM TB_BOARD
119
- WHERE
120
- BOARD_ID = #{{boardId}}
121
- </delete>
122
-
123
- </mapper>
package/bak/mcp-server.js DELETED
@@ -1,498 +0,0 @@
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
-
@@ -1,16 +0,0 @@
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}
@@ -1,62 +0,0 @@
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
- }}
@@ -1,21 +0,0 @@
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
- }}