@thinkdata/cli 0.2.3 → 0.3.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.
@@ -0,0 +1,652 @@
1
+ /**
2
+ * MCP Tools 등록
3
+ *
4
+ * Tools는 부작용(Side-effect)이 있는 액션을 노출합니다.
5
+ * AI 에이전트가 ThinkERD 데이터를 조회하거나 스키마 변경을 제안할 수 있습니다.
6
+ *
7
+ * @module tools
8
+ */
9
+ import { z } from 'zod';
10
+ export function registerTools(server, client) {
11
+ // ── get_entity: 엔터티 상세 조회 ──
12
+ server.tool('get_entity', 'ThinkERD 다이어그램에서 특정 엔터티의 컬럼, 타입, PK/FK 정보를 조회합니다.', {
13
+ diagramId: z.string().describe('다이어그램 ID'),
14
+ entityId: z.string().describe('엔터티 ID'),
15
+ }, async ({ diagramId, entityId }) => {
16
+ try {
17
+ const entity = await client.getEntity(diagramId, entityId);
18
+ return {
19
+ content: [{
20
+ type: 'text',
21
+ text: JSON.stringify(entity, null, 2),
22
+ }],
23
+ };
24
+ }
25
+ catch (err) {
26
+ return {
27
+ content: [{
28
+ type: 'text',
29
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
30
+ }],
31
+ isError: true,
32
+ };
33
+ }
34
+ });
35
+ // ── search_entities: 엔터티 검색 ──
36
+ server.tool('search_entities', 'ThinkERD 다이어그램에서 이름으로 엔터티를 검색합니다. 논리명/물리명 모두 검색 가능합니다.', {
37
+ diagramId: z.string().describe('다이어그램 ID'),
38
+ query: z.string().describe('검색 키워드 (엔터티 논리명 또는 물리명)'),
39
+ }, async ({ diagramId, query }) => {
40
+ try {
41
+ const entities = await client.searchEntities(diagramId, query);
42
+ const summary = entities.map(e => ({
43
+ id: e.id,
44
+ name: e.name,
45
+ tableName: e.tableName,
46
+ role: e.entityRole,
47
+ columnCount: e.columns.length,
48
+ columns: e.columns.map(c => `${c.name} (${c.type})${c.isPrimaryKey ? ' [PK]' : ''}${c.isForeignKey ? ' [FK]' : ''}`),
49
+ }));
50
+ return {
51
+ content: [{
52
+ type: 'text',
53
+ text: JSON.stringify(summary, null, 2),
54
+ }],
55
+ };
56
+ }
57
+ catch (err) {
58
+ return {
59
+ content: [{
60
+ type: 'text',
61
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
62
+ }],
63
+ isError: true,
64
+ };
65
+ }
66
+ });
67
+ // ── generate_ddl: DDL 생성 ──
68
+ server.tool('generate_ddl', 'ThinkERD 다이어그램의 DDL(CREATE TABLE 구문)을 생성합니다. PostgreSQL, MySQL, Oracle 등 다양한 방언을 지원합니다.', {
69
+ diagramId: z.string().describe('다이어그램 ID'),
70
+ dialect: z.enum(['postgresql', 'mysql', 'oracle', 'mssql', 'sqlite'])
71
+ .optional()
72
+ .default('postgresql')
73
+ .describe('SQL 방언 (기본값: postgresql)'),
74
+ }, async ({ diagramId, dialect }) => {
75
+ try {
76
+ const ddl = await client.generateDDL(diagramId, dialect);
77
+ return {
78
+ content: [{
79
+ type: 'text',
80
+ text: typeof ddl === 'string' ? ddl : JSON.stringify(ddl),
81
+ }],
82
+ };
83
+ }
84
+ catch (err) {
85
+ return {
86
+ content: [{
87
+ type: 'text',
88
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
89
+ }],
90
+ isError: true,
91
+ };
92
+ }
93
+ });
94
+ // ── validate_schema: 스키마 검증 ──
95
+ server.tool('validate_schema', 'ThinkERD 다이어그램의 스키마를 검증합니다. 누락된 PK, 미연결 FK, 비표준 네이밍 등을 점검합니다.', {
96
+ diagramId: z.string().describe('다이어그램 ID'),
97
+ }, async ({ diagramId }) => {
98
+ try {
99
+ const data = await client.getDiagram(diagramId);
100
+ const issues = [];
101
+ // PK 누락 검사
102
+ for (const entity of data.entities) {
103
+ const hasPK = entity.columns.some(c => c.isPrimaryKey);
104
+ if (!hasPK) {
105
+ issues.push(`⚠️ [${entity.name}] PK(Primary Key)가 없습니다.`);
106
+ }
107
+ }
108
+ // 컬럼 없는 엔터티 검사
109
+ for (const entity of data.entities) {
110
+ if (entity.columns.length === 0) {
111
+ issues.push(`⚠️ [${entity.name}] 컬럼이 하나도 없습니다.`);
112
+ }
113
+ }
114
+ // 고립 엔터티 검사 (관계가 하나도 없는)
115
+ const connectedIds = new Set();
116
+ for (const rel of data.relationships) {
117
+ connectedIds.add(rel.sourceEntityId);
118
+ connectedIds.add(rel.targetEntityId);
119
+ }
120
+ for (const entity of data.entities) {
121
+ if (!connectedIds.has(entity.id) && data.entities.length > 1) {
122
+ issues.push(`ℹ️ [${entity.name}] 다른 엔터티와 관계가 없는 고립 엔터티입니다.`);
123
+ }
124
+ }
125
+ const summary = issues.length === 0
126
+ ? `✅ 스키마 검증 완료: ${data.entities.length}개 엔터티, 문제 없음.`
127
+ : `🔍 스키마 검증 완료: ${data.entities.length}개 엔터티, ${issues.length}개 이슈 발견.\n\n${issues.join('\n')}`;
128
+ return {
129
+ content: [{
130
+ type: 'text',
131
+ text: summary,
132
+ }],
133
+ };
134
+ }
135
+ catch (err) {
136
+ return {
137
+ content: [{
138
+ type: 'text',
139
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
140
+ }],
141
+ isError: true,
142
+ };
143
+ }
144
+ });
145
+ // ── harvest_schema_semantics: 역공학 컨텍스트 추출 ──
146
+ server.tool('harvest_schema_semantics', 'ThinkERD 다이어그램의 물리 테이블 목록을 기반으로 역공학 추론을 실행하여 논리명, 설명, 관계 등의 시멘틱 메타데이터를 반환합니다.', {
147
+ diagramId: z.string().describe('다이어그램 ID'),
148
+ tableNames: z.array(z.string()).optional().describe('추출할 물리 테이블명 목록. 생략 시 전체 다이어그램 대상'),
149
+ }, async ({ diagramId, tableNames }) => {
150
+ try {
151
+ const data = await client.getDiagram(diagramId);
152
+ let entities = data.entities;
153
+ if (tableNames && tableNames.length > 0) {
154
+ const tableSet = new Set(tableNames.map(t => t.toUpperCase()));
155
+ entities = entities.filter(e => e.tableName && tableSet.has(e.tableName.toUpperCase()));
156
+ }
157
+ const { extractFrequentTokens } = await import('@thinkdata/semantic-core');
158
+ const harvested = extractFrequentTokens(entities, [], 1);
159
+ const summary = {
160
+ extractedSemantics: harvested,
161
+ relationships: data.relationships.filter(r => entities.some(e => e.id === r.sourceEntityId) ||
162
+ entities.some(e => e.id === r.targetEntityId))
163
+ };
164
+ return {
165
+ content: [{
166
+ type: 'text',
167
+ text: JSON.stringify(summary, null, 2),
168
+ }],
169
+ };
170
+ }
171
+ catch (err) {
172
+ return {
173
+ content: [{
174
+ type: 'text',
175
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
176
+ }],
177
+ isError: true,
178
+ };
179
+ }
180
+ });
181
+ // ── propose_schema_change: 스키마 변경 제안 ──
182
+ server.tool('propose_schema_change', '외부 AI가 ThinkERD 다이어그램에 스키마 변경을 제안합니다. 사용자 승인이 필요한 Draft로 전송됩니다.', {
183
+ diagramId: z.string().describe('대상 다이어그램 ID'),
184
+ entityName: z.string().describe('변경 대상 엔터티명 (논리명 또는 물리명)'),
185
+ changeType: z.enum(['add_column', 'modify_column', 'add_entity'])
186
+ .describe('변경 유형'),
187
+ changeData: z.record(z.unknown())
188
+ .describe('변경 데이터 (JSON). add_column: {name, type, logicalName}, add_entity: {name, tableName, columns}'),
189
+ reason: z.string().describe('변경 사유'),
190
+ }, async ({ diagramId, entityName, changeType, changeData, reason }) => {
191
+ try {
192
+ const draft = await client.proposeSchemaChange(diagramId, {
193
+ entityName,
194
+ changeType,
195
+ changeData,
196
+ reason,
197
+ });
198
+ return {
199
+ content: [{
200
+ type: 'text',
201
+ text: `✅ 스키마 변경 제안이 ThinkERD에 전송되었습니다.\n\n` +
202
+ `Draft ID: ${draft.draft_id}\n` +
203
+ `상태: ${draft.status}\n` +
204
+ `대상: ${entityName}\n` +
205
+ `유형: ${changeType}\n` +
206
+ `사유: ${reason}\n\n` +
207
+ `⚠️ ThinkERD에서 사용자가 이 제안을 승인해야 적용됩니다.`,
208
+ }],
209
+ };
210
+ }
211
+ catch (err) {
212
+ return {
213
+ content: [{
214
+ type: 'text',
215
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
216
+ }],
217
+ isError: true,
218
+ };
219
+ }
220
+ });
221
+ // ── get_business_dictionary: 표준 단어/용어 사전 조회 ──
222
+ server.tool('get_business_dictionary', '사내 표준 단어/용어 사전에서 키워드를 검색하여 논리명-물리명 매핑 가이드를 제공합니다. '
223
+ + '각 단어의 동의어(synonyms)도 함께 반환하므로, 사용자가 쓴 말이 사내에서 어떤 표준 단어인지 되짚을 수 있습니다.', {
224
+ projectId: z.string().describe('프로젝트 ID'),
225
+ keyword: z.string().optional().describe('검색어 (생략 시 전체 조회). 동의어로도 검색됩니다'),
226
+ }, async ({ projectId, keyword }) => {
227
+ try {
228
+ let dictionary = await client.getDictionary(projectId);
229
+ if (keyword) {
230
+ const lowerKeyword = keyword.toLowerCase();
231
+ // 동의어까지 훑는다 — 이 도구의 값은 "사용자가 쓴 말"에서 표준 단어로
232
+ // 되짚는 데 있고, 그 말은 정의상 논리명이 아니라 동의어 쪽에 있다
233
+ dictionary = dictionary.filter(w => w.logicalName.toLowerCase().includes(lowerKeyword) ||
234
+ w.physicalName.toLowerCase().includes(lowerKeyword) ||
235
+ (w.synonyms ?? []).some(s => s.toLowerCase().includes(lowerKeyword)) ||
236
+ (w.classifier && w.classifier.toLowerCase().includes(lowerKeyword)));
237
+ }
238
+ return {
239
+ content: [{
240
+ type: 'text',
241
+ text: JSON.stringify(dictionary, null, 2),
242
+ }],
243
+ };
244
+ }
245
+ catch (err) {
246
+ return {
247
+ content: [{
248
+ type: 'text',
249
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
250
+ }],
251
+ isError: true,
252
+ };
253
+ }
254
+ });
255
+ // [2026-08-30 삭제] `push_ddl_and_harvest`와 `update_erd_from_code`가 여기 있었다.
256
+ // 서버 쪽 엔드포인트가 아무 일도 하지 않으면서 `status: "success"`를 돌려주고
257
+ // 메시지 끝에만 "(Mock)"을 붙였다 — 사람은 메시지를 읽지만 **에이전트는 status를
258
+ // 읽는다.** 도구 목록은 LLM이 읽는 메뉴판이고, 메뉴에 있는데 안 나오는 음식은
259
+ // 없는 음식보다 나쁘다.
260
+ //
261
+ // 스키마 변경은 `propose_schema_change` → ThinkERD에서 승인하는 경로로만 간다.
262
+ // ── analyze_legacy_sql: 레거시 SQL 분석 ──
263
+ server.tool('analyze_legacy_sql', '복잡한 레거시 SQL 쿼리를 분석하여 ERD 다이어그램에 기반한 테이블들의 비즈니스 의미 및 연관 관계를 반환합니다.', {
264
+ diagramId: z.string().describe('다이어그램 ID'),
265
+ sqlQuery: z.string().describe('분석할 대상 SQL 쿼리'),
266
+ }, async ({ diagramId, sqlQuery }) => {
267
+ try {
268
+ const data = await client.getDiagram(diagramId);
269
+ const queryUpper = sqlQuery.toUpperCase();
270
+ // SQL에 포함된 테이블명 추출 (단순 포함 여부 확인)
271
+ const matchedEntities = data.entities.filter(e => e.tableName && queryUpper.includes(e.tableName.toUpperCase()));
272
+ const matchedEntityIds = new Set(matchedEntities.map(e => e.id));
273
+ const matchedRelationships = data.relationships.filter(r => matchedEntityIds.has(r.sourceEntityId) || matchedEntityIds.has(r.targetEntityId));
274
+ const { extractFrequentTokens } = await import('@thinkdata/semantic-core');
275
+ const harvested = extractFrequentTokens(matchedEntities, [], 1);
276
+ const result = {
277
+ message: "SQL 내에서 식별된 테이블에 대한 비즈니스 분석 결과입니다.",
278
+ matchedTables: matchedEntities.map(e => ({
279
+ tableName: e.tableName,
280
+ logicalName: e.logicalName,
281
+ description: e.columns.map((c) => `${c.name}(${c.type}) ${c.logicalName || ''}`).join(', ')
282
+ })),
283
+ semanticHarvest: harvested,
284
+ relevantRelationships: matchedRelationships
285
+ };
286
+ return {
287
+ content: [{
288
+ type: 'text',
289
+ text: JSON.stringify(result, null, 2),
290
+ }],
291
+ };
292
+ }
293
+ catch (err) {
294
+ return {
295
+ content: [{
296
+ type: 'text',
297
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
298
+ }],
299
+ isError: true,
300
+ };
301
+ }
302
+ });
303
+ // ── semantic_search: 비즈니스 용어 기반 시멘틱 검색 ──
304
+ server.tool('semantic_search', '비즈니스 용어(자연어)로 다이어그램의 엔터티·컬럼을 검색합니다. 동의어, 설명, 논리명을 모두 활용하여 매칭하며, 매칭 유형(논리명/동의어/설명/부분매칭/컬럼)을 함께 반환합니다.', {
305
+ diagramId: z.string().describe('다이어그램 ID'),
306
+ query: z.string().describe('검색 키워드 (비즈니스 용어, 논리명, 동의어 등)'),
307
+ maxResults: z.number().optional().describe('최대 결과 수 (기본 10)'),
308
+ }, async ({ diagramId, query, maxResults }) => {
309
+ try {
310
+ const data = await client.getDiagram(diagramId);
311
+ const entities = data.entities || [];
312
+ const limit = maxResults || 10;
313
+ const q = query.toLowerCase();
314
+ const results = [];
315
+ for (const entity of entities) {
316
+ const e = entity;
317
+ // 논리명 매칭
318
+ if (e.name?.toLowerCase().includes(q) || e.logicalName?.toLowerCase().includes(q)) {
319
+ results.push({
320
+ entityId: e.id,
321
+ entityName: e.name,
322
+ tableName: e.tableName,
323
+ matchType: 'logicalName',
324
+ matchedOn: e.name || e.logicalName,
325
+ description: e.description || null,
326
+ });
327
+ continue;
328
+ }
329
+ // 동의어 매칭
330
+ if (Array.isArray(e.synonyms)) {
331
+ const syn = e.synonyms.find((s) => s.toLowerCase().includes(q));
332
+ if (syn) {
333
+ results.push({
334
+ entityId: e.id,
335
+ entityName: e.name,
336
+ tableName: e.tableName,
337
+ matchType: 'synonym',
338
+ matchedOn: syn,
339
+ description: e.description || null,
340
+ });
341
+ continue;
342
+ }
343
+ }
344
+ // 설명 매칭
345
+ if (e.description?.toLowerCase().includes(q)) {
346
+ results.push({
347
+ entityId: e.id,
348
+ entityName: e.name,
349
+ tableName: e.tableName,
350
+ matchType: 'description',
351
+ matchedOn: e.description,
352
+ });
353
+ continue;
354
+ }
355
+ // 컬럼 매칭
356
+ for (const col of entity.columns) {
357
+ const c = col;
358
+ if (c.logicalName?.toLowerCase().includes(q) || c.name?.toLowerCase().includes(q) || c.description?.toLowerCase().includes(q)) {
359
+ results.push({
360
+ entityId: e.id,
361
+ entityName: e.name,
362
+ tableName: e.tableName,
363
+ matchType: 'column',
364
+ matchedColumn: c.name,
365
+ matchedColumnLogical: c.logicalName,
366
+ });
367
+ break; // 한 엔터티에서 하나만
368
+ }
369
+ }
370
+ }
371
+ return {
372
+ content: [{
373
+ type: 'text',
374
+ text: JSON.stringify({
375
+ query,
376
+ totalResults: results.length,
377
+ results: results.slice(0, limit),
378
+ }, null, 2),
379
+ }],
380
+ };
381
+ }
382
+ catch (err) {
383
+ return {
384
+ content: [{
385
+ type: 'text',
386
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
387
+ }],
388
+ isError: true,
389
+ };
390
+ }
391
+ });
392
+ // ── get_business_context: 엔터티의 비즈니스 컨텍스트 조회 ──
393
+ server.tool('get_business_context', '특정 엔터티의 비즈니스 의미, 동의어, 코드값, 관계를 포함한 전체 비즈니스 컨텍스트를 조회합니다. Text-to-SQL이나 문서 생성의 참고자료로 활용됩니다.', {
394
+ diagramId: z.string().describe('다이어그램 ID'),
395
+ entityId: z.string().describe('엔터티 ID'),
396
+ }, async ({ diagramId, entityId }) => {
397
+ try {
398
+ const entity = await client.getEntity(diagramId, entityId);
399
+ const data = await client.getDiagram(diagramId);
400
+ const e = entity;
401
+ // 관련 관계 조회
402
+ const relationships = (data.relationships || []).filter(r => r.sourceEntityId === entityId || r.targetEntityId === entityId);
403
+ const businessContext = {
404
+ entity: {
405
+ id: e.id,
406
+ name: e.name,
407
+ tableName: e.tableName,
408
+ logicalName: e.logicalName || e.name,
409
+ description: e.description || null,
410
+ synonyms: e.synonyms || [],
411
+ entityRole: e.entityRole || null,
412
+ },
413
+ columns: entity.columns.map((c) => ({
414
+ name: c.name,
415
+ logicalName: c.logicalName || null,
416
+ type: c.type,
417
+ isPrimaryKey: c.isPrimaryKey || false,
418
+ isForeignKey: c.isForeignKey || false,
419
+ description: c.description || null,
420
+ domainValues: c.domainValues || null,
421
+ })),
422
+ relationships: relationships.map(r => ({
423
+ relatedEntity: r.sourceEntityId === entityId ? r.targetEntityName : r.sourceEntityName,
424
+ direction: r.sourceEntityId === entityId ? 'outgoing' : 'incoming',
425
+ type: r.type,
426
+ cardinality: `${r.sourceCardinality}:${r.targetCardinality}`,
427
+ })),
428
+ };
429
+ return {
430
+ content: [{
431
+ type: 'text',
432
+ text: JSON.stringify(businessContext, null, 2),
433
+ }],
434
+ };
435
+ }
436
+ catch (err) {
437
+ return {
438
+ content: [{
439
+ type: 'text',
440
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
441
+ }],
442
+ isError: true,
443
+ };
444
+ }
445
+ });
446
+ // ── generate_sql: 자연어 → SQL 생성 ──
447
+ server.tool('generate_sql', '**한 다이어그램의 물리 스키마만** 보고 SQL 작성용 컨텍스트를 반환합니다. '
448
+ + '⚠️ 이 도구는 프로젝트 전역 의미 계층(비즈니스 모델 발행본·표준 사전 동의어·온톨로지)을 **보지 않습니다.** '
449
+ + '업무 용어("고객", "배송중인 주문")가 섞인 질문이라면 **`get_project_context`를 먼저 호출하십시오.** '
450
+ + '이 도구는 대상 다이어그램이 이미 특정되어 있고 물리 컬럼 구조만 필요할 때 쓰십시오.', {
451
+ diagramId: z.string().describe('다이어그램 ID'),
452
+ question: z.string().describe('자연어 질문 (예: "부서별 평균 급여가 5천만원 이상인 부서")'),
453
+ dialect: z.string().optional().describe('SQL 방언 (postgresql, mysql, oracle, mssql). 기본: postgresql'),
454
+ }, async ({ diagramId, question, dialect }) => {
455
+ try {
456
+ const data = await client.getDiagram(diagramId);
457
+ const entities = data.entities || [];
458
+ const relationships = data.relationships || [];
459
+ // 시멘틱 컨텍스트 구성
460
+ const schemaContext = entities.map(e => {
461
+ const ea = e;
462
+ return {
463
+ tableName: e.tableName,
464
+ logicalName: e.name || ea.logicalName,
465
+ description: ea.description || null,
466
+ synonyms: ea.synonyms || [],
467
+ columns: e.columns.map((c) => ({
468
+ name: c.name,
469
+ logicalName: c.logicalName || null,
470
+ type: c.type,
471
+ isPrimaryKey: c.isPrimaryKey || false,
472
+ isForeignKey: c.isForeignKey || false,
473
+ domainValues: c.domainValues || null,
474
+ })),
475
+ };
476
+ });
477
+ const relationshipContext = relationships.map(r => ({
478
+ from: `${r.sourceEntityName} (${r.sourceEntityId})`,
479
+ to: `${r.targetEntityName} (${r.targetEntityId})`,
480
+ type: r.type,
481
+ }));
482
+ // LLM 호출 대신 컨텍스트를 반환하여 외부 AI가 SQL 생성에 활용하도록 함
483
+ const result = {
484
+ question,
485
+ dialect: dialect || 'postgresql',
486
+ schemaContext,
487
+ relationshipContext,
488
+ hint: `위 스키마 컨텍스트를 기반으로 "${question}"에 대한 ${dialect || 'postgresql'} SQL을 생성하세요. tableName을 테이블 참조에, columns[].name을 컬럼 참조에 사용하세요. 동의어(synonyms)와 코드값(domainValues)을 활용하여 정확한 매핑을 수행하세요.`,
489
+ };
490
+ return {
491
+ content: [{
492
+ type: 'text',
493
+ text: JSON.stringify(result, null, 2),
494
+ }],
495
+ };
496
+ }
497
+ catch (err) {
498
+ return {
499
+ content: [{
500
+ type: 'text',
501
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
502
+ }],
503
+ isError: true,
504
+ };
505
+ }
506
+ });
507
+ // ══════════════════════════════════════════════════════════════════
508
+ // ── 프로젝트 전체 온톨로지 Tools (Context Provider 연동) ──
509
+ // ══════════════════════════════════════════════════════════════════
510
+ // ── get_project_context: 프로젝트 시멘틱 컨텍스트 검색 ──
511
+ server.tool('get_project_context', '**업무 용어가 섞인 질문에는 이 도구를 먼저 쓰십시오.** 자연어 질문으로 프로젝트 전체에서 관련 엔터티를 '
512
+ + '시멘틱 검색(Graph RAG)합니다. 이 조직이 쓰는 **동의어·코드값의 뜻·비즈니스 모델 발행본**까지 보므로, '
513
+ + '"고객"처럼 물리 스키마에 없는 말도 실제 테이블로 연결합니다. '
514
+ + '응답의 `usedFallback: true`는 **질문의 개념을 찾지 못했다**는 뜻입니다 — 그 컨텍스트로 SQL을 만들지 말고 '
515
+ + '모른다고 답하거나 사용자에게 용어를 되물으십시오.', {
516
+ projectId: z.string().describe('프로젝트 ID'),
517
+ question: z.string().describe('자연어 질문 (예: "부서별 급여 총액", "VIP 고객의 최근 주문")'),
518
+ }, async ({ projectId, question }) => {
519
+ try {
520
+ const result = await client.getProjectContext(projectId, question);
521
+ return {
522
+ content: [{
523
+ type: 'text',
524
+ text: JSON.stringify(result, null, 2),
525
+ }],
526
+ };
527
+ }
528
+ catch (err) {
529
+ return {
530
+ content: [{
531
+ type: 'text',
532
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
533
+ }],
534
+ isError: true,
535
+ };
536
+ }
537
+ });
538
+ // ── find_join_path: JOIN 경로 탐색 ──
539
+ server.tool('find_join_path', '두 엔터티(테이블) 간 최적 JOIN 경로를 탐색합니다. BFS 기반으로 최단 경로를 찾으며, 중간 테이블과 관계명을 포함합니다. ' +
540
+ '각 구간에 물리 테이블명·FK 컬럼쌍(on)·진행 방향 카디널리티가 실려 있으므로, **조인 조건을 추측하지 말고 on 값을 그대로 ON 절에 쓰십시오.** ' +
541
+ 'joinable=false면 FK 매핑이나 물리 테이블명이 없어 SQL을 조립할 수 없다는 뜻이며, 사유는 warnings에 있습니다.', {
542
+ projectId: z.string().describe('프로젝트 ID'),
543
+ from: z.string().describe('출발 엔터티 이름 (논리명 또는 물리명)'),
544
+ to: z.string().describe('도착 엔터티 이름 (논리명 또는 물리명)'),
545
+ }, async ({ projectId, from, to }) => {
546
+ try {
547
+ const result = await client.findJoinPath(projectId, from, to);
548
+ return {
549
+ content: [{
550
+ type: 'text',
551
+ text: JSON.stringify(result, null, 2),
552
+ }],
553
+ };
554
+ }
555
+ catch (err) {
556
+ return {
557
+ content: [{
558
+ type: 'text',
559
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
560
+ }],
561
+ isError: true,
562
+ };
563
+ }
564
+ });
565
+ // ── get_project_summary: 프로젝트 스키마 요약 ──
566
+ server.tool('get_project_summary', '프로젝트 전체의 데이터 모델 요약을 반환합니다. 엔터티 수, 관계 수, 주제영역(Subject Area) 구성, RDF 트리플 수 등을 포함합니다. 프로젝트 온보딩이나 전체 구조 파악에 유용합니다.', {
567
+ projectId: z.string().describe('프로젝트 ID'),
568
+ }, async ({ projectId }) => {
569
+ try {
570
+ const result = await client.getProjectSummary(projectId);
571
+ return {
572
+ content: [{
573
+ type: 'text',
574
+ text: JSON.stringify(result, null, 2),
575
+ }],
576
+ };
577
+ }
578
+ catch (err) {
579
+ return {
580
+ content: [{
581
+ type: 'text',
582
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
583
+ }],
584
+ isError: true,
585
+ };
586
+ }
587
+ });
588
+ // ── query_ontology: SPARQL 직접 실행 ──
589
+ server.tool('query_ontology', '프로젝트의 온톨로지 그래프에 SPARQL 쿼리를 직접 실행합니다. 고급 사용자가 정교한 시멘틱 질의를 수행할 때 사용합니다.', {
590
+ projectId: z.string().describe('프로젝트 ID'),
591
+ query: z.string().describe('SPARQL SELECT 쿼리문'),
592
+ }, async ({ projectId, query }) => {
593
+ try {
594
+ const result = await client.queryOntology(projectId, query);
595
+ return {
596
+ content: [{
597
+ type: 'text',
598
+ text: JSON.stringify(result, null, 2),
599
+ }],
600
+ };
601
+ }
602
+ catch (err) {
603
+ return {
604
+ content: [{
605
+ type: 'text',
606
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
607
+ }],
608
+ isError: true,
609
+ };
610
+ }
611
+ });
612
+ // ── check_metadata_quality: 메타데이터 품질 점검 ──
613
+ server.tool('check_metadata_quality', '프로젝트의 메타데이터 풍부도를 분석합니다. 설명, 동의어, 속성 입력률을 기반으로 전체 품질 점수를 산출하고, 개선이 필요한 엔터티 목록을 제안합니다.', {
614
+ projectId: z.string().describe('프로젝트 ID'),
615
+ }, async ({ projectId }) => {
616
+ try {
617
+ const result = await client.checkMetadataQuality(projectId);
618
+ // 사람이 읽기 쉬운 요약 포맷
619
+ const summary = [
620
+ `📊 메타데이터 품질 보고서`,
621
+ `평균 풍부도: ${result.avgRichness}%`,
622
+ `총 엔터티: ${result.entityCount}개`,
623
+ ``,
624
+ `🟢 완전 (≥70%): ${result.breakdown?.complete || 0}개`,
625
+ `🟡 부분 (40-69%): ${result.breakdown?.partial || 0}개`,
626
+ `🔴 미입력 (<40%): ${result.breakdown?.missing || 0}개`,
627
+ ];
628
+ if (result.suggestions && result.suggestions.length > 0) {
629
+ summary.push('', '💡 개선 제안 (풍부도 낮은 순):');
630
+ for (const s of result.suggestions.slice(0, 10)) {
631
+ summary.push(` • ${s.entity} (${s.richness}%) — 누락: ${s.missing.join(', ')}`);
632
+ }
633
+ }
634
+ return {
635
+ content: [{
636
+ type: 'text',
637
+ text: summary.join('\n'),
638
+ }],
639
+ };
640
+ }
641
+ catch (err) {
642
+ return {
643
+ content: [{
644
+ type: 'text',
645
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
646
+ }],
647
+ isError: true,
648
+ };
649
+ }
650
+ });
651
+ }
652
+ //# sourceMappingURL=tools.js.map