@axis-ds/mcp 1.1.5 → 1.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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/dist/index.js +651 -31
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -25,6 +25,19 @@ Claude Desktop 또는 기타 MCP 호환 클라이언트에서 설정:
25
25
  }
26
26
  ```
27
27
 
28
+ ## 브랜드 도구
29
+
30
+ 대상 프로젝트에 설치된 `@axis-ds/tokens`(1.3.0 이상)의 브랜드를 읽습니다. 패키지에 데이터를 번들하지 않으며, CLI `axis theme list` · `axis theme info` 와 같은 내용을 돌려줍니다.
31
+
32
+ | 도구 | 인자 | 응답 |
33
+ |------|------|------|
34
+ | `axis.list_brands` | `targetDir?` | 브랜드 이름 · 설명 · 테마 · import 경로 |
35
+ | `axis.get_brand` | `name`, `targetDir?` | import 경로 · Provider 예시(`defaultBrand`) · 주요 토큰(라이트 · 다크) · 보정 내역(a11y 메타) |
36
+
37
+ - `targetDir` 를 주지 않으면 서버의 현재 디렉터리를 봅니다.
38
+ - `@axis-ds/tokens` 가 없거나 1.3.0 미만이거나 빌드 산출물이 없으면 오류(`isError`)가 아니라 설치 · 업그레이드 안내를 돌려줍니다. 빈 목록으로 답하지 않습니다.
39
+ - 프로젝트에 적용하는 일(import 추가 · `axis.config.json` 의 `brand`)은 CLI `axis theme add <brand>` 가 맡습니다.
40
+
28
41
  ## 문서
29
42
 
30
43
  자세한 사용법은 [AXIS Design System 문서](https://axis.minu.best)를 참고하세요.
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { createRequire as createRequire2 } from "module";
4
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
7
  import { z } from "zod";
@@ -64,10 +65,10 @@ function listTokens(category) {
64
65
  }
65
66
  return registry.tokens;
66
67
  }
67
- function getToken(path4) {
68
+ function getToken(path5) {
68
69
  const registry = getRegistry();
69
70
  return registry.tokens.find(
70
- (t) => t.path.toLowerCase() === path4.toLowerCase()
71
+ (t) => t.path.toLowerCase() === path5.toLowerCase()
71
72
  );
72
73
  }
73
74
  function getComponentStats() {
@@ -151,11 +152,11 @@ function handleGetComponent(params) {
151
152
  return component;
152
153
  }
153
154
  function handleGetToken(params) {
154
- const { path: path4 } = params;
155
- if (!path4 || path4.trim() === "") {
155
+ const { path: path5 } = params;
156
+ if (!path5 || path5.trim() === "") {
156
157
  throw new Error("\uD1A0\uD070 \uACBD\uB85C(path)\uB294 \uD544\uC218\uC785\uB2C8\uB2E4.");
157
158
  }
158
- const token = getToken(path4);
159
+ const token = getToken(path5);
159
160
  if (!token) {
160
161
  return null;
161
162
  }
@@ -2068,6 +2069,423 @@ function formatSaveResult(result) {
2068
2069
  import fetch from "node-fetch";
2069
2070
  import fs3 from "fs-extra";
2070
2071
  import path3 from "path";
2072
+
2073
+ // src/tools/brand-core.ts
2074
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync } from "fs";
2075
+ import { createRequire } from "module";
2076
+ import { dirname as dirname4, join as join4, relative, resolve as resolve2, sep } from "path";
2077
+ var TOKENS_PACKAGE = "@axis-ds/tokens";
2078
+ var MIN_BRAND_TOKENS_VERSION = "1.3.0";
2079
+ var KEY_TOKENS = [
2080
+ "--primary",
2081
+ "--primary-foreground",
2082
+ "--background",
2083
+ "--foreground",
2084
+ "--ring",
2085
+ "--radius",
2086
+ "--axis-surface-default",
2087
+ "--axis-text-primary",
2088
+ "--axis-text-brand",
2089
+ "--axis-button-bg-default",
2090
+ "--axis-border-focus",
2091
+ "--axis-font-family-sans"
2092
+ ];
2093
+ var BRAND_ADJUSTMENT_REFERENCE = "WI-0025 PRD \uBD80\uB85D B (docs/workitems/WI-0025-brand-theme/prd.md)";
2094
+ function readJsonFile(file) {
2095
+ return JSON.parse(readFileSync5(file, "utf8"));
2096
+ }
2097
+ function errorCode(err) {
2098
+ return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
2099
+ }
2100
+ function findTokensPackageJson(baseDir) {
2101
+ let dir = resolve2(baseDir);
2102
+ for (; ; ) {
2103
+ const candidate = join4(dir, "node_modules", TOKENS_PACKAGE, "package.json");
2104
+ if (existsSync4(candidate)) return candidate;
2105
+ const parent = dirname4(dir);
2106
+ if (parent === dir) return void 0;
2107
+ dir = parent;
2108
+ }
2109
+ }
2110
+ function projectRequire(baseDir) {
2111
+ return createRequire(join4(resolve2(baseDir), "package.json"));
2112
+ }
2113
+ function tokensSpecifier(subpath) {
2114
+ return `${TOKENS_PACKAGE}/${subpath.replace(/^\.\//, "")}`;
2115
+ }
2116
+ function resolveTokens(baseDir) {
2117
+ const pkgJson = findTokensPackageJson(baseDir);
2118
+ if (!pkgJson) return { status: "not-installed" };
2119
+ const pkg = readJsonFile(pkgJson);
2120
+ const version = typeof pkg.version === "string" ? pkg.version : void 0;
2121
+ try {
2122
+ const indexPath = projectRequire(baseDir).resolve(tokensSpecifier("./brands"));
2123
+ return { status: "ok", version, indexPath };
2124
+ } catch (err) {
2125
+ const code = errorCode(err);
2126
+ if (code === "ERR_PACKAGE_PATH_NOT_EXPORTED") return { status: "unsupported", version };
2127
+ if (code === "MODULE_NOT_FOUND") return { status: "not-built", version };
2128
+ throw err;
2129
+ }
2130
+ }
2131
+ function brandImportPath(entry) {
2132
+ return tokensSpecifier(entry.css);
2133
+ }
2134
+ var GLOBAL_CSS_CANDIDATES = ["app/globals.css", "src/app/globals.css", "globals.css"];
2135
+ function isFile(file) {
2136
+ try {
2137
+ return statSync(file).isFile();
2138
+ } catch {
2139
+ return false;
2140
+ }
2141
+ }
2142
+ function findGlobalCss(baseDir, config) {
2143
+ const root = resolve2(baseDir);
2144
+ const result = {};
2145
+ const configured = config?.globalCss;
2146
+ if (typeof configured === "string" && configured.trim() !== "") {
2147
+ const file = resolve2(root, configured);
2148
+ if (isFile(file)) return { path: file };
2149
+ result.configuredMissing = configured;
2150
+ }
2151
+ for (const candidate of GLOBAL_CSS_CANDIDATES) {
2152
+ const file = join4(root, candidate);
2153
+ if (isFile(file)) {
2154
+ result.path = file;
2155
+ break;
2156
+ }
2157
+ }
2158
+ return result;
2159
+ }
2160
+ function findBrandImports(css) {
2161
+ const names = [];
2162
+ const pattern = /@import\s+(?:url\(\s*)?["']?@axis-ds\/tokens\/css\/brands\/([a-z][a-z0-9-]*)(?:\.css)?["']?\s*\)?\s*(?=[;\s]|$)/g;
2163
+ for (const m of stripComments(css).matchAll(pattern)) {
2164
+ names.push(m[1]);
2165
+ }
2166
+ return names;
2167
+ }
2168
+ var AXIS_CONFIG_FILE_NAME = "axis.config.json";
2169
+ function readBrandConfig(baseDir) {
2170
+ let parsed;
2171
+ try {
2172
+ parsed = JSON.parse(readFileSync5(join4(resolve2(baseDir), AXIS_CONFIG_FILE_NAME), "utf8"));
2173
+ } catch {
2174
+ return null;
2175
+ }
2176
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
2177
+ return parsed;
2178
+ }
2179
+ function inspectBrandImport(baseDir) {
2180
+ const config = readBrandConfig(baseDir);
2181
+ const brand = config?.brand;
2182
+ if (typeof brand !== "string" || brand.trim() === "") return null;
2183
+ const root = resolve2(baseDir);
2184
+ const location = findGlobalCss(root, config);
2185
+ const base = { brand };
2186
+ if (location.configuredMissing !== void 0) base.configuredMissing = location.configuredMissing;
2187
+ if (!location.path) {
2188
+ return { ...base, state: "css-missing", imports: [] };
2189
+ }
2190
+ const cssFile = relative(root, location.path).split(sep).join("/");
2191
+ const imports = [...new Set(findBrandImports(readFileSync5(location.path, "utf8")))];
2192
+ let state;
2193
+ if (imports.includes(brand)) state = "match";
2194
+ else if (imports.length === 0) state = "import-missing";
2195
+ else state = "other-brand";
2196
+ return { ...base, state, cssFile, imports };
2197
+ }
2198
+ function isMissingBuildFile(err) {
2199
+ const code = errorCode(err);
2200
+ return code === "MODULE_NOT_FOUND" || code === "ENOENT";
2201
+ }
2202
+ function readBrandIndex(indexPath) {
2203
+ const data = readJsonFile(indexPath);
2204
+ if (!Array.isArray(data.brands)) {
2205
+ throw new Error(`${indexPath}: "brands" \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4`);
2206
+ }
2207
+ return data.brands;
2208
+ }
2209
+ function readSummary(baseDir, entry) {
2210
+ const metaPath = projectRequire(baseDir).resolve(tokensSpecifier(entry.meta));
2211
+ const meta = readJsonFile(metaPath);
2212
+ return {
2213
+ name: entry.name,
2214
+ description: typeof meta.description === "string" ? meta.description : "",
2215
+ themes: Array.isArray(meta.themes) ? meta.themes.map(String) : [],
2216
+ a11y: typeof meta.a11y === "object" && meta.a11y !== null ? meta.a11y : {},
2217
+ importPath: brandImportPath(entry)
2218
+ };
2219
+ }
2220
+ function listBrands(baseDir) {
2221
+ const tokens = resolveTokens(baseDir);
2222
+ if (tokens.status !== "ok" || !tokens.indexPath) {
2223
+ return { status: tokens.status, version: tokens.version, brands: [] };
2224
+ }
2225
+ const entries = readBrandIndex(tokens.indexPath);
2226
+ let brands;
2227
+ try {
2228
+ brands = entries.map((entry) => readSummary(baseDir, entry));
2229
+ } catch (err) {
2230
+ if (isMissingBuildFile(err)) return { status: "not-built", version: tokens.version, brands: [] };
2231
+ throw err;
2232
+ }
2233
+ return { status: "ok", version: tokens.version, brands };
2234
+ }
2235
+ function stripComments(css) {
2236
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
2237
+ }
2238
+ function parseDeclarations(body) {
2239
+ const out = {};
2240
+ for (const m of body.matchAll(/(--[A-Za-z0-9_-]+)\s*:\s*([^;]+);/g)) {
2241
+ out[m[1]] = m[2].trim();
2242
+ }
2243
+ return out;
2244
+ }
2245
+ function isDarkSelector(selector) {
2246
+ return /\.dark\b/.test(selector) || /\[data-theme=["']?dark["']?\]/.test(selector);
2247
+ }
2248
+ function parseBrandCss(css, name) {
2249
+ const result = { light: {}, dark: {} };
2250
+ let lightFound = false;
2251
+ let darkFound = false;
2252
+ const brandAttr = new RegExp(`\\[data-brand=["']?${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']?\\]`);
2253
+ for (const m of stripComments(css).matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
2254
+ const selectors = m[1].split(",").map((s) => s.trim());
2255
+ if (!selectors.some((s) => brandAttr.test(s))) continue;
2256
+ if (selectors.some(isDarkSelector)) {
2257
+ if (!darkFound) {
2258
+ result.dark = parseDeclarations(m[2]);
2259
+ darkFound = true;
2260
+ }
2261
+ } else if (!lightFound) {
2262
+ result.light = parseDeclarations(m[2]);
2263
+ lightFound = true;
2264
+ }
2265
+ }
2266
+ return result;
2267
+ }
2268
+ function pickKeyTokens(decls) {
2269
+ const out = {};
2270
+ for (const key of KEY_TOKENS) {
2271
+ if (Object.prototype.hasOwnProperty.call(decls, key)) out[key] = decls[key];
2272
+ }
2273
+ return out;
2274
+ }
2275
+ function providerExample(name) {
2276
+ return [
2277
+ 'import { ThemeProvider } from "@axis-ds/theme";',
2278
+ "",
2279
+ `<ThemeProvider defaultBrand="${name}">{children}</ThemeProvider>`
2280
+ ].join("\n");
2281
+ }
2282
+ function getBrand(baseDir, name) {
2283
+ const tokens = resolveTokens(baseDir);
2284
+ if (tokens.status !== "ok" || !tokens.indexPath) {
2285
+ return { status: tokens.status, version: tokens.version, brand: null, available: [] };
2286
+ }
2287
+ const entries = readBrandIndex(tokens.indexPath);
2288
+ const available = entries.map((e) => e.name);
2289
+ const entry = entries.find((e) => e.name === name);
2290
+ if (!entry) {
2291
+ return { status: "ok", version: tokens.version, brand: null, available };
2292
+ }
2293
+ let summary;
2294
+ let parsed;
2295
+ try {
2296
+ summary = readSummary(baseDir, entry);
2297
+ const cssPath = projectRequire(baseDir).resolve(summary.importPath);
2298
+ parsed = parseBrandCss(readFileSync5(cssPath, "utf8"), name);
2299
+ } catch (err) {
2300
+ if (isMissingBuildFile(err)) return { status: "not-built", version: tokens.version, brand: null, available: [] };
2301
+ throw err;
2302
+ }
2303
+ return {
2304
+ status: "ok",
2305
+ version: tokens.version,
2306
+ available,
2307
+ brand: {
2308
+ ...summary,
2309
+ tokens: { light: pickKeyTokens(parsed.light), dark: pickKeyTokens(parsed.dark) },
2310
+ providerExample: providerExample(name),
2311
+ adjustments: { a11y: summary.a11y, reference: BRAND_ADJUSTMENT_REFERENCE }
2312
+ }
2313
+ };
2314
+ }
2315
+
2316
+ // src/tools/tailwind-core.ts
2317
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2318
+ import { join as join5, resolve as resolve3 } from "path";
2319
+ function hasDependency(pkg, name) {
2320
+ for (const field of ["dependencies", "devDependencies"]) {
2321
+ const deps = pkg[field];
2322
+ if (typeof deps === "object" && deps !== null && name in deps) {
2323
+ return String(deps[name]);
2324
+ }
2325
+ }
2326
+ return void 0;
2327
+ }
2328
+ function isPlainObject(value) {
2329
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2330
+ }
2331
+ var VERSION = String.raw`v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?`;
2332
+ function versionParts(token) {
2333
+ const m = new RegExp(`^${VERSION}$`).exec(token);
2334
+ if (!m) return void 0;
2335
+ if (/^v?0\.0\.0-/.test(token)) return void 0;
2336
+ return { major: Number(m[1]), minor: m[2], patch: m[3] };
2337
+ }
2338
+ var isWild = (part) => part === void 0 || /^[xX*]$/.test(part);
2339
+ function comparatorBounds(op, v) {
2340
+ switch (op) {
2341
+ case ">=":
2342
+ return { lower: v.major };
2343
+ case ">":
2344
+ return { lower: isWild(v.minor) ? v.major + 1 : v.major };
2345
+ case "<=":
2346
+ return { upper: v.major };
2347
+ case "<": {
2348
+ const zeroOrWild = (part) => isWild(part) || Number(part) === 0;
2349
+ return { upper: zeroOrWild(v.minor) && zeroOrWild(v.patch) ? v.major - 1 : v.major };
2350
+ }
2351
+ default:
2352
+ return { lower: v.major, upper: v.major };
2353
+ }
2354
+ }
2355
+ function tailwindMajorBounds(range) {
2356
+ const r = range.trim();
2357
+ if (r === "" || r.includes("||")) return void 0;
2358
+ const simple = new RegExp(`^(?:\\^|~|=)?\\s*(${VERSION})$`).exec(r);
2359
+ if (simple) {
2360
+ const v = versionParts(simple[1]);
2361
+ return v ? { lower: v.major, upper: v.major } : void 0;
2362
+ }
2363
+ const hyphen = new RegExp(`^(${VERSION})\\s+-\\s+(${VERSION})$`).exec(r);
2364
+ if (hyphen) {
2365
+ const tokens2 = r.split(/\s+-\s+/);
2366
+ const a = versionParts(tokens2[0]);
2367
+ const b = versionParts(tokens2[1]);
2368
+ return a && b && a.major <= b.major ? { lower: a.major, upper: b.major } : void 0;
2369
+ }
2370
+ const tokens = r.replace(/(>=|<=|>|<|=)\s+/g, "$1").split(/\s+/);
2371
+ const lowers = [];
2372
+ const uppers = [];
2373
+ for (const t of tokens) {
2374
+ const c = /^(>=|<=|>|<|=)(.+)$/.exec(t);
2375
+ if (!c) return void 0;
2376
+ const v = versionParts(c[2]);
2377
+ if (v === void 0) return void 0;
2378
+ const b = comparatorBounds(c[1], v);
2379
+ if (b.lower !== void 0) lowers.push(b.lower);
2380
+ if (b.upper !== void 0) uppers.push(b.upper);
2381
+ }
2382
+ if (lowers.length === 0) return void 0;
2383
+ const lower = Math.max(...lowers);
2384
+ const upper = uppers.length > 0 ? Math.min(...uppers) : void 0;
2385
+ if (upper !== void 0 && upper < lower) return void 0;
2386
+ return upper !== void 0 ? { lower, upper } : { lower };
2387
+ }
2388
+ function majorWithin(major, bounds) {
2389
+ return major >= bounds.lower && (bounds.upper === void 0 || major <= bounds.upper);
2390
+ }
2391
+ function majorFromVersion(version) {
2392
+ if (typeof version !== "string") return void 0;
2393
+ const v = version.trim();
2394
+ const m = /^v?(\d+)\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]*)?$/.exec(v);
2395
+ if (!m || /^v?0\.0\.0-/.test(v)) return void 0;
2396
+ return Number(m[1]);
2397
+ }
2398
+ function detectTailwind(baseDirInput, pkg) {
2399
+ const baseDir = resolve3(baseDirInput);
2400
+ const installedFile = join5(baseDir, "node_modules", "tailwindcss", "package.json");
2401
+ const declared = pkg ? hasDependency(pkg, "tailwindcss") : void 0;
2402
+ const bounds = declared !== void 0 ? tailwindMajorBounds(declared) : void 0;
2403
+ const declaredMajor = bounds?.lower;
2404
+ try {
2405
+ const installed = JSON.parse(readFileSync6(installedFile, "utf8"));
2406
+ const version = isPlainObject(installed) ? installed.version : void 0;
2407
+ const major = majorFromVersion(version);
2408
+ if (major !== void 0) {
2409
+ const found = { major, source: "installed", value: String(version) };
2410
+ if (declared !== void 0 && bounds !== void 0 && !majorWithin(major, bounds)) {
2411
+ found.mismatch = { declared, declaredMajor: bounds.lower };
2412
+ }
2413
+ return found;
2414
+ }
2415
+ } catch {
2416
+ }
2417
+ if (declared !== void 0) {
2418
+ if (declaredMajor !== void 0) return { major: declaredMajor, source: "declared", value: declared };
2419
+ return { source: "none", value: declared };
2420
+ }
2421
+ return { source: "none" };
2422
+ }
2423
+ var TAILWIND_CONFIG_FILES = [
2424
+ "tailwind.config.ts",
2425
+ "tailwind.config.js",
2426
+ "tailwind.config.mjs",
2427
+ "tailwind.config.cjs"
2428
+ ];
2429
+ var TAILWIND_CSS_CANDIDATES = [
2430
+ "globals.css",
2431
+ "app/globals.css",
2432
+ "src/app/globals.css",
2433
+ "src/globals.css",
2434
+ "styles/globals.css"
2435
+ ];
2436
+ var TAILWIND_IMPORT = /@import\s+(?:url\(\s*)?["']tailwindcss["']/;
2437
+ function hasTailwindImport(css) {
2438
+ return TAILWIND_IMPORT.test(css.replace(/\/\*[\s\S]*?\*\//g, ""));
2439
+ }
2440
+ function readPackageJson(baseDir) {
2441
+ try {
2442
+ const parsed = JSON.parse(readFileSync6(join5(baseDir, "package.json"), "utf8"));
2443
+ return isPlainObject(parsed) ? parsed : void 0;
2444
+ } catch {
2445
+ return void 0;
2446
+ }
2447
+ }
2448
+ function checkTailwindSetup(baseDirInput) {
2449
+ const baseDir = resolve3(baseDirInput);
2450
+ const tailwind = detectTailwind(baseDir, readPackageJson(baseDir));
2451
+ if (tailwind.major !== void 0 && tailwind.major >= 4) {
2452
+ const label = `Tailwind ${tailwind.major}`;
2453
+ for (const candidate of TAILWIND_CSS_CANDIDATES) {
2454
+ const file = join5(baseDir, candidate);
2455
+ if (!existsSync5(file)) continue;
2456
+ const found = hasTailwindImport(readFileSync6(file, "utf8"));
2457
+ return {
2458
+ name: "tailwind.config",
2459
+ severity: found ? "pass" : "warn",
2460
+ message: found ? `${label}: ${candidate}\uC5D0 @import "tailwindcss" \uAC00 \uC788\uC2B5\uB2C8\uB2E4` : `${label}: ${candidate}\uC5D0 @import "tailwindcss" \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4`
2461
+ };
2462
+ }
2463
+ return {
2464
+ name: "tailwind.config",
2465
+ severity: "warn",
2466
+ message: `${label}: @import "tailwindcss" \uB97C \uD655\uC778\uD560 CSS \uD30C\uC77C\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4`
2467
+ };
2468
+ }
2469
+ const [tsFile, ...otherFiles] = TAILWIND_CONFIG_FILES;
2470
+ const tsPath = join5(baseDir, tsFile);
2471
+ if (existsSync5(tsPath)) {
2472
+ const content = readFileSync6(tsPath, "utf8");
2473
+ const hasTokens = content.includes("--background") || content.includes("hsl(var(");
2474
+ return {
2475
+ name: "tailwind.config",
2476
+ severity: hasTokens ? "pass" : "warn",
2477
+ message: hasTokens ? "tailwind.config.ts\uC5D0 AXIS \uD1A0\uD070\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4" : "tailwind.config.ts\uC5D0 AXIS \uD1A0\uD070 \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4"
2478
+ };
2479
+ }
2480
+ for (const file of otherFiles) {
2481
+ if (existsSync5(join5(baseDir, file))) {
2482
+ return { name: "tailwind.config", severity: "warn", message: `${file} \uC874\uC7AC (TypeScript \uAD8C\uC7A5)` };
2483
+ }
2484
+ }
2485
+ return { name: "tailwind.config", severity: "fail", message: "tailwind.config \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4" };
2486
+ }
2487
+
2488
+ // src/tools/template.ts
2071
2489
  var TEMPLATE_REGISTRY_URL = process.env.AXIS_TEMPLATE_URL || "https://axis.minu.best/templates";
2072
2490
  async function fetchTemplateIndex() {
2073
2491
  const url = `${TEMPLATE_REGISTRY_URL}/index.json`;
@@ -2188,6 +2606,36 @@ async function handleDiffTemplate(params) {
2188
2606
  };
2189
2607
  return { templateName: detail.name, entries, summary };
2190
2608
  }
2609
+ function brandCheckResult(check) {
2610
+ const { brand } = check;
2611
+ const suffix = check.configuredMissing !== void 0 ? ` (globalCss ${check.configuredMissing} \uC5C6\uC74C)` : "";
2612
+ switch (check.state) {
2613
+ case "match":
2614
+ return {
2615
+ name: "brand",
2616
+ severity: "pass",
2617
+ message: `brand "${brand}" import \uAC00 ${check.cssFile}\uC5D0 \uC788\uC2B5\uB2C8\uB2E4${suffix}`
2618
+ };
2619
+ case "css-missing":
2620
+ return {
2621
+ name: "brand",
2622
+ severity: "warn",
2623
+ message: `brand "${brand}" \uAC00 \uC124\uC815\uB410\uC9C0\uB9CC CSS \uD30C\uC77C\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4${suffix}`
2624
+ };
2625
+ case "import-missing":
2626
+ return {
2627
+ name: "brand",
2628
+ severity: "warn",
2629
+ message: `brand "${brand}" import \uAC00 ${check.cssFile}\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4 \u2192 'axis theme add ${brand}'${suffix}`
2630
+ };
2631
+ case "other-brand":
2632
+ return {
2633
+ name: "brand",
2634
+ severity: "warn",
2635
+ message: `brand "${brand}" \uC640 import(${check.imports.join(", ")})\uAC00 \uB2E4\uB985\uB2C8\uB2E4${suffix}`
2636
+ };
2637
+ }
2638
+ }
2191
2639
  async function handleCheckProject(params) {
2192
2640
  const baseDir = path3.resolve(params.targetDir || ".");
2193
2641
  const results = [];
@@ -2197,29 +2645,7 @@ async function handleCheckProject(params) {
2197
2645
  severity: configExists ? "pass" : "fail",
2198
2646
  message: configExists ? "axis.config.json \uD30C\uC77C\uC774 \uC874\uC7AC\uD569\uB2C8\uB2E4" : "axis.config.json \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2192 'axis init'\uC744 \uC2E4\uD589\uD558\uC138\uC694"
2199
2647
  });
2200
- const twTs = path3.join(baseDir, "tailwind.config.ts");
2201
- const twJs = path3.join(baseDir, "tailwind.config.js");
2202
- if (await fs3.pathExists(twTs)) {
2203
- const content = await fs3.readFile(twTs, "utf-8");
2204
- const hasTokens = content.includes("--background") || content.includes("hsl(var(");
2205
- results.push({
2206
- name: "tailwind.config",
2207
- severity: hasTokens ? "pass" : "warn",
2208
- message: hasTokens ? "tailwind.config.ts\uC5D0 AXIS \uD1A0\uD070\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4" : "tailwind.config.ts\uC5D0 AXIS \uD1A0\uD070 \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4"
2209
- });
2210
- } else if (await fs3.pathExists(twJs)) {
2211
- results.push({
2212
- name: "tailwind.config",
2213
- severity: "warn",
2214
- message: "tailwind.config.js \uC874\uC7AC (TypeScript \uAD8C\uC7A5)"
2215
- });
2216
- } else {
2217
- results.push({
2218
- name: "tailwind.config",
2219
- severity: "fail",
2220
- message: "tailwind.config \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4"
2221
- });
2222
- }
2648
+ results.push(checkTailwindSetup(baseDir));
2223
2649
  const cssCandidates = [
2224
2650
  "globals.css",
2225
2651
  "app/globals.css",
@@ -2312,6 +2738,8 @@ async function handleCheckProject(params) {
2312
2738
  message: "package.json \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4"
2313
2739
  });
2314
2740
  }
2741
+ const brandCheck = inspectBrandImport(baseDir);
2742
+ if (brandCheck) results.push(brandCheckResult(brandCheck));
2315
2743
  const summary = {
2316
2744
  pass: results.filter((r) => r.severity === "pass").length,
2317
2745
  warn: results.filter((r) => r.severity === "warn").length,
@@ -2426,10 +2854,137 @@ function formatCheckProjectResult(result) {
2426
2854
  return lines.join("\n");
2427
2855
  }
2428
2856
 
2857
+ // src/tools/brand.ts
2858
+ import path4 from "path";
2859
+ var TOKENS_INSTALL_COMMAND = `pnpm add ${TOKENS_PACKAGE}@^${MIN_BRAND_TOKENS_VERSION}`;
2860
+ function baseDirOf(targetDir) {
2861
+ return path4.resolve(targetDir || process.cwd());
2862
+ }
2863
+ async function handleListBrands(params) {
2864
+ return listBrands(baseDirOf(params.targetDir));
2865
+ }
2866
+ async function handleGetBrand(params) {
2867
+ const { name } = params;
2868
+ if (!name || name.trim() === "") {
2869
+ throw new Error("\uBE0C\uB79C\uB4DC \uC774\uB984(name)\uC740 \uD544\uC218\uC785\uB2C8\uB2E4.");
2870
+ }
2871
+ return getBrand(baseDirOf(params.targetDir), name);
2872
+ }
2873
+ function formatTokensStatusGuide(status, version) {
2874
+ switch (status) {
2875
+ case "not-installed":
2876
+ return [
2877
+ `${TOKENS_PACKAGE} \uAC00 \uC124\uCE58\uB3FC \uC788\uC9C0 \uC54A\uC544 \uBE0C\uB79C\uB4DC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`,
2878
+ "",
2879
+ `- \uC124\uCE58: \`${TOKENS_INSTALL_COMMAND}\``
2880
+ ].join("\n");
2881
+ case "unsupported":
2882
+ return [
2883
+ `${TOKENS_PACKAGE} ${version ?? "(\uBC84\uC804 \uBAA8\uB984)"} \uC740 \uBE0C\uB79C\uB4DC\uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(${MIN_BRAND_TOKENS_VERSION} \uC774\uC0C1 \uD544\uC694).`,
2884
+ "",
2885
+ `- \uC5C5\uADF8\uB808\uC774\uB4DC: \`${TOKENS_INSTALL_COMMAND}\``
2886
+ ].join("\n");
2887
+ case "not-built":
2888
+ return [
2889
+ `${TOKENS_PACKAGE}${version ? ` ${version}` : ""} \uB294 \uC788\uC9C0\uB9CC \uBE4C\uB4DC \uC0B0\uCD9C\uBB3C\uC774 \uC5C6\uC5B4 \uBE0C\uB79C\uB4DC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`,
2890
+ "",
2891
+ `- \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uBA74 \`pnpm --filter ${TOKENS_PACKAGE} build\`, \uC544\uB2C8\uBA74 \uC7AC\uC124\uCE58: \`${TOKENS_INSTALL_COMMAND}\``
2892
+ ].join("\n");
2893
+ }
2894
+ }
2895
+ function formatListBrandsResult(result) {
2896
+ const lines = ["## AXIS \uBE0C\uB79C\uB4DC\n"];
2897
+ if (result.status !== "ok") {
2898
+ lines.push(formatTokensStatusGuide(result.status, result.version));
2899
+ return lines.join("\n");
2900
+ }
2901
+ if (result.brands.length === 0) {
2902
+ lines.push(`\uC124\uCE58\uB41C ${TOKENS_PACKAGE} ${result.version ?? ""} \uC5D0 \uBE0C\uB79C\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`);
2903
+ return lines.join("\n");
2904
+ }
2905
+ lines.push(`${TOKENS_PACKAGE} ${result.version ?? ""} \uC5D0 \uBE0C\uB79C\uB4DC ${result.brands.length}\uAC1C\uAC00 \uC788\uC2B5\uB2C8\uB2E4.`, "");
2906
+ for (const b of result.brands) {
2907
+ lines.push(`- **${b.name}**: ${b.description} (${b.themes.join(" \xB7 ")})`);
2908
+ lines.push(` - import: \`${b.importPath}\``);
2909
+ }
2910
+ lines.push("", "\uC0C1\uC138: `axis.get_brand` (name)");
2911
+ return lines.join("\n");
2912
+ }
2913
+ function a11yValue(value) {
2914
+ return value === null || value === void 0 || value === "" ? "-" : String(value);
2915
+ }
2916
+ function tokenTable(brand) {
2917
+ const rows = KEY_TOKENS.filter((k) => k in brand.tokens.light || k in brand.tokens.dark);
2918
+ const missing = KEY_TOKENS.filter((k) => !(k in brand.tokens.light) && !(k in brand.tokens.dark));
2919
+ const lines = [];
2920
+ if (rows.length > 0) {
2921
+ lines.push("| \uD1A0\uD070 | \uB77C\uC774\uD2B8 | \uB2E4\uD06C |", "|---|---|---|");
2922
+ for (const k of rows) {
2923
+ lines.push(`| \`${k}\` | ${brand.tokens.light[k] ?? "-"} | ${brand.tokens.dark[k] ?? "-"} |`);
2924
+ }
2925
+ }
2926
+ if (missing.length > 0) {
2927
+ if (lines.length > 0) lines.push("");
2928
+ lines.push(`\uBE0C\uB79C\uB4DC\uAC00 \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC74C: ${missing.join(", ")}`);
2929
+ }
2930
+ return lines;
2931
+ }
2932
+ function formatGetBrandResult(result, name) {
2933
+ if (result.status !== "ok") {
2934
+ return [`## \uBE0C\uB79C\uB4DC: ${name}
2935
+ `, formatTokensStatusGuide(result.status, result.version)].join("\n");
2936
+ }
2937
+ const brand = result.brand;
2938
+ if (!brand) {
2939
+ return [
2940
+ `## \uBE0C\uB79C\uB4DC: ${name}
2941
+ `,
2942
+ `\uBE0C\uB79C\uB4DC '${name}' \uAC00 \uC124\uCE58\uB41C ${TOKENS_PACKAGE} ${result.version ?? ""} \uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4.`,
2943
+ "",
2944
+ `- \uC124\uCE58\uB41C \uBE0C\uB79C\uB4DC: ${result.available.length > 0 ? result.available.join(", ") : "(\uC5C6\uC74C)"}`,
2945
+ "- \uBAA9\uB85D: `axis.list_brands`"
2946
+ ].join("\n");
2947
+ }
2948
+ return [
2949
+ `## \uBE0C\uB79C\uB4DC: ${brand.name}
2950
+ `,
2951
+ `- \uC124\uBA85: ${brand.description}`,
2952
+ `- \uD14C\uB9C8: ${brand.themes.join(" \xB7 ")}`,
2953
+ "",
2954
+ "### Import (globals.css, @charset \xB7 \uB2E4\uB978 @import \uB2E4\uC74C)",
2955
+ "",
2956
+ "```css",
2957
+ `@import "${brand.importPath}";`,
2958
+ "```",
2959
+ "",
2960
+ "### Provider (@axis-ds/theme 1.3.0 \uC774\uC0C1)",
2961
+ "",
2962
+ "```tsx",
2963
+ brand.providerExample,
2964
+ "```",
2965
+ "",
2966
+ `ThemeProvider \uB098 <html data-brand="${brand.name}"> \uAC00 \uC5C6\uC73C\uBA74 import \uB9CC\uC73C\uB85C\uB294 \uD654\uBA74\uC774 \uBC14\uB00C\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`,
2967
+ "",
2968
+ "### \uC8FC\uC694 \uD1A0\uD070",
2969
+ "",
2970
+ ...tokenTable(brand),
2971
+ "",
2972
+ "### \uBCF4\uC815 \uB0B4\uC5ED",
2973
+ "",
2974
+ `- a11y \uC0C1\uD0DC: ${a11yValue(brand.adjustments.a11y.status)}`,
2975
+ `- \uC810\uAC80\uC77C: ${a11yValue(brand.adjustments.a11y.checkedAt)}`,
2976
+ `- \uAC8C\uC774\uD2B8: ${a11yValue(brand.adjustments.a11y.gate)}`,
2977
+ `- \uADFC\uAC70 \uBB38\uC11C: ${brand.adjustments.reference}`,
2978
+ "",
2979
+ "\uBCF4\uC815 \uC804 \xB7 \uD6C4 \uAC12\uC740 \uC774 \uBC84\uC804\uC5D0\uC11C \uCD9C\uB825\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
2980
+ ].join("\n");
2981
+ }
2982
+
2429
2983
  // src/index.ts
2984
+ var { version: SERVER_VERSION } = createRequire2(import.meta.url)("../package.json");
2430
2985
  var server = new McpServer({
2431
2986
  name: "axis-design-system",
2432
- version: "0.1.0"
2987
+ version: SERVER_VERSION
2433
2988
  });
2434
2989
  var ComponentCategorySchema = z.enum(["core", "agentic", "form", "layout"]).optional().describe("\uCEF4\uD3EC\uB10C\uD2B8 \uCE74\uD14C\uACE0\uB9AC \uD544\uD130");
2435
2990
  var TokenCategorySchema = z.enum(["color", "typography", "spacing", "radius", "shadow", "animation"]).optional().describe("\uD1A0\uD070 \uCE74\uD14C\uACE0\uB9AC \uD544\uD130");
@@ -2575,9 +3130,9 @@ server.tool(
2575
3130
  {
2576
3131
  path: z.string().describe("\uD1A0\uD070 \uACBD\uB85C (\uC608: colors.primary, spacing.4)")
2577
3132
  },
2578
- async ({ path: path4 }) => {
3133
+ async ({ path: path5 }) => {
2579
3134
  try {
2580
- const token = handleGetToken({ path: path4 });
3135
+ const token = handleGetToken({ path: path5 });
2581
3136
  const formatted = formatTokenDetail(token);
2582
3137
  return {
2583
3138
  content: [
@@ -2978,6 +3533,71 @@ server.tool(
2978
3533
  }
2979
3534
  }
2980
3535
  );
3536
+ server.tool(
3537
+ "axis.list_brands",
3538
+ "\uB300\uC0C1 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uC124\uCE58\uB41C @axis-ds/tokens \uC758 \uBE0C\uB79C\uB4DC \uBAA9\uB85D(\uC774\uB984 \xB7 \uC124\uBA85 \xB7 \uD14C\uB9C8 \xB7 import \uACBD\uB85C)\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
3539
+ {
3540
+ targetDir: z.string().optional().describe("\uB300\uC0C1 \uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD1A0\uB9AC (\uAE30\uBCF8: \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC)")
3541
+ },
3542
+ async ({ targetDir }) => {
3543
+ try {
3544
+ const result = await handleListBrands({ targetDir });
3545
+ const formatted = formatListBrandsResult(result);
3546
+ return {
3547
+ content: [
3548
+ {
3549
+ type: "text",
3550
+ text: formatted
3551
+ }
3552
+ ]
3553
+ };
3554
+ } catch (error) {
3555
+ const message = error instanceof Error ? error.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
3556
+ return {
3557
+ content: [
3558
+ {
3559
+ type: "text",
3560
+ text: `\uC624\uB958: ${message}`
3561
+ }
3562
+ ],
3563
+ isError: true
3564
+ };
3565
+ }
3566
+ }
3567
+ );
3568
+ server.tool(
3569
+ "axis.get_brand",
3570
+ "\uBE0C\uB79C\uB4DC \uD558\uB098\uC758 import \uACBD\uB85C \xB7 Provider \uC124\uC815 \uC608\uC2DC(defaultBrand) \xB7 \uC8FC\uC694 \uD1A0\uD070 \xB7 \uBCF4\uC815 \uB0B4\uC5ED\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
3571
+ {
3572
+ name: z.string().describe("\uBE0C\uB79C\uB4DC \uC774\uB984 (\uC608: ontology)"),
3573
+ targetDir: z.string().optional().describe("\uB300\uC0C1 \uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD1A0\uB9AC (\uAE30\uBCF8: \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC)")
3574
+ },
3575
+ async ({ name, targetDir }) => {
3576
+ try {
3577
+ const result = await handleGetBrand({ name, targetDir });
3578
+ const formatted = formatGetBrandResult(result, name);
3579
+ return {
3580
+ content: [
3581
+ {
3582
+ type: "text",
3583
+ text: formatted
3584
+ }
3585
+ ]
3586
+ };
3587
+ } catch (error) {
3588
+ const message = error instanceof Error ? error.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
3589
+ return {
3590
+ content: [
3591
+ {
3592
+ type: "text",
3593
+ text: `\uC624\uB958: ${message}`
3594
+ }
3595
+ ],
3596
+ isError: true
3597
+ };
3598
+ }
3599
+ }
3600
+ );
2981
3601
  async function main() {
2982
3602
  const transport = new StdioServerTransport();
2983
3603
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axis-ds/mcp",
3
- "version": "1.1.5",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "AXIS Design System - MCP Server for AI-assisted component discovery",
6
6
  "bin": {
@@ -25,7 +25,8 @@
25
25
  "@types/fs-extra": "^11.0.0",
26
26
  "@types/node": "^22.0.0",
27
27
  "tsup": "^8.0.0",
28
- "typescript": "^5.7.0"
28
+ "typescript": "^5.7.0",
29
+ "vitest": "^2.1.0"
29
30
  },
30
31
  "keywords": [
31
32
  "mcp",
@@ -45,6 +46,7 @@
45
46
  "build": "tsup src/index.ts --format esm --dts --clean && npm run copy-data",
46
47
  "copy-data": "node -e \"const fs=require('fs');fs.cpSync('src/registry/data','dist/data',{recursive:true})\"",
47
48
  "dev": "tsup src/index.ts --format esm --watch",
48
- "clean": "rm -rf dist"
49
+ "clean": "rm -rf dist",
50
+ "test": "vitest run"
49
51
  }
50
52
  }