@design-ai/cli 4.55.0 → 4.57.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/.claude-plugin/plugin.json +109 -25
- package/CHANGELOG.md +75 -0
- package/README.ko.md +15 -32
- package/README.md +18 -31
- package/cli/bin/design-ai.mjs +1 -0
- package/cli/commands/help.mjs +7 -4
- package/cli/commands/index.mjs +292 -0
- package/cli/commands/learn-help.mjs +149 -0
- package/cli/commands/learn-print-profile.mjs +373 -0
- package/cli/commands/learn-print-restore.mjs +349 -0
- package/cli/commands/learn-print-signals.mjs +453 -0
- package/cli/commands/learn.mjs +60 -1281
- package/cli/commands/pack.mjs +7 -3
- package/cli/commands/prompt.mjs +7 -3
- package/cli/commands/search.mjs +94 -1
- package/cli/lib/dispatch.mjs +3 -0
- package/cli/lib/embedding-index.mjs +199 -0
- package/cli/lib/embedding-provider.mjs +121 -0
- package/cli/lib/embedding-rerank.mjs +52 -0
- package/cli/lib/learn-args.mjs +490 -0
- package/cli/lib/learn-backup.mjs +748 -0
- package/cli/lib/learn-curation.mjs +612 -0
- package/cli/lib/learn-eval.mjs +459 -0
- package/cli/lib/learn-profile.mjs +763 -0
- package/cli/lib/learn-select.mjs +223 -0
- package/cli/lib/learn-shared.mjs +158 -0
- package/cli/lib/learn-test-support.mjs +218 -0
- package/cli/lib/learn-usage.mjs +360 -0
- package/cli/lib/learn.mjs +83 -3694
- package/cli/lib/lexical.mjs +137 -0
- package/cli/lib/local-config.mjs +110 -0
- package/cli/lib/mcp-server.mjs +221 -13
- package/cli/lib/pack.mjs +31 -4
- package/cli/lib/prompt.mjs +42 -4
- package/cli/lib/recall.mjs +193 -0
- package/cli/lib/retrieval-index.mjs +0 -0
- package/cli/lib/search-ranked.mjs +170 -0
- package/cli/lib/search.mjs +11 -1
- package/cli/lib/signals-backlog-commands.mjs +673 -0
- package/cli/lib/signals-backlog.mjs +361 -0
- package/cli/lib/signals-eval.mjs +176 -0
- package/cli/lib/signals-readiness.mjs +247 -0
- package/cli/lib/signals-registry.mjs +377 -0
- package/cli/lib/signals-render.mjs +478 -0
- package/cli/lib/signals-shared.mjs +75 -0
- package/cli/lib/signals.mjs +16 -2337
- package/cli/lib/site-analysis.mjs +297 -0
- package/cli/lib/site-args.mjs +433 -0
- package/cli/lib/site-bundle-build.mjs +127 -0
- package/cli/lib/site-bundle-check.mjs +454 -0
- package/cli/lib/site-bundle-commands.mjs +95 -0
- package/cli/lib/site-bundle-compare.mjs +157 -0
- package/cli/lib/site-bundle-contract.mjs +79 -0
- package/cli/lib/site-bundle-files.mjs +87 -0
- package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
- package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
- package/cli/lib/site-bundle-handoff-runbook-actions.mjs +113 -0
- package/cli/lib/site-bundle-handoff-runbook-evidence-fields.mjs +164 -0
- package/cli/lib/site-bundle-handoff-runbook-evidence.mjs +334 -0
- package/cli/lib/site-bundle-handoff-runbook-format.mjs +31 -0
- package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
- package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
- package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
- package/cli/lib/site-bundle-handoff-runbook-stage-metadata.mjs +84 -0
- package/cli/lib/site-bundle-handoff-runbook.mjs +444 -0
- package/cli/lib/site-bundle-handoff-summary.mjs +183 -0
- package/cli/lib/site-bundle-handoff.mjs +271 -0
- package/cli/lib/site-bundle-readme.mjs +98 -0
- package/cli/lib/site-bundle-repair-report.mjs +143 -0
- package/cli/lib/site-bundle-repair.mjs +68 -0
- package/cli/lib/site-content.mjs +399 -0
- package/cli/lib/site-evidence.mjs +35 -0
- package/cli/lib/site-mcp-commands.mjs +28 -0
- package/cli/lib/site-mcp-probes.mjs +159 -0
- package/cli/lib/site-mcp-readiness.mjs +157 -0
- package/cli/lib/site-mcp-report.mjs +324 -0
- package/cli/lib/site-next-actions.mjs +333 -0
- package/cli/lib/site-options.mjs +104 -0
- package/cli/lib/site-prompts.mjs +332 -0
- package/cli/lib/site-starter.mjs +153 -0
- package/cli/lib/site-strings.mjs +23 -0
- package/cli/lib/site-tasks.mjs +93 -0
- package/cli/lib/site-test-support.mjs +68 -0
- package/cli/lib/site-workflow-graph.mjs +309 -0
- package/cli/lib/site-workspace.mjs +492 -0
- package/cli/lib/site.mjs +108 -6617
- package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
- package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
- package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
- package/cli/lib/skill-proposals-generate.mjs +298 -0
- package/cli/lib/skill-proposals-render.mjs +532 -0
- package/cli/lib/skill-proposals-review.mjs +262 -0
- package/cli/lib/skill-proposals.mjs +15 -2046
- package/cli/lib/workspace-args.mjs +83 -0
- package/cli/lib/workspace-git.mjs +169 -0
- package/cli/lib/workspace-learning.mjs +483 -0
- package/cli/lib/workspace-repo.mjs +139 -0
- package/cli/lib/workspace-report.mjs +283 -0
- package/cli/lib/workspace-test-support.mjs +99 -0
- package/cli/lib/workspace.mjs +30 -1056
- package/docs/AI-LEARNING-PHASE2.md +215 -0
- package/docs/DISTRIBUTION.ko.md +35 -6
- package/docs/DISTRIBUTION.md +35 -8
- package/docs/NEXT-SURFACE-DECISION.md +125 -0
- package/docs/PRODUCT-READINESS.md +3 -3
- package/docs/RELEASE-CHECKLIST.md +20 -3
- package/docs/RELEASE-GATES.ko.md +35 -0
- package/docs/RELEASE-GATES.md +234 -0
- package/docs/ROADMAP.md +2232 -8
- package/docs/external-status.md +20 -7
- package/docs/inspection-20260630.md +169 -0
- package/docs/integrations/design-ai-mcp-server.md +35 -3
- package/docs/integrations/vscode-walkthrough.ko.md +3 -3
- package/docs/integrations/vscode-walkthrough.md +3 -3
- package/docs/reference/ant-design.md +413 -0
- package/docs/reference/awesome-design-md.md +439 -0
- package/docs/reference/mui.md +783 -0
- package/docs/reference/shadcn-ui.md +298 -0
- package/docs/site-overrides/main.html +1 -1
- package/examples/component-accordion-actions.md +5 -5
- package/examples/component-accordion-details.md +5 -5
- package/examples/component-accordion-summary.md +5 -5
- package/examples/component-accordion.md +3 -3
- package/examples/component-affix.md +1 -1
- package/examples/component-alert-dialog.md +1 -1
- package/examples/component-alert-title.md +1 -1
- package/examples/component-alert.md +3 -3
- package/examples/component-anchor.md +1 -1
- package/examples/component-app-bar.md +1 -1
- package/examples/component-aspect-ratio.md +1 -1
- package/examples/component-auto-complete.md +2 -2
- package/examples/component-avatar-group.md +5 -5
- package/examples/component-avatar.md +3 -3
- package/examples/component-back-top.md +1 -1
- package/examples/component-backdrop.md +1 -1
- package/examples/component-badge.md +3 -3
- package/examples/component-border-beam.md +3 -3
- package/examples/component-bottom-navigation.md +1 -1
- package/examples/component-box.md +1 -1
- package/examples/component-breadcrumb.md +3 -3
- package/examples/component-button-base.md +3 -3
- package/examples/component-button-group.md +2 -2
- package/examples/component-button.md +3 -3
- package/examples/component-calendar.md +2 -2
- package/examples/component-card-actions.md +1 -1
- package/examples/component-card-content.md +1 -1
- package/examples/component-card-header.md +1 -1
- package/examples/component-card-media.md +1 -1
- package/examples/component-card.md +3 -3
- package/examples/component-carousel.md +2 -2
- package/examples/component-cascader.md +1 -1
- package/examples/component-chart.md +1 -1
- package/examples/component-checkbox.md +3 -3
- package/examples/component-click-away-listener.md +1 -1
- package/examples/component-code.md +1 -1
- package/examples/component-collapsible.md +1 -1
- package/examples/component-color-picker.md +1 -1
- package/examples/component-combobox.md +1 -1
- package/examples/component-command.md +1 -1
- package/examples/component-config-provider.md +3 -3
- package/examples/component-context-menu.md +1 -1
- package/examples/component-css-baseline.md +3 -3
- package/examples/component-date-picker.md +2 -2
- package/examples/component-descriptions.md +1 -1
- package/examples/component-dialog-actions.md +1 -1
- package/examples/component-dialog-content-text.md +1 -1
- package/examples/component-dialog-content.md +1 -1
- package/examples/component-dialog-title.md +1 -1
- package/examples/component-dialog.md +2 -2
- package/examples/component-divider.md +3 -3
- package/examples/component-drawer.md +3 -3
- package/examples/component-dropdown.md +3 -3
- package/examples/component-empty.md +2 -2
- package/examples/component-fade.md +1 -1
- package/examples/component-field.md +1 -1
- package/examples/component-filled-input.md +1 -1
- package/examples/component-flex.md +1 -1
- package/examples/component-float-button.md +2 -2
- package/examples/component-form-control-label.md +1 -1
- package/examples/component-form-control.md +1 -1
- package/examples/component-form-controls.md +1 -1
- package/examples/component-form-group.md +1 -1
- package/examples/component-form-helper-text.md +1 -1
- package/examples/component-form-label.md +1 -1
- package/examples/component-form.md +2 -2
- package/examples/component-grid.md +2 -2
- package/examples/component-grow.md +1 -1
- package/examples/component-hover-card.md +1 -1
- package/examples/component-icon-button.md +1 -1
- package/examples/component-icon.md +1 -1
- package/examples/component-image-list.md +1 -1
- package/examples/component-image.md +1 -1
- package/examples/component-input-adornment.md +1 -1
- package/examples/component-input-base.md +1 -1
- package/examples/component-input-number.md +1 -1
- package/examples/component-input-otp.md +2 -2
- package/examples/component-input.md +3 -3
- package/examples/component-item.md +1 -1
- package/examples/component-label.md +1 -1
- package/examples/component-layout.md +1 -1
- package/examples/component-link.md +1 -1
- package/examples/component-list-item-avatar.md +1 -1
- package/examples/component-list-item-button.md +1 -1
- package/examples/component-list-item-icon.md +1 -1
- package/examples/component-list-item-text.md +1 -1
- package/examples/component-list-item.md +1 -1
- package/examples/component-list-subheader.md +1 -1
- package/examples/component-list.md +2 -2
- package/examples/component-masonry.md +1 -1
- package/examples/component-mentions.md +1 -1
- package/examples/component-menu-item.md +1 -1
- package/examples/component-menu-list.md +1 -1
- package/examples/component-menu.md +1 -1
- package/examples/component-menubar.md +1 -1
- package/examples/component-message.md +1 -1
- package/examples/component-mobile-stepper.md +1 -1
- package/examples/component-modal.md +5 -5
- package/examples/component-navigation-menu.md +1 -1
- package/examples/component-notification.md +1 -1
- package/examples/component-outlined-input.md +1 -1
- package/examples/component-pagination.md +3 -3
- package/examples/component-paper.md +1 -1
- package/examples/component-popconfirm.md +1 -1
- package/examples/component-popover.md +3 -3
- package/examples/component-popper.md +1 -1
- package/examples/component-progress.md +2 -2
- package/examples/component-qr-code.md +1 -1
- package/examples/component-radio.md +3 -3
- package/examples/component-rate.md +2 -2
- package/examples/component-resizable.md +1 -1
- package/examples/component-result.md +1 -1
- package/examples/component-scroll-area.md +1 -1
- package/examples/component-segmented.md +3 -3
- package/examples/component-select.md +3 -3
- package/examples/component-separator.md +1 -1
- package/examples/component-sheet.md +1 -1
- package/examples/component-sidebar.md +1 -1
- package/examples/component-skeleton.md +3 -3
- package/examples/component-slide.md +1 -1
- package/examples/component-slider.md +3 -3
- package/examples/component-snackbar-content.md +1 -1
- package/examples/component-snackbar.md +1 -1
- package/examples/component-sonner.md +1 -1
- package/examples/component-space.md +1 -1
- package/examples/component-speed-dial-action.md +1 -1
- package/examples/component-speed-dial.md +1 -1
- package/examples/component-spin.md +2 -2
- package/examples/component-spinner.md +1 -1
- package/examples/component-splitter.md +2 -2
- package/examples/component-stack.md +1 -1
- package/examples/component-statistic.md +1 -1
- package/examples/component-step-button.md +4 -4
- package/examples/component-step-connector.md +4 -4
- package/examples/component-step-content.md +1 -1
- package/examples/component-step-icon.md +1 -1
- package/examples/component-step-label.md +1 -1
- package/examples/component-step.md +2 -2
- package/examples/component-steps.md +2 -2
- package/examples/component-swipeable-drawer.md +1 -1
- package/examples/component-switch.md +3 -3
- package/examples/component-tab-scroll-button.md +5 -5
- package/examples/component-tab.md +1 -1
- package/examples/component-table-body.md +1 -1
- package/examples/component-table-cell.md +1 -1
- package/examples/component-table-container.md +1 -1
- package/examples/component-table-footer.md +1 -1
- package/examples/component-table-head.md +1 -1
- package/examples/component-table-pagination.md +1 -1
- package/examples/component-table-row.md +1 -1
- package/examples/component-table-sort-label.md +1 -1
- package/examples/component-table.md +2 -2
- package/examples/component-tabs.md +3 -3
- package/examples/component-tag-badge.md +2 -2
- package/examples/component-tag.md +1 -1
- package/examples/component-textarea-autosize.md +1 -1
- package/examples/component-textarea.md +1 -1
- package/examples/component-time-picker.md +1 -1
- package/examples/component-timeline.md +1 -1
- package/examples/component-toast.md +4 -4
- package/examples/component-toggle-button.md +1 -1
- package/examples/component-toggle.md +1 -1
- package/examples/component-toolbar.md +1 -1
- package/examples/component-tooltip.md +3 -3
- package/examples/component-tour.md +1 -1
- package/examples/component-transfer.md +1 -1
- package/examples/component-tree-select.md +1 -1
- package/examples/component-tree.md +1 -1
- package/examples/component-typography.md +2 -2
- package/examples/component-upload.md +1 -1
- package/examples/component-watermark.md +1 -1
- package/examples/component-zoom.md +1 -1
- package/knowledge/COVERAGE.md +4 -3
- package/knowledge/components/INDEX.md +202 -202
- package/knowledge/patterns/brand-references.md +72 -72
- package/package.json +1 -1
- package/tools/audit/local-ci.py +16 -1
- package/tools/audit/package-smoke.py +717 -9
- package/tools/audit/registry-smoke.py +912 -10
- package/tools/audit/release-metadata.py +47 -1
- package/tools/audit/smoke_assertions.py +998 -35
- package/tools/migrations/refs-links-to-reference-pages.py +179 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Shared deterministic lexical (BM25-style) scoring for corpus and learning retrieval.
|
|
2
|
+
// Phase A of docs/AI-LEARNING-PHASE2.md: zero-dependency, no randomness, no time-dependent scoring.
|
|
3
|
+
|
|
4
|
+
export const BM25_K1 = 1.2;
|
|
5
|
+
export const BM25_B = 0.75;
|
|
6
|
+
|
|
7
|
+
// Hangul syllable block (U+AC00–U+D7A3). Korean surface forms are written without
|
|
8
|
+
// spaces between stem and particle (버튼을, 접근성이), so a whitespace-delimited token
|
|
9
|
+
// is an atomic surface form. To let a stem query match particle-attached documents
|
|
10
|
+
// (and vice versa), we deterministically expand every Hangul run of length >= 2 into
|
|
11
|
+
// its overlapping character bigrams and emit them ALONGSIDE the original token.
|
|
12
|
+
// 버튼을 -> keep 버튼을, add 버튼, 튼을
|
|
13
|
+
// 접근성 -> keep 접근성, add 접근, 근성
|
|
14
|
+
// 버튼 -> the 2-char run's only bigram is itself; kept via dedupe
|
|
15
|
+
// Latin/number tokens and mixed tokens' non-Hangul parts are unchanged. The rule is
|
|
16
|
+
// pure and deterministic (no locale, no time, no randomness); order is normalized by
|
|
17
|
+
// the dedupe+sort in lexicalQueryTokens and by termFrequencies for stats.
|
|
18
|
+
const HANGUL_RUN = /[가-힣]{2,}/gu;
|
|
19
|
+
|
|
20
|
+
function hangulBigrams(token) {
|
|
21
|
+
const bigrams = [];
|
|
22
|
+
for (const run of token.match(HANGUL_RUN) || []) {
|
|
23
|
+
for (let i = 0; i + 2 <= run.length; i += 1) {
|
|
24
|
+
bigrams.push(run.slice(i, i + 2));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return bigrams;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Same base token model as learn-select.mjs: Unicode letters/numbers, lowercased,
|
|
31
|
+
// length >= 2. Hangul tokens additionally emit character bigrams (see HANGUL_RUN).
|
|
32
|
+
export function lexicalTokens(text) {
|
|
33
|
+
const base = (String(text || "")
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.match(/[\p{L}\p{N}]+/gu) || [])
|
|
36
|
+
.filter((token) => token.length >= 2);
|
|
37
|
+
|
|
38
|
+
const expanded = [];
|
|
39
|
+
for (const token of base) {
|
|
40
|
+
expanded.push(token);
|
|
41
|
+
for (const bigram of hangulBigrams(token)) {
|
|
42
|
+
// A 2-char Hangul run's only bigram is the token itself; do not re-emit it,
|
|
43
|
+
// which would double-count its term frequency and distort BM25.
|
|
44
|
+
if (bigram !== token) expanded.push(bigram);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return expanded;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function lexicalQueryTokens(query) {
|
|
51
|
+
return Array.from(new Set(lexicalTokens(query))).sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function termFrequencies(tokens) {
|
|
55
|
+
const terms = {};
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
terms[token] = (terms[token] || 0) + 1;
|
|
58
|
+
}
|
|
59
|
+
const sorted = {};
|
|
60
|
+
for (const term of Object.keys(terms).sort()) {
|
|
61
|
+
sorted[term] = terms[term];
|
|
62
|
+
}
|
|
63
|
+
return sorted;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// documents: [{ id, text }] -> deterministic index stats with sorted ids and term keys.
|
|
67
|
+
export function buildLexicalStats(documents) {
|
|
68
|
+
const indexed = [...documents]
|
|
69
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
70
|
+
.map((doc) => {
|
|
71
|
+
const tokens = lexicalTokens(doc.text);
|
|
72
|
+
return { id: doc.id, length: tokens.length, terms: termFrequencies(tokens) };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const docFrequency = {};
|
|
76
|
+
for (const doc of indexed) {
|
|
77
|
+
for (const term of Object.keys(doc.terms)) {
|
|
78
|
+
docFrequency[term] = (docFrequency[term] || 0) + 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const sortedDocFrequency = {};
|
|
82
|
+
for (const term of Object.keys(docFrequency).sort()) {
|
|
83
|
+
sortedDocFrequency[term] = docFrequency[term];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const totalLength = indexed.reduce((sum, doc) => sum + doc.length, 0);
|
|
87
|
+
return {
|
|
88
|
+
documentCount: indexed.length,
|
|
89
|
+
termCount: Object.keys(sortedDocFrequency).length,
|
|
90
|
+
avgDocLength: indexed.length > 0 ? totalLength / indexed.length : 0,
|
|
91
|
+
docFrequency: sortedDocFrequency,
|
|
92
|
+
documents: indexed,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function inverseDocumentFrequency(documentCount, docFrequency) {
|
|
97
|
+
return Math.log(1 + (documentCount - docFrequency + 0.5) / (docFrequency + 0.5));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function bm25Score(queryTokens, doc, stats) {
|
|
101
|
+
if (stats.documentCount === 0 || doc.length === 0) return { score: 0, matchedTokens: [] };
|
|
102
|
+
|
|
103
|
+
let score = 0;
|
|
104
|
+
const matchedTokens = [];
|
|
105
|
+
for (const token of queryTokens) {
|
|
106
|
+
const tf = doc.terms[token] || 0;
|
|
107
|
+
if (tf === 0) continue;
|
|
108
|
+
const df = stats.docFrequency[token] || 0;
|
|
109
|
+
const idf = inverseDocumentFrequency(stats.documentCount, df);
|
|
110
|
+
const lengthNorm = 1 - BM25_B + BM25_B * (doc.length / (stats.avgDocLength || 1));
|
|
111
|
+
score += idf * ((tf * (BM25_K1 + 1)) / (tf + BM25_K1 * lengthNorm));
|
|
112
|
+
matchedTokens.push(token);
|
|
113
|
+
}
|
|
114
|
+
return { score, matchedTokens };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fully ordered ranking: score desc, then id asc. Zero-score documents are excluded.
|
|
118
|
+
export function rankLexical(query, stats, { limit = 20 } = {}) {
|
|
119
|
+
const queryTokens = lexicalQueryTokens(query);
|
|
120
|
+
if (queryTokens.length === 0) return [];
|
|
121
|
+
|
|
122
|
+
const ranked = [];
|
|
123
|
+
for (const doc of stats.documents) {
|
|
124
|
+
const { score, matchedTokens } = bm25Score(queryTokens, doc, stats);
|
|
125
|
+
if (score > 0) {
|
|
126
|
+
ranked.push({ id: doc.id, score, matchedTokens });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
ranked.sort((a, b) => {
|
|
130
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
131
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
132
|
+
});
|
|
133
|
+
return ranked.slice(0, limit).map((hit) => ({
|
|
134
|
+
...hit,
|
|
135
|
+
score: Number(hit.score.toFixed(6)),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Local, user-authored configuration for optional Phase B features
|
|
2
|
+
// (docs/AI-LEARNING-PHASE2.md, "Phase A implementation review", decision 5 / FU-4).
|
|
3
|
+
//
|
|
4
|
+
// design-ai never writes this file. It is durable, per-machine, user-authored
|
|
5
|
+
// configuration — the CLI only reads it. Presence of a configured provider is
|
|
6
|
+
// NOT enough to enable embeddings: every consumer still requires the explicit
|
|
7
|
+
// per-invocation --embeddings flag (config supplies the provider, the flag arms it).
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
export function defaultConfigFile() {
|
|
14
|
+
return process.env.DESIGN_AI_CONFIG_FILE || path.join(homedir(), ".design-ai", "config.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TOP_LEVEL_KEYS = new Set(["version", "embeddings"]);
|
|
18
|
+
const EMBEDDINGS_KEYS = new Set(["provider", "modelLabel"]);
|
|
19
|
+
const PROVIDER_KEYS = new Set(["command", "args"]);
|
|
20
|
+
|
|
21
|
+
function fail(message) {
|
|
22
|
+
return { present: true, config: null, error: message };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPlainObject(value) {
|
|
26
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateProvider(provider) {
|
|
30
|
+
if (!isPlainObject(provider)) return "embeddings.provider must be an object";
|
|
31
|
+
|
|
32
|
+
const unknown = Object.keys(provider).filter((key) => !PROVIDER_KEYS.has(key));
|
|
33
|
+
if (unknown.length > 0) return `embeddings.provider has unknown key(s): ${unknown.join(", ")}`;
|
|
34
|
+
|
|
35
|
+
if (typeof provider.command !== "string" || provider.command.trim() === "") {
|
|
36
|
+
return "embeddings.provider.command is required and must be a non-empty string";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (provider.args !== undefined) {
|
|
40
|
+
if (!Array.isArray(provider.args) || !provider.args.every((arg) => typeof arg === "string")) {
|
|
41
|
+
return "embeddings.provider.args must be an array of strings";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateEmbeddings(embeddings) {
|
|
49
|
+
if (!isPlainObject(embeddings)) return "embeddings must be an object";
|
|
50
|
+
|
|
51
|
+
const unknown = Object.keys(embeddings).filter((key) => !EMBEDDINGS_KEYS.has(key));
|
|
52
|
+
if (unknown.length > 0) return `embeddings has unknown key(s): ${unknown.join(", ")}`;
|
|
53
|
+
|
|
54
|
+
if (embeddings.provider === undefined) return "embeddings.provider is required";
|
|
55
|
+
const providerError = validateProvider(embeddings.provider);
|
|
56
|
+
if (providerError) return providerError;
|
|
57
|
+
|
|
58
|
+
if (embeddings.modelLabel !== undefined && typeof embeddings.modelLabel !== "string") {
|
|
59
|
+
return "embeddings.modelLabel must be a string";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateConfig(config) {
|
|
66
|
+
if (!isPlainObject(config)) return "config must be a JSON object";
|
|
67
|
+
|
|
68
|
+
const unknown = Object.keys(config).filter((key) => !TOP_LEVEL_KEYS.has(key));
|
|
69
|
+
if (unknown.length > 0) return `config has unknown top-level key(s): ${unknown.join(", ")}`;
|
|
70
|
+
|
|
71
|
+
if (config.version !== 1) return "config.version must be 1";
|
|
72
|
+
|
|
73
|
+
if (config.embeddings !== undefined) {
|
|
74
|
+
const embeddingsError = validateEmbeddings(config.embeddings);
|
|
75
|
+
if (embeddingsError) return embeddingsError;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Never writes; design-ai only reads this user-authored file.
|
|
82
|
+
export function loadLocalConfig(filePath = defaultConfigFile()) {
|
|
83
|
+
if (!existsSync(filePath)) return { present: false, config: null, error: "" };
|
|
84
|
+
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = readFileSync(filePath, "utf8");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return fail(`unreadable config file: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(raw);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return fail(`config file is not valid JSON: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const error = validateConfig(parsed);
|
|
100
|
+
if (error) return fail(error);
|
|
101
|
+
|
|
102
|
+
return { present: true, config: parsed, error: "" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Convenience accessor: the configured embedding provider, or null when absent
|
|
106
|
+
// or the config is malformed/missing. Callers still must gate on --embeddings.
|
|
107
|
+
export function configuredEmbeddingProvider(filePath = defaultConfigFile()) {
|
|
108
|
+
const { config } = loadLocalConfig(filePath);
|
|
109
|
+
return config?.embeddings?.provider || null;
|
|
110
|
+
}
|
package/cli/lib/mcp-server.mjs
CHANGED
|
@@ -10,6 +10,14 @@ import { DESIGN_AI_HOME, PACKAGE_ROOT } from "./paths.mjs";
|
|
|
10
10
|
const PROTOCOL_VERSION = "2025-11-25";
|
|
11
11
|
const MAX_TOOL_OUTPUT_BYTES = 220_000;
|
|
12
12
|
const DESIGN_AI_BIN = path.join(PACKAGE_ROOT, "cli", "bin", "design-ai.mjs");
|
|
13
|
+
const MCP_RESPONSE_METHODS = new Set([
|
|
14
|
+
"initialize",
|
|
15
|
+
"ping",
|
|
16
|
+
"tools/list",
|
|
17
|
+
"tools/call",
|
|
18
|
+
"resources/list",
|
|
19
|
+
"prompts/list",
|
|
20
|
+
]);
|
|
13
21
|
|
|
14
22
|
function optionalString(description = "") {
|
|
15
23
|
return { type: "string", description };
|
|
@@ -42,7 +50,7 @@ export const MCP_TOOLS = [
|
|
|
42
50
|
{
|
|
43
51
|
name: "design_ai_prompt",
|
|
44
52
|
title: "Generate an agent prompt",
|
|
45
|
-
description: "Generate a ready-to-use design-ai agent prompt from a brief. Read-only unless withLearning is true, which records local usage metadata.",
|
|
53
|
+
description: "Generate a ready-to-use design-ai agent prompt from a brief. Read-only unless withLearning is true, which records local usage metadata. Supports opt-in recall augmentation via withRecall, which enriches the output with brief-relevant shipped corpus knowledge.",
|
|
46
54
|
inputSchema: {
|
|
47
55
|
type: "object",
|
|
48
56
|
properties: {
|
|
@@ -51,6 +59,8 @@ export const MCP_TOOLS = [
|
|
|
51
59
|
withLearning: optionalBoolean("Include local learning preferences and record local usage metadata."),
|
|
52
60
|
learningCategory: optionalString("Optional learning category when withLearning is true."),
|
|
53
61
|
learningLimit: optionalInteger({ description: "Maximum learning entries, 1-100.", minimum: 1, maximum: 100 }),
|
|
62
|
+
withRecall: optionalBoolean("Opt-in recall augmentation: enriches the output with brief-relevant shipped corpus knowledge ranked by the deterministic lexical scorer. Requires no index and makes no network call."),
|
|
63
|
+
recallLimit: optionalInteger({ description: "Maximum recall passages, 1-20.", minimum: 1, maximum: 20 }),
|
|
54
64
|
json: optionalBoolean("Return machine-readable prompt plan JSON instead of Markdown."),
|
|
55
65
|
},
|
|
56
66
|
required: ["brief"],
|
|
@@ -60,7 +70,7 @@ export const MCP_TOOLS = [
|
|
|
60
70
|
{
|
|
61
71
|
name: "design_ai_pack",
|
|
62
72
|
title: "Generate prompt plus bounded context",
|
|
63
|
-
description: "Generate a prompt pack with relevant design-ai context files. Read-only unless withLearning is true, which records local usage metadata.",
|
|
73
|
+
description: "Generate a prompt pack with relevant design-ai context files. Read-only unless withLearning is true, which records local usage metadata. Supports opt-in recall augmentation via withRecall, which enriches the output with brief-relevant shipped corpus knowledge.",
|
|
64
74
|
inputSchema: {
|
|
65
75
|
type: "object",
|
|
66
76
|
properties: {
|
|
@@ -70,6 +80,8 @@ export const MCP_TOOLS = [
|
|
|
70
80
|
withLearning: optionalBoolean("Include local learning preferences and record local usage metadata."),
|
|
71
81
|
learningCategory: optionalString("Optional learning category when withLearning is true."),
|
|
72
82
|
learningLimit: optionalInteger({ description: "Maximum learning entries, 1-100.", minimum: 1, maximum: 100 }),
|
|
83
|
+
withRecall: optionalBoolean("Opt-in recall augmentation: enriches the output with brief-relevant shipped corpus knowledge ranked by the deterministic lexical scorer. Requires no index and makes no network call."),
|
|
84
|
+
recallLimit: optionalInteger({ description: "Maximum recall passages, 1-20.", minimum: 1, maximum: 20 }),
|
|
73
85
|
json: optionalBoolean("Return machine-readable pack JSON instead of Markdown."),
|
|
74
86
|
},
|
|
75
87
|
required: ["brief"],
|
|
@@ -79,13 +91,14 @@ export const MCP_TOOLS = [
|
|
|
79
91
|
{
|
|
80
92
|
name: "design_ai_search",
|
|
81
93
|
title: "Search design-ai corpus",
|
|
82
|
-
description: "Search knowledge, examples, skills, docs, agents, or commands. Read-only.",
|
|
94
|
+
description: "Search knowledge, examples, skills, docs, agents, or commands. Read-only. Supports an opt-in ranked mode for deterministic BM25-style results.",
|
|
83
95
|
inputSchema: {
|
|
84
96
|
type: "object",
|
|
85
97
|
properties: {
|
|
86
98
|
query: { type: "string", minLength: 2, description: "Search query." },
|
|
87
99
|
dir: optionalString("Optional corpus directory: knowledge, examples, skills, docs, agents, commands."),
|
|
88
100
|
limit: optionalInteger({ description: "Maximum hits, 1-500.", minimum: 1, maximum: 500 }),
|
|
101
|
+
ranked: optionalBoolean("Opt-in deterministic BM25-style ranked results. Requires no index and never builds one; falls back to a live corpus scan when no index is present."),
|
|
89
102
|
},
|
|
90
103
|
required: ["query"],
|
|
91
104
|
additionalProperties: false,
|
|
@@ -196,6 +209,51 @@ function assertString(value, name) {
|
|
|
196
209
|
return value;
|
|
197
210
|
}
|
|
198
211
|
|
|
212
|
+
function assertMcpInputValue(name, value, schema) {
|
|
213
|
+
if (schema.type === "string") {
|
|
214
|
+
if (typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
215
|
+
if (schema.minLength && value.trim() === "") {
|
|
216
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
217
|
+
}
|
|
218
|
+
if (schema.minLength && value.length < schema.minLength) {
|
|
219
|
+
throw new Error(`${name} must be at least ${schema.minLength} characters`);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (schema.type === "boolean") {
|
|
225
|
+
if (typeof value !== "boolean") throw new Error(`${name} must be a boolean`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (schema.type === "integer") {
|
|
230
|
+
if (!Number.isInteger(value)) throw new Error(`${name} must be an integer`);
|
|
231
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
232
|
+
throw new Error(`${name} must be at least ${schema.minimum}`);
|
|
233
|
+
}
|
|
234
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
235
|
+
throw new Error(`${name} must be at most ${schema.maximum}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function assertMcpToolInput(tool, input) {
|
|
241
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
242
|
+
throw new Error(`${tool.name} arguments must be an object`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const properties = tool.inputSchema.properties || {};
|
|
246
|
+
for (const name of tool.inputSchema.required || []) {
|
|
247
|
+
if (!Object.hasOwn(input, name)) throw new Error(`${tool.name}.${name} is required`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const [name, value] of Object.entries(input)) {
|
|
251
|
+
const schema = properties[name];
|
|
252
|
+
if (!schema) throw new Error(`Unknown argument for ${tool.name}: ${name}`);
|
|
253
|
+
assertMcpInputValue(`${tool.name}.${name}`, value, schema);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
199
257
|
function maybePush(args, flag, value) {
|
|
200
258
|
if (value !== undefined && value !== null && value !== "") args.push(flag, String(value));
|
|
201
259
|
}
|
|
@@ -228,6 +286,8 @@ export function buildCliInvocation(toolName, input = {}) {
|
|
|
228
286
|
maybeBool(args, "--with-learning", input.withLearning);
|
|
229
287
|
maybePush(args, "--learning-category", input.learningCategory);
|
|
230
288
|
maybePush(args, "--learning-limit", input.learningLimit);
|
|
289
|
+
maybeBool(args, "--with-recall", input.withRecall);
|
|
290
|
+
maybePush(args, "--recall-limit", input.recallLimit);
|
|
231
291
|
maybeBool(args, "--json", input.json);
|
|
232
292
|
return { args, stdin };
|
|
233
293
|
}
|
|
@@ -239,6 +299,8 @@ export function buildCliInvocation(toolName, input = {}) {
|
|
|
239
299
|
maybeBool(args, "--with-learning", input.withLearning);
|
|
240
300
|
maybePush(args, "--learning-category", input.learningCategory);
|
|
241
301
|
maybePush(args, "--learning-limit", input.learningLimit);
|
|
302
|
+
maybeBool(args, "--with-recall", input.withRecall);
|
|
303
|
+
maybePush(args, "--recall-limit", input.recallLimit);
|
|
242
304
|
maybeBool(args, "--json", input.json);
|
|
243
305
|
return { args, stdin };
|
|
244
306
|
}
|
|
@@ -247,6 +309,7 @@ export function buildCliInvocation(toolName, input = {}) {
|
|
|
247
309
|
args.push("search", assertString(input.query, "query"), "--json");
|
|
248
310
|
maybePush(args, "--dir", input.dir);
|
|
249
311
|
maybePush(args, "--limit", input.limit);
|
|
312
|
+
maybeBool(args, "--ranked", input.ranked);
|
|
250
313
|
return { args, stdin };
|
|
251
314
|
}
|
|
252
315
|
|
|
@@ -328,6 +391,9 @@ export function runDesignAiCli(args, { stdin = "" } = {}) {
|
|
|
328
391
|
}
|
|
329
392
|
|
|
330
393
|
export async function callMcpTool(name, input = {}, runCli = runDesignAiCli) {
|
|
394
|
+
const tool = MCP_TOOLS.find((item) => item.name === name);
|
|
395
|
+
if (tool) assertMcpToolInput(tool, input || {});
|
|
396
|
+
|
|
331
397
|
const invocation = buildCliInvocation(name, input || {});
|
|
332
398
|
const result = await runCli(invocation.args, { stdin: invocation.stdin });
|
|
333
399
|
const text = [
|
|
@@ -349,14 +415,131 @@ function errorResponse(id, code, message) {
|
|
|
349
415
|
return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
|
|
350
416
|
}
|
|
351
417
|
|
|
418
|
+
function chooseProtocolVersion(requestedVersion) {
|
|
419
|
+
return requestedVersion === PROTOCOL_VERSION ? requestedVersion : PROTOCOL_VERSION;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isObjectRecord(value) {
|
|
423
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validateMcpRequestEnvelope(message) {
|
|
427
|
+
if (!isObjectRecord(message)) {
|
|
428
|
+
return "Invalid MCP request: request must be an object";
|
|
429
|
+
}
|
|
430
|
+
if (message.jsonrpc !== "2.0") {
|
|
431
|
+
return 'Invalid MCP request: jsonrpc must be "2.0"';
|
|
432
|
+
}
|
|
433
|
+
if (hasRequestId(message) && !isValidMcpRequestId(message.id)) {
|
|
434
|
+
return "Invalid MCP request: id must be a string, number, or null";
|
|
435
|
+
}
|
|
436
|
+
return "";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function validateMcpRequestMethod(message) {
|
|
440
|
+
if (!Object.hasOwn(message, "method")) {
|
|
441
|
+
return "Invalid MCP request: missing method";
|
|
442
|
+
}
|
|
443
|
+
if (typeof message.method !== "string" || message.method.trim() === "") {
|
|
444
|
+
return "Invalid MCP request: method must be a non-empty string";
|
|
445
|
+
}
|
|
446
|
+
return "";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function isMcpNotificationMethod(method) {
|
|
450
|
+
return typeof method === "string" && method.startsWith("notifications/");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function validateMcpNotificationRequest(message) {
|
|
454
|
+
if (isMcpNotificationMethod(message.method) && hasRequestId(message)) {
|
|
455
|
+
return `Invalid MCP request: ${message.method} must not include id`;
|
|
456
|
+
}
|
|
457
|
+
return "";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function validateMcpRequestIdRequirement(message) {
|
|
461
|
+
if (MCP_RESPONSE_METHODS.has(message.method) && !hasRequestId(message)) {
|
|
462
|
+
return `Invalid MCP request: ${message.method} must include id`;
|
|
463
|
+
}
|
|
464
|
+
return "";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function validateOptionalObjectParams(method, params) {
|
|
468
|
+
if (params === undefined || isObjectRecord(params)) return "";
|
|
469
|
+
return `${method} params must be an object when provided`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function validateToolCallParams(params) {
|
|
473
|
+
if (!isObjectRecord(params)) {
|
|
474
|
+
return "tools/call params must be an object";
|
|
475
|
+
}
|
|
476
|
+
if (typeof params.name !== "string" || params.name.trim() === "") {
|
|
477
|
+
return "tools/call params.name must be a non-empty string";
|
|
478
|
+
}
|
|
479
|
+
if (Object.hasOwn(params, "arguments") && params.arguments !== undefined && !isObjectRecord(params.arguments)) {
|
|
480
|
+
return "tools/call params.arguments must be an object when provided";
|
|
481
|
+
}
|
|
482
|
+
return "";
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function validateInitializeParams(params) {
|
|
486
|
+
if (!isObjectRecord(params)) {
|
|
487
|
+
return "initialize params must be an object";
|
|
488
|
+
}
|
|
489
|
+
if (Object.hasOwn(params, "protocolVersion") && typeof params.protocolVersion !== "string") {
|
|
490
|
+
return "initialize params.protocolVersion must be a string";
|
|
491
|
+
}
|
|
492
|
+
if (typeof params.protocolVersion === "string" && params.protocolVersion.trim() === "") {
|
|
493
|
+
return "initialize params.protocolVersion must be a non-empty string";
|
|
494
|
+
}
|
|
495
|
+
return "";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function hasRequestId(message) {
|
|
499
|
+
return isObjectRecord(message) && Object.hasOwn(message, "id");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function isValidMcpRequestId(id) {
|
|
503
|
+
return id === null || typeof id === "string" || (typeof id === "number" && Number.isFinite(id));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function responseIdForMessage(message) {
|
|
507
|
+
if (!hasRequestId(message)) return undefined;
|
|
508
|
+
return isValidMcpRequestId(message.id) ? message.id : undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function isInvalidRequestResponse(response) {
|
|
512
|
+
return response?.error?.code === -32600;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function shouldWriteMcpResponse(message, response) {
|
|
516
|
+
if (!response) return false;
|
|
517
|
+
return hasRequestId(message) || isInvalidRequestResponse(response);
|
|
518
|
+
}
|
|
519
|
+
|
|
352
520
|
export async function handleMcpRequest(message, { runCli = runDesignAiCli } = {}) {
|
|
353
|
-
const
|
|
521
|
+
const id = responseIdForMessage(message);
|
|
522
|
+
const envelopeError = validateMcpRequestEnvelope(message);
|
|
523
|
+
if (envelopeError) return errorResponse(id, -32600, envelopeError);
|
|
524
|
+
|
|
525
|
+
const methodError = validateMcpRequestMethod(message);
|
|
526
|
+
if (methodError) return errorResponse(id, -32600, methodError);
|
|
527
|
+
|
|
528
|
+
const notificationError = validateMcpNotificationRequest(message);
|
|
529
|
+
if (notificationError) return errorResponse(id, -32600, notificationError);
|
|
530
|
+
|
|
531
|
+
const requestIdError = validateMcpRequestIdRequirement(message);
|
|
532
|
+
if (requestIdError) return errorResponse(id, -32600, requestIdError);
|
|
354
533
|
|
|
355
|
-
|
|
534
|
+
const { method, params } = message;
|
|
356
535
|
|
|
357
536
|
if (method === "initialize") {
|
|
537
|
+
const initializeParams = params === undefined ? {} : params;
|
|
538
|
+
const paramsError = validateInitializeParams(initializeParams);
|
|
539
|
+
if (paramsError) return errorResponse(id, -32602, paramsError);
|
|
540
|
+
|
|
358
541
|
return successResponse(id, {
|
|
359
|
-
protocolVersion:
|
|
542
|
+
protocolVersion: chooseProtocolVersion(initializeParams.protocolVersion),
|
|
360
543
|
capabilities: {
|
|
361
544
|
tools: { listChanged: false },
|
|
362
545
|
},
|
|
@@ -368,19 +551,36 @@ export async function handleMcpRequest(message, { runCli = runDesignAiCli } = {}
|
|
|
368
551
|
});
|
|
369
552
|
}
|
|
370
553
|
|
|
371
|
-
if (method
|
|
372
|
-
if (method === "ping")
|
|
554
|
+
if (isMcpNotificationMethod(method)) return null;
|
|
555
|
+
if (method === "ping") {
|
|
556
|
+
const paramsError = validateOptionalObjectParams(method, params);
|
|
557
|
+
if (paramsError) return errorResponse(id, -32602, paramsError);
|
|
558
|
+
return successResponse(id, {});
|
|
559
|
+
}
|
|
373
560
|
|
|
374
561
|
if (method === "tools/list") {
|
|
562
|
+
const paramsError = validateOptionalObjectParams(method, params);
|
|
563
|
+
if (paramsError) return errorResponse(id, -32602, paramsError);
|
|
375
564
|
return successResponse(id, { tools: MCP_TOOLS });
|
|
376
565
|
}
|
|
377
566
|
|
|
378
567
|
if (method === "tools/call") {
|
|
568
|
+
const paramsError = validateToolCallParams(params);
|
|
569
|
+
if (paramsError) return errorResponse(id, -32602, paramsError);
|
|
570
|
+
|
|
379
571
|
const name = params.name;
|
|
380
572
|
const tool = MCP_TOOLS.find((item) => item.name === name);
|
|
381
573
|
if (!tool) return errorResponse(id, -32602, `Unknown tool: ${name}`);
|
|
574
|
+
|
|
575
|
+
const toolArguments = params.arguments === undefined ? {} : params.arguments;
|
|
382
576
|
try {
|
|
383
|
-
|
|
577
|
+
assertMcpToolInput(tool, toolArguments);
|
|
578
|
+
} catch (validationError) {
|
|
579
|
+
return errorResponse(id, -32602, validationError.message || String(validationError));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
const result = await callMcpTool(name, toolArguments, runCli);
|
|
384
584
|
return successResponse(id, result);
|
|
385
585
|
} catch (error) {
|
|
386
586
|
return successResponse(id, {
|
|
@@ -390,8 +590,16 @@ export async function handleMcpRequest(message, { runCli = runDesignAiCli } = {}
|
|
|
390
590
|
}
|
|
391
591
|
}
|
|
392
592
|
|
|
393
|
-
if (method === "resources/list")
|
|
394
|
-
|
|
593
|
+
if (method === "resources/list") {
|
|
594
|
+
const paramsError = validateOptionalObjectParams(method, params);
|
|
595
|
+
if (paramsError) return errorResponse(id, -32602, paramsError);
|
|
596
|
+
return successResponse(id, { resources: [] });
|
|
597
|
+
}
|
|
598
|
+
if (method === "prompts/list") {
|
|
599
|
+
const paramsError = validateOptionalObjectParams(method, params);
|
|
600
|
+
if (paramsError) return errorResponse(id, -32602, paramsError);
|
|
601
|
+
return successResponse(id, { prompts: [] });
|
|
602
|
+
}
|
|
395
603
|
|
|
396
604
|
return errorResponse(id, -32601, `Method not found: ${method}`);
|
|
397
605
|
}
|
|
@@ -411,11 +619,11 @@ export function startMcpStdioServer({ input = process.stdin, output = process.st
|
|
|
411
619
|
|
|
412
620
|
try {
|
|
413
621
|
const response = await handleMcpRequest(message);
|
|
414
|
-
if (
|
|
622
|
+
if (shouldWriteMcpResponse(message, response)) {
|
|
415
623
|
output.write(`${JSON.stringify(response)}\n`);
|
|
416
624
|
}
|
|
417
625
|
} catch (requestError) {
|
|
418
|
-
const id = message
|
|
626
|
+
const id = responseIdForMessage(message) ?? null;
|
|
419
627
|
output.write(`${JSON.stringify(errorResponse(id, -32603, requestError.message || String(requestError)))}\n`);
|
|
420
628
|
}
|
|
421
629
|
});
|