@mandujs/mcp 0.35.0 → 0.36.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/package.json +2 -2
- package/src/tools/design.ts +313 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.1",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@mandujs/core": "^0.
|
|
37
|
+
"@mandujs/core": "^0.53.0",
|
|
38
38
|
"@mandujs/ate": "^0.25.1",
|
|
39
39
|
"@mandujs/skills": "^0.19.0",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
package/src/tools/design.ts
CHANGED
|
@@ -23,9 +23,17 @@ import { promises as fs } from "node:fs";
|
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
25
25
|
import {
|
|
26
|
+
diffDesignSpecs,
|
|
27
|
+
extractDesignTokens,
|
|
28
|
+
fetchUpstreamDesignMd,
|
|
26
29
|
parseDesignMd,
|
|
30
|
+
patchDesignMd,
|
|
31
|
+
patchDesignMdBatch,
|
|
27
32
|
type DesignSpec,
|
|
28
33
|
type DesignSectionId,
|
|
34
|
+
type ExtractKind,
|
|
35
|
+
type PatchOperation,
|
|
36
|
+
type PatchableSection,
|
|
29
37
|
DESIGN_SECTION_IDS,
|
|
30
38
|
} from "@mandujs/core/design";
|
|
31
39
|
import { checkFileForDesignInlineClasses } from "@mandujs/core/guard/design-inline-class";
|
|
@@ -423,6 +431,211 @@ function escapeRegex(s: string): string {
|
|
|
423
431
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
424
432
|
}
|
|
425
433
|
|
|
434
|
+
// ─── mandu.design.extract (write tool — read-only, returns proposals)
|
|
435
|
+
|
|
436
|
+
interface DesignExtractInput {
|
|
437
|
+
scope?: string[];
|
|
438
|
+
kinds?: ExtractKind[];
|
|
439
|
+
min_occurrences?: number;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function designExtract(rootDir: string, input: DesignExtractInput): Promise<unknown> {
|
|
443
|
+
const file = await readDesignMd(rootDir);
|
|
444
|
+
const existing = file ? parseDesignMd(file.source) : undefined;
|
|
445
|
+
const result = await extractDesignTokens(rootDir, {
|
|
446
|
+
scope: input.scope,
|
|
447
|
+
kinds: input.kinds,
|
|
448
|
+
minOccurrences: input.min_occurrences,
|
|
449
|
+
existing,
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
scanned_files: result.scannedFiles,
|
|
453
|
+
proposals: result.proposals.map((p) => ({
|
|
454
|
+
kind: p.kind,
|
|
455
|
+
section: p.section,
|
|
456
|
+
key: p.key,
|
|
457
|
+
value: p.value,
|
|
458
|
+
occurrences: p.occurrences,
|
|
459
|
+
files: p.files,
|
|
460
|
+
confidence: p.confidence,
|
|
461
|
+
})),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ─── mandu.design.patch ──────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
interface DesignPatchInput {
|
|
468
|
+
/** Single op shorthand. */
|
|
469
|
+
section?: PatchableSection;
|
|
470
|
+
operation?: PatchOperation["operation"];
|
|
471
|
+
key?: string;
|
|
472
|
+
value?: string;
|
|
473
|
+
role?: string;
|
|
474
|
+
/** Batch — overrides single-op fields when present. */
|
|
475
|
+
ops?: PatchOperation[];
|
|
476
|
+
/** Default true — return diff without writing. */
|
|
477
|
+
dry_run?: boolean;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function designPatch(rootDir: string, input: DesignPatchInput): Promise<unknown> {
|
|
481
|
+
const file = await readDesignMd(rootDir);
|
|
482
|
+
if (!file) {
|
|
483
|
+
return {
|
|
484
|
+
error: "DESIGN.md not found",
|
|
485
|
+
hint: "Run `mandu design init` first.",
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const ops: PatchOperation[] = input.ops ?? (
|
|
489
|
+
input.section && input.operation && input.key
|
|
490
|
+
? [{
|
|
491
|
+
section: input.section,
|
|
492
|
+
operation: input.operation,
|
|
493
|
+
key: input.key,
|
|
494
|
+
value: input.value,
|
|
495
|
+
role: input.role,
|
|
496
|
+
}]
|
|
497
|
+
: []
|
|
498
|
+
);
|
|
499
|
+
if (ops.length === 0) {
|
|
500
|
+
return {
|
|
501
|
+
error: "No operations supplied",
|
|
502
|
+
hint: "Pass either { section, operation, key, value? } or `ops: [...]`.",
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const batch = patchDesignMdBatch(file.source, ops);
|
|
506
|
+
const dryRun = input.dry_run !== false;
|
|
507
|
+
if (!dryRun && batch.appliedCount > 0) {
|
|
508
|
+
await fs.writeFile(file.path, batch.next, "utf8");
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
path: file.path,
|
|
512
|
+
dry_run: dryRun,
|
|
513
|
+
applied_count: batch.appliedCount,
|
|
514
|
+
written: !dryRun && batch.appliedCount > 0,
|
|
515
|
+
results: batch.results.map((r, i) => ({
|
|
516
|
+
op: ops[i],
|
|
517
|
+
applied: r.applied,
|
|
518
|
+
reason: r.reason,
|
|
519
|
+
before: r.before,
|
|
520
|
+
after: r.after,
|
|
521
|
+
})),
|
|
522
|
+
next_source: dryRun ? batch.next : undefined,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ─── mandu.design.propose (extract + dry-run patch in one call) ──────
|
|
527
|
+
|
|
528
|
+
interface DesignProposeInput {
|
|
529
|
+
scope?: string[];
|
|
530
|
+
kinds?: ExtractKind[];
|
|
531
|
+
min_occurrences?: number;
|
|
532
|
+
/** Default true — never writes; returns proposals + dry-run patches. */
|
|
533
|
+
dry_run?: boolean;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function designPropose(rootDir: string, input: DesignProposeInput): Promise<unknown> {
|
|
537
|
+
const extractResult = await designExtract(rootDir, input) as {
|
|
538
|
+
scanned_files: number;
|
|
539
|
+
proposals: Array<{ section: PatchableSection; key: string; value: string; occurrences: number; confidence: number; files: string[]; kind: string }>;
|
|
540
|
+
};
|
|
541
|
+
if ("error" in extractResult) return extractResult;
|
|
542
|
+
|
|
543
|
+
const file = await readDesignMd(rootDir);
|
|
544
|
+
if (!file) {
|
|
545
|
+
return {
|
|
546
|
+
...extractResult,
|
|
547
|
+
error: "DESIGN.md not found — proposals cannot be patched in. Run `mandu design init` first.",
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Convert top proposals (color/typography/layout/shadows only — not
|
|
552
|
+
// components, which need richer human input) into add operations.
|
|
553
|
+
const ops: PatchOperation[] = extractResult.proposals
|
|
554
|
+
.filter((p) => p.section !== "components")
|
|
555
|
+
.map((p) => ({
|
|
556
|
+
section: p.section,
|
|
557
|
+
operation: "add" as const,
|
|
558
|
+
key: synthesiseKeyForProposal(p),
|
|
559
|
+
value: p.value,
|
|
560
|
+
}));
|
|
561
|
+
const batch = patchDesignMdBatch(file.source, ops);
|
|
562
|
+
const dryRun = input.dry_run !== false;
|
|
563
|
+
if (!dryRun && batch.appliedCount > 0) {
|
|
564
|
+
await fs.writeFile(file.path, batch.next, "utf8");
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
...extractResult,
|
|
568
|
+
path: file.path,
|
|
569
|
+
dry_run: dryRun,
|
|
570
|
+
applied_count: batch.appliedCount,
|
|
571
|
+
written: !dryRun && batch.appliedCount > 0,
|
|
572
|
+
patch_results: batch.results.map((r, i) => ({
|
|
573
|
+
op: ops[i],
|
|
574
|
+
applied: r.applied,
|
|
575
|
+
reason: r.reason,
|
|
576
|
+
})),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function synthesiseKeyForProposal(p: { kind: string; key: string }): string {
|
|
581
|
+
// Color literals like "#ff8c42" → "Orange-FF8C42" — give the user
|
|
582
|
+
// something readable they can rename in the next patch round.
|
|
583
|
+
if (p.kind === "color") {
|
|
584
|
+
const stripped = p.key.replace(/^#/, "").toUpperCase();
|
|
585
|
+
return `Color-${stripped}`;
|
|
586
|
+
}
|
|
587
|
+
if (p.kind === "typography") {
|
|
588
|
+
const head = p.key.split(/[,\s]/)[0] ?? p.key;
|
|
589
|
+
return head.replace(/[`'"]/g, "");
|
|
590
|
+
}
|
|
591
|
+
return p.key;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ─── mandu.design.diff_upstream ──────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
interface DesignDiffUpstreamInput {
|
|
597
|
+
/** awesome-design-md slug or any raw URL. Required. */
|
|
598
|
+
slug?: string;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function designDiffUpstream(rootDir: string, input: DesignDiffUpstreamInput): Promise<unknown> {
|
|
602
|
+
if (!input.slug) {
|
|
603
|
+
return {
|
|
604
|
+
error: "`slug` is required",
|
|
605
|
+
hint: 'Pass an awesome-design-md slug (e.g. "stripe") or a raw URL.',
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const file = await readDesignMd(rootDir);
|
|
609
|
+
if (!file) {
|
|
610
|
+
return {
|
|
611
|
+
error: "Local DESIGN.md not found",
|
|
612
|
+
hint: "Run `mandu design init --from <slug>` first.",
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const local = parseDesignMd(file.source);
|
|
616
|
+
let upstreamSource: string;
|
|
617
|
+
try {
|
|
618
|
+
upstreamSource = await fetchUpstreamDesignMd(input.slug);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return {
|
|
621
|
+
error: `Failed to fetch upstream "${input.slug}": ${err instanceof Error ? err.message : String(err)}`,
|
|
622
|
+
hint: "Check your network or the slug; see github.com/VoltAgent/awesome-design-md for valid slugs.",
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const upstream = parseDesignMd(upstreamSource);
|
|
626
|
+
const diff = diffDesignSpecs(local, upstream);
|
|
627
|
+
return {
|
|
628
|
+
slug: input.slug,
|
|
629
|
+
local_path: file.path,
|
|
630
|
+
total_changes: diff.totalChanges,
|
|
631
|
+
section_presence_changed: diff.sectionPresenceChanged,
|
|
632
|
+
color_palette: diff.colorPalette,
|
|
633
|
+
typography: diff.typography,
|
|
634
|
+
layout: diff.layout,
|
|
635
|
+
shadows: diff.shadows,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
426
639
|
// ─── MCP definitions + handlers ───────────────────────────────────────
|
|
427
640
|
|
|
428
641
|
export const designToolDefinitions: Tool[] = [
|
|
@@ -498,6 +711,98 @@ export const designToolDefinitions: Tool[] = [
|
|
|
498
711
|
required: [],
|
|
499
712
|
},
|
|
500
713
|
},
|
|
714
|
+
{
|
|
715
|
+
name: "mandu.design.extract",
|
|
716
|
+
description:
|
|
717
|
+
"Scan src/** + app/** for token candidates (color literals, font-families, repeated className combos) and return proposals to patch into DESIGN.md. Read-only — does not modify any file. Filters out tokens already represented in DESIGN.md.",
|
|
718
|
+
annotations: { readOnlyHint: true },
|
|
719
|
+
inputSchema: {
|
|
720
|
+
type: "object",
|
|
721
|
+
properties: {
|
|
722
|
+
scope: {
|
|
723
|
+
type: "array",
|
|
724
|
+
items: { type: "string" },
|
|
725
|
+
description: "Project-relative roots to scan. Default ['src', 'app'].",
|
|
726
|
+
},
|
|
727
|
+
kinds: {
|
|
728
|
+
type: "array",
|
|
729
|
+
items: { type: "string", enum: ["color", "typography", "spacing", "component"] },
|
|
730
|
+
description: "Restrict the proposal kinds. Default: all four.",
|
|
731
|
+
},
|
|
732
|
+
min_occurrences: {
|
|
733
|
+
type: "number",
|
|
734
|
+
description: "Minimum occurrence threshold. Default 3.",
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
required: [],
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: "mandu.design.patch",
|
|
742
|
+
description:
|
|
743
|
+
"Section-safe DESIGN.md patcher (add / update / remove a token). Defaults to dry-run — pass `dry_run: false` to actually rewrite the file. Pass either single-op fields (`section`, `operation`, `key`, `value?`, `role?`) or a batch via `ops: [...]`.",
|
|
744
|
+
annotations: { readOnlyHint: false },
|
|
745
|
+
inputSchema: {
|
|
746
|
+
type: "object",
|
|
747
|
+
properties: {
|
|
748
|
+
section: {
|
|
749
|
+
type: "string",
|
|
750
|
+
enum: ["color-palette", "typography", "layout", "shadows", "components"],
|
|
751
|
+
},
|
|
752
|
+
operation: { type: "string", enum: ["add", "update", "remove"] },
|
|
753
|
+
key: { type: "string" },
|
|
754
|
+
value: { type: "string" },
|
|
755
|
+
role: { type: "string" },
|
|
756
|
+
ops: {
|
|
757
|
+
type: "array",
|
|
758
|
+
description: "Batch — overrides single-op fields.",
|
|
759
|
+
},
|
|
760
|
+
dry_run: {
|
|
761
|
+
type: "boolean",
|
|
762
|
+
description: "When true (default), return the diff without writing.",
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
required: [],
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "mandu.design.propose",
|
|
770
|
+
description:
|
|
771
|
+
"Combined extract + dry-run patch. Runs `mandu.design.extract`, converts color / typography / layout / shadow proposals into add operations, and returns both the proposal list and the patch results. Always dry-run by default; pass `dry_run: false` to apply.",
|
|
772
|
+
annotations: { readOnlyHint: false },
|
|
773
|
+
inputSchema: {
|
|
774
|
+
type: "object",
|
|
775
|
+
properties: {
|
|
776
|
+
scope: { type: "array", items: { type: "string" } },
|
|
777
|
+
kinds: {
|
|
778
|
+
type: "array",
|
|
779
|
+
items: { type: "string", enum: ["color", "typography", "spacing", "component"] },
|
|
780
|
+
},
|
|
781
|
+
min_occurrences: { type: "number" },
|
|
782
|
+
dry_run: {
|
|
783
|
+
type: "boolean",
|
|
784
|
+
description: "Default true (preview only).",
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
required: [],
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "mandu.design.diff_upstream",
|
|
792
|
+
description:
|
|
793
|
+
"Compare the local DESIGN.md against an upstream brand spec from awesome-design-md. Returns per-section added / changed / removed entries plus a `total_changes` counter and `section_presence_changed` list. Read-only; produces a diff the caller can patch in selectively via `mandu.design.patch`.",
|
|
794
|
+
annotations: { readOnlyHint: true },
|
|
795
|
+
inputSchema: {
|
|
796
|
+
type: "object",
|
|
797
|
+
properties: {
|
|
798
|
+
slug: {
|
|
799
|
+
type: "string",
|
|
800
|
+
description: "awesome-design-md slug (e.g. 'stripe') or a raw DESIGN.md URL.",
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
required: ["slug"],
|
|
804
|
+
},
|
|
805
|
+
},
|
|
501
806
|
];
|
|
502
807
|
|
|
503
808
|
export function designTools(projectRoot: string) {
|
|
@@ -507,6 +812,14 @@ export function designTools(projectRoot: string) {
|
|
|
507
812
|
"mandu.design.check": async (args) => designCheck(projectRoot, args as DesignCheckInput),
|
|
508
813
|
"mandu.component.list": async (args) =>
|
|
509
814
|
componentList(projectRoot, args as ComponentListInput),
|
|
815
|
+
"mandu.design.extract": async (args) =>
|
|
816
|
+
designExtract(projectRoot, args as DesignExtractInput),
|
|
817
|
+
"mandu.design.patch": async (args) =>
|
|
818
|
+
designPatch(projectRoot, args as DesignPatchInput),
|
|
819
|
+
"mandu.design.propose": async (args) =>
|
|
820
|
+
designPropose(projectRoot, args as DesignProposeInput),
|
|
821
|
+
"mandu.design.diff_upstream": async (args) =>
|
|
822
|
+
designDiffUpstream(projectRoot, args as DesignDiffUpstreamInput),
|
|
510
823
|
};
|
|
511
824
|
return handlers;
|
|
512
825
|
}
|