@infomind-ux/infoux-mcp 0.1.0 → 0.2.1

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
@@ -14,7 +14,9 @@ INFOMIND UX팀의 퍼블리싱 기준(infoUX)을 MCP로 제공한다. 팀원이
14
14
  | `get_tokens` | 색상·폰트·브레이크포인트 토큰. `query`로 필터, `raw`로 tokens.css 원본 |
15
15
  | `get_rules` | 코딩 규칙 R-01~R-22 (위반·준수 예시 포함) |
16
16
  | `get_reference` | 접근성·금지패턴·Tailwind 매핑·HTML 시맨틱·사이트 유형 프로필 |
17
+ | `get_profile` | 사이트 유형 프리셋 — section 흐름·우선 컴포넌트·밀도·표현 등급 |
17
18
  | `get_workflow` | 작업 절차 — 페이지·폼·위젯 설계, 컴포넌트 생성, 토큰 변경, UI 리뷰, 프로젝트 초기화 |
19
+ | `get_art_direction` | 프로필별 아트 디렉션 — 표현 등급 상세·타이포 페어링·팔레트 프리셋·한글 조판·안티패턴 색인 |
18
20
  | `search_docs` | 어느 문서를 봐야 할지 모를 때 전체 검색 |
19
21
 
20
22
  서버는 접속 시 **지시문**도 함께 넘긴다 — 사이트 유형 판정, 토큰 강제, 카탈로그 우선, 규칙 준수 순서가 에이전트에 자동으로 걸린다.
@@ -64,7 +66,7 @@ args = ["-y", "@infomind-ux/infoux-mcp"]
64
66
  npx -y @infomind-ux/infoux-mcp
65
67
  ```
66
68
 
67
- `infoUX MCP 준비됨 — 빌드 <sha>, 도구 8종`이 stderr로 나오면 정상이다. stdout은 프로토콜 채널이라 로그를 싣지 않는다.
69
+ `infoUX MCP 준비됨 — 빌드 <sha>, 도구 <n>종`이 stderr로 나오면 정상이다. stdout은 프로토콜 채널이라 로그를 싣지 않는다.
68
70
 
69
71
  ## 데이터 갱신
70
72
 
@@ -74,7 +76,7 @@ npx -y @infomind-ux/infoux-mcp
74
76
  npm run build:mcp
75
77
  ```
76
78
 
77
- 원본이 바뀌면(`rules.json`, `skill/`, `src/snippets/`, `tokens/`) 재생성 후 커밋한다. `npm run build`와 `npm test`에 이미 포함돼 있다.
79
+ 원본이 바뀌면(`rules.json`, `contracts/`, `references/`, `src/snippets/`, `tokens/`) 재생성 후 커밋한다. `npm run build`와 `npm test`에 이미 포함돼 있다.
78
80
 
79
81
  ## 범위
80
82
 
package/bin/server.js CHANGED
@@ -27,6 +27,8 @@ function readData(...segments) {
27
27
 
28
28
  const manifest = JSON.parse(readData('manifest.json'))
29
29
  const rules = JSON.parse(readData('rules.json'))
30
+ const profileSpec = JSON.parse(readData('profiles.json'))
31
+ const artDirection = JSON.parse(readData('art-direction.json'))
30
32
 
31
33
  // ─────────────────────────────────────────────────────
32
34
  // 서버 지시문 — 에이전트가 도구를 언제 써야 하는지 알려준다
@@ -38,6 +40,9 @@ const INSTRUCTIONS = `INFOMIND UX팀의 HTML/CSS 퍼블리싱 기준(infoUX)을
38
40
 
39
41
  1. 사이트 유형을 판정한다 — 일반사이트 / 공공서비스 / 공공기관 / CMS·관리자 / 커머스·예약.
40
42
  판단이 서지 않으면 get_reference("project-profiles")를 읽는다.
43
+ 판정했으면 get_profile(id)로 section 흐름·우선 컴포넌트·밀도를 가져간다.
44
+ 프로젝트에 infoux.json이 있으면 그 profile 값이 판정 결과다 — 다시 판정하지 않는다.
45
+ 프리셋과 함께 get_art_direction(profile)로 표현 등급·타이포·팔레트·카피 톤 기준을 가져간다.
41
46
  2. 색상은 반드시 토큰을 쓴다. hex/rgb/hsl 직접 작성 금지. get_tokens로 확인한다.
42
47
  토큰명을 지어내지 않는다 — 목록에 없으면 사용자에게 확인한다.
43
48
  3. 컴포넌트는 카탈로그를 먼저 본다. list_components → get_component 순으로 확인하고
@@ -45,6 +50,9 @@ const INSTRUCTIONS = `INFOMIND UX팀의 HTML/CSS 퍼블리싱 기준(infoUX)을
45
50
  페이지·폼·위젯 설계나 컴포넌트 신규 생성처럼 절차가 정해진 작업은 get_workflow를 먼저 읽는다.
46
51
  4. 규칙 R-01~R-22를 지킨다. get_rules로 확인한다. BEM, 접근성, 금지 패턴이 여기 있다.
47
52
  5. 간격·크기·타이포 스케일·반경·모션은 토큰이 아니라 CSS/Tailwind 직접값으로 쓴다.
53
+ 6. 원칙이 충돌하면 get_reference("trade-off-rules")의 우선순위를 따른다. 접근성이 1순위다.
54
+ 화면을 마무리했으면 get_reference("release-checklist")로 점검한다 — 접근성·과업 흐름·품질은
55
+ 100% 통과가 조건이다.
48
56
 
49
57
  기술 스택은 Tailwind v4 + 표준 CSS nesting + BEM + ITCSS 5계층이다. SCSS는 쓰지 않는다.`
50
58
 
@@ -117,6 +125,19 @@ const TOOLS = [
117
125
  }
118
126
  }
119
127
  },
128
+ {
129
+ name: 'get_profile',
130
+ description:
131
+ '사이트 유형 프리셋을 반환한다. name 없이 부르면 5종 목록을 준다. ' +
132
+ 'section 흐름, 우선 컴포넌트, 밀도 기준, 정부 아이덴티티 조건이 들어 있다. ' +
133
+ '사이트 유형을 판정한 직후에 읽어 구조를 잡는다.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ name: { type: 'string', description: '유형 id (general-site, public-service, public-institution, cms-admin, commerce-reservation)' }
138
+ }
139
+ }
140
+ },
120
141
  {
121
142
  name: 'get_workflow',
122
143
  description:
@@ -129,6 +150,19 @@ const TOOLS = [
129
150
  }
130
151
  }
131
152
  },
153
+ {
154
+ name: 'get_art_direction',
155
+ description:
156
+ '프로필별 아트 디렉션 — 표현 등급 상세, 타이포 페어링 후보, 팔레트 프리셋 후보, 한글 조판 공통값, ' +
157
+ '안티패턴 색인. 인자 없이 호출하면 전체 색인. 밀도·section 흐름은 get_profile 소유라 반복하지 않는다.',
158
+ inputSchema: {
159
+ type: 'object',
160
+ properties: {
161
+ profile: { type: 'string', description: '사이트 유형 id (general-site, public-service, public-institution, cms-admin, commerce-reservation). 생략 시 등급 정의와 전체 후보 색인' },
162
+ expression: { type: 'string', description: '표현 등급 덮어쓰기 (utility, restrained, expressive). 생략 시 프로필 기본값. Task Contract의 expression과 같은 값' }
163
+ }
164
+ }
165
+ },
132
166
  {
133
167
  name: 'search_docs',
134
168
  description:
@@ -209,6 +243,67 @@ function getRules({ id, category, severity } = {}) {
209
243
  return text(lines.join('\n'))
210
244
  }
211
245
 
246
+ function getProfile(name) {
247
+ const profiles = profileSpec.profiles
248
+
249
+ if (!name) {
250
+ const lines = ['# infoUX 사이트 유형', '', '판정 후 get_profile(id)로 프리셋을 가져간다.', '']
251
+ for (const p of profiles) {
252
+ lines.push(`- **${p.id}** (${p.label}) — ${p.appliesTo}`)
253
+ }
254
+ lines.push('', '판정이 서지 않으면 get_reference("project-profiles")의 판정 절차를 읽는다.')
255
+ return text(lines.join('\n'))
256
+ }
257
+
258
+ const profile = profiles.find(p => p.id === name)
259
+ if (!profile) return notFound(`사이트 유형 "${name}"`, profiles.map(p => p.id))
260
+
261
+ const density = profileSpec.density[profile.density]
262
+ const expression = profileSpec.expressionLevels[profile.expression]
263
+ const identity = profile.governmentIdentity === 'excluded'
264
+ ? '제외 — 정부 상징·공식 배너·운영기관 식별자를 생성하지 않는다.'
265
+ : '조건부 — 과업지시서나 기관 정책이 확인된 경우에만 생성한다.'
266
+
267
+ const lines = [
268
+ `# ${profile.label} (${profile.id})`,
269
+ '',
270
+ `적용 대상: ${profile.appliesTo}`,
271
+ `기본 생성: ${profile.focus}`,
272
+ `표현 등급: ${expression.label} — 상세는 get_art_direction`,
273
+ '',
274
+ '## 기본 section 흐름',
275
+ '',
276
+ profile.sectionFlow.map(s => `section--${s}`).join(' → ')
277
+ ]
278
+ if (profile.sectionFlowAlt) {
279
+ lines.push('', '대안 흐름:', profile.sectionFlowAlt.map(s => `section--${s}`).join(' → '))
280
+ }
281
+ lines.push(
282
+ '',
283
+ '## 우선 컴포넌트',
284
+ '',
285
+ profile.priorityComponents.map(c => `- ${c}`).join('\n'),
286
+ '',
287
+ `## 밀도 — ${density.label}`,
288
+ '',
289
+ `- section 패딩: PC ${density.sectionPaddingPc} / 모바일 ${density.sectionPaddingMobile}`,
290
+ `- 폼 행 간격: ${density.formRowGap}`,
291
+ `- 표 셀 패딩: ${density.tableCellPadding}`,
292
+ `- ${density.note}`,
293
+ '',
294
+ '## 정부 아이덴티티',
295
+ '',
296
+ identity,
297
+ '',
298
+ '## 주의',
299
+ '',
300
+ profile.note,
301
+ '',
302
+ '간격은 토큰이 아니라 직접값이다. 위 수치는 출발점이며 프로젝트 맥락에서 조정한다.'
303
+ )
304
+ return text(lines.join('\n'))
305
+ }
306
+
212
307
  function getWorkflow(name) {
213
308
  if (!name) {
214
309
  const lines = ['# infoUX 작업 절차', '', '작업을 시작하기 전에 해당 절차를 읽는다.', '']
@@ -235,6 +330,143 @@ function getReference(name) {
235
330
  return text(readData('references', entry.file))
236
331
  }
237
332
 
333
+ /** 프리셋 파일에서 라이트 모드 50단계 대표색을 읽는다 — hex 정본은 프리셋이라 값을 복사해 두지 않는다. */
334
+ function presetRepresentative(id) {
335
+ const preset = JSON.parse(readData('presets', `${id}.json`))
336
+ const pick = group => preset.primitive.color.light[group]['50'].value
337
+ return {
338
+ primary: pick('primary'),
339
+ secondary: pick('secondary'),
340
+ point: pick('point'),
341
+ pointException: preset.$meta?.preset?.pointException
342
+ }
343
+ }
344
+
345
+ function hangulLines() {
346
+ const h = artDirection.hangul
347
+ return [
348
+ `- word-break: ${h.wordBreak} / 본문 line-height ${h.bodyLineHeight.min}~${h.bodyLineHeight.max}(기본 ${h.bodyLineHeight.default}) / 제목 ${h.headingLineHeight.min}~${h.headingLineHeight.max}`,
349
+ `- 큰 제목(${h.headingLetterSpacing.appliesFromRem}rem+) letter-spacing ${h.headingLetterSpacing.min}~${h.headingLetterSpacing.max} / weight: 본문 ${h.weights.bodyBase} 기본, 제목 ${h.weights.headingMax}은 h1~h2 한정, 화면당 ${h.weights.maxPerScreen}종 이하`
350
+ ]
351
+ }
352
+
353
+ function antiPatternLines() {
354
+ return [
355
+ '- 기계 검출: 가짜 콘텐츠(R-23) · 한글 조판 하한(R-24) · 섹션 리듬(R-25) · 폰트 한글 fallback(R-26)',
356
+ '- 산문 안티패턴 10건·납품 전 리뷰 체크리스트: get_reference("art-direction")'
357
+ ]
358
+ }
359
+
360
+ function typographyLines(entry) {
361
+ const lines = [
362
+ `### ${entry.id} — ${entry.label}`,
363
+ `- 제목: ${entry.heading.stack} ${entry.heading.weights.join('/')}, ls ${entry.heading.letterSpacing}, lh ${entry.heading.lineHeight} / 본문: ${entry.body.weights.join('/')}, lh ${entry.body.lineHeight}`,
364
+ `- 라이선스: ${[...new Set(entry.fonts.map(f => f.license.type))].join(', ')} / 셀프호스팅 woff2: assets/fonts/${entry.id}/${entry.fonts.some(f => f.sha256 === '입수 대기') ? ' (입수 대기)' : ''}`,
365
+ '- 적용: brand.json font.family.heading(차등 시) → npm run build:tokens'
366
+ ]
367
+ for (const caution of entry.cautions) {
368
+ lines.push(`- [${caution.severity}] ${caution.text}`)
369
+ }
370
+ return lines
371
+ }
372
+
373
+ function paletteLines(palette) {
374
+ const rep = presetRepresentative(palette.id)
375
+ const lines = [
376
+ `### ${palette.id} — ${palette.label}`,
377
+ `- 대표색: primary 50 ${rep.primary} / secondary 50 ${rep.secondary} / point 50 ${rep.point}`,
378
+ `- 적용: cp tokens/presets/${palette.id}.json tokens/brand.json → npm run build:tokens → npm run check:contrast`
379
+ ]
380
+ if (rep.pointException === 'krds-heritage') {
381
+ lines.push('- [예외] point 크림슨은 krds-heritage 승계 — 위험 액션 버튼 사용 금지, danger 알림 인접 40px 내 배치 금지.')
382
+ }
383
+ return lines
384
+ }
385
+
386
+ function getArtDirection({ profile, expression } = {}) {
387
+ const levels = profileSpec.expressionLevels
388
+
389
+ if (expression && !levels[expression]) {
390
+ return notFound(`표현 등급 "${expression}"`, Object.keys(levels))
391
+ }
392
+
393
+ if (!profile) {
394
+ const lines = ['# 아트 디렉션 — 전체 색인', '', '프로필을 주면 후보를 걸러 상세로 답한다 — get_art_direction(profile).', '']
395
+ lines.push('## 표현 등급', '')
396
+ for (const [id, level] of Object.entries(levels)) {
397
+ lines.push(`- **${id}** (${level.label}) — ${level.definition}`)
398
+ }
399
+ lines.push('', '프로필 기본값: ' + profileSpec.profiles.map(p => `${p.id}=${p.expression}`).join(', '))
400
+ lines.push('', '## 한글 조판 공통값', '', ...hangulLines())
401
+ lines.push('', '## 타이포 카탈로그', '')
402
+ for (const entry of artDirection.typography) {
403
+ lines.push(`- **${entry.id}** — ${entry.label} (${entry.mood.join('·')}) · 등급 ${entry.expression.join(', ')} · 프로필 ${entry.profiles.join(', ')}`)
404
+ }
405
+ lines.push('', '## 팔레트 프리셋', '')
406
+ for (const palette of artDirection.palettes) {
407
+ const rep = presetRepresentative(palette.id)
408
+ lines.push(`- **${palette.id}** — ${palette.label} · primary 50 ${rep.primary} · 등급 ${palette.expression.join(', ')} · 프로필 ${palette.profiles.join(', ')}`)
409
+ }
410
+ lines.push('', '## 안티패턴 색인', '', ...antiPatternLines())
411
+ return text(lines.join('\n'))
412
+ }
413
+
414
+ const p = profileSpec.profiles.find(item => item.id === profile)
415
+ if (!p) return notFound(`사이트 유형 "${profile}"`, profileSpec.profiles.map(item => item.id))
416
+
417
+ const effective = expression || p.expression
418
+ const level = levels[effective]
419
+ const map = artDirection.profiles[profile]
420
+ const overridden = effective !== p.expression
421
+
422
+ const lines = [`# 아트 디렉션 — ${p.label}`, '']
423
+ lines.push(`## 표현 등급: ${effective} (${level.label})${overridden ? ` — 기본 ${p.expression}에서 덮어씀` : ''}`)
424
+ if (overridden) {
425
+ lines.push('- 덮어쓰기는 Task Contract의 expression 필드와 근거 기록이 전제다. publicIdentity가 required면 상향은 무효(상한 restrained).')
426
+ }
427
+ const signature = level.signature.maxCount === 0
428
+ ? '없음'
429
+ : `최대 ${level.signature.maxCount} — ${level.signature.types.join(', ')}`
430
+ lines.push(`- 모션 예산: ${level.motion.effects.join(', ')} · ${level.motion.durationMs[0]}~${level.motion.durationMs[1]}ms. prefers-reduced-motion 가드 필수(R-22). ${level.motion.note}`)
431
+ lines.push(`- 시그니처 요소: ${signature} / hero: ${level.hero.join('·')} / 제목 폰트: ${level.displayFont} / 레이아웃: ${level.layout}`)
432
+ lines.push(`- ${level.note}`)
433
+ lines.push('- 표현은 trade-off 서열 최하위다 — 접근성·과업 완수와 충돌하면 양보한다. 등급은 상한이지 목표가 아니다.')
434
+ lines.push('- 결제·인증·폼 페이지의 task contract는 utility로 강등해 작성한다.')
435
+
436
+ if (map) {
437
+ lines.push('', '## 리듬·카피 톤', '', `- 섹션 리듬: ${map.rhythm}`, `- 카피 톤: ${map.copyTone}`)
438
+ }
439
+
440
+ lines.push('', '## 한글 조판 공통값', '', ...hangulLines())
441
+
442
+ const typography = artDirection.typography.filter(
443
+ entry => entry.profiles.includes(profile) && entry.expression.includes(effective)
444
+ )
445
+ lines.push('', `## 타이포 후보 (${typography.length})`, '')
446
+ for (const entry of typography) {
447
+ lines.push(...typographyLines(entry), '')
448
+ }
449
+
450
+ const palettes = artDirection.palettes.filter(
451
+ palette => palette.profiles.includes(profile) && palette.expression.includes(effective)
452
+ )
453
+ lines.push(`## 팔레트 프리셋 후보 (${palettes.length})`, '')
454
+ for (const palette of palettes) {
455
+ lines.push(...paletteLines(palette))
456
+ }
457
+ lines.push('- 전 프리셋은 대비 검사 전량 통과 상태로 입고된다 — 위반 프리셋은 존재하지 않는다.')
458
+
459
+ lines.push('', '## 안티패턴 색인', '', ...antiPatternLines())
460
+ lines.push(
461
+ '',
462
+ '## 이 도구가 다루지 않는 것',
463
+ '',
464
+ `- section 흐름·우선 컴포넌트·밀도 → get_profile("${profile}")`,
465
+ '- 상태 셋 명세 → ui-states / 모션 수치 → interaction-timing·R-22 / 카피 문장 공식 → microcopy'
466
+ )
467
+ return text(lines.join('\n'))
468
+ }
469
+
238
470
  function searchDocs(query) {
239
471
  const needle = query.toLowerCase()
240
472
  const hits = []
@@ -309,6 +541,10 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
309
541
  return getReference(args.name)
310
542
  case 'get_workflow':
311
543
  return getWorkflow(args.name)
544
+ case 'get_profile':
545
+ return getProfile(args.name)
546
+ case 'get_art_direction':
547
+ return getArtDirection(args)
312
548
  case 'search_docs':
313
549
  return searchDocs(args.query)
314
550
  default: