docuking-mcp 2.9.3 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,22 +2,42 @@
2
2
 
3
3
  AI가 문서를 Push/Pull 할 수 있게 해주는 MCP 서버
4
4
 
5
- ## 설치
5
+ ## 설치 (글로벌 설치 권장)
6
6
 
7
7
  ```bash
8
- npx docuking-mcp
8
+ # 1. 글로벌 설치
9
+ npm install -g docuking-mcp
10
+
11
+ # 2. MCP 등록 (Claude Code)
12
+ claude mcp add docuking -s user -- docuking-mcp
13
+ ```
14
+
15
+ ### 업데이트
16
+
17
+ ```bash
18
+ npm update -g docuking-mcp
9
19
  ```
10
20
 
21
+ > **참고**: MCP는 새 버전이 있으면 첫 도구 호출 시 자동으로 알려줍니다.
22
+
11
23
  ## MCP 설정
12
24
 
13
- ### Claude Code
25
+ ### Claude Code (CLI로 등록)
26
+
27
+ ```bash
28
+ claude mcp add docuking -s user -- docuking-mcp
29
+ ```
30
+
31
+ ### Claude Code (JSON으로 등록)
32
+
33
+ `~/.claude.json`에 추가:
14
34
 
15
35
  ```json
16
36
  {
17
37
  "mcpServers": {
18
38
  "docuking": {
19
- "command": "npx",
20
- "args": ["-y", "docuking-mcp"]
39
+ "command": "docuking-mcp",
40
+ "args": []
21
41
  }
22
42
  }
23
43
  }
@@ -31,8 +51,23 @@ npx docuking-mcp
31
51
  {
32
52
  "mcpServers": {
33
53
  "docuking": {
34
- "command": "npx",
35
- "args": ["-y", "docuking-mcp"]
54
+ "command": "docuking-mcp",
55
+ "args": []
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### Gemini CLI
62
+
63
+ `~/.gemini/antigravity/mcp_config.json`에 추가:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "docuking": {
69
+ "command": "docuking-mcp",
70
+ "args": []
36
71
  }
37
72
  }
38
73
  }
@@ -50,28 +85,35 @@ npx docuking-mcp
50
85
  | docuking_log | 커밋 히스토리 조회 |
51
86
  | docuking_diff | 버전 간 차이 비교 |
52
87
  | docuking_rollback | 특정 커밋으로 롤백 |
53
- | docuking_talk | AI 대화록 저장 (zz_ai_Talk/) |
54
- | docuking_plan | 작업 계획 관리 (zzz_ai_Plan/) |
88
+ | docuking_talk | AI 대화록 저장 (zz_ai_1_Talk/) |
89
+ | docuking_plan | 작업 계획 관리 (zz_ai_3_Plan/) |
55
90
  | docuking_done | 작업 완료 처리 |
56
- | docuking_todo | 킹투두 관리 (zz_ai_Todo/) |
91
+ | docuking_todo | 킹투두 관리 (zz_ai_2_Todo/) |
92
+ | docuking_urgent | 긴급 보고 (xx_Urgent/) |
93
+ | docuking_delete | 서버 파일 삭제 |
94
+ | docuking_validate | 정책 준수 검증 |
57
95
 
58
96
  ## 폴더 구조
59
97
 
60
98
  ```
61
99
  프로젝트/
62
- ├── yy_All_Docu/ ← 동기화 대상 (Push/Pull)
63
- ├── yy_Coworker_{이름}/ 협업자 작업 폴더 (동기화 대상)
64
- ├── zz_ai_1_Talk/ 협업자의 AI 대화록 (동기화 대상)
65
- ├── zz_ai_2_Todo/ 협업자의 AI 투두 (동기화 대상)
66
- │ └── zz_ai_3_Plan/ 협업자의 AI 플랜 (동기화 대상)
67
- ├── zz_ai_1_Talk/ 오너의 AI 대화록 (동기화 대상)
68
- ├── zz_ai_2_Todo/ 오너의 AI 투두 (동기화 대상)
69
- └── zz_ai_3_Plan/ 오너의 AI 플랜 (동기화 대상)
100
+ ├── xx_Infra_Config/ 시스템 설정 (동기화 대상)
101
+ ├── xx_Policy/ 정책 문서 (동기화 대상, 킹캐스트)
102
+ ├── xx_Urgent/ 긴급 보고 (동기화 대상)
103
+ ├── yy_All_Docu/ 문서 허브 (동기화 대상)
104
+ ├── yy_Coworker_{이름}/ 협업자 작업 폴더 (동기화 대상)
105
+ ├── zz_ai_1_Talk/ 협업자의 AI 대화록
106
+ ├── zz_ai_2_Todo/ 협업자의 AI 투두
107
+ └── zz_ai_3_Plan/ 협업자의 AI 플랜
108
+ ├── zz_ai_1_Talk/ ← 오너의 AI 대화록 (동기화 대상)
109
+ ├── zz_ai_2_Todo/ ← 오너의 AI 투두 (동기화 대상)
110
+ └── zz_ai_3_Plan/ ← 오너의 AI 플랜 (동기화 대상)
70
111
  ```
71
112
 
72
113
  **접두사 규칙:**
73
- - `yy_*`: 사람용 폴더 (동기화 대상)
74
- - `zz_ai_*`: AI용 폴더 (동기화 대상)
114
+ - `xx_*`: 시스템/인프라 폴더
115
+ - `yy_*`: 사람용 폴더
116
+ - `zz_ai_*`: AI용 폴더
75
117
 
76
118
  ## 웹사이트
77
119
 
package/handlers/sync.js CHANGED
@@ -540,9 +540,10 @@ docuking_init을 먼저 실행하세요.`,
540
540
  console.error('[DocuKing] Sync 시작 알림 실패:', e.message);
541
541
  }
542
542
 
543
- // 서버에서 파일 해시 조회 (Git 스타일 동기화용)
543
+ // 서버에서 파일 해시 및 메타 정보 조회 (Git 스타일 동기화용)
544
544
  let serverPathToHash = {};
545
545
  let serverHashToPath = {};
546
+ let serverPathToMeta = {}; // source, type, deletedAt 정보 포함
546
547
  let serverAllPaths = [];
547
548
  try {
548
549
  const hashResponse = await fetch(
@@ -557,6 +558,7 @@ docuking_init을 먼저 실행하세요.`,
557
558
  const hashData = await hashResponse.json();
558
559
  serverPathToHash = hashData.pathToHash || {};
559
560
  serverHashToPath = hashData.hashToPath || {};
561
+ serverPathToMeta = hashData.pathToMeta || {};
560
562
  serverAllPaths = hashData.allPaths || [];
561
563
  }
562
564
  } catch (e) {
@@ -734,24 +736,48 @@ docuking_init을 먼저 실행하세요.`,
734
736
  }
735
737
  }
736
738
 
739
+ // ========================================
737
740
  // 4. 서버에만 있고 로컬에 없는 파일 soft-delete (deleted_at 기반)
741
+ // ========================================
738
742
  // 단, 협업자 폴더(yy_Coworker_*)는 삭제하지 않음 (오너가 협업자 파일을 삭제하면 안됨)
739
- // 기존 hard delete(delete-batch) soft delete(soft-delete)로 변경
740
- // 3일 후 크론잡에서 hard delete 수행
743
+ // source: 'web' 파일은 MCP에서 삭제 시도하지 않음 (클라이언트에서 필터링)
741
744
  // 중요: localPathsBeforePull 사용 (Pull 전에 수집한 목록)
742
745
  // processedLocalPaths는 Pull로 내려받은 파일도 포함하므로 사용 금지
743
746
  let deleted = 0;
744
747
  const deletedFilePaths = [];
745
748
  let protectedFiles = []; // source: 'web' 파일 (보호됨)
749
+ const webProtectedPaths = []; // MCP에서 미리 필터링한 웹 파일
750
+
746
751
  if (serverAllPaths.length > 0 && !isCoworker) {
747
- // 코워커가 아닌 경우에만 삭제 수행 (오너 전용)
752
+ // 오너만 삭제 수행
748
753
  // yy_Coworker_*로 시작하는 경로는 삭제 대상에서 제외
749
754
  // localPathsBeforePull: Pull 전에 수집한 로컬 파일 목록 (좀비 파일 방지)
750
- const pathsToDelete = serverAllPaths.filter(p =>
751
- !localPathsBeforePull.has(p) && !p.startsWith('yy_Coworker_')
752
- );
755
+ //
756
+ // 핵심: source: 'web' 파일은 삭제 대상에서 미리 제외!
757
+ // 이렇게 하면 웹에서 생성한 파일이 절대 삭제되지 않음
758
+ const pathsToDelete = serverAllPaths.filter(p => {
759
+ // 1. 로컬에 있으면 제외 (삭제 대상 아님)
760
+ if (localPathsBeforePull.has(p)) return false;
761
+
762
+ // 2. 협업자 폴더는 제외
763
+ if (p.startsWith('yy_Coworker_')) return false;
764
+
765
+ // 3. source: 'web' 파일은 제외 (웹에서 생성된 파일 보호)
766
+ const meta = serverPathToMeta[p];
767
+ if (meta && meta.source === 'web') {
768
+ webProtectedPaths.push(p);
769
+ return false;
770
+ }
753
771
 
754
- console.error(`[DocuKing] soft-delete 대상: ${pathsToDelete.length}개 (서버에만 있고 Pull 전 로컬에 없던 파일)`);
772
+ return true;
773
+ });
774
+
775
+ // 웹 보호 파일 로그
776
+ if (webProtectedPaths.length > 0) {
777
+ console.error(`[DocuKing] source:web 파일 ${webProtectedPaths.length}개 삭제에서 제외:`, webProtectedPaths.slice(0, 5));
778
+ }
779
+
780
+ console.error(`[DocuKing] soft-delete 대상: ${pathsToDelete.length}개 (서버에만 있고 Pull 전 로컬에 없던 파일, source:local만)`);
755
781
 
756
782
  if (pathsToDelete.length > 0) {
757
783
  try {
@@ -774,15 +800,18 @@ docuking_init을 먼저 실행하세요.`,
774
800
  deletedFilePaths.push(...(deleteResult.deletedPaths || []));
775
801
  protectedFiles = deleteResult.protected || [];
776
802
 
777
- // 보호된 파일이 있으면 로그 출력
803
+ // 보호된 파일이 있으면 로그 출력 (Backend에서 추가로 보호한 파일)
778
804
  if (protectedFiles.length > 0) {
779
- console.error(`[DocuKing] 보호된 파일 ${protectedFiles.length} (source: web):`, protectedFiles.slice(0, 5));
805
+ console.error(`[DocuKing] Backend에서 추가 보호된 파일 ${protectedFiles.length}개:`, protectedFiles.slice(0, 5));
780
806
  }
781
807
  }
782
808
  } catch (e) {
783
809
  console.error('[DocuKing] 파일 soft-delete 실패:', e.message);
784
810
  }
785
811
  }
812
+
813
+ // MCP에서 미리 제외한 웹 파일도 보호 목록에 추가 (결과 표시용)
814
+ protectedFiles = [...protectedFiles, ...webProtectedPaths];
786
815
  }
787
816
 
788
817
  // 5. 빈 폴더 생성
@@ -1018,54 +1047,79 @@ export async function handlePullInternal(args) {
1018
1047
  fs.mkdirSync(mainFolderPath, { recursive: true });
1019
1048
  }
1020
1049
 
1021
- // 파일 목록 조회
1022
- let files = [];
1023
- let deletedFiles = []; // 삭제된 파일 목록 (deleted_at 기반)
1050
+ // ========================================
1051
+ // 서버 메타정보 조회 (pathToMeta: source, type, deletedAt 포함)
1052
+ // ========================================
1053
+ let serverPathToHash = {};
1054
+ let serverHashToPath = {};
1055
+ let serverPathToMeta = {};
1056
+ let serverAllPaths = [];
1057
+ let deletedFiles = [];
1024
1058
 
1025
1059
  try {
1026
- const response = await fetch(
1027
- `${API_ENDPOINT}/files/tree?projectId=${projectId}`,
1060
+ const hashResponse = await fetch(
1061
+ `${API_ENDPOINT}/files/hashes-for-sync?projectId=${projectId}`,
1028
1062
  {
1029
1063
  headers: {
1030
1064
  'Authorization': `Bearer ${apiKey}`,
1031
1065
  },
1032
1066
  }
1033
1067
  );
1034
-
1035
- if (!response.ok) {
1036
- throw new Error(await response.text());
1068
+ if (hashResponse.ok) {
1069
+ const hashData = await hashResponse.json();
1070
+ serverPathToHash = hashData.pathToHash || {};
1071
+ serverHashToPath = hashData.hashToPath || {};
1072
+ serverPathToMeta = hashData.pathToMeta || {};
1073
+ serverAllPaths = hashData.allPaths || [];
1074
+ deletedFiles = hashData.deletedFiles || [];
1037
1075
  }
1038
-
1039
- const data = await response.json();
1040
- files = flattenTree(data.tree || []);
1041
1076
  } catch (e) {
1042
- throw new Error(`파일 목록 조회 실패 - ${e.message}`);
1077
+ console.error('[DocuKing] 서버 메타정보 조회 실패:', e.message);
1043
1078
  }
1044
1079
 
1045
- // 서버에서 삭제된 파일 목록 조회 (deleted_at 기반 동기화)
1080
+ // 파일 트리 조회 (폴더 포함)
1081
+ let files = [];
1046
1082
  try {
1047
- const hashResponse = await fetch(
1048
- `${API_ENDPOINT}/files/hashes-for-sync?projectId=${projectId}`,
1083
+ const response = await fetch(
1084
+ `${API_ENDPOINT}/files/tree?projectId=${projectId}`,
1049
1085
  {
1050
1086
  headers: {
1051
1087
  'Authorization': `Bearer ${apiKey}`,
1052
1088
  },
1053
1089
  }
1054
1090
  );
1055
- if (hashResponse.ok) {
1056
- const hashData = await hashResponse.json();
1057
- deletedFiles = hashData.deletedFiles || [];
1091
+
1092
+ if (!response.ok) {
1093
+ throw new Error(await response.text());
1058
1094
  }
1095
+
1096
+ const data = await response.json();
1097
+ files = flattenTree(data.tree || [], '', true); // 폴더 포함
1059
1098
  } catch (e) {
1060
- console.error('[DocuKing] 삭제 파일 목록 조회 실패:', e.message);
1099
+ throw new Error(`파일 목록 조회 실패 - ${e.message}`);
1061
1100
  }
1062
1101
 
1063
1102
  if (filePath) {
1064
1103
  files = files.filter(f => f.path === filePath || f.path.startsWith(filePath + '/'));
1065
1104
  }
1066
1105
 
1067
- if (files.length === 0) {
1068
- // 파일은 에러가 아님 - 정상 결과 반환
1106
+ // ========================================
1107
+ // 완전판 Pull 로직
1108
+ // 1. 폴더 먼저 정렬 (상위 폴더 → 하위 폴더)
1109
+ // 2. 파일 다운로드 (source:web 파일도 Pull)
1110
+ // 3. mark-as-pulled 호출 (source: web → local 변경)
1111
+ // ========================================
1112
+
1113
+ // 폴더를 먼저 정렬 (상위 폴더부터 생성해야 함)
1114
+ const sortedFiles = [...files].sort((a, b) => {
1115
+ // 폴더 먼저
1116
+ if (a.type === 'folder' && b.type !== 'folder') return -1;
1117
+ if (a.type !== 'folder' && b.type === 'folder') return 1;
1118
+ // 경로 깊이 순 (상위 먼저)
1119
+ return a.path.split('/').length - b.path.split('/').length;
1120
+ });
1121
+
1122
+ if (sortedFiles.length === 0 && deletedFiles.length === 0) {
1069
1123
  return {
1070
1124
  text: 'Pull할 파일이 없습니다.',
1071
1125
  total: 0,
@@ -1076,20 +1130,64 @@ export async function handlePullInternal(args) {
1076
1130
  };
1077
1131
  }
1078
1132
 
1079
- // 파일 다운로드
1080
1133
  const results = [];
1081
- const total = files.length;
1134
+ const total = sortedFiles.length;
1082
1135
  let current = 0;
1083
1136
  let downloaded = 0; // 새로 다운로드
1084
1137
  let updated = 0; // 업데이트 (변경됨)
1085
1138
  let skipped = 0; // 스킵 (변경 없음)
1086
1139
  let failed = 0; // 실패
1140
+ let foldersCreated = 0; // 생성된 폴더
1087
1141
 
1088
- for (const file of files) {
1142
+ // Pull 후 mark-as-pulled할 파일 목록 (source: web 파일들)
1143
+ const filesToMarkAsPulled = [];
1144
+
1145
+ for (const file of sortedFiles) {
1089
1146
  current++;
1090
1147
  const progress = `${current}/${total}`;
1091
1148
 
1149
+ // 서버 메타 정보 조회
1150
+ const serverMeta = serverPathToMeta[file.path];
1151
+
1152
+ // 서버 경로에 따라 로컬 저장 위치 결정
1153
+ let fullPath;
1154
+ if (file.path.startsWith('xx_') ||
1155
+ file.path.startsWith('yy_All_Docu/') ||
1156
+ file.path.startsWith('yy_All_Docu') ||
1157
+ file.path.startsWith('yy_Coworker_') ||
1158
+ file.path.startsWith('zz_ai_')) {
1159
+ fullPath = path.join(localPath, file.path);
1160
+ } else {
1161
+ fullPath = path.join(mainFolderPath, file.path);
1162
+ }
1163
+
1092
1164
  try {
1165
+ // ========================================
1166
+ // 폴더 처리 (type === 'folder')
1167
+ // ========================================
1168
+ if (file.type === 'folder') {
1169
+ if (fs.existsSync(fullPath)) {
1170
+ // 이미 있음 → 스킵
1171
+ results.push({ type: 'skip', path: file.path, itemType: 'folder' });
1172
+ skipped++;
1173
+ } else {
1174
+ // 폴더 생성
1175
+ fs.mkdirSync(fullPath, { recursive: true });
1176
+ results.push({ type: 'download', path: file.path, itemType: 'folder' });
1177
+ foldersCreated++;
1178
+ downloaded++;
1179
+
1180
+ // source: 'web' 폴더면 mark-as-pulled 대상
1181
+ if (serverMeta && serverMeta.source === 'web') {
1182
+ filesToMarkAsPulled.push(file.path);
1183
+ }
1184
+ }
1185
+ continue;
1186
+ }
1187
+
1188
+ // ========================================
1189
+ // 파일 처리 (type === 'file')
1190
+ // ========================================
1093
1191
  const response = await fetch(
1094
1192
  `${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
1095
1193
  {
@@ -1107,25 +1205,6 @@ export async function handlePullInternal(args) {
1107
1205
 
1108
1206
  const data = await response.json();
1109
1207
 
1110
- // 서버 경로에 따라 로컬 저장 위치 결정
1111
- // 프로젝트 루트 기준 절대경로 그대로 사용
1112
- // - xx_Infra_Config/xxx → xx_Infra_Config/xxx (시스템/인프라)
1113
- // - xx_Policy/xxx → xx_Policy/xxx (정책)
1114
- // - yy_All_Docu/xxx → yy_All_Docu/xxx (오너 문서)
1115
- // - yy_Coworker_xxx/yyy → yy_Coworker_xxx/yyy (협업자 폴더)
1116
- // - zz_ai_xxx/yyy → zz_ai_xxx/yyy (AI 폴더)
1117
- let fullPath;
1118
- if (file.path.startsWith('xx_') ||
1119
- file.path.startsWith('yy_All_Docu/') ||
1120
- file.path.startsWith('yy_Coworker_') ||
1121
- file.path.startsWith('zz_ai_')) {
1122
- // 서버 경로 그대로 로컬에 저장
1123
- fullPath = path.join(localPath, file.path);
1124
- } else {
1125
- // 기타 (구버전 호환): yy_All_Docu/ 안에 저장
1126
- fullPath = path.join(mainFolderPath, file.path);
1127
- }
1128
-
1129
1208
  // 인코딩에 따라 저장
1130
1209
  const content = data.file?.content || data.content || '';
1131
1210
  const encoding = data.file?.encoding || data.encoding || 'utf-8';
@@ -1145,6 +1224,13 @@ export async function handlePullInternal(args) {
1145
1224
 
1146
1225
  if (localExists) {
1147
1226
  try {
1227
+ const localStat = fs.statSync(fullPath);
1228
+ if (localStat.isDirectory()) {
1229
+ // 로컬에 같은 이름의 폴더가 있으면 건너뜀
1230
+ results.push({ type: 'skip', path: file.path, error: '같은 이름의 폴더 존재' });
1231
+ skipped++;
1232
+ continue;
1233
+ }
1148
1234
  const localBuffer = fs.readFileSync(fullPath);
1149
1235
  localHash = crypto.createHash('sha256').update(localBuffer).digest('hex');
1150
1236
  } catch (e) {
@@ -1171,20 +1257,49 @@ export async function handlePullInternal(args) {
1171
1257
  }
1172
1258
 
1173
1259
  if (localExists) {
1174
- // 기존 파일 업데이트
1175
1260
  results.push({ type: 'update', path: file.path });
1176
1261
  updated++;
1177
1262
  } else {
1178
- // 새로 다운로드
1179
1263
  results.push({ type: 'download', path: file.path });
1180
1264
  downloaded++;
1181
1265
  }
1266
+
1267
+ // source: 'web' 파일이면 mark-as-pulled 대상
1268
+ if (serverMeta && serverMeta.source === 'web') {
1269
+ filesToMarkAsPulled.push(file.path);
1270
+ }
1182
1271
  } catch (e) {
1183
1272
  results.push({ type: 'fail', path: file.path, error: e.message });
1184
1273
  failed++;
1185
1274
  }
1186
1275
  }
1187
1276
 
1277
+ // ========================================
1278
+ // mark-as-pulled 호출 (source: 'web' → 'local' 변경)
1279
+ // Pull된 웹 파일/폴더는 이제 로컬이 관리권한을 가짐
1280
+ // ========================================
1281
+ if (filesToMarkAsPulled.length > 0) {
1282
+ try {
1283
+ const markResponse = await fetch(`${API_ENDPOINT}/files/mark-as-pulled`, {
1284
+ method: 'POST',
1285
+ headers: {
1286
+ 'Content-Type': 'application/json',
1287
+ 'Authorization': `Bearer ${apiKey}`,
1288
+ },
1289
+ body: JSON.stringify({
1290
+ projectId,
1291
+ paths: filesToMarkAsPulled,
1292
+ }),
1293
+ });
1294
+
1295
+ if (markResponse.ok) {
1296
+ console.error(`[DocuKing] mark-as-pulled 완료: ${filesToMarkAsPulled.length}개`);
1297
+ }
1298
+ } catch (e) {
1299
+ console.error('[DocuKing] mark-as-pulled 실패:', e.message);
1300
+ }
1301
+ }
1302
+
1188
1303
  // 상세 결과 구성 (Push 스타일)
1189
1304
  let resultText = `✓ Pull 완료!
1190
1305
 
package/index.js CHANGED
@@ -62,11 +62,17 @@ import {
62
62
 
63
63
  import { handleValidate } from './handlers/validate.js';
64
64
 
65
- // MCP 서버 생성
65
+ // 버전 체크 모듈
66
+ import { checkForUpdates, getVersionWarningMessage, getCurrentVersion } from './lib/version-check.js';
67
+
68
+ // 세션당 1회 알림 플래그
69
+ let versionWarningShown = false;
70
+
71
+ // MCP 서버 생성 (package.json에서 버전 읽기)
66
72
  const server = new Server(
67
73
  {
68
74
  name: 'docuking-mcp',
69
- version: '1.0.0',
75
+ version: getCurrentVersion(),
70
76
  },
71
77
  {
72
78
  capabilities: {
@@ -628,29 +634,55 @@ DocuKing은 문서 버전 관리 시스템입니다. Git이 코드를 관리하
628
634
 
629
635
  작동 방식: 로컬 yy_All_Docu/ → Push → 킹폴더 → 캔버스에서 시각화
630
636
 
631
- ## MCP 설정과 프로젝트 연결의 차이 (매우 중요!)
632
-
633
- ### MCP 설정 (전역, 한 번만)
634
- - **목적**: MCP 서버를 AI 도구(Cursor, Claude Code 등)에 등록
635
- - **설정 파일 위치**:
636
- - Cursor: \`~/.cursor/mcp.json\` (Windows: \`%USERPROFILE%\\.cursor\\mcp.json\`)
637
- - Claude Code: \`~/.claude.json\`
638
- - 기타 VSCode 계열: 각각의 설정 파일
639
- - **설정 내용**: \`docuking\` 서버를 \`mcpServers\` 객체에 추가
640
- - **재시작 필요**: MCP 설정 파일을 처음 만들었을 때만 필요 (Cursor/Claude Code 재시작)
641
- - **중요**: 이미 다른 MCP 서버가 설정되어 있으면, 기존 설정을 덮어쓰지 말고 \`docuking\` 항목만 추가해야 함
642
-
643
- ### 프로젝트 연결 (각 폴더마다)
644
- - **목적**: 특정 로컬 폴더를 DocuKing 서버의 프로젝트 ID와 연결
645
- - **실행 방법**: \`docuking_init(projectId, localPath)\` 도구 호출
646
- - **결과**: 해당 폴더에 \`yy_All_Docu/\` 폴더 생성 및 프로젝트 매핑 저장
647
- - **재시작 불필요**: MCP가 이미 작동 중이면 바로 실행 가능
648
- - **다중 프로젝트**: 한 컴퓨터에서 여러 폴더를 각각 다른 DocuKing 프로젝트와 연결 가능
649
-
650
- **핵심 원칙:**
651
- 1. MCP 설정은 한 번만 (모든 폴더 공통)
652
- 2. 프로젝트 연결은 각 폴더마다 (폴더별로 다른 프로젝트 ID)
653
- 3. 이미 MCP가 작동 중이면 재시작 없이 바로 프로젝트 연결 가능
637
+ ## MCP 설치 설정 (매우 중요!)
638
+
639
+ ### 1단계: 글로벌 설치 (필수)
640
+ \`\`\`bash
641
+ npm install -g docuking-mcp
642
+ \`\`\`
643
+
644
+ ### 2단계: MCP 등록 (AI 도구별)
645
+
646
+ **Claude Code:**
647
+ \`\`\`bash
648
+ claude mcp add docuking -s user -- docuking-mcp
649
+ \`\`\`
650
+
651
+ **Cursor:** \`~/.cursor/mcp.json\`에 추가
652
+ \`\`\`json
653
+ {
654
+ "mcpServers": {
655
+ "docuking": {
656
+ "command": "docuking-mcp",
657
+ "args": []
658
+ }
659
+ }
660
+ }
661
+ \`\`\`
662
+
663
+ **Gemini CLI:** \`~/.gemini/antigravity/mcp_config.json\`에 추가
664
+ \`\`\`json
665
+ {
666
+ "mcpServers": {
667
+ "docuking": {
668
+ "command": "docuking-mcp",
669
+ "args": []
670
+ }
671
+ }
672
+ }
673
+ \`\`\`
674
+
675
+ ### 업데이트
676
+ 새 버전이 있으면 첫 도구 호출 시 자동으로 알려줍니다.
677
+ \`\`\`bash
678
+ npm update -g docuking-mcp
679
+ \`\`\`
680
+
681
+ ### MCP 설정 vs 프로젝트 연결
682
+ - **MCP 설정**: 전역, 한 번만 (위의 설치/등록)
683
+ - **프로젝트 연결**: 각 폴더마다 \`docuking_init\` 실행
684
+ - **중요**: 이미 다른 MCP 서버가 있으면 덮어쓰지 말고 \`docuking\` 항목만 추가
685
+
654
686
 
655
687
  ## .gitignore 설정 (중요!)
656
688
 
@@ -713,13 +745,30 @@ zz_ai_3_Plan/
713
745
  - planId로 계획 찾기
714
746
  - 완료 요약 및 산출물 기록
715
747
 
716
- ### 12. docuking_delete
748
+ ### 12. docuking_todo
749
+ 킹투두를 관리합니다 (\`zz_ai_2_Todo/z_King_Todo.md\`).
750
+ - action: "add" (추가), "done" (완료), "list" (조회)
751
+ - 프로젝트 공식 할일 목록 (Push하면 웹에서 확인 가능)
752
+
753
+ ### 13. docuking_urgent
754
+ 긴급 보고를 \`xx_Urgent/\` 폴더에 작성합니다 (킹어전트).
755
+ - 정책에 없는 예외 상황 발견 시 오너에게 보고
756
+ - 협업자도 xx_Urgent/에 쓰기 가능 (유일한 예외)
757
+ - 오너가 Pull할 때 "긴급 보고 N건" 알림
758
+
759
+ ### 14. docuking_delete
717
760
  서버에서 파일/폴더를 직접 삭제합니다.
718
761
  - 로컬에서 삭제한 파일이 Push 후에도 서버에 남아있을 때
719
762
  - 서버에만 존재하는 불필요한 파일 정리
720
763
  - soft-delete 처리 (3일 후 영구 삭제)
721
764
  - 권한: 오너는 자기 영역, 협업자는 자기 폴더만
722
765
 
766
+ ### 15. docuking_validate
767
+ 코드가 정책을 준수하는지 검증합니다 (킹밸리데이트).
768
+ - .claude/rules/local/ 정책 기준으로 검증
769
+ - 폴더 구조, 명명 규칙, API 규칙 등 체크
770
+ - 위반 목록과 수정 제안 반환
771
+
723
772
  ## Git과의 유사성
724
773
 
725
774
  | DocuKing | Git | 설명 |
@@ -818,40 +867,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
818
867
  const { name, arguments: args } = request.params;
819
868
 
820
869
  try {
870
+ let result;
821
871
  switch (name) {
822
872
  case 'docuking_init':
823
- return await handleInit(args);
873
+ result = await handleInit(args);
874
+ break;
824
875
  case 'docuking_push':
825
- return await handlePush(args);
876
+ result = await handlePush(args);
877
+ break;
826
878
  case 'docuking_pull':
827
- return await handlePull(args);
879
+ result = await handlePull(args);
880
+ break;
828
881
  case 'docuking_list':
829
- return await handleList(args);
882
+ result = await handleList(args);
883
+ break;
830
884
  case 'docuking_status':
831
- return await handleStatus(args);
885
+ result = await handleStatus(args);
886
+ break;
832
887
  case 'docuking_log':
833
- return await handleLog(args);
888
+ result = await handleLog(args);
889
+ break;
834
890
  case 'docuking_diff':
835
- return await handleDiff(args);
891
+ result = await handleDiff(args);
892
+ break;
836
893
  case 'docuking_rollback':
837
- return await handleRollback(args);
894
+ result = await handleRollback(args);
895
+ break;
838
896
  case 'docuking_talk':
839
- return await handleTalk(args);
897
+ result = await handleTalk(args);
898
+ break;
840
899
  case 'docuking_plan':
841
- return await handlePlan(args);
900
+ result = await handlePlan(args);
901
+ break;
842
902
  case 'docuking_done':
843
- return await handleDone(args);
903
+ result = await handleDone(args);
904
+ break;
844
905
  case 'docuking_todo':
845
- return await handleTodo(args);
906
+ result = await handleTodo(args);
907
+ break;
846
908
  case 'docuking_urgent':
847
- return await handleUrgent(args);
909
+ result = await handleUrgent(args);
910
+ break;
848
911
  case 'docuking_delete':
849
- return await handleDelete(args);
912
+ result = await handleDelete(args);
913
+ break;
850
914
  case 'docuking_validate':
851
- return await handleValidate(args);
915
+ result = await handleValidate(args);
916
+ break;
852
917
  default:
853
918
  throw new Error(`Unknown tool: ${name}`);
854
919
  }
920
+
921
+ // 버전 체크 알림 (세션당 1회)
922
+ if (!versionWarningShown) {
923
+ try {
924
+ const updateInfo = await checkForUpdates();
925
+ const warning = getVersionWarningMessage(updateInfo);
926
+ if (warning) {
927
+ versionWarningShown = true;
928
+ result.content.push({
929
+ type: 'text',
930
+ text: warning
931
+ });
932
+ }
933
+ } catch (e) {
934
+ // 버전 체크 실패해도 무시 (서버 작동에 영향 없음)
935
+ }
936
+ }
937
+
938
+ return result;
855
939
  } catch (error) {
856
940
  return {
857
941
  content: [
package/lib/files.js CHANGED
@@ -193,18 +193,33 @@ export function collectFiles(basePath, relativePath, results, excludedFiles = nu
193
193
  }
194
194
 
195
195
  /**
196
- * 트리 구조를 평탄화
196
+ * 트리 구조를 평탄화 (파일 + 폴더 모두 포함)
197
+ * @param {Array} tree - 트리 구조
198
+ * @param {string} prefix - 경로 접두어
199
+ * @param {boolean} includeEmpty - 빈 폴더도 포함 (기본: true)
200
+ * @returns {Array} - { path, name, type } 배열
197
201
  */
198
- export function flattenTree(tree, prefix = '') {
202
+ export function flattenTree(tree, prefix = '', includeEmpty = true) {
199
203
  const results = [];
200
204
 
201
205
  for (const item of tree) {
202
206
  const itemPath = prefix ? `${prefix}/${item.name}` : item.name;
203
207
 
204
208
  if (item.type === 'file') {
205
- results.push({ path: itemPath, name: item.name });
209
+ // 파일
210
+ results.push({ path: itemPath, name: item.name, type: 'file' });
211
+ } else if (item.type === 'folder') {
212
+ // 폴더도 결과에 추가 (빈 폴더 동기화를 위해)
213
+ if (includeEmpty) {
214
+ results.push({ path: itemPath, name: item.name, type: 'folder' });
215
+ }
216
+ // 자식 탐색
217
+ if (item.children && item.children.length > 0) {
218
+ results.push(...flattenTree(item.children, itemPath, includeEmpty));
219
+ }
206
220
  } else if (item.children) {
207
- results.push(...flattenTree(item.children, itemPath));
221
+ // 구버전 호환: type 없이 children 있는 경우
222
+ results.push(...flattenTree(item.children, itemPath, includeEmpty));
208
223
  }
209
224
  }
210
225
 
@@ -0,0 +1,119 @@
1
+ /**
2
+ * DocuKing MCP 버전 체크 모듈
3
+ *
4
+ * npm registry에서 최신 버전을 확인하고,
5
+ * 현재 버전과 비교하여 업데이트 필요 여부를 알려줍니다.
6
+ */
7
+
8
+ import { readFileSync } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+
15
+ // 캐시 (세션 동안 유지)
16
+ let cachedUpdateInfo = null;
17
+ let lastCheckTime = 0;
18
+ const CHECK_INTERVAL = 60 * 60 * 1000; // 1시간
19
+
20
+ /**
21
+ * npm registry에서 최신 버전 확인 후 업데이트 필요 여부 반환
22
+ * - 1시간 캐시
23
+ * - 네트워크 오류 시 graceful degradation
24
+ */
25
+ export async function checkForUpdates() {
26
+ const now = Date.now();
27
+
28
+ // 1시간 내 체크했으면 캐시 반환
29
+ if (cachedUpdateInfo && (now - lastCheckTime) < CHECK_INTERVAL) {
30
+ return cachedUpdateInfo;
31
+ }
32
+
33
+ try {
34
+ const current = getCurrentVersion();
35
+ const latest = await getLatestVersion();
36
+
37
+ cachedUpdateInfo = {
38
+ current,
39
+ latest,
40
+ available: compareVersions(latest, current) > 0
41
+ };
42
+ lastCheckTime = now;
43
+
44
+ if (cachedUpdateInfo.available) {
45
+ console.error(`[DocuKing MCP] 새 버전 ${latest} 출시! (현재: ${current})`);
46
+ }
47
+
48
+ return cachedUpdateInfo;
49
+ } catch (error) {
50
+ // 네트워크 오류 등은 무시 (서버 작동에 영향 없음)
51
+ console.error('[DocuKing MCP] 버전 체크 실패:', error.message);
52
+ return { available: false };
53
+ }
54
+ }
55
+
56
+ /**
57
+ * package.json에서 현재 버전 읽기
58
+ */
59
+ export function getCurrentVersion() {
60
+ const pkgPath = join(__dirname, '..', 'package.json');
61
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
62
+ return pkg.version;
63
+ }
64
+
65
+ /**
66
+ * npm registry에서 최신 버전 조회
67
+ */
68
+ async function getLatestVersion() {
69
+ const controller = new AbortController();
70
+ const timeout = setTimeout(() => controller.abort(), 5000); // 5초 타임아웃
71
+
72
+ try {
73
+ const response = await fetch('https://registry.npmjs.org/docuking-mcp/latest', {
74
+ signal: controller.signal
75
+ });
76
+ const data = await response.json();
77
+ return data.version;
78
+ } finally {
79
+ clearTimeout(timeout);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 버전 비교 (semver 단순 비교)
85
+ * @returns 1 if a > b, -1 if a < b, 0 if equal
86
+ */
87
+ function compareVersions(a, b) {
88
+ const partsA = a.split('.').map(Number);
89
+ const partsB = b.split('.').map(Number);
90
+
91
+ for (let i = 0; i < 3; i++) {
92
+ const numA = partsA[i] || 0;
93
+ const numB = partsB[i] || 0;
94
+ if (numA > numB) return 1;
95
+ if (numA < numB) return -1;
96
+ }
97
+ return 0;
98
+ }
99
+
100
+ /**
101
+ * AI에게 보여줄 업데이트 경고 메시지 생성
102
+ */
103
+ export function getVersionWarningMessage(updateInfo) {
104
+ if (!updateInfo || !updateInfo.available) return null;
105
+
106
+ return `
107
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
108
+ ⚠️ DocuKing MCP 업데이트 필요
109
+
110
+ 현재 버전: ${updateInfo.current}
111
+ 최신 버전: ${updateInfo.latest}
112
+
113
+ 업데이트 명령어:
114
+ npm update -g docuking-mcp
115
+
116
+ 새 버전에서 버그 수정 및 기능 개선이 있습니다.
117
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118
+ `;
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docuking-mcp",
3
- "version": "2.9.3",
3
+ "version": "2.11.0",
4
4
  "description": "DocuKing MCP Server - AI 시대의 문서 협업 플랫폼",
5
5
  "type": "module",
6
6
  "main": "index.js",