docuking-mcp 1.0.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.
Files changed (4) hide show
  1. package/DEPLOY_TASK.md +33 -0
  2. package/README.md +118 -0
  3. package/index.js +1594 -0
  4. package/package.json +30 -0
package/index.js ADDED
@@ -0,0 +1,1594 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * DocuKing MCP Server
5
+ *
6
+ * AI 시대의 문서 협업 플랫폼 - AI가 문서를 Push/Pull 할 수 있게 해주는 MCP 서버
7
+ *
8
+ * 도구:
9
+ * - docuking_init: 레포 연결, Z_DocuKing/ 폴더 생성
10
+ * - docuking_push: 로컬 → 서버
11
+ * - docuking_pull: 서버 → 로컬
12
+ */
13
+
14
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import {
17
+ CallToolRequestSchema,
18
+ ListToolsRequestSchema,
19
+ ListResourcesRequestSchema,
20
+ ReadResourceRequestSchema,
21
+ } from '@modelcontextprotocol/sdk/types.js';
22
+ import fs from 'fs';
23
+ import path from 'path';
24
+ import crypto from 'crypto';
25
+
26
+ // 환경변수에서 설정 로드
27
+ const API_KEY = process.env.DOCUKING_API_KEY || '';
28
+ const API_ENDPOINT = process.env.DOCUKING_API_ENDPOINT || 'https://docuking.ai/api';
29
+ const REPOS_JSON = process.env.DOCUKING_REPOS || '{}';
30
+
31
+ // 레포 매핑 파싱
32
+ // 형식: { "경로": { "id": "프로젝트ID", "name": "프로젝트이름" } }
33
+ // 또는 기존 형식: { "경로": "프로젝트ID" } (하위 호환)
34
+ let repoMapping = {};
35
+ try {
36
+ const parsed = JSON.parse(REPOS_JSON);
37
+ // 기존 형식(문자열)과 새 형식(객체) 모두 지원
38
+ for (const [path, value] of Object.entries(parsed)) {
39
+ if (typeof value === 'string') {
40
+ // 기존 형식: "경로": "프로젝트ID"
41
+ repoMapping[path] = { id: value, name: null };
42
+ } else if (typeof value === 'object' && value.id) {
43
+ // 새 형식: "경로": { "id": "...", "name": "..." }
44
+ repoMapping[path] = value;
45
+ }
46
+ }
47
+ } catch (e) {
48
+ console.error('[DocuKing] DOCUKING_REPOS 파싱 실패:', e.message);
49
+ }
50
+
51
+ // 프로젝트 정보 조회 (로컬 매핑에서, 서버 호출 없음)
52
+ // Git처럼 사용자가 설정한 매핑을 신뢰
53
+ function getProjectInfo(localPath) {
54
+ const mapped = repoMapping[localPath];
55
+ if (!mapped) {
56
+ return {
57
+ error: `오류: 이 프로젝트는 DocuKing에 연결되지 않았습니다.
58
+ 먼저 docuking_init을 실행하세요.`,
59
+ };
60
+ }
61
+
62
+ return {
63
+ projectId: mapped.id,
64
+ projectName: mapped.name,
65
+ };
66
+ }
67
+
68
+ // MCP 서버 생성
69
+ const server = new Server(
70
+ {
71
+ name: 'docuking-mcp',
72
+ version: '1.0.0',
73
+ },
74
+ {
75
+ capabilities: {
76
+ tools: {},
77
+ resources: {},
78
+ },
79
+ }
80
+ );
81
+
82
+ // 도구 목록
83
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
84
+ return {
85
+ tools: [
86
+ {
87
+ name: 'docuking_init',
88
+ description: '프로젝트를 DocuKing에 연결합니다. Z_DocuKing/ 폴더를 생성하고 레포 매핑을 추가합니다.\n\n**필수 파라미터:**\n- projectId: 프로젝트 UUID\n- projectName: 프로젝트 이름 (표시용)\n- localPath: 로컬 프로젝트 경로',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ projectId: {
93
+ type: 'string',
94
+ description: 'DocuKing 프로젝트 ID',
95
+ },
96
+ projectName: {
97
+ type: 'string',
98
+ description: 'DocuKing 프로젝트 이름 (표시용)',
99
+ },
100
+ localPath: {
101
+ type: 'string',
102
+ description: '로컬 프로젝트 경로 (예: /Users/user/my-project)',
103
+ },
104
+ },
105
+ required: ['projectId', 'projectName', 'localPath'],
106
+ },
107
+ },
108
+ {
109
+ name: 'docuking_push',
110
+ description: 'z_DocuKing/ 폴더의 문서를 서버에 업로드합니다. "DocuKing에 올려줘" 요청 시 사용. Git의 add + commit + push를 한 번에 수행.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ localPath: {
115
+ type: 'string',
116
+ description: '로컬 프로젝트 경로 (레포 매핑에서 projectId 찾음)',
117
+ },
118
+ filePath: {
119
+ type: 'string',
120
+ description: '푸시할 파일 경로 (Z_DocuKing/ 기준 상대경로). 생략 시 전체 동기화.',
121
+ },
122
+ message: {
123
+ type: 'string',
124
+ description: '커밋 메시지 (필수). Git 커밋 메시지처럼 명확하고 구체적으로 작성. 예: "README에 설치 가이드 추가"',
125
+ },
126
+ author: {
127
+ type: 'string',
128
+ description: '작성자 (선택). 생략 시 현재 사용자.',
129
+ },
130
+ },
131
+ required: ['localPath', 'message'],
132
+ },
133
+ },
134
+ {
135
+ name: 'docuking_pull',
136
+ description: '서버에서 문서를 다운로드하여 z_DocuKing/ 폴더에 저장합니다. "DocuKing에서 가져와" 요청 시 사용.',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ localPath: {
141
+ type: 'string',
142
+ description: '로컬 프로젝트 경로',
143
+ },
144
+ filePath: {
145
+ type: 'string',
146
+ description: '풀할 파일 경로 (생략 시 전체 동기화)',
147
+ },
148
+ },
149
+ required: ['localPath'],
150
+ },
151
+ },
152
+ {
153
+ name: 'docuking_list',
154
+ description: '서버에 저장된 파일 목록을 조회합니다.',
155
+ inputSchema: {
156
+ type: 'object',
157
+ properties: {
158
+ localPath: {
159
+ type: 'string',
160
+ description: '로컬 프로젝트 경로 (레포 매핑에서 projectId 찾음)',
161
+ },
162
+ },
163
+ required: ['localPath'],
164
+ },
165
+ },
166
+ {
167
+ name: 'docuking_status',
168
+ description: '로컬과 서버의 동기화 상태를 확인합니다. 사용자 권한(오너/참여자), 변경/추가/삭제된 파일 목록 표시.',
169
+ inputSchema: {
170
+ type: 'object',
171
+ properties: {
172
+ localPath: {
173
+ type: 'string',
174
+ description: '로컬 프로젝트 경로',
175
+ },
176
+ },
177
+ required: ['localPath'],
178
+ },
179
+ },
180
+ {
181
+ name: 'docuking_log',
182
+ description: '커밋 히스토리를 조회합니다. Git log와 동일.',
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: {
186
+ localPath: {
187
+ type: 'string',
188
+ description: '로컬 프로젝트 경로',
189
+ },
190
+ path: {
191
+ type: 'string',
192
+ description: '특정 파일/폴더의 히스토리만 조회 (선택)',
193
+ },
194
+ limit: {
195
+ type: 'number',
196
+ description: '최근 N개 커밋만 조회 (기본값: 20)',
197
+ },
198
+ },
199
+ required: ['localPath'],
200
+ },
201
+ },
202
+ {
203
+ name: 'docuking_diff',
204
+ description: '버전 간 차이를 비교합니다.',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ localPath: {
209
+ type: 'string',
210
+ description: '로컬 프로젝트 경로',
211
+ },
212
+ path: {
213
+ type: 'string',
214
+ description: '파일 경로',
215
+ },
216
+ version: {
217
+ type: 'string',
218
+ description: '비교할 커밋 ID (선택, 생략 시 최신 vs 이전)',
219
+ },
220
+ },
221
+ required: ['localPath', 'path'],
222
+ },
223
+ },
224
+ {
225
+ name: 'docuking_rollback',
226
+ description: '특정 커밋으로 되돌립니다.',
227
+ inputSchema: {
228
+ type: 'object',
229
+ properties: {
230
+ localPath: {
231
+ type: 'string',
232
+ description: '로컬 프로젝트 경로',
233
+ },
234
+ commitId: {
235
+ type: 'string',
236
+ description: '되돌릴 커밋 ID',
237
+ },
238
+ path: {
239
+ type: 'string',
240
+ description: '특정 파일만 롤백 (선택, 생략 시 전체)',
241
+ },
242
+ },
243
+ required: ['localPath', 'commitId'],
244
+ },
245
+ },
246
+ ],
247
+ };
248
+ });
249
+
250
+ // Resources 목록
251
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
252
+ return {
253
+ resources: [
254
+ {
255
+ uri: "docuking://docs/manual",
256
+ name: "DocuKing 사용 설명서",
257
+ description: "AI가 DocuKing을 이해하고 사용하기 위한 기초 지식",
258
+ mimeType: "text/markdown"
259
+ }
260
+ ]
261
+ };
262
+ });
263
+
264
+ // Resource 읽기
265
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
266
+ if (request.params.uri === "docuking://docs/manual") {
267
+ const manual = `# DocuKing - AI 시대의 문서 협업 플랫폼 (AI용 매뉴얼)
268
+
269
+ DocuKing은 문서 버전 관리 시스템입니다. Git이 코드를 관리하듯, DocuKing은 문서를 관리합니다.
270
+
271
+ ## 핵심 개념
272
+
273
+ - **로컬**: 사용자의 z_DocuKing/ 폴더
274
+ - **웹탐색기**: DocuKing 서버의 파일 저장소 (로컬 미러)
275
+ - **캔버스**: 선택된 파일을 시각화하는 작업 공간
276
+
277
+ 작동 방식: 로컬 문서 → Push → 웹탐색기 → 캔버스에서 시각화
278
+
279
+ ## MCP 설정과 프로젝트 연결의 차이 (매우 중요!)
280
+
281
+ ### MCP 설정 (전역, 한 번만)
282
+ - **목적**: MCP 서버를 AI 도구(Cursor, Claude Code 등)에 등록
283
+ - **설정 파일 위치**:
284
+ - Cursor: \`~/.cursor/mcp.json\` (Windows: \`%USERPROFILE%\\.cursor\\mcp.json\`)
285
+ - Claude Code: \`~/.claude.json\`
286
+ - 기타 VSCode 계열: 각각의 설정 파일
287
+ - **설정 내용**: \`docuking\` 서버를 \`mcpServers\` 객체에 추가
288
+ - **재시작 필요**: MCP 설정 파일을 처음 만들었을 때만 필요 (Cursor/Claude Code 재시작)
289
+ - **중요**: 이미 다른 MCP 서버가 설정되어 있으면, 기존 설정을 덮어쓰지 말고 \`docuking\` 항목만 추가해야 함
290
+
291
+ ### 프로젝트 연결 (각 폴더마다)
292
+ - **목적**: 특정 로컬 폴더를 DocuKing 서버의 프로젝트 ID와 연결
293
+ - **실행 방법**: \`docuking_init(projectId, localPath)\` 도구 호출
294
+ - **결과**: 해당 폴더에 \`z_DocuKing/\` 폴더 생성 및 프로젝트 매핑 저장
295
+ - **재시작 불필요**: MCP가 이미 작동 중이면 바로 실행 가능
296
+ - **다중 프로젝트**: 한 컴퓨터에서 여러 폴더를 각각 다른 DocuKing 프로젝트와 연결 가능
297
+
298
+ **핵심 원칙:**
299
+ 1. MCP 설정은 한 번만 (모든 폴더 공통)
300
+ 2. 프로젝트 연결은 각 폴더마다 (폴더별로 다른 프로젝트 ID)
301
+ 3. 이미 MCP가 작동 중이면 재시작 없이 바로 프로젝트 연결 가능
302
+
303
+ ## MCP 도구 목록
304
+
305
+ ### 1. docuking_init
306
+ 프로젝트를 DocuKing에 연결합니다.
307
+
308
+ ### 2. docuking_push
309
+ 로컬 문서를 서버에 업로드합니다. Git의 add + commit + push를 한 번에 수행.
310
+ **message 파라미터 필수** (커밋 메시지)
311
+
312
+ ### 3. docuking_pull
313
+ 서버 문서를 로컬로 다운로드합니다.
314
+
315
+ ### 4. docuking_list
316
+ 서버 파일 목록을 조회합니다.
317
+
318
+ ### 5. docuking_status
319
+ 로컬과 서버의 동기화 상태를 확인합니다. 사용자 권한(오너/참여자), 변경/추가/삭제된 파일 목록 표시.
320
+
321
+ ### 6. docuking_log
322
+ 커밋 히스토리를 조회합니다. (웹 탐색기에서 사용 가능)
323
+
324
+ ### 7. docuking_diff
325
+ 버전 간 차이를 비교합니다. (웹 탐색기에서 사용 가능)
326
+
327
+ ### 8. docuking_rollback
328
+ 특정 커밋으로 되돌립니다. (웹 탐색기에서 사용 가능)
329
+
330
+ ## Git과의 유사성
331
+
332
+ | DocuKing | Git | 설명 |
333
+ |----------|-----|------|
334
+ | docuking_push | git add . && git commit -m "..." && git push | 스테이징 + 커밋 + 푸시 통합 |
335
+ | docuking_pull | git pull | 서버 → 로컬 동기화 |
336
+ | docuking_status | git status | 변경 사항 확인 |
337
+ | docuking_log | git log | 커밋 히스토리 |
338
+ | docuking_diff | git diff | 버전 비교 |
339
+ | docuking_rollback | git revert | 되돌리기 |
340
+
341
+ **핵심 차이점:**
342
+ - Git은 3단계 (add → commit → push)
343
+ - DocuKing은 1단계 (push만으로 완료, 단 message는 필수)
344
+ - 더 간단하지만 커밋 개념은 동일하게 유지
345
+
346
+ ## 자연어 명령어 매핑
347
+
348
+ | 사용자 말 | MCP 도구 호출 |
349
+ |----------|--------------|
350
+ | "프로젝트 [ID] 연결해줘" | docuking_init({ projectId, localPath }) |
351
+ | "DocuKing에 올려줘" | docuking_push({ localPath, message: "..." }) |
352
+ | "DocuKing에서 가져와" | docuking_pull({ localPath }) |
353
+ | "DocuKing에 뭐 있어?" | docuking_list({ localPath }) |
354
+
355
+ ## 사용 예시
356
+
357
+ \`\`\`
358
+ 사용자: "프로젝트 3b8f95c1 연결해줘"
359
+ AI: docuking_init({ projectId: "3b8f95c1-f557-4a1d-8f1e-f34adb010256", localPath: "/current/path" })
360
+
361
+ 사용자: "DocuKing에 올려줘"
362
+ AI: "어떤 변경인가요? 커밋 메시지를 알려주세요."
363
+ 사용자: "문서 업데이트"
364
+ AI: docuking_push({ localPath: "/current/path", message: "문서 업데이트" })
365
+
366
+ 사용자: "DocuKing에서 가져와"
367
+ AI: docuking_pull({ localPath: "/current/path" })
368
+ \`\`\`
369
+
370
+ ## 사용자 권한 및 사용 시나리오 (매우 중요!)
371
+
372
+ DocuKing에는 **오너(Owner)**와 **참여자(Co-worker)** 두 가지 권한이 있습니다.
373
+
374
+ ### 오너 (Owner) - 프로젝트 생성자
375
+
376
+ **특징:**
377
+ - 프로젝트를 직접 생성한 사람
378
+ - **모든 폴더**에 문서를 올릴 수 있음 (제한 없음)
379
+ - API Key: sk_로 시작
380
+ - 프로젝트 설정 변경 가능
381
+ - 참여자 초대 가능
382
+
383
+ **사용 시나리오:**
384
+ 1. 프로젝트 생성 (웹에서)
385
+ 2. MCP 설정 (한 번만)
386
+ 3. 프로젝트 연결 (docuking_init)
387
+ 4. 문서 작성 (어디든 자유롭게)
388
+ 5. Push (docuking_push)
389
+
390
+ **예시:**
391
+ \`\`\`
392
+ z_DocuKing/
393
+ ├── 정책/
394
+ │ └── README.md ← 오너가 작성
395
+ ├── 기획/
396
+ │ └── 요구사항.md ← 오너가 작성
397
+ └── 개발/
398
+ └── API.md ← 오너가 작성
399
+ \`\`\`
400
+
401
+ ### 참여자 (Co-worker) - 초대받은 사람
402
+
403
+ **특징:**
404
+ - 프로젝트에 초대받아 참여한 사람
405
+ - **읽기**: 전체 문서 Pull 가능 (오너의 문서도 볼 수 있음)
406
+ - **쓰기**: 자신의 폴더(\`Co-worker/{이름}/\`)에만 Push 가능
407
+ - API Key: \`sk_cw_\`로 시작
408
+ - 프로젝트 설정 변경 불가능
409
+
410
+ **사용 시나리오:**
411
+ 1. 초대 수락 (웹에서)
412
+ 2. MCP 설정 (한 번만)
413
+ 3. 프로젝트 연결 (\`docuking_init\`)
414
+ 4. Pull로 오너의 문서 가져오기 (\`docuking_pull\`)
415
+ 5. 내 폴더에 문서 작성 (\`Co-worker/{이름}/\`)
416
+ 6. Push (\`docuking_push\`)
417
+
418
+ **폴더 구조:**
419
+ \`\`\`
420
+ z_DocuKing/
421
+ ├── 정책/
422
+ │ └── README.md ← 오너의 파일 (읽기만 가능)
423
+ ├── 기획/
424
+ │ └── 요구사항.md ← 오너의 파일 (읽기만 가능)
425
+ └── Co-worker/
426
+ └── 김개발/ ← 참여자 "김개발"의 폴더
427
+ ├── 제안서.md ← 여기에만 Push 가능
428
+ └── 수정안.md ← 여기에만 Push 가능
429
+ \`\`\`
430
+
431
+ **중요 규칙:**
432
+ - 참여자가 오너의 폴더를 Pull해서 가지고 있어도, Push할 때는 자동으로 자신의 폴더만 필터링됨
433
+ - 참여자가 다른 폴더에 Push 시도 시 오류 메시지 표시
434
+ - \`docuking_status\`로 현재 권한 확인 가능
435
+
436
+ **참여자가 오너의 파일을 수정하고 싶을 때:**
437
+ 1. 오너의 파일을 Pull해서 로컬에 가져옴 (예: \`정책/README.md\`)
438
+ 2. 로컬에서 수정함 (로컬 파일이므로 수정 가능)
439
+ 3. Push 시도 → ❌ 오류 발생: "협업자는 Co-worker/{이름}/ 폴더에만 Push할 수 있습니다"
440
+ 4. **해결 방법**: 수정한 내용을 자신의 폴더에 새 파일로 작성
441
+ - 예: \`정책/README.md\`를 수정했다면
442
+ - → \`Co-worker/김개발/정책_README_수정제안.md\`로 작성 후 Push
443
+
444
+ **AI가 참여자에게 안내해야 할 내용:**
445
+ - 참여자가 Push를 요청하면, 파일이 \`Co-worker/{이름}/\` 폴더 안에 있는지 확인
446
+ - 다른 폴더에 있는 파일을 Push하려고 하면, 오류 메시지를 명확히 설명하고 해결 방법 제시
447
+ - 오너의 파일을 수정하고 싶다면, 자신의 폴더에 제안서 형태로 작성하도록 안내
448
+
449
+ ## AI 응답 가이드 (중요!)
450
+
451
+ ### docuking_push 사용 시
452
+
453
+ **사용자 요청 예시:**
454
+ - "DocuKing에 올려줘"
455
+ - "문서 올려줘"
456
+ - "변경사항 업로드해줘"
457
+
458
+ **AI가 해야 할 일:**
459
+ 1. 커밋 메시지가 없으면 반드시 물어보기: "어떤 변경인가요? 커밋 메시지를 알려주세요."
460
+ 2. 메시지를 받으면 docuking_push 호출
461
+ 3. **결과를 사용자에게 명확하게 전달:**
462
+ - 총 파일 개수
463
+ - 업로드된 파일 개수
464
+ - 스킵된 파일 개수 (변경 없음)
465
+ - 실패한 파일 개수 (있을 경우)
466
+ - 업로드된 파일 목록 표시
467
+ - 스킵된 파일 목록 표시 (변경 없어서 스킵됨)
468
+
469
+ **응답 예시:**
470
+ Push 완료! 총 10개 파일 중 3개 업로드, 6개 스킵(변경 없음), 1개 실패
471
+
472
+ ### docuking_pull 사용 시
473
+
474
+ **사용자 요청 예시:**
475
+ - "DocuKing에서 가져와"
476
+ - "서버에서 문서 가져와"
477
+ - "최신 버전 가져와"
478
+
479
+ **AI가 해야 할 일:**
480
+ 1. docuking_pull 호출
481
+ 2. **결과를 사용자에게 명확하게 전달:**
482
+ - 가져온 파일 개수
483
+ - 가져온 파일 목록 표시
484
+ - 실패한 파일이 있으면 표시
485
+
486
+ ### docuking_status 사용 시
487
+
488
+ **사용자 요청 예시:**
489
+ - "DocuKing 상태 확인"
490
+ - "동기화 상태 보여줘"
491
+ - "변경사항 확인"
492
+
493
+ **AI가 해야 할 일:**
494
+ 1. docuking_status 호출
495
+ 2. 결과를 그대로 사용자에게 전달 (권한, 파일 개수, 동기화 상태 등)
496
+
497
+ ### 핵심 원칙 (AI 행동 지침)
498
+
499
+ 1. **프로젝트 = 폴더**: 하나의 폴더가 하나의 프로젝트
500
+ 2. **각 폴더마다 z_DocuKing/ 생성**: 여러 폴더를 각각 연결 가능
501
+ 3. **현재 열려있는 폴더 기준**: 명령어는 현재 작업 중인 폴더에 적용
502
+ 4. **커밋 메시지 필수**: push 시 반드시 message 파라미터 포함
503
+ 5. **변경 없는 파일 자동 스킵**: 해시 비교로 변경 감지, 스킵된 파일은 결과에 명시
504
+ 6. **진행상태 표시**: 총 개수, 진행 중인 개수, 완료 개수를 명확히 표시
505
+ 7. **친절한 응답**: 도구 호출 후 상세한 결과를 사용자에게 전달
506
+ 8. **MCP 설정 vs 프로젝트 연결 구분**: MCP 설정은 전역(한 번만), 프로젝트 연결은 각 폴더마다
507
+ 9. **재시작 최소화**: 이미 MCP가 작동 중이면 재시작 없이 바로 프로젝트 연결 진행
508
+ 10. **기존 설정 보호**: MCP 설정 시 기존 서버 설정을 덮어쓰지 말고 추가만
509
+
510
+ ### MCP 설정 관련 (AI가 처리해야 할 경우)
511
+
512
+ **사용자 요청 예시:**
513
+ - "MCP 설정해줘"
514
+ - "AI 연결하기"
515
+ - "DocuKing MCP 설정"
516
+
517
+ **AI가 해야 할 일:**
518
+ 1. **먼저 확인**: 이미 MCP가 작동 중인지 확인 (도구 호출 가능 여부로 판단)
519
+ 2. **이미 작동 중이면**: "MCP는 이미 설정되어 있습니다. 바로 프로젝트 연결을 진행하겠습니다."라고 안내하고 프로젝트 연결로 진행
520
+ 3. **설정이 필요하면**:
521
+ - 사용자의 AI 도구 종류 확인 (Cursor, Claude Code 등)
522
+ - 해당 설정 파일 경로 안내
523
+ - **기존 MCP 서버가 있으면 덮어쓰지 말고 추가만 하도록 명확히 안내**
524
+ - 설정 완료 후 재시작 안내 (재시작 후 프로젝트 연결 단계로 진행)
525
+
526
+ **MCP 설정 파일 예시 (기존 서버가 있는 경우):**
527
+ \`\`\`json
528
+ {
529
+ "mcpServers": {
530
+ "filesystem": {
531
+ "command": "npx",
532
+ "args": ["-y", "@modelcontextprotocol/server-filesystem"]
533
+ },
534
+ "github": {
535
+ "command": "npx",
536
+ "args": ["-y", "@modelcontextprotocol/server-github"]
537
+ },
538
+ "docuking": {
539
+ "command": "npx",
540
+ "args": ["-y", "docuking-mcp"],
541
+ "env": {
542
+ "DOCUKING_API_KEY": "sk_xxx",
543
+ "DOCUKING_API_ENDPOINT": "https://docuking.ai/api"
544
+ }
545
+ }
546
+ }
547
+ }
548
+ \`\`\`
549
+ **⚠️ 중요**: 기존 설정을 덮어쓰지 말고 \`docuking\` 항목만 추가해야 함!
550
+
551
+ ### 프로젝트 연결
552
+
553
+ **사용자 요청 예시:**
554
+ - "이 프로젝트를 DocuKing에 연결해줘. 프로젝트 ID는 [ID]"
555
+ - "프로젝트 [ID] 연결해줘"
556
+ - "이 폴더를 DocuKing에 연결해줘"
557
+
558
+ **AI가 해야 할 일:**
559
+ 1. 현재 폴더 경로 확인 (작업 중인 디렉토리)
560
+ 2. 프로젝트 ID 확인 (사용자가 제공하거나 요청)
561
+ 3. \`docuking_init(projectId, localPath)\` 호출
562
+ 4. 연결 완료 메시지 전달:
563
+ - "DocuKing 연결 완료! 📁 z_DocuKing/ 폴더가 생성되었습니다."
564
+ - "이제 'DocuKing에 올려줘' 명령을 사용할 수 있습니다."
565
+
566
+ **⚠️ 매우 중요:**
567
+ - **MCP가 이미 작동 중이면 재시작 없이 바로 \`docuking_init\`을 호출하면 됩니다**
568
+ - 재시작이 필요한 경우는 MCP 설정 파일을 처음 만들었을 때뿐입니다
569
+ - 사용자가 "재시작이 필요합니다"라고 말해도, 이미 MCP가 작동 중이면 무시하고 바로 프로젝트 연결을 진행하세요
570
+
571
+ ### 여러 프로젝트 관리 (다중 프로젝트)
572
+
573
+ **핵심 개념:**
574
+ - **프로젝트 = 하나의 폴더**
575
+ - 한 컴퓨터에서 여러 폴더를 각각 다른 DocuKing 프로젝트와 연결 가능
576
+ - 각 폴더마다 \`z_DocuKing/\` 폴더가 독립적으로 생성됨
577
+
578
+ **예시:**
579
+ \`\`\`
580
+ C:\\Projects\\MyApp\\
581
+ ├── src/
582
+ ├── package.json
583
+ └── z_DocuKing/ ← 프로젝트 A와 연결
584
+
585
+ C:\\Projects\\MyWebsite\\
586
+ ├── pages/
587
+ ├── components/
588
+ └── z_DocuKing/ ← 프로젝트 B와 연결
589
+
590
+ D:\\Work\\ClientProject\\
591
+ ├── docs/
592
+ └── z_DocuKing/ ← 프로젝트 C와 연결
593
+ \`\`\`
594
+
595
+ **AI가 해야 할 일:**
596
+ - 각 폴더에서 \`docuking_init\`을 호출하면 해당 폴더만 연결됨
597
+ - 다른 폴더로 이동하면 해당 폴더의 \`z_DocuKing/\`가 사용됨
598
+ - 여러 프로젝트를 동시에 관리할 수 있음을 인지하고, 현재 작업 중인 폴더 기준으로 동작
599
+
600
+ **고급: DOCUKING_REPOS 환경변수 (선택사항)**
601
+ - 여러 프로젝트를 미리 등록하려면 MCP 설정의 \`env\`에 \`DOCUKING_REPOS\` 추가 가능
602
+ - 형식: \`{"경로1":{"id":"프로젝트ID1","name":"프로젝트이름1"},"경로2":{"id":"프로젝트ID2","name":"프로젝트이름2"}}\`
603
+ - 기존 형식도 지원: \`{"경로1":"프로젝트ID1","경로2":"프로젝트ID2"}\` (하위 호환)
604
+ - 이 방법은 선택사항이며, 각 폴더에서 \`docuking_init\`을 실행해도 됨
605
+
606
+ 웹 탐색기: https://docuking.ai
607
+ `;
608
+
609
+ return {
610
+ contents: [{
611
+ uri: "docuking://docs/manual",
612
+ mimeType: "text/markdown",
613
+ text: manual
614
+ }]
615
+ };
616
+ }
617
+
618
+ throw new Error("Unknown resource");
619
+ });
620
+
621
+ // 도구 실행
622
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
623
+ const { name, arguments: args } = request.params;
624
+
625
+ try {
626
+ switch (name) {
627
+ case 'docuking_init':
628
+ return await handleInit(args);
629
+ case 'docuking_push':
630
+ return await handlePush(args);
631
+ case 'docuking_pull':
632
+ return await handlePull(args);
633
+ case 'docuking_list':
634
+ return await handleList(args);
635
+ case 'docuking_status':
636
+ return await handleStatus(args);
637
+ case 'docuking_log':
638
+ return await handleLog(args);
639
+ case 'docuking_diff':
640
+ return await handleDiff(args);
641
+ case 'docuking_rollback':
642
+ return await handleRollback(args);
643
+ default:
644
+ throw new Error(`Unknown tool: ${name}`);
645
+ }
646
+ } catch (error) {
647
+ return {
648
+ content: [
649
+ {
650
+ type: 'text',
651
+ text: `오류: ${error.message}`,
652
+ },
653
+ ],
654
+ };
655
+ }
656
+ });
657
+
658
+ // docuking_init 구현
659
+ async function handleInit(args) {
660
+ const { projectId, projectName, localPath } = args;
661
+
662
+ if (!API_KEY) {
663
+ return {
664
+ content: [
665
+ {
666
+ type: 'text',
667
+ text: `오류: DOCUKING_API_KEY가 설정되지 않았습니다.
668
+
669
+ MCP 설정에 다음을 추가하세요:
670
+ "env": {
671
+ "DOCUKING_API_KEY": "your-api-key"
672
+ }`,
673
+ },
674
+ ],
675
+ };
676
+ }
677
+
678
+ // z_DocuKing 폴더 생성 (항상 새로 만듦)
679
+ const folderName = 'z_DocuKing';
680
+ const docuKingPath = path.join(localPath, folderName);
681
+
682
+ if (!fs.existsSync(docuKingPath)) {
683
+ fs.mkdirSync(docuKingPath, { recursive: true });
684
+ }
685
+
686
+ // JSON 이스케이프 처리
687
+ const escapedPath = localPath.replace(/\\/g, '\\\\');
688
+ const repoConfig = `{\\"${escapedPath}\\":{\\"id\\":\\"${projectId}\\",\\"name\\":\\"${projectName}\\"}}`;
689
+
690
+ // 연결 완료 안내
691
+ return {
692
+ content: [
693
+ {
694
+ type: 'text',
695
+ text: `DocuKing 연결 완료!
696
+
697
+ 📁 프로젝트: ${projectName}
698
+ 📂 z_DocuKing/ 폴더가 생성되었습니다.
699
+
700
+ 이제부터 문서 관리는 DocuKing에서 시작합니다:
701
+ - z_DocuKing/ 하위에 문서를 넣으면 DocuKing 서버로 암호화되어 저장됩니다
702
+ - 협업자들과 안전하게 문서를 공유할 수 있습니다
703
+
704
+ 사용법:
705
+ - "DocuKing에 올려줘" → 로컬 문서를 서버에 Push
706
+ - "DocuKing에서 가져와" → 서버 문서를 로컬로 Pull
707
+
708
+ MCP 설정에 레포 매핑 추가 필요:
709
+ "DOCUKING_REPOS": "${repoConfig}"`,
710
+ },
711
+ ],
712
+ };
713
+ }
714
+
715
+ // docuking_push 구현
716
+ async function handlePush(args) {
717
+ const { localPath, filePath, message, author } = args;
718
+
719
+ // 커밋 메시지 필수 체크
720
+ if (!message || message.trim() === '') {
721
+ return {
722
+ content: [
723
+ {
724
+ type: 'text',
725
+ text: `오류: 커밋 메시지가 필요합니다.
726
+
727
+ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
728
+ 예: "README에 설치 가이드 추가"`,
729
+ },
730
+ ],
731
+ };
732
+ }
733
+
734
+ // 프로젝트 정보 조회 (로컬 매핑에서)
735
+ const projectInfo = getProjectInfo(localPath);
736
+ if (projectInfo.error) {
737
+ return {
738
+ content: [
739
+ {
740
+ type: 'text',
741
+ text: projectInfo.error,
742
+ },
743
+ ],
744
+ };
745
+ }
746
+
747
+ const projectId = projectInfo.projectId;
748
+ const projectName = projectInfo.projectName;
749
+
750
+ // DocuKing 폴더 찾기
751
+ const folderName = findDocuKingFolder(localPath);
752
+ if (!folderName) {
753
+ return {
754
+ content: [
755
+ {
756
+ type: 'text',
757
+ text: `오류: DocuKing 폴더가 없습니다.
758
+ docuking_init을 먼저 실행하세요.`,
759
+ },
760
+ ],
761
+ };
762
+ }
763
+ const docuKingPath = path.join(localPath, folderName);
764
+
765
+ // Co-worker 권한은 API Key 형식에서 판단 (sk_xxx_cw_이름_)
766
+ const coworkerMatch = API_KEY.match(/^sk_[a-f0-9]+_cw_([^_]+)_/);
767
+ const allowedPathPrefix = coworkerMatch ? `Co-worker/${coworkerMatch[1]}/` : null;
768
+
769
+ // 파일 목록 수집
770
+ const filesToPush = [];
771
+ const excludedFiles = []; // 제외된 파일 목록
772
+
773
+ if (filePath) {
774
+ // 특정 파일만
775
+ const fullPath = path.join(docuKingPath, filePath);
776
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
777
+ const fileType = getFileType(filePath);
778
+ if (fileType === 'excluded') {
779
+ return {
780
+ content: [
781
+ {
782
+ type: 'text',
783
+ text: `오류: ${filePath}는 지원하지 않는 파일 형식입니다.\n\n📦 압축/설치 파일(.zip, .jar, .exe 등)은 DocuKing에 업로드되지 않습니다.\n💡 이런 파일은 별도 공유 방법(Google Drive, NAS 등)을 사용하고,\n 문서에 다운로드 링크나 설치 가이드를 작성하세요.`,
784
+ },
785
+ ],
786
+ };
787
+ }
788
+
789
+ // 참여자인 경우 경로 체크
790
+ if (allowedPathPrefix) {
791
+ const normalizedPath = filePath.toLowerCase();
792
+ const normalizedPrefix = allowedPathPrefix.toLowerCase();
793
+ if (!normalizedPath.startsWith(normalizedPrefix)) {
794
+ return {
795
+ content: [
796
+ {
797
+ type: 'text',
798
+ text: `오류: 협업자는 ${allowedPathPrefix} 폴더에만 Push할 수 있습니다.\n\n요청한 파일: ${filePath}\n허용된 경로: ${allowedPathPrefix}\n\n💡 참고: 오너의 파일을 로컬에서 수정하셨다면, 수정 내용을 ${allowedPathPrefix} 폴더에 새 파일로 작성해주세요.\n예: 정책/README.md를 수정했다면 → ${allowedPathPrefix}정책_README_수정제안.md`,
799
+ },
800
+ ],
801
+ };
802
+ }
803
+ }
804
+
805
+ filesToPush.push({ path: filePath, fullPath, fileType });
806
+ }
807
+ } else {
808
+ // 전체 파일 - 제외된 파일 목록도 수집
809
+ collectFiles(docuKingPath, '', filesToPush, excludedFiles);
810
+
811
+ // 참여자인 경우 허용된 경로의 파일만 필터링
812
+ if (allowedPathPrefix) {
813
+ const normalizedPrefix = allowedPathPrefix.toLowerCase();
814
+ const filteredFiles = filesToPush.filter(file => {
815
+ const normalizedPath = file.path.toLowerCase();
816
+ return normalizedPath.startsWith(normalizedPrefix);
817
+ });
818
+
819
+ if (filteredFiles.length === 0) {
820
+ return {
821
+ content: [
822
+ {
823
+ type: 'text',
824
+ text: `Push할 파일이 없습니다.\n\n협업자는 ${allowedPathPrefix} 폴더에만 Push할 수 있습니다.\n\n현재 폴더 구조를 확인하고, 해당 폴더에 파일을 배치한 후 다시 시도해주세요.`,
825
+ },
826
+ ],
827
+ };
828
+ }
829
+
830
+ // 필터링된 파일로 교체
831
+ filesToPush.length = 0;
832
+ filesToPush.push(...filteredFiles);
833
+
834
+ console.log(`[DocuKing] 협업자 권한: ${filteredFiles.length}개 파일만 Push (${allowedPathPrefix})`);
835
+ }
836
+ }
837
+
838
+ if (filesToPush.length === 0) {
839
+ return {
840
+ content: [
841
+ {
842
+ type: 'text',
843
+ text: 'Push할 파일이 없습니다.',
844
+ },
845
+ ],
846
+ };
847
+ }
848
+
849
+ // 파일 업로드 (진행률 표시)
850
+ const results = [];
851
+ const total = filesToPush.length;
852
+ let current = 0;
853
+ let skipped = 0;
854
+
855
+ // 시작 안내 메시지 출력 (AI가 사용자에게 전달할 수 있도록)
856
+ console.error(`[DocuKing] Push 시작: ${total}개 파일`);
857
+ console.error(`[DocuKing] 💡 실시간 진행상황은 DocuKing 웹(https://docuking.ai)에서 확인하세요`);
858
+
859
+ // Sync 시작 알림 (웹에서 프로그레스바 표시용)
860
+ try {
861
+ await fetch(`${API_ENDPOINT}/projects/${projectId}/sync/start`, {
862
+ method: 'POST',
863
+ headers: {
864
+ 'Content-Type': 'application/json',
865
+ 'Authorization': `Bearer ${API_KEY}`,
866
+ },
867
+ body: JSON.stringify({ totalFiles: total }),
868
+ });
869
+ } catch (e) {
870
+ console.error('[DocuKing] Sync 시작 알림 실패:', e.message);
871
+ }
872
+
873
+ // 서버에서 파일 해시 조회 (변경 감지용)
874
+ let serverFileHashes = {};
875
+ try {
876
+ const hashResponse = await fetch(
877
+ `${API_ENDPOINT}/files/hashes?projectId=${projectId}`,
878
+ {
879
+ headers: {
880
+ 'Authorization': `Bearer ${API_KEY}`,
881
+ },
882
+ }
883
+ );
884
+ if (hashResponse.ok) {
885
+ const hashData = await hashResponse.json();
886
+ serverFileHashes = hashData.hashes || {};
887
+ }
888
+ } catch (e) {
889
+ // 해시 조회 실패는 무시 (처음 Push하는 경우 등)
890
+ console.error('[DocuKing] 파일 해시 조회 실패:', e.message);
891
+ }
892
+
893
+ for (const file of filesToPush) {
894
+ current++;
895
+ const progress = `${current}/${total}`;
896
+
897
+ try {
898
+ // 파일 해시 계산 (변경 감지)
899
+ let fileHash;
900
+ let content;
901
+ let encoding = 'utf-8';
902
+
903
+ if (file.fileType === 'binary') {
904
+ // 바이너리 파일은 Base64로 인코딩
905
+ const buffer = fs.readFileSync(file.fullPath);
906
+ fileHash = crypto.createHash('sha256').update(buffer).digest('hex');
907
+ content = buffer.toString('base64');
908
+ encoding = 'base64';
909
+ } else {
910
+ // 텍스트 파일은 UTF-8
911
+ content = fs.readFileSync(file.fullPath, 'utf-8');
912
+ fileHash = crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
913
+ }
914
+
915
+ // 변경 감지: 서버에 같은 해시가 있으면 스킵
916
+ if (serverFileHashes[file.path] === fileHash) {
917
+ const resultText = `${progress} ⊘ ${file.path} (변경 없음)`;
918
+ results.push(resultText);
919
+ console.log(resultText);
920
+ skipped++;
921
+ continue;
922
+ }
923
+
924
+ // 재시도 로직 (최대 3회)
925
+ let lastError = null;
926
+ let success = false;
927
+ for (let attempt = 1; attempt <= 3; attempt++) {
928
+ try {
929
+ // 대용량 파일 업로드를 위한 타임아웃 설정 (10분)
930
+ const controller = new AbortController();
931
+ const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 10분
932
+
933
+ let response;
934
+ try {
935
+ response = await fetch(`${API_ENDPOINT}/files/push`, {
936
+ method: 'POST',
937
+ headers: {
938
+ 'Content-Type': 'application/json',
939
+ 'Authorization': `Bearer ${API_KEY}`,
940
+ },
941
+ body: JSON.stringify({
942
+ projectId,
943
+ path: file.path,
944
+ content,
945
+ encoding, // 'utf-8' 또는 'base64'
946
+ message, // 커밋 메시지
947
+ author, // 작성자 (optional)
948
+ fileHash, // 파일 해시 (변경 감지용)
949
+ }),
950
+ signal: controller.signal,
951
+ });
952
+ clearTimeout(timeoutId);
953
+ } catch (e) {
954
+ clearTimeout(timeoutId);
955
+ if (e.name === 'AbortError') {
956
+ throw new Error(`파일 업로드 타임아웃 (10분 초과): ${file.path}`);
957
+ }
958
+ throw e;
959
+ }
960
+
961
+ if (response.ok) {
962
+ const resultText = attempt > 1
963
+ ? `${progress} ✓ ${file.path} (재시도 ${attempt}회 성공)`
964
+ : `${progress} ✓ ${file.path}`;
965
+ results.push(resultText);
966
+ console.log(resultText);
967
+ success = true;
968
+ break; // 성공하면 재시도 중단
969
+ } else {
970
+ const error = await response.text();
971
+ lastError = error;
972
+ // 4xx 에러는 재시도하지 않음 (클라이언트 오류)
973
+ if (response.status >= 400 && response.status < 500) {
974
+ throw new Error(error);
975
+ }
976
+ // 5xx 에러만 재시도
977
+ if (attempt < 3) {
978
+ const waitTime = attempt * 1000; // 1초, 2초, 3초
979
+ console.log(`${progress} ⚠ ${file.path}: 재시도 ${attempt}/3 (${waitTime}ms 후)`);
980
+ await new Promise(resolve => setTimeout(resolve, waitTime));
981
+ }
982
+ }
983
+ } catch (e) {
984
+ lastError = e.message;
985
+ // 네트워크 오류 등은 재시도
986
+ if (attempt < 3 && !e.message.includes('타임아웃')) {
987
+ const waitTime = attempt * 1000;
988
+ console.log(`${progress} ⚠ ${file.path}: 재시도 ${attempt}/3 (${waitTime}ms 후) - ${e.message}`);
989
+ await new Promise(resolve => setTimeout(resolve, waitTime));
990
+ } else {
991
+ throw e;
992
+ }
993
+ }
994
+ }
995
+
996
+ if (!success) {
997
+ const errorText = `${progress} ✗ ${file.path}: ${lastError}`;
998
+ results.push(errorText);
999
+ console.error(errorText);
1000
+ }
1001
+
1002
+ // 진행 상황 업데이트 (매 파일마다 또는 5개마다)
1003
+ if (current % 5 === 0 || current === total || current === 1) {
1004
+ try {
1005
+ await fetch(`${API_ENDPOINT}/projects/${projectId}/sync/progress`, {
1006
+ method: 'POST',
1007
+ headers: {
1008
+ 'Content-Type': 'application/json',
1009
+ 'Authorization': `Bearer ${API_KEY}`,
1010
+ },
1011
+ body: JSON.stringify({ progress: current }),
1012
+ });
1013
+ } catch (e) {
1014
+ // 진행 상황 업데이트 실패는 무시
1015
+ }
1016
+ }
1017
+ } catch (e) {
1018
+ results.push(`${progress} ✗ ${file.path}: ${e.message}`);
1019
+ }
1020
+ }
1021
+
1022
+ // Sync 완료 알림
1023
+ try {
1024
+ await fetch(`${API_ENDPOINT}/projects/${projectId}/sync/complete`, {
1025
+ method: 'POST',
1026
+ headers: {
1027
+ 'Content-Type': 'application/json',
1028
+ 'Authorization': `Bearer ${API_KEY}`,
1029
+ },
1030
+ });
1031
+ } catch (e) {
1032
+ console.error('[DocuKing] Sync 완료 알림 실패:', e.message);
1033
+ }
1034
+
1035
+ const successCount = results.filter(r => r.includes('✓')).length;
1036
+ const failCount = results.filter(r => r.includes('✗')).length;
1037
+ const skippedCount = skipped; // 이미 계산된 스킵 개수 사용
1038
+ const excludedCount = excludedFiles.length;
1039
+
1040
+ // 요약 정보
1041
+ let summary = `\n📦 커밋 메시지: "${message}"\n\n📊 처리 결과:\n - 총 파일: ${total}개\n - 업로드: ${successCount}개\n - 스킵 (변경 없음): ${skippedCount}개\n - 실패: ${failCount}개`;
1042
+ if (excludedCount > 0) {
1043
+ summary += `\n - 제외 (압축/설치파일): ${excludedCount}개`;
1044
+ }
1045
+
1046
+ // 상세 결과를 표시 (Git처럼)
1047
+ let resultText = `✓ Push 완료!${summary}`;
1048
+
1049
+ // 업로드된 파일이 있으면 상세 목록 표시
1050
+ if (successCount > 0) {
1051
+ const uploadedFiles = results.filter(r => r.includes('✓') && !r.includes('재시도'));
1052
+ resultText += `\n\n📤 업로드된 파일 (${successCount}개):\n${uploadedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
1053
+ }
1054
+
1055
+ // 스킵된 파일이 있으면 표시
1056
+ if (skippedCount > 0) {
1057
+ const skippedFiles = results.filter(r => r.includes('⊘'));
1058
+ resultText += `\n\n⏭️ 스킵된 파일 (${skippedCount}개, 변경 없음):\n${skippedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
1059
+ }
1060
+
1061
+ // 실패한 파일이 있으면 표시
1062
+ if (failCount > 0) {
1063
+ const failedFiles = results.filter(r => r.includes('✗'));
1064
+ resultText += `\n\n❌ 실패한 파일 (${failCount}개):\n${failedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
1065
+ }
1066
+
1067
+ // 제외된 파일이 있으면 표시
1068
+ if (excludedCount > 0) {
1069
+ resultText += `\n\n📦 제외된 파일 (${excludedCount}개, 압축/설치파일):\n${excludedFiles.slice(0, 10).map(f => ` ⊖ ${f}`).join('\n')}`;
1070
+ if (excludedCount > 10) {
1071
+ resultText += `\n ... 외 ${excludedCount - 10}개`;
1072
+ }
1073
+ resultText += `\n\n💡 압축/설치 파일은 DocuKing에 저장되지 않습니다.\n 이런 파일은 별도 공유(Google Drive, NAS 등)를 사용하고,\n 문서에 다운로드 링크나 설치 가이드를 작성하세요.`;
1074
+ }
1075
+
1076
+ resultText += `\n\n🌐 웹 탐색기에서 커밋 히스토리를 확인할 수 있습니다: https://docuking.ai`;
1077
+
1078
+ return {
1079
+ content: [
1080
+ {
1081
+ type: 'text',
1082
+ text: resultText,
1083
+ },
1084
+ ],
1085
+ };
1086
+ }
1087
+
1088
+ // docuking_pull 구현
1089
+ async function handlePull(args) {
1090
+ const { localPath, filePath } = args;
1091
+
1092
+ // 프로젝트 정보 조회 (로컬 매핑에서)
1093
+ const projectInfo = getProjectInfo(localPath);
1094
+ if (projectInfo.error) {
1095
+ return {
1096
+ content: [
1097
+ {
1098
+ type: 'text',
1099
+ text: projectInfo.error,
1100
+ },
1101
+ ],
1102
+ };
1103
+ }
1104
+
1105
+ const projectId = projectInfo.projectId;
1106
+
1107
+ // DocuKing 폴더 찾기 (없으면 기본값으로 생성)
1108
+ let folderName = findDocuKingFolder(localPath);
1109
+ if (!folderName) {
1110
+ folderName = 'z_DocuKing';
1111
+ }
1112
+ const docuKingPath = path.join(localPath, folderName);
1113
+
1114
+ // 파일 목록 조회
1115
+ let files = [];
1116
+
1117
+ try {
1118
+ const response = await fetch(
1119
+ `${API_ENDPOINT}/files/tree?projectId=${projectId}`,
1120
+ {
1121
+ headers: {
1122
+ 'Authorization': `Bearer ${API_KEY}`,
1123
+ },
1124
+ }
1125
+ );
1126
+
1127
+ if (!response.ok) {
1128
+ throw new Error(await response.text());
1129
+ }
1130
+
1131
+ const data = await response.json();
1132
+ files = flattenTree(data.tree || []);
1133
+ } catch (e) {
1134
+ return {
1135
+ content: [
1136
+ {
1137
+ type: 'text',
1138
+ text: `오류: 파일 목록 조회 실패 - ${e.message}`,
1139
+ },
1140
+ ],
1141
+ };
1142
+ }
1143
+
1144
+ if (filePath) {
1145
+ files = files.filter(f => f.path === filePath || f.path.startsWith(filePath + '/'));
1146
+ }
1147
+
1148
+ if (files.length === 0) {
1149
+ return {
1150
+ content: [
1151
+ {
1152
+ type: 'text',
1153
+ text: 'Pull할 파일이 없습니다.',
1154
+ },
1155
+ ],
1156
+ };
1157
+ }
1158
+
1159
+ // 파일 다운로드
1160
+ const results = [];
1161
+ for (const file of files) {
1162
+ try {
1163
+ const response = await fetch(
1164
+ `${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
1165
+ {
1166
+ headers: {
1167
+ 'Authorization': `Bearer ${API_KEY}`,
1168
+ },
1169
+ }
1170
+ );
1171
+
1172
+ if (!response.ok) {
1173
+ results.push(`✗ ${file.path}: ${await response.text()}`);
1174
+ continue;
1175
+ }
1176
+
1177
+ const data = await response.json();
1178
+ const fullPath = path.join(docuKingPath, file.path);
1179
+
1180
+ // 디렉토리 생성
1181
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
1182
+
1183
+ // 인코딩에 따라 저장
1184
+ const content = data.file?.content || data.content || '';
1185
+ const encoding = data.file?.encoding || data.encoding || 'utf-8';
1186
+
1187
+ if (encoding === 'base64') {
1188
+ // Base64 디코딩 후 바이너리로 저장
1189
+ const buffer = Buffer.from(content, 'base64');
1190
+ fs.writeFileSync(fullPath, buffer);
1191
+ } else {
1192
+ // UTF-8 텍스트로 저장
1193
+ fs.writeFileSync(fullPath, content, 'utf-8');
1194
+ }
1195
+
1196
+ results.push(`✓ ${file.path}`);
1197
+ } catch (e) {
1198
+ results.push(`✗ ${file.path}: ${e.message}`);
1199
+ }
1200
+ }
1201
+
1202
+ return {
1203
+ content: [
1204
+ {
1205
+ type: 'text',
1206
+ text: `Pull 완료!\n\n${results.join('\n')}`,
1207
+ },
1208
+ ],
1209
+ };
1210
+ }
1211
+
1212
+ // docuking_list 구현
1213
+ async function handleList(args) {
1214
+ const { localPath } = args;
1215
+
1216
+ // 프로젝트 정보 조회 (로컬 매핑에서)
1217
+ const projectInfo = getProjectInfo(localPath);
1218
+ if (projectInfo.error) {
1219
+ return {
1220
+ content: [
1221
+ {
1222
+ type: 'text',
1223
+ text: projectInfo.error,
1224
+ },
1225
+ ],
1226
+ };
1227
+ }
1228
+
1229
+ const projectId = projectInfo.projectId;
1230
+
1231
+ try {
1232
+ const response = await fetch(
1233
+ `${API_ENDPOINT}/files/tree?projectId=${projectId}`,
1234
+ {
1235
+ headers: {
1236
+ 'Authorization': `Bearer ${API_KEY}`,
1237
+ },
1238
+ }
1239
+ );
1240
+
1241
+ if (!response.ok) {
1242
+ throw new Error(await response.text());
1243
+ }
1244
+
1245
+ const data = await response.json();
1246
+ const files = flattenTree(data.tree || []);
1247
+
1248
+ if (files.length === 0) {
1249
+ return {
1250
+ content: [
1251
+ {
1252
+ type: 'text',
1253
+ text: '서버에 저장된 파일이 없습니다.',
1254
+ },
1255
+ ],
1256
+ };
1257
+ }
1258
+
1259
+ const fileList = files.map(f => ` ${f.path}`).join('\n');
1260
+ return {
1261
+ content: [
1262
+ {
1263
+ type: 'text',
1264
+ text: `서버 파일 목록:\n\n${fileList}`,
1265
+ },
1266
+ ],
1267
+ };
1268
+ } catch (e) {
1269
+ return {
1270
+ content: [
1271
+ {
1272
+ type: 'text',
1273
+ text: `오류: ${e.message}`,
1274
+ },
1275
+ ],
1276
+ };
1277
+ }
1278
+ }
1279
+
1280
+ // 파일 타입 정의
1281
+ const FILE_TYPES = {
1282
+ // 텍스트 파일 (UTF-8)
1283
+ TEXT: [
1284
+ '.md', '.txt', '.json', '.xml', '.yaml', '.yml',
1285
+ '.html', '.htm', '.css', '.js', '.ts', '.jsx', '.tsx',
1286
+ '.py', '.java', '.go', '.rs', '.rb', '.php', '.c', '.cpp', '.h',
1287
+ '.csv', '.svg', '.sql', '.sh', '.ps1', '.bat', '.cmd',
1288
+ '.env', '.gitignore', '.dockerignore', '.editorconfig',
1289
+ '.properties', '.ini', '.toml', '.conf', '.cfg',
1290
+ ],
1291
+
1292
+ // 바이너리 파일 (Base64)
1293
+ BINARY: [
1294
+ // 이미지
1295
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.tiff',
1296
+ // 오피스
1297
+ '.xlsx', '.xls', '.docx', '.doc', '.pptx', '.ppt',
1298
+ // PDF
1299
+ '.pdf',
1300
+ // 한글
1301
+ '.hwpx', '.hwp',
1302
+ ],
1303
+
1304
+ // 제외 파일 (업로드 거부) - 설치파일/대용량 파일은 별도 공유 권장
1305
+ EXCLUDED: [
1306
+ // 압축/아카이브 (설치파일 포함 가능성 높음)
1307
+ '.zip', '.tar', '.gz', '.tgz', '.7z', '.rar', '.tar.Z',
1308
+ // Java 아카이브
1309
+ '.jar', '.war', '.ear', '.class',
1310
+ // 실행파일
1311
+ '.exe', '.msi', '.dll', '.so', '.dylib', '.com', '.app', '.pkg', '.deb', '.rpm',
1312
+ // macOS 스크립트
1313
+ '.scpt',
1314
+ // 동영상
1315
+ '.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v',
1316
+ // 오디오
1317
+ '.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a',
1318
+ // 디스크 이미지
1319
+ '.iso', '.dmg', '.img', '.vhd', '.vmdk',
1320
+ ],
1321
+ };
1322
+
1323
+ function getFileType(fileName) {
1324
+ const ext = path.extname(fileName).toLowerCase();
1325
+
1326
+ // .tar.gz, .tar.Z 등 복합 확장자 처리
1327
+ if (fileName.endsWith('.tar.gz') || fileName.endsWith('.tar.Z')) {
1328
+ return 'binary';
1329
+ }
1330
+
1331
+ if (FILE_TYPES.EXCLUDED.includes(ext)) {
1332
+ return 'excluded';
1333
+ }
1334
+ if (FILE_TYPES.BINARY.includes(ext)) {
1335
+ return 'binary';
1336
+ }
1337
+ if (FILE_TYPES.TEXT.includes(ext)) {
1338
+ return 'text';
1339
+ }
1340
+
1341
+ // 알 수 없는 확장자는 바이너리로 처리 (안전)
1342
+ return 'binary';
1343
+ }
1344
+
1345
+ // 유틸: DocuKing 폴더 찾기 (docuking 포함, 대소문자 무관)
1346
+ function findDocuKingFolder(projectPath) {
1347
+ try {
1348
+ const entries = fs.readdirSync(projectPath, { withFileTypes: true });
1349
+ for (const entry of entries) {
1350
+ if (entry.isDirectory() && entry.name.toLowerCase().includes('docuking')) {
1351
+ return entry.name;
1352
+ }
1353
+ }
1354
+ } catch (e) {
1355
+ // 디렉토리 읽기 실패
1356
+ }
1357
+ return null;
1358
+ }
1359
+
1360
+ // 유틸: 디렉토리 재귀 탐색
1361
+ // excludedFiles: 제외된 파일 목록을 수집할 배열 (선택)
1362
+ function collectFiles(basePath, relativePath, results, excludedFiles = null) {
1363
+ const fullPath = path.join(basePath, relativePath);
1364
+ const entries = fs.readdirSync(fullPath, { withFileTypes: true });
1365
+
1366
+ for (const entry of entries) {
1367
+ const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1368
+
1369
+ if (entry.isDirectory()) {
1370
+ collectFiles(basePath, entryRelPath, results, excludedFiles);
1371
+ } else if (entry.isFile()) {
1372
+ const fileType = getFileType(entry.name);
1373
+
1374
+ // 제외 파일은 건너뜀 (excludedFiles 배열이 있으면 수집)
1375
+ if (fileType === 'excluded') {
1376
+ console.error(`[DocuKing] 제외됨: ${entryRelPath} (지원하지 않는 파일 형식)`);
1377
+ if (excludedFiles) {
1378
+ excludedFiles.push(entryRelPath);
1379
+ }
1380
+ continue;
1381
+ }
1382
+
1383
+ results.push({
1384
+ path: entryRelPath,
1385
+ fullPath: path.join(fullPath, entry.name),
1386
+ fileType, // 'text' 또는 'binary'
1387
+ });
1388
+ }
1389
+ }
1390
+ }
1391
+
1392
+ // 유틸: 트리 구조를 평탄화
1393
+ function flattenTree(tree, prefix = '') {
1394
+ const results = [];
1395
+
1396
+ for (const item of tree) {
1397
+ const itemPath = prefix ? `${prefix}/${item.name}` : item.name;
1398
+
1399
+ if (item.type === 'file') {
1400
+ results.push({ path: itemPath, name: item.name });
1401
+ } else if (item.children) {
1402
+ results.push(...flattenTree(item.children, itemPath));
1403
+ }
1404
+ }
1405
+
1406
+ return results;
1407
+ }
1408
+
1409
+ // docuking_status 구현
1410
+ async function handleStatus(args) {
1411
+ const { localPath } = args;
1412
+
1413
+ // 프로젝트 정보 조회 (로컬 매핑에서)
1414
+ const projectInfo = getProjectInfo(localPath);
1415
+ if (projectInfo.error) {
1416
+ return {
1417
+ content: [
1418
+ {
1419
+ type: 'text',
1420
+ text: projectInfo.error,
1421
+ },
1422
+ ],
1423
+ };
1424
+ }
1425
+
1426
+ const projectId = projectInfo.projectId;
1427
+ const projectName = projectInfo.projectName;
1428
+
1429
+ // Co-worker 권한은 API Key 형식에서 판단 (sk_xxx_cw_이름_)
1430
+ const coworkerMatch = API_KEY.match(/^sk_[a-f0-9]+_cw_([^_]+)_/);
1431
+ const isCoworker = !!coworkerMatch;
1432
+ const coworkerName = coworkerMatch ? coworkerMatch[1] : null;
1433
+ const allowedPathPrefix = isCoworker ? `Co-worker/${coworkerName}/` : null;
1434
+
1435
+ // 권한 정보 구성
1436
+ let permissionInfo = '';
1437
+ if (isCoworker) {
1438
+ permissionInfo = `\n\n## 현재 권한: 참여자 (Co-worker)
1439
+ - 이름: ${coworkerName}
1440
+ - 읽기 권한: 전체 문서 (제한 없음)
1441
+ - 쓰기 권한: ${allowedPathPrefix} 폴더만
1442
+ - 설명: 다른 폴더의 파일을 Pull해서 가지고 있어도, Push할 때는 자동으로 ${allowedPathPrefix} 폴더의 파일만 Push됩니다.`;
1443
+ } else {
1444
+ permissionInfo = `\n\n## 현재 권한: 오너 (Owner)
1445
+ - 읽기 권한: 전체 문서
1446
+ - 쓰기 권한: 전체 문서 (제한 없음)
1447
+ - 설명: 프로젝트의 모든 폴더에 Push할 수 있습니다.`;
1448
+ }
1449
+
1450
+ // 서버 파일 목록 조회
1451
+ let serverFiles = [];
1452
+ try {
1453
+ const response = await fetch(
1454
+ `${API_ENDPOINT}/files/tree?projectId=${projectId}`,
1455
+ {
1456
+ headers: {
1457
+ 'Authorization': `Bearer ${API_KEY}`,
1458
+ },
1459
+ }
1460
+ );
1461
+ if (response.ok) {
1462
+ const data = await response.json();
1463
+ serverFiles = flattenTree(data.tree || []);
1464
+ }
1465
+ } catch (e) {
1466
+ console.error('[DocuKing] 파일 목록 조회 실패:', e.message);
1467
+ }
1468
+
1469
+ // 로컬 파일 목록 조회
1470
+ const folderName = findDocuKingFolder(localPath);
1471
+ const docuKingPath = folderName ? path.join(localPath, folderName) : null;
1472
+ let localFiles = [];
1473
+ if (docuKingPath && fs.existsSync(docuKingPath)) {
1474
+ collectFiles(docuKingPath, '', localFiles);
1475
+ }
1476
+
1477
+ // 참여자인 경우 Push 가능한 파일만 필터링
1478
+ let pushableFiles = localFiles;
1479
+ if (allowedPathPrefix) {
1480
+ const normalizedPrefix = allowedPathPrefix.toLowerCase();
1481
+ pushableFiles = localFiles.filter(file => {
1482
+ const normalizedPath = file.path.toLowerCase();
1483
+ return normalizedPath.startsWith(normalizedPrefix);
1484
+ });
1485
+ }
1486
+
1487
+ const projectNameInfo = projectName ? ` (${projectName})` : '';
1488
+ const statusText = `DocuKing 동기화 상태
1489
+
1490
+ **프로젝트**: ${projectId}${projectNameInfo}
1491
+ **로컬 파일**: ${localFiles.length}개
1492
+ **서버 파일**: ${serverFiles.length}개${allowedPathPrefix ? `\n**Push 가능한 파일**: ${pushableFiles.length}개 (${allowedPathPrefix})` : ''}${permissionInfo}
1493
+
1494
+ ## 사용 가능한 작업
1495
+ - **Push**: docuking_push({ localPath, message: "..." })
1496
+ - **Pull**: docuking_pull({ localPath })
1497
+ - **목록 조회**: docuking_list({ localPath })`;
1498
+
1499
+ return {
1500
+ content: [
1501
+ {
1502
+ type: 'text',
1503
+ text: statusText,
1504
+ },
1505
+ ],
1506
+ };
1507
+ }
1508
+
1509
+ // docuking_log 구현
1510
+ async function handleLog(args) {
1511
+ const { localPath, path: filePath, limit = 20 } = args;
1512
+
1513
+ const projectId = repoMapping[localPath];
1514
+ if (!projectId) {
1515
+ return {
1516
+ content: [
1517
+ {
1518
+ type: 'text',
1519
+ text: `오류: 이 프로젝트는 DocuKing에 연결되지 않았습니다.`,
1520
+ },
1521
+ ],
1522
+ };
1523
+ }
1524
+
1525
+ return {
1526
+ content: [
1527
+ {
1528
+ type: 'text',
1529
+ text: `log 도구는 아직 구현 중입니다.\n\n웹 탐색기(https://docuking.ai)에서 커밋 히스토리를 확인할 수 있습니다.`,
1530
+ },
1531
+ ],
1532
+ };
1533
+ }
1534
+
1535
+ // docuking_diff 구현
1536
+ async function handleDiff(args) {
1537
+ const { localPath, path, version } = args;
1538
+
1539
+ const projectId = repoMapping[localPath];
1540
+ if (!projectId) {
1541
+ return {
1542
+ content: [
1543
+ {
1544
+ type: 'text',
1545
+ text: `오류: 이 프로젝트는 DocuKing에 연결되지 않았습니다.`,
1546
+ },
1547
+ ],
1548
+ };
1549
+ }
1550
+
1551
+ return {
1552
+ content: [
1553
+ {
1554
+ type: 'text',
1555
+ text: `diff 도구는 아직 구현 중입니다.\n\n웹 탐색기에서 파일 버전 비교를 사용할 수 있습니다.`,
1556
+ },
1557
+ ],
1558
+ };
1559
+ }
1560
+
1561
+ // docuking_rollback 구현
1562
+ async function handleRollback(args) {
1563
+ const { localPath, commitId, path } = args;
1564
+
1565
+ const projectId = repoMapping[localPath];
1566
+ if (!projectId) {
1567
+ return {
1568
+ content: [
1569
+ {
1570
+ type: 'text',
1571
+ text: `오류: 이 프로젝트는 DocuKing에 연결되지 않았습니다.`,
1572
+ },
1573
+ ],
1574
+ };
1575
+ }
1576
+
1577
+ return {
1578
+ content: [
1579
+ {
1580
+ type: 'text',
1581
+ text: `rollback 도구는 아직 구현 중입니다.\n\n웹 탐색기에서 파일 롤백을 사용할 수 있습니다.`,
1582
+ },
1583
+ ],
1584
+ };
1585
+ }
1586
+
1587
+ // 서버 시작
1588
+ async function main() {
1589
+ const transport = new StdioServerTransport();
1590
+ await server.connect(transport);
1591
+ console.error('[DocuKing MCP] 서버 시작됨');
1592
+ }
1593
+
1594
+ main().catch(console.error);