syr-d2c-workflow-mcp 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -5,10 +5,126 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
5
5
  import { z } from "zod";
6
6
  import { glob } from "glob";
7
7
  import * as fs from "fs/promises";
8
+ import * as path from "path";
8
9
  // 환경 변수에서 설정 읽기
9
10
  const RULES_PATHS = process.env.RULES_PATHS?.split(",").map((p) => p.trim()) || [];
10
11
  const RULES_GLOB = process.env.RULES_GLOB || "";
11
12
  const CONFIG_PATH = process.env.D2C_CONFIG_PATH || "";
13
+ const PROJECT_ROOT = process.env.D2C_PROJECT_ROOT || process.cwd();
14
+ // OpenSpec 규칙 탐지 경로
15
+ const OPENSPEC_SEARCH_PATHS = [
16
+ "openspec/specs/*/spec.md",
17
+ ".cursor/openspec/specs/*/spec.md",
18
+ "docs/openspec/specs/*/spec.md",
19
+ ];
20
+ // OpenSpec 규칙 캐시
21
+ let cachedOpenSpecRules = null;
22
+ // OpenSpec spec.md 파싱
23
+ async function parseOpenSpecFile(filePath) {
24
+ try {
25
+ const content = await fs.readFile(filePath, "utf-8");
26
+ const specName = path.basename(path.dirname(filePath));
27
+ const requirements = [];
28
+ // Requirement 섹션 파싱
29
+ const reqRegex = /### Requirement: (.+?)\n\n([\s\S]*?)(?=### Requirement:|---|\n## |$)/g;
30
+ let reqMatch;
31
+ while ((reqMatch = reqRegex.exec(content)) !== null) {
32
+ const reqName = reqMatch[1].trim();
33
+ const reqContent = reqMatch[2];
34
+ // Scenario 파싱
35
+ const scenarios = [];
36
+ const scenarioRegex = /#### Scenario: (.+?)\n\n([\s\S]*?)(?=#### Scenario:|### Requirement:|---|\n## |$)/g;
37
+ let scenarioMatch;
38
+ while ((scenarioMatch = scenarioRegex.exec(reqContent)) !== null) {
39
+ const scenarioName = scenarioMatch[1].trim();
40
+ const scenarioContent = scenarioMatch[2];
41
+ const givenMatch = scenarioContent.match(/- \*\*GIVEN\*\* (.+)/);
42
+ const whenMatch = scenarioContent.match(/- \*\*WHEN\*\* (.+)/);
43
+ const thenMatch = scenarioContent.match(/- \*\*THEN\*\* (.+)/);
44
+ scenarios.push({
45
+ name: scenarioName,
46
+ given: givenMatch?.[1] || "",
47
+ when: whenMatch?.[1] || "",
48
+ then: thenMatch?.[1] || "",
49
+ });
50
+ }
51
+ // 설명 추출 (첫 번째 문단)
52
+ const descMatch = reqContent.match(/^(.+?)(?:\n\n|$)/);
53
+ requirements.push({
54
+ name: reqName,
55
+ description: descMatch?.[1]?.trim() || "",
56
+ scenarios,
57
+ });
58
+ }
59
+ return {
60
+ specName,
61
+ filePath,
62
+ requirements,
63
+ };
64
+ }
65
+ catch (e) {
66
+ console.error(`Failed to parse OpenSpec file: ${filePath}`, e);
67
+ return null;
68
+ }
69
+ }
70
+ // OpenSpec 규칙 탐지 및 로드
71
+ async function loadOpenSpecRules(forceReload = false) {
72
+ if (cachedOpenSpecRules && !forceReload) {
73
+ return cachedOpenSpecRules;
74
+ }
75
+ const rules = [];
76
+ for (const searchPath of OPENSPEC_SEARCH_PATHS) {
77
+ const fullPattern = path.join(PROJECT_ROOT, searchPath);
78
+ const files = await glob(fullPattern);
79
+ for (const file of files) {
80
+ const rule = await parseOpenSpecFile(file);
81
+ if (rule) {
82
+ rules.push(rule);
83
+ }
84
+ }
85
+ }
86
+ cachedOpenSpecRules = rules;
87
+ return rules;
88
+ }
89
+ // Phase별 Tasks 정의
90
+ const PHASE_TASKS = {
91
+ 1: {
92
+ name: "Phase 1: Figma MCP 추출",
93
+ target: 60,
94
+ tasks: [
95
+ { id: "1.1", content: "Figma 디자인 컨텍스트 가져오기" },
96
+ { id: "1.2", content: "Figma MCP로 코드 추출" },
97
+ { id: "1.3", content: "Playwright 렌더링" },
98
+ { id: "1.4", content: "스크린샷 비교 (toHaveScreenshot)" },
99
+ { id: "1.5", content: "d2c_phase1_compare 호출" },
100
+ { id: "1.6", content: "HITL 확인" },
101
+ ],
102
+ },
103
+ 2: {
104
+ name: "Phase 2: LLM 이미지 Diff",
105
+ target: 70,
106
+ tasks: [
107
+ { id: "2.1", content: "Playwright 이미지 diff 분석" },
108
+ { id: "2.2", content: "diff 영역 식별" },
109
+ { id: "2.3", content: "LLM이 코드 수정" },
110
+ { id: "2.4", content: "렌더링 후 스크린샷 비교" },
111
+ { id: "2.5", content: "d2c_phase2_image_diff 호출" },
112
+ { id: "2.6", content: "HITL 확인" },
113
+ ],
114
+ },
115
+ 3: {
116
+ name: "Phase 3: LLM DOM 비교",
117
+ target: 90,
118
+ tasks: [
119
+ { id: "3.1", content: "Playwright DOM 스냅샷 추출" },
120
+ { id: "3.2", content: "DOM 구조 비교" },
121
+ { id: "3.3", content: "LLM이 DOM 기반 수정" },
122
+ { id: "3.4", content: "렌더링 후 DOM 비교" },
123
+ { id: "3.5", content: "d2c_phase3_dom_compare 호출" },
124
+ { id: "3.6", content: "HITL 확인" },
125
+ ],
126
+ },
127
+ };
12
128
  // 서비스 식별자 - AI가 이 키워드를 감지하면 이 MCP를 사용
13
129
  const SERVICE_IDENTIFIERS = `
14
130
  🎯 **서비스 식별자**: 다음 키워드가 포함되면 이 MCP를 사용하세요:
@@ -104,7 +220,7 @@ const DEFAULT_RULES = `
104
220
  // MCP 서버 생성
105
221
  const server = new Server({
106
222
  name: "syr-d2c-workflow-mcp",
107
- version: "0.3.0",
223
+ version: "0.4.0",
108
224
  }, {
109
225
  capabilities: {
110
226
  tools: {},
@@ -193,60 +309,6 @@ ${SERVICE_IDENTIFIERS}
193
309
  required: ["code", "componentName"],
194
310
  },
195
311
  },
196
- // compare_with_design - 디자인 비교
197
- {
198
- name: "d2c_compare_with_design",
199
- description: `Figma 디자인 스크린샷과 렌더링 결과를 비교 분석합니다.
200
- ${SERVICE_IDENTIFIERS}
201
-
202
- 📊 **비교 항목 (각 0-100점)**:
203
- - 레이아웃 일치도
204
- - 색상/타이포그래피 일치도
205
- - 간격/여백 일치도
206
- - 누락된 요소
207
-
208
- 💡 **사용법**:
209
- 1. figma-mcp.get_screenshot으로 원본 이미지 획득
210
- 2. playwright-mcp로 렌더링 결과 스크린샷
211
- 3. 이 도구로 비교 분석 (scores 필수 입력)`,
212
- inputSchema: {
213
- type: "object",
214
- properties: {
215
- designDescription: {
216
- type: "string",
217
- description: "Figma 디자인 설명 (get_design_context 결과)",
218
- },
219
- renderedDescription: {
220
- type: "string",
221
- description: "렌더링된 결과 설명",
222
- },
223
- differences: {
224
- type: "array",
225
- items: { type: "string" },
226
- description: "발견된 차이점 목록",
227
- },
228
- iteration: {
229
- type: "number",
230
- description: "현재 반복 횟수",
231
- },
232
- maxIterations: {
233
- type: "number",
234
- description: "최대 반복 횟수 (기본: 5)",
235
- },
236
- scores: {
237
- type: "object",
238
- properties: {
239
- layout: { type: "number", description: "레이아웃 점수 (0-100)" },
240
- colors: { type: "number", description: "색상 점수 (0-100)" },
241
- typography: { type: "number", description: "타이포그래피 점수 (0-100)" },
242
- spacing: { type: "number", description: "간격 점수 (0-100)" },
243
- },
244
- description: "항목별 점수 (0-100)",
245
- },
246
- },
247
- required: ["designDescription", "renderedDescription", "scores"],
248
- },
249
- },
250
312
  // log_step - 실시간 진행 로그
251
313
  {
252
314
  name: "d2c_log_step",
@@ -282,44 +344,6 @@ ${SERVICE_IDENTIFIERS}
282
344
  required: ["step", "stepName", "status"],
283
345
  },
284
346
  },
285
- // iteration_check - 반복 제어
286
- {
287
- name: "d2c_iteration_check",
288
- description: `반복 계속 여부를 판단합니다.
289
- ${SERVICE_IDENTIFIERS}
290
-
291
- 📊 **판단 기준**:
292
- - 70점 미만: 자동으로 계속 진행
293
- - 70점 이상: 사용자 확인 필요
294
- - 최대 반복 도달 또는 점수 하락: 중단 권장`,
295
- inputSchema: {
296
- type: "object",
297
- properties: {
298
- currentScore: {
299
- type: "number",
300
- description: "현재 종합 점수 (0-100)",
301
- },
302
- targetScore: {
303
- type: "number",
304
- description: "목표 점수 (기본: 70)",
305
- },
306
- iteration: {
307
- type: "number",
308
- description: "현재 반복 횟수",
309
- },
310
- maxIterations: {
311
- type: "number",
312
- description: "최대 반복 횟수 (기본: 5)",
313
- },
314
- previousScores: {
315
- type: "array",
316
- items: { type: "number" },
317
- description: "이전 반복의 점수들",
318
- },
319
- },
320
- required: ["currentScore", "iteration"],
321
- },
322
- },
323
347
  // ============ 3단계 PHASE 도구들 ============
324
348
  // Phase 1: Figma MCP 기반 스크린샷 비교
325
349
  {
@@ -510,6 +534,97 @@ ${SERVICE_IDENTIFIERS}
510
534
  required: ["currentPhase"],
511
535
  },
512
536
  },
537
+ // ============ OpenSpec 통합 도구들 ============
538
+ // OpenSpec 규칙 로드
539
+ {
540
+ name: "d2c_load_openspec_rules",
541
+ description: `사용자 프로젝트의 OpenSpec 규칙을 자동으로 탐지하고 로드합니다.
542
+ ${SERVICE_IDENTIFIERS}
543
+
544
+ 📋 **탐지 경로**:
545
+ - ./openspec/specs/*/spec.md
546
+ - ./.cursor/openspec/specs/*/spec.md
547
+ - ./docs/openspec/specs/*/spec.md
548
+
549
+ 🔍 **반환 정보**:
550
+ - 발견된 spec 이름 및 경로
551
+ - 각 spec의 Requirements 목록
552
+ - 각 Requirement의 Scenarios`,
553
+ inputSchema: {
554
+ type: "object",
555
+ properties: {
556
+ forceReload: {
557
+ type: "boolean",
558
+ description: "캐시 무시하고 다시 로드 (기본: false)",
559
+ },
560
+ specNames: {
561
+ type: "array",
562
+ items: { type: "string" },
563
+ description: "특정 spec만 필터링 (예: ['figma-standard', 'design-rules'])",
564
+ },
565
+ },
566
+ },
567
+ },
568
+ // 워크플로우 Tasks 체크리스트
569
+ {
570
+ name: "d2c_get_workflow_tasks",
571
+ description: `현재 Phase에 맞는 tasks.md 형식 체크리스트를 반환합니다.
572
+ ${SERVICE_IDENTIFIERS}
573
+
574
+ 📋 **체크리스트 포함 내용**:
575
+ - Phase 이름 및 목표 성공률
576
+ - 세부 Task 목록 (완료 상태 표시)
577
+ - 적용될 OpenSpec 규칙 목록`,
578
+ inputSchema: {
579
+ type: "object",
580
+ properties: {
581
+ phase: {
582
+ type: "number",
583
+ enum: [1, 2, 3],
584
+ description: "현재 Phase (1, 2, 3)",
585
+ },
586
+ completedTasks: {
587
+ type: "array",
588
+ items: { type: "string" },
589
+ description: "완료된 task ID 목록 (예: ['1.1', '1.2'])",
590
+ },
591
+ includeRules: {
592
+ type: "boolean",
593
+ description: "적용 규칙 목록 포함 (기본: true)",
594
+ },
595
+ },
596
+ required: ["phase"],
597
+ },
598
+ },
599
+ // OpenSpec 규칙 기반 검증
600
+ {
601
+ name: "d2c_validate_against_spec",
602
+ description: `생성된 코드가 OpenSpec 규칙을 준수하는지 검증합니다.
603
+ ${SERVICE_IDENTIFIERS}
604
+
605
+ 🔍 **검증 내용**:
606
+ - 각 Requirement별 pass/fail/warn 상태
607
+ - 위반 시 구체적인 메시지
608
+ - 수정 가이드 제공`,
609
+ inputSchema: {
610
+ type: "object",
611
+ properties: {
612
+ code: {
613
+ type: "string",
614
+ description: "검증할 코드",
615
+ },
616
+ specName: {
617
+ type: "string",
618
+ description: "검증에 사용할 spec 이름 (없으면 모든 spec 적용)",
619
+ },
620
+ componentName: {
621
+ type: "string",
622
+ description: "컴포넌트 이름",
623
+ },
624
+ },
625
+ required: ["code"],
626
+ },
627
+ },
513
628
  // get_component_template - 템플릿 생성
514
629
  {
515
630
  name: "d2c_get_component_template",
@@ -751,113 +866,6 @@ ${input.message ? ` → ${input.message}` : ""}
751
866
  ],
752
867
  };
753
868
  }
754
- case "d2c_iteration_check": {
755
- const input = z
756
- .object({
757
- currentScore: z.number(),
758
- targetScore: z.number().optional().default(70),
759
- iteration: z.number(),
760
- maxIterations: z.number().optional().default(5),
761
- previousScores: z.array(z.number()).optional(),
762
- })
763
- .parse(args);
764
- const { currentScore, targetScore, iteration, maxIterations, previousScores } = input;
765
- // 점수 변화 계산
766
- const lastScore = previousScores?.length ? previousScores[previousScores.length - 1] : null;
767
- const scoreDiff = lastScore !== null ? currentScore - lastScore : null;
768
- const isImproving = scoreDiff === null || scoreDiff >= 0;
769
- // 판단 로직
770
- let recommendation;
771
- let reason;
772
- if (iteration >= maxIterations) {
773
- recommendation = "stop";
774
- reason = `최대 반복 횟수(${maxIterations}회) 도달`;
775
- }
776
- else if (!isImproving && scoreDiff !== null && scoreDiff < -10) {
777
- recommendation = "stop";
778
- reason = `점수 하락 감지 (${scoreDiff}점)`;
779
- }
780
- else if (currentScore >= targetScore) {
781
- recommendation = "user_confirm";
782
- reason = `목표 점수(${targetScore}점) 달성! 사용자 확인 필요`;
783
- }
784
- else {
785
- recommendation = "continue";
786
- reason = `목표 점수(${targetScore}점) 미달, 자동 계속`;
787
- }
788
- const statusEmoji = recommendation === "continue" ? "🔄" : recommendation === "user_confirm" ? "✋" : "🛑";
789
- const diffText = scoreDiff !== null ? ` (${scoreDiff >= 0 ? "+" : ""}${scoreDiff})` : "";
790
- return {
791
- content: [
792
- {
793
- type: "text",
794
- text: `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
795
- ${statusEmoji} **반복 ${iteration}/${maxIterations} 판단 결과**
796
-
797
- 📊 현재 점수: **${currentScore}점**${diffText}
798
- 🎯 목표 점수: ${targetScore}점
799
-
800
- **권장**: ${recommendation === "continue" ? "계속 진행" : recommendation === "user_confirm" ? "사용자 확인" : "중단"}
801
- **이유**: ${reason}
802
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
803
- },
804
- ],
805
- };
806
- }
807
- case "d2c_compare_with_design": {
808
- const input = z
809
- .object({
810
- designDescription: z.string(),
811
- renderedDescription: z.string(),
812
- differences: z.array(z.string()).optional(),
813
- iteration: z.number().optional(),
814
- maxIterations: z.number().optional().default(5),
815
- scores: z.object({
816
- layout: z.number(),
817
- colors: z.number(),
818
- typography: z.number(),
819
- spacing: z.number(),
820
- }),
821
- })
822
- .parse(args);
823
- const { scores, iteration, maxIterations } = input;
824
- const avgScore = Math.round((scores.layout + scores.colors + scores.typography + scores.spacing) / 4);
825
- // 점수 바 생성 함수
826
- const scoreBar = (score) => {
827
- const filled = Math.round(score / 10);
828
- return "█".repeat(filled) + "░".repeat(10 - filled);
829
- };
830
- const checkMark = (score) => score >= 70 ? "✓" : "✗";
831
- const iterationHeader = iteration ? `반복 ${iteration}/${maxIterations}` : "";
832
- return {
833
- content: [
834
- {
835
- type: "text",
836
- text: `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
837
- 📊 **디자인 비교 결과** ${iterationHeader}
838
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
839
-
840
- ┌────────────┬────────────┬──────┬──────┐
841
- │ 항목 │ 점수바 │ 점수 │ 상태 │
842
- ├────────────┼────────────┼──────┼──────┤
843
- │ 레이아웃 │ ${scoreBar(scores.layout)} │ ${String(scores.layout).padStart(3)} │ ${checkMark(scores.layout)} │
844
- │ 색상 │ ${scoreBar(scores.colors)} │ ${String(scores.colors).padStart(3)} │ ${checkMark(scores.colors)} │
845
- │ 타이포 │ ${scoreBar(scores.typography)} │ ${String(scores.typography).padStart(3)} │ ${checkMark(scores.typography)} │
846
- │ 간격 │ ${scoreBar(scores.spacing)} │ ${String(scores.spacing).padStart(3)} │ ${checkMark(scores.spacing)} │
847
- ├────────────┼────────────┼──────┼──────┤
848
- │ **종합** │ ${scoreBar(avgScore)} │ **${String(avgScore).padStart(3)}** │ ${checkMark(avgScore)} │
849
- └────────────┴────────────┴──────┴──────┘
850
-
851
- ${input.differences?.length ? `
852
- ## 발견된 차이점
853
- ${input.differences.map((d) => `- ${d}`).join("\n")}
854
- ` : ""}
855
- ## 다음 단계
856
- → \`d2c_iteration_check\` 호출하여 계속 여부 판단`,
857
- },
858
- ],
859
- };
860
- }
861
869
  // ============ 3단계 PHASE 핸들러 ============
862
870
  case "d2c_phase1_compare": {
863
871
  const input = z
@@ -1157,6 +1165,277 @@ ${input.currentPhase === 1 ? " ↑ 현재" : input.currentPhase === 2 ? "
1157
1165
  ],
1158
1166
  };
1159
1167
  }
1168
+ // ============ OpenSpec 통합 핸들러 ============
1169
+ case "d2c_load_openspec_rules": {
1170
+ const input = z
1171
+ .object({
1172
+ forceReload: z.boolean().optional().default(false),
1173
+ specNames: z.array(z.string()).optional(),
1174
+ })
1175
+ .parse(args);
1176
+ const rules = await loadOpenSpecRules(input.forceReload);
1177
+ let filteredRules = rules;
1178
+ if (input.specNames?.length) {
1179
+ filteredRules = rules.filter(r => input.specNames.includes(r.specName));
1180
+ }
1181
+ if (filteredRules.length === 0) {
1182
+ return {
1183
+ content: [
1184
+ {
1185
+ type: "text",
1186
+ text: `📋 **OpenSpec 규칙 로드 결과**
1187
+
1188
+ ## 발견된 규칙
1189
+ 없음
1190
+
1191
+ ## 탐지 경로
1192
+ ${OPENSPEC_SEARCH_PATHS.map(p => `- ${path.join(PROJECT_ROOT, p)}`).join("\n")}
1193
+
1194
+ ## 대안
1195
+ - 환경변수 RULES_PATHS로 규칙 파일 지정
1196
+ - \`d2c_get_design_rules\`로 기본 규칙 사용
1197
+
1198
+ 💡 프로젝트에 OpenSpec 규칙을 추가하려면:
1199
+ \`\`\`
1200
+ mkdir -p openspec/specs/figma-standard
1201
+ touch openspec/specs/figma-standard/spec.md
1202
+ \`\`\``,
1203
+ },
1204
+ ],
1205
+ };
1206
+ }
1207
+ const rulesText = filteredRules.map(rule => {
1208
+ const reqList = rule.requirements.map(req => {
1209
+ const scenarioCount = req.scenarios.length;
1210
+ return ` - ${req.name} (${scenarioCount}개 시나리오)`;
1211
+ }).join("\n");
1212
+ return `### ${rule.specName}
1213
+ - 경로: \`${rule.filePath}\`
1214
+ - Requirements (${rule.requirements.length}개):
1215
+ ${reqList}`;
1216
+ }).join("\n\n");
1217
+ return {
1218
+ content: [
1219
+ {
1220
+ type: "text",
1221
+ text: `📋 **OpenSpec 규칙 로드 결과**
1222
+
1223
+ ## 발견된 규칙 (${filteredRules.length}개)
1224
+
1225
+ ${rulesText}
1226
+
1227
+ ## 사용법
1228
+ 1. \`d2c_get_workflow_tasks\`로 체크리스트에서 규칙 확인
1229
+ 2. \`d2c_validate_against_spec\`로 코드 검증
1230
+ 3. 각 Phase에서 규칙 준수 여부 자동 확인`,
1231
+ },
1232
+ ],
1233
+ };
1234
+ }
1235
+ case "d2c_get_workflow_tasks": {
1236
+ const input = z
1237
+ .object({
1238
+ phase: z.number(),
1239
+ completedTasks: z.array(z.string()).optional().default([]),
1240
+ includeRules: z.boolean().optional().default(true),
1241
+ })
1242
+ .parse(args);
1243
+ const phaseInfo = PHASE_TASKS[input.phase];
1244
+ if (!phaseInfo) {
1245
+ throw new Error(`Invalid phase: ${input.phase}. Must be 1, 2, or 3.`);
1246
+ }
1247
+ // 체크리스트 생성
1248
+ const taskList = phaseInfo.tasks.map(task => {
1249
+ const isCompleted = input.completedTasks.includes(task.id);
1250
+ return `- [${isCompleted ? "x" : " "}] ${task.id} ${task.content}`;
1251
+ }).join("\n");
1252
+ // 완료율 계산
1253
+ const completedCount = phaseInfo.tasks.filter(t => input.completedTasks.includes(t.id)).length;
1254
+ const totalCount = phaseInfo.tasks.length;
1255
+ const progressPercent = Math.round((completedCount / totalCount) * 100);
1256
+ // OpenSpec 규칙 섹션
1257
+ let rulesSection = "";
1258
+ if (input.includeRules) {
1259
+ const rules = await loadOpenSpecRules();
1260
+ if (rules.length > 0) {
1261
+ const rulesList = rules.map(rule => {
1262
+ const keyReqs = rule.requirements.slice(0, 3).map(r => r.name).join(", ");
1263
+ return `- **${rule.specName}**: ${keyReqs}${rule.requirements.length > 3 ? " 외 " + (rule.requirements.length - 3) + "개" : ""}`;
1264
+ }).join("\n");
1265
+ rulesSection = `\n### 적용 규칙\n${rulesList}\n`;
1266
+ }
1267
+ else {
1268
+ rulesSection = `\n### 적용 규칙\n- (없음) 기본 규칙 사용\n`;
1269
+ }
1270
+ }
1271
+ return {
1272
+ content: [
1273
+ {
1274
+ type: "text",
1275
+ text: `## ${phaseInfo.name} (목표 ${phaseInfo.target}%)
1276
+
1277
+ ### 진행률: ${progressPercent}% (${completedCount}/${totalCount})
1278
+ ${"█".repeat(Math.round(progressPercent / 10))}${"░".repeat(10 - Math.round(progressPercent / 10))}
1279
+
1280
+ ### Tasks
1281
+ ${taskList}
1282
+ ${rulesSection}
1283
+ ### 다음 단계
1284
+ ${completedCount === totalCount
1285
+ ? `✅ Phase ${input.phase} 완료! ${input.phase < 3 ? `Phase ${input.phase + 1}로 진행하세요.` : "워크플로우 완료!"}`
1286
+ : `➡️ ${phaseInfo.tasks.find(t => !input.completedTasks.includes(t.id))?.id} ${phaseInfo.tasks.find(t => !input.completedTasks.includes(t.id))?.content} 진행`}`,
1287
+ },
1288
+ ],
1289
+ };
1290
+ }
1291
+ case "d2c_validate_against_spec": {
1292
+ const input = z
1293
+ .object({
1294
+ code: z.string(),
1295
+ specName: z.string().optional(),
1296
+ componentName: z.string().optional(),
1297
+ })
1298
+ .parse(args);
1299
+ const rules = await loadOpenSpecRules();
1300
+ let targetRules = rules;
1301
+ if (input.specName) {
1302
+ targetRules = rules.filter(r => r.specName === input.specName);
1303
+ }
1304
+ const results = [];
1305
+ // 기본 검증 규칙 (항상 적용)
1306
+ const code = input.code;
1307
+ const componentName = input.componentName || "Component";
1308
+ // 1. PascalCase 컴포넌트 네이밍
1309
+ if (componentName && /^[A-Z][a-zA-Z0-9]*$/.test(componentName)) {
1310
+ results.push({
1311
+ specName: "default",
1312
+ requirement: "컴포넌트 네이밍 규칙",
1313
+ status: "pass",
1314
+ message: `${componentName}은(는) PascalCase 준수`,
1315
+ });
1316
+ }
1317
+ else if (componentName) {
1318
+ results.push({
1319
+ specName: "default",
1320
+ requirement: "컴포넌트 네이밍 규칙",
1321
+ status: "fail",
1322
+ message: `${componentName}은(는) PascalCase가 아님. 권장: ${componentName.split(/[-_]/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join("")}`,
1323
+ });
1324
+ }
1325
+ // 2. Props 인터페이스
1326
+ if (code.includes("interface") && code.includes("Props")) {
1327
+ results.push({
1328
+ specName: "default",
1329
+ requirement: "Props 인터페이스 정의",
1330
+ status: "pass",
1331
+ message: "TypeScript Props 인터페이스 정의됨",
1332
+ });
1333
+ }
1334
+ else if (code.includes(": {") || code.includes("Props")) {
1335
+ results.push({
1336
+ specName: "default",
1337
+ requirement: "Props 인터페이스 정의",
1338
+ status: "warn",
1339
+ message: "Props 타입이 있으나 명시적 인터페이스 권장",
1340
+ });
1341
+ }
1342
+ else {
1343
+ results.push({
1344
+ specName: "default",
1345
+ requirement: "Props 인터페이스 정의",
1346
+ status: "fail",
1347
+ message: "Props 인터페이스가 없음. interface ComponentProps {} 추가 권장",
1348
+ });
1349
+ }
1350
+ // 3. 접근성
1351
+ const a11yPatterns = ["aria-", "role=", "tabIndex", "alt="];
1352
+ const hasA11y = a11yPatterns.some(p => code.includes(p));
1353
+ results.push({
1354
+ specName: "default",
1355
+ requirement: "접근성 속성",
1356
+ status: hasA11y ? "pass" : "warn",
1357
+ message: hasA11y ? "접근성 속성 포함됨" : "aria-*, role 속성 추가 권장",
1358
+ });
1359
+ // OpenSpec 규칙 기반 검증
1360
+ for (const rule of targetRules) {
1361
+ for (const req of rule.requirements) {
1362
+ // 키워드 기반 간단한 검증
1363
+ const keywords = req.name.toLowerCase().split(/\s+/);
1364
+ let matched = false;
1365
+ let status = "warn";
1366
+ // 네이밍 관련
1367
+ if (keywords.some(k => ["naming", "네이밍", "이름"].includes(k))) {
1368
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(componentName || "")) {
1369
+ matched = true;
1370
+ status = "pass";
1371
+ }
1372
+ }
1373
+ // Props 관련
1374
+ if (keywords.some(k => ["props", "인터페이스", "interface"].includes(k))) {
1375
+ if (code.includes("interface") && code.includes("Props")) {
1376
+ matched = true;
1377
+ status = "pass";
1378
+ }
1379
+ }
1380
+ // 접근성 관련
1381
+ if (keywords.some(k => ["접근성", "a11y", "accessibility", "aria"].includes(k))) {
1382
+ if (hasA11y) {
1383
+ matched = true;
1384
+ status = "pass";
1385
+ }
1386
+ }
1387
+ if (!matched) {
1388
+ results.push({
1389
+ specName: rule.specName,
1390
+ requirement: req.name,
1391
+ status: "warn",
1392
+ message: `검증 필요: ${req.description || req.name}`,
1393
+ });
1394
+ }
1395
+ else {
1396
+ results.push({
1397
+ specName: rule.specName,
1398
+ requirement: req.name,
1399
+ status,
1400
+ message: status === "pass" ? "규칙 준수" : "검토 필요",
1401
+ });
1402
+ }
1403
+ }
1404
+ }
1405
+ // 결과 집계
1406
+ const passCount = results.filter(r => r.status === "pass").length;
1407
+ const failCount = results.filter(r => r.status === "fail").length;
1408
+ const warnCount = results.filter(r => r.status === "warn").length;
1409
+ const totalCount = results.length;
1410
+ const passRate = Math.round((passCount / totalCount) * 100);
1411
+ const statusIcon = (s) => s === "pass" ? "✅" : s === "fail" ? "❌" : "⚠️";
1412
+ const resultText = results.map(r => `${statusIcon(r.status)} **${r.requirement}** (${r.specName})\n ${r.message}`).join("\n\n");
1413
+ return {
1414
+ content: [
1415
+ {
1416
+ type: "text",
1417
+ text: `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1418
+ 📋 **OpenSpec 규칙 검증 결과**
1419
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1420
+
1421
+ ## 요약
1422
+ - 통과: ${passCount}개 ✅
1423
+ - 실패: ${failCount}개 ❌
1424
+ - 경고: ${warnCount}개 ⚠️
1425
+ - **준수율: ${passRate}%**
1426
+
1427
+ ${"█".repeat(Math.round(passRate / 10))}${"░".repeat(10 - Math.round(passRate / 10))} ${passRate}%
1428
+
1429
+ ## 상세 결과
1430
+
1431
+ ${resultText}
1432
+
1433
+ ${failCount > 0 ? `\n## 수정 필요 항목\n${results.filter(r => r.status === "fail").map(r => `- ${r.requirement}: ${r.message}`).join("\n")}` : ""}
1434
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
1435
+ },
1436
+ ],
1437
+ };
1438
+ }
1160
1439
  case "d2c_get_component_template": {
1161
1440
  const input = z
1162
1441
  .object({
@@ -1347,6 +1626,13 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1347
1626
 
1348
1627
  ---
1349
1628
 
1629
+ ### 🔰 Step 0: OpenSpec 규칙 로드
1630
+ 1. **\`d2c_load_openspec_rules\`** 호출하여 프로젝트 규칙 확인
1631
+ 2. 발견된 규칙(예: figma-standard, design-rules)을 워크플로우에 적용
1632
+ 3. 규칙이 없으면 기본 규칙 사용
1633
+
1634
+ ---
1635
+
1350
1636
  ### Step 1: 사전 검사
1351
1637
  1. \`d2c_log_step(step:1, stepName:"사전 검사", status:"start")\`
1352
1638
  2. \`d2c_preflight_check\` 호출
@@ -1362,46 +1648,52 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1362
1648
  ---
1363
1649
 
1364
1650
  ### 🔄 Phase 1: Figma MCP 추출 (목표 60%)
1365
- 1. \`d2c_log_step(step:3, stepName:"Phase 1", status:"start", iteration:1)\`
1366
- 2. \`d2c_get_component_template\`로 템플릿 생성
1367
- 3. **Figma MCP로 코드 추출/수정**
1368
- 4. \`playwright-mcp.browser_navigate\`로 렌더링
1369
- 5. \`playwright-mcp.browser_screenshot\`으로 스크린샷
1370
- 6. **Playwright toHaveScreenshot()으로 비교하여 성공률 계산**
1371
- 7. **\`d2c_phase1_compare\`** 호출 (successRate, iteration 필수!)
1372
- 8. **HITL 확인**: 사용자 응답에 따라:
1373
- - [Y] 60% 미달이면 반복, 달성이면 Phase 2로
1374
- - [M] 수동 수정 후 재비교
1375
- - [N] → 현재 상태로 다음 단계
1376
- 9. \`d2c_log_step(step:3, stepName:"Phase 1", status:"done")\`
1651
+ 1. **\`d2c_get_workflow_tasks(phase:1)\`**로 체크리스트 확인
1652
+ 2. \`d2c_log_step(step:3, stepName:"Phase 1", status:"start", iteration:1)\`
1653
+ 3. \`d2c_get_component_template\`로 템플릿 생성
1654
+ 4. **Figma MCP로 코드 추출/수정**
1655
+ 5. \`playwright-mcp.browser_navigate\`로 렌더링
1656
+ 6. \`playwright-mcp.browser_screenshot\`으로 스크린샷
1657
+ 7. **Playwright toHaveScreenshot()으로 비교하여 성공률 계산**
1658
+ 8. **\`d2c_phase1_compare\`** 호출 (successRate, iteration 필수!)
1659
+ 9. **\`d2c_validate_against_spec\`**로 OpenSpec 규칙 검증
1660
+ 10. **HITL 확인**: 사용자 응답에 따라:
1661
+ - [Y] → 60% 미달이면 반복, 달성이면 Phase 2로
1662
+ - [M] 수동 수정 후 재비교
1663
+ - [N] → 현재 상태로 다음 단계
1664
+ 11. \`d2c_log_step(step:3, stepName:"Phase 1", status:"done")\`
1377
1665
 
1378
1666
  ---
1379
1667
 
1380
1668
  ### 🔄 Phase 2: LLM 이미지 Diff (목표 70%)
1381
- 1. \`d2c_log_step(step:4, stepName:"Phase 2", status:"start", iteration:1)\`
1382
- 2. **Playwright 이미지 diff 분석**
1383
- 3. diff 결과 기반으로 **LLM이 코드 수정**
1384
- 4. 렌더링 스크린샷 비교
1385
- 5. **\`d2c_phase2_image_diff\`** 호출 (successRate, diffAreas 포함!)
1386
- 6. **HITL 확인**: 사용자 응답에 따라:
1669
+ 1. **\`d2c_get_workflow_tasks(phase:2)\`**로 체크리스트 확인
1670
+ 2. \`d2c_log_step(step:4, stepName:"Phase 2", status:"start", iteration:1)\`
1671
+ 3. **Playwright 이미지 diff 분석**
1672
+ 4. diff 결과 기반으로 **LLM이 코드 수정**
1673
+ 5. 렌더링 스크린샷 비교
1674
+ 6. **\`d2c_phase2_image_diff\`** 호출 (successRate, diffAreas 포함!)
1675
+ 7. **\`d2c_validate_against_spec\`**로 OpenSpec 규칙 검증
1676
+ 8. **HITL 확인**: 사용자 응답에 따라:
1387
1677
  - [Y] → 70% 미달이면 LLM 수정 반복, 달성이면 Phase 3로
1388
1678
  - [M] → 수동 수정 후 재비교
1389
1679
  - [N] → 현재 상태로 다음 단계
1390
- 7. \`d2c_log_step(step:4, stepName:"Phase 2", status:"done")\`
1680
+ 9. \`d2c_log_step(step:4, stepName:"Phase 2", status:"done")\`
1391
1681
 
1392
1682
  ---
1393
1683
 
1394
1684
  ### 🔄 Phase 3: LLM DOM 비교 (목표 90%)
1395
- 1. \`d2c_log_step(step:5, stepName:"Phase 3", status:"start", iteration:1)\`
1396
- 2. **Playwright DOM 스냅샷 비교**
1397
- 3. DOM 차이 기반으로 **LLM이 코드 수정**
1398
- 4. 렌더링 DOM 비교
1399
- 5. **\`d2c_phase3_dom_compare\`** 호출 (successRate, domDiffs 포함!)
1400
- 6. **HITL 확인**: 사용자 응답에 따라:
1685
+ 1. **\`d2c_get_workflow_tasks(phase:3)\`**로 체크리스트 확인
1686
+ 2. \`d2c_log_step(step:5, stepName:"Phase 3", status:"start", iteration:1)\`
1687
+ 3. **Playwright DOM 스냅샷 비교**
1688
+ 4. DOM 차이 기반으로 **LLM이 코드 수정**
1689
+ 5. 렌더링 DOM 비교
1690
+ 6. **\`d2c_phase3_dom_compare\`** 호출 (successRate, domDiffs 포함!)
1691
+ 7. **\`d2c_validate_against_spec\`**로 OpenSpec 규칙 최종 검증
1692
+ 8. **HITL 확인**: 사용자 응답에 따라:
1401
1693
  - [Y] → 90% 미달이면 LLM 수정 반복, 달성이면 완료
1402
1694
  - [M] → 수동 수정 후 재비교
1403
1695
  - [N] → 현재 상태로 완료
1404
- 7. \`d2c_log_step(step:5, stepName:"Phase 3", status:"done")\`
1696
+ 9. \`d2c_log_step(step:5, stepName:"Phase 3", status:"done")\`
1405
1697
 
1406
1698
  ---
1407
1699
 
@@ -1414,10 +1706,22 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1414
1706
  ---
1415
1707
 
1416
1708
  **⚠️ 중요 규칙**:
1709
+ - **워크플로우 시작 시 \`d2c_load_openspec_rules\`로 규칙 로드**
1710
+ - **각 Phase에서 \`d2c_get_workflow_tasks\`로 체크리스트 확인**
1711
+ - **코드 수정 후 \`d2c_validate_against_spec\`로 규칙 검증**
1417
1712
  - 매 Phase마다 **반드시 HITL 확인** (사용자에게 계속 여부 질문)
1418
1713
  - 모든 Phase에서 사용자가 수동 수정 가능 ([M] 옵션)
1419
1714
  - 성공률은 Playwright 비교 결과를 기반으로 객관적으로 측정
1420
- - \`d2c_workflow_status\`로 언제든 전체 진행 상황 확인 가능`,
1715
+ - \`d2c_workflow_status\`로 언제든 전체 진행 상황 확인 가능
1716
+
1717
+ ---
1718
+
1719
+ ### 📋 OpenSpec 도구 사용법
1720
+ | 도구 | 용도 | 호출 시점 |
1721
+ |------|------|----------|
1722
+ | \`d2c_load_openspec_rules\` | 프로젝트 규칙 로드 | 워크플로우 시작 시 |
1723
+ | \`d2c_get_workflow_tasks\` | Phase별 체크리스트 | 각 Phase 시작 시 |
1724
+ | \`d2c_validate_against_spec\` | 규칙 준수 검증 | 코드 수정 후 |`,
1421
1725
  },
1422
1726
  },
1423
1727
  ],
@@ -1489,9 +1793,10 @@ export default Component;
1489
1793
  async function main() {
1490
1794
  const transport = new StdioServerTransport();
1491
1795
  await server.connect(transport);
1492
- console.error("SYR D2C Workflow MCP server running on stdio (v0.1.0)");
1796
+ console.error("SYR D2C Workflow MCP server running on stdio (v0.4.0)");
1493
1797
  console.error(` Rules paths: ${RULES_PATHS.join(", ") || "(none)"}`);
1494
1798
  console.error(` Rules glob: ${RULES_GLOB || "(none)"}`);
1799
+ console.error(` OpenSpec paths: ${OPENSPEC_SEARCH_PATHS.map(p => path.join(PROJECT_ROOT, p)).join(", ")}`);
1495
1800
  }
1496
1801
  main().catch((error) => {
1497
1802
  console.error("Fatal error:", error);