@infomind-ux/infoux-mcp 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,10 +9,10 @@ INFOMIND UX팀의 퍼블리싱 기준(infoUX)을 MCP로 제공한다. 팀원이
9
9
  | 도구 | 용도 |
10
10
  |---|---|
11
11
  | `get_contract` | infoUX 작업 컨트랙트 전문. UI 작업 시작 전에 한 번 읽는다 |
12
- | `list_components` | 컴포넌트 카탈로그 29종 |
12
+ | `list_components` | 컴포넌트 카탈로그 (KRDS 28종 + infoUX 확장 — 푸터·모바일 메뉴·공지 띠·오류 페이지 등) |
13
13
  | `get_component` | 컴포넌트 마크업 스니펫 + 접근성 요건 |
14
14
  | `get_tokens` | 색상·폰트·브레이크포인트 토큰. `query`로 필터, `raw`로 tokens.css 원본 |
15
- | `get_rules` | 코딩 규칙 R-01~R-22 (위반·준수 예시 포함) |
15
+ | `get_rules` | 코딩 규칙 R-01~R-27 (위반·준수 예시 포함) |
16
16
  | `get_reference` | 접근성·금지패턴·Tailwind 매핑·HTML 시맨틱·사이트 유형 프로필 |
17
17
  | `get_profile` | 사이트 유형 프리셋 — section 흐름·우선 컴포넌트·밀도·표현 등급 |
18
18
  | `get_workflow` | 작업 절차 — 페이지·폼·위젯 설계, 컴포넌트 생성, 토큰 변경, UI 리뷰, 프로젝트 초기화 |
package/bin/server.js CHANGED
@@ -48,9 +48,13 @@ const INSTRUCTIONS = `INFOMIND UX팀의 HTML/CSS 퍼블리싱 기준(infoUX)을
48
48
  3. 컴포넌트는 카탈로그를 먼저 본다. list_components → get_component 순으로 확인하고
49
49
  기존 스니펫을 조합한다. 카탈로그 밖 컴포넌트는 임의 생성하지 않는다.
50
50
  페이지·폼·위젯 설계나 컴포넌트 신규 생성처럼 절차가 정해진 작업은 get_workflow를 먼저 읽는다.
51
- 4. 규칙 R-01~R-22를 지킨다. get_rules로 확인한다. BEM, 접근성, 금지 패턴이 여기 있다.
52
- 5. 간격·크기·타이포 스케일·반경·모션은 토큰이 아니라 CSS/Tailwind 직접값으로 쓴다.
53
- 6. 원칙이 충돌하면 get_reference("trade-off-rules")의 우선순위를 따른다. 접근성이 1순위다.
51
+ 4. 아이콘도 카탈로그에서 가져온다. list_icons → get_icon 순으로 확인한다.
52
+ **아이콘 이름을 지어내지 않는다** — 목록에 없는 이름을 쓰면 화면에 아무것도 안 나온다.
53
+ 필요한 아이콘이 없으면 UX팀에 요청한다 (R-27). 장식용은 aria-hidden, 의미를 담으면
54
+ role="img"+aria-label을 붙인다.
55
+ 5. 규칙 R-01~R-27을 지킨다. get_rules로 확인한다. BEM, 접근성, 금지 패턴이 여기 있다.
56
+ 6. 간격·크기·타이포 스케일·반경·모션은 토큰이 아니라 CSS/Tailwind 직접값으로 쓴다.
57
+ 7. 원칙이 충돌하면 get_reference("trade-off-rules")의 우선순위를 따른다. 접근성이 1순위다.
54
58
  화면을 마무리했으면 get_reference("release-checklist")로 점검한다 — 접근성·과업 흐름·품질은
55
59
  100% 통과가 조건이다.
56
60
 
@@ -87,6 +91,31 @@ const TOOLS = [
87
91
  required: ['name']
88
92
  }
89
93
  },
94
+ {
95
+ name: 'list_icons',
96
+ description:
97
+ '쓸 수 있는 아이콘 목록을 반환한다. 아이콘이 필요하면 **반드시 먼저 확인한다** — ' +
98
+ '목록에 없는 이름을 지어내면 화면에 아무것도 안 나온다. query로 걸러 낼 수 있다.',
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ query: { type: 'string', description: '이름·분류 필터 (예: arrow, 폼, calendar)' }
103
+ }
104
+ }
105
+ },
106
+ {
107
+ name: 'get_icon',
108
+ description:
109
+ '아이콘 하나의 마크업과 접근성 요건을 반환한다. 장식용인지 의미를 담는지에 따라 ' +
110
+ 'aria 처리가 달라지므로 그대로 복사해 쓴다.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ name: { type: 'string', description: '아이콘 이름 (예: search, chevron-right, calendar)' }
115
+ },
116
+ required: ['name']
117
+ }
118
+ },
90
119
  {
91
120
  name: 'get_tokens',
92
121
  description:
@@ -103,7 +132,7 @@ const TOOLS = [
103
132
  {
104
133
  name: 'get_rules',
105
134
  description:
106
- 'infoUX 코딩 규칙 R-01~R-22를 반환한다. CSS·BEM·HTML·접근성 규칙과 위반 예시가 들어 있다.',
135
+ 'infoUX 코딩 규칙 R-01~R-27을 반환한다. CSS·BEM·HTML·접근성 규칙과 위반 예시가 들어 있다.',
107
136
  inputSchema: {
108
137
  type: 'object',
109
138
  properties: {
@@ -205,6 +234,161 @@ function getComponent(name) {
205
234
  return text(readData('snippets', entry.file))
206
235
  }
207
236
 
237
+ function loadIconLedger() {
238
+ try {
239
+ return JSON.parse(readData('icons.json'))
240
+ } catch {
241
+ return null
242
+ }
243
+ }
244
+
245
+ function listIcons({ query } = {}) {
246
+ const ledger = loadIconLedger()
247
+ if (!ledger) return text('아이콘 카탈로그가 이 번들에 없다. npm run build:mcp로 다시 만든다.')
248
+
249
+ const all = Object.entries(ledger.icons).map(([name, meta]) => ({ name, ...meta }))
250
+ const needle = query ? String(query).toLowerCase() : null
251
+ // 한국어로도 찾는다 — 「달력」·「즐겨찾기」·「찾아오는길」. 이름이 영어라
252
+ // 이 다리가 없으면 AI가 「없다」고 판단해 이름을 지어낸다 (R-27)
253
+ const rows = needle
254
+ ? all.filter(i =>
255
+ i.name.includes(needle) ||
256
+ (i.category || '').includes(needle) ||
257
+ (i.label || '').includes(needle) ||
258
+ (i.keywords || []).some(k => String(k).toLowerCase().includes(needle))
259
+ )
260
+ : all
261
+
262
+ if (rows.length === 0) {
263
+ return text(
264
+ `"${query}"에 해당하는 아이콘이 없다. **이름을 지어내지 말고** list_icons()로 전체 목록을 확인한다.\n` +
265
+ '필요한 아이콘이 카탈로그에 없으면 UX팀에 요청한다 (R-27).'
266
+ )
267
+ }
268
+
269
+ const byCat = new Map()
270
+ for (const r of rows) {
271
+ const c = r.category || '기타'
272
+ if (!byCat.has(c)) byCat.set(c, [])
273
+ byCat.get(c).push(r.name)
274
+ }
275
+
276
+ const withFill = all.filter((i) => (i.variants || []).includes('fill')).length
277
+ const lines = ['# infoUX 아이콘 카탈로그', '']
278
+ lines.push(query ? `"${query}" 검색 — ${rows.length}종` : `총 ${rows.length}종.`, '')
279
+ if (needle) {
280
+ // 이름에 없는 말로 걸린 것은 왜 걸렸는지 밝힌다 — 엉뚱한 결과처럼 보이지 않게
281
+ const via = rows
282
+ .filter(i => !i.name.includes(needle))
283
+ .map(i => `${i.name}(${(i.keywords || []).find(k => String(k).toLowerCase().includes(needle))})`)
284
+ if (via.length > 0) lines.push(`검색어로 걸린 것 — ${via.join(', ')}`, '')
285
+ }
286
+ for (const [cat, names] of byCat) {
287
+ lines.push(`- **${cat}** — ${names.join(', ')}`)
288
+ }
289
+ lines.push(
290
+ '',
291
+ `표정은 슬림 · 레귤러(기본) · 볼드 · 필 네 가지다. 슬림·볼드는 전부 있고, 필은 ${withFill}종에만 있다 — ` +
292
+ '채울 면이 없는 형태(돋보기·화살표 등)에는 만들지 않는다. **어느 아이콘에 무엇이 있는지는 get_icon이 알려 준다.**',
293
+ '',
294
+ 'get_icon(name)으로 마크업을 가져온다.',
295
+ '**목록에 없는 이름을 쓰지 않는다** — 화면에 아무것도 안 나온다. 필요하면 UX팀에 요청한다 (R-27).'
296
+ )
297
+ return text(lines.join('\n'))
298
+ }
299
+
300
+ /**
301
+ * 이 아이콘이 가진 표정만 알려 준다.
302
+ *
303
+ * 전부 나열하면 AI가 없는 표정을 골라 쓴다 — 화면에는 빈 네모가 나온다.
304
+ * 대장(icon-codepoints.json)의 variants가 정본이다.
305
+ */
306
+ function iconVariantSection(name, meta) {
307
+ const has = Array.isArray(meta.variants) ? meta.variants : []
308
+ const label = { slim: '슬림(가늘게)', bold: '볼드(강조)', fill: '필(선택·활성)' }
309
+
310
+ if (has.length === 0) {
311
+ return [
312
+ '## 표정',
313
+ '',
314
+ `이 아이콘은 기본(레귤러) 하나뿐이다. \`icon-font--slim\` 같은 표정 클래스를 붙이면 빈 네모가 나온다.`,
315
+ ''
316
+ ]
317
+ }
318
+
319
+ return [
320
+ '## 표정',
321
+ '',
322
+ `이 아이콘이 가진 표정 — 레귤러(기본) · ${has.map((v) => label[v] || v).join(' · ')}`,
323
+ '',
324
+ '```html',
325
+ ...has.map((v) => `<span class="icon-font icon-font--${v} icon-font--${name}" aria-hidden="true"></span>`),
326
+ ...has.map((v) => `<svg class="icon" aria-hidden="true"><use href="/assets/icons/sprite-${v}.svg#${name}"></use></svg>`),
327
+ '```',
328
+ '',
329
+ '폰트는 클래스가, SVG는 스프라이트 파일이 표정을 정한다 — SVG에 `icon--bold` 같은',
330
+ '클래스를 붙이지 않는다(아무 일도 하지 않는다). **여기 없는 표정은 쓰지 않는다.**',
331
+ '한 화면에서 표정을 섞지 않는다 — 굵기가 뒤섞이면 중요도가 다른 것처럼 읽힌다.',
332
+ ''
333
+ ]
334
+ }
335
+
336
+ function getIcon(name) {
337
+ const ledger = loadIconLedger()
338
+ if (!ledger) return text('아이콘 카탈로그가 이 번들에 없다. npm run build:mcp로 다시 만든다.')
339
+
340
+ const meta = ledger.icons[name]
341
+ if (!meta) {
342
+ const near = Object.keys(ledger.icons).filter(n => n.includes(String(name).split('-')[0])).slice(0, 8)
343
+ return notFound(`아이콘 "${name}"`, near.length > 0 ? near : Object.keys(ledger.icons).slice(0, 12))
344
+ }
345
+
346
+ const lines = [
347
+ `# ${name}`,
348
+ '',
349
+ `분류 ${meta.category} · 출처 ${meta.source} · 코드포인트 ${meta.codepoint}`,
350
+ ...(Array.isArray(meta.keywords) && meta.keywords.length > 0
351
+ ? ['', `이 아이콘을 부르는 말 — ${meta.keywords.join(' · ')}`]
352
+ : []),
353
+ '',
354
+ '## 붙여 넣을 코드 (기본 — 폰트)',
355
+ '',
356
+ '```html',
357
+ `<span class="icon-font icon-font--${name}" aria-hidden="true"></span>`,
358
+ '```',
359
+ '',
360
+ '아이콘에는 항상 aria-hidden을 붙인다. 뜻은 옆의 텍스트나 버튼의 aria-label이 전한다 —',
361
+ '폰트 아이콘은 스크린리더가 PUA 코드포인트를 엉뚱하게 읽는다.',
362
+ '',
363
+ '```html',
364
+ `<button class="btn"><span class="icon-font icon-font--${name}" aria-hidden="true"></span>검색</button>`,
365
+ `<button class="btn btn--text" aria-label="설명"><span class="icon-font icon-font--${name}" aria-hidden="true"></span></button>`,
366
+ '```',
367
+ '',
368
+ '## SVG 태그로 넣을 때',
369
+ '',
370
+ '폰트를 못 쓰는 곳이나 아이콘 하나만 색을 달리해야 할 때만 쓴다.',
371
+ '',
372
+ '```html',
373
+ `<svg class="icon" aria-hidden="true"><use href="/assets/icons/sprite.svg#${name}"></use></svg>`,
374
+ '```',
375
+ '',
376
+ ...iconVariantSection(name, meta),
377
+ '## 크기',
378
+ '',
379
+ '`.icon-font`(24) · `--xsmall`(16) · `--small`(20) · `--large`(32) · `--inherit`(글자 크기)',
380
+ 'SVG 방식은 `.icon` + 같은 어휘를 쓴다.',
381
+ '',
382
+ '## 지킬 것',
383
+ '',
384
+ '- 색을 아이콘에 넣지 않는다 — `fill: currentColor`가 부모 `color`를 따라간다 (R-01)',
385
+ '- 아이콘만 있는 버튼은 버튼에도 `aria-label`을 준다',
386
+ '- 클릭 영역은 아이콘 크기가 아니라 44×44px 이상 (R-13)',
387
+ '- 폰트로 쓸 때는 `.icon-font .icon-font--' + name + '` + `aria-hidden` + 텍스트 라벨 (여벌 경로)'
388
+ ]
389
+ return text(lines.join('\n'))
390
+ }
391
+
208
392
  function getTokens({ query, raw } = {}) {
209
393
  if (raw) return text(readData('tokens.css'))
210
394
 
@@ -533,6 +717,10 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
533
717
  return listComponents()
534
718
  case 'get_component':
535
719
  return getComponent(args.name)
720
+ case 'list_icons':
721
+ return listIcons(args)
722
+ case 'get_icon':
723
+ return getIcon(args.name)
536
724
  case 'get_tokens':
537
725
  return getTokens(args)
538
726
  case 'get_rules':