@handamade/psi-mcp 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index-builder.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/init.js +1 -0
- package/dist/psi-index.json +284 -4
- package/dist/server.js +8 -4
- package/dist/store.d.ts +5 -3
- package/dist/store.js +18 -1
- package/dist/types.d.ts +32 -0
- package/llms.txt +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @handamade/psi-mcp
|
|
2
2
|
|
|
3
|
-
MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the 13 React 19 components, every semantic token with its OKLCH derivation formula resolved across all four themes, and the usage guidance — through two read-only tools. It does not write code.
|
|
3
|
+
MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the 13 React 19 components, 3 composition patterns (D47) that recipe them together with gap-annotated compose trees, every semantic token with its OKLCH derivation formula resolved across all four themes, and the usage guidance — through two read-only tools. It does not write code.
|
|
4
4
|
|
|
5
5
|
## Connect
|
|
6
6
|
|
package/dist/index-builder.js
CHANGED
|
@@ -32,6 +32,9 @@ async function readJson(path) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function buildIndex(inputs) {
|
|
34
34
|
const manifest = (await readJson(join(inputs.reactDist, "manifest.json")));
|
|
35
|
+
// Fail-fast is deliberate: requires @handamade/psi-react built first — same posture
|
|
36
|
+
// as the manifest/guidance reads above.
|
|
37
|
+
const patternsFile = (await readJson(join(inputs.reactDist, "patterns.json")));
|
|
35
38
|
const guidance = (await readJson(join(inputs.tokensDist, "guidance.json")));
|
|
36
39
|
const resolved = {};
|
|
37
40
|
for (const theme of THEMES) {
|
|
@@ -61,6 +64,7 @@ export async function buildIndex(inputs) {
|
|
|
61
64
|
themes: [...THEMES],
|
|
62
65
|
components,
|
|
63
66
|
tokens,
|
|
67
|
+
patterns: patternsFile.patterns,
|
|
64
68
|
scales: light.scales,
|
|
65
69
|
topics: {
|
|
66
70
|
...guidance,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PsiIndex } from "./types.js";
|
|
2
|
-
export type { PsiIndex, ComponentEntry, TokenEntry, PropDoc } from "./types.js";
|
|
2
|
+
export type { PsiIndex, ComponentEntry, TokenEntry, PropDoc, PatternEntry, PatternNode, PatternParameter, } from "./types.js";
|
|
3
3
|
export { buildIndex, GETTING_STARTED } from "./index-builder.js";
|
|
4
4
|
export { createStore } from "./store.js";
|
|
5
5
|
export type { Store, Brief, Detail } from "./store.js";
|
package/dist/init.js
CHANGED
|
@@ -28,6 +28,7 @@ export function renderAgentsBlock(index) {
|
|
|
28
28
|
"- Never hardcode colors — bind var(--psi-*) custom properties.",
|
|
29
29
|
"- Import all four token CSS files: base.css, one theme css, components.css, utilities.css (utilities is REQUIRED — .psi-container + reduced-motion zeroing) — PLUS @handamade/psi-react/styles (component CSS; separate export, never auto-imported).",
|
|
30
30
|
`- Themes: ${index.themes.join(" | ")} — set data-psi-theme on <html> or a subtree root.`,
|
|
31
|
+
"- Before composing multi-component UI, search for a matching pattern and ask its clarifying parameters; only freehand when no pattern matches.",
|
|
31
32
|
"",
|
|
32
33
|
`## Components (${index.components.length})`,
|
|
33
34
|
...index.components.map((c) => `- ${c.name}${axes(c)}`),
|
package/dist/psi-index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.6.0",
|
|
3
3
|
"themes": [
|
|
4
4
|
"light",
|
|
5
5
|
"dark",
|
|
@@ -297,7 +297,26 @@
|
|
|
297
297
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
298
298
|
}
|
|
299
299
|
],
|
|
300
|
-
"slots": [
|
|
300
|
+
"slots": [
|
|
301
|
+
{
|
|
302
|
+
"name": "label",
|
|
303
|
+
"accepts": {},
|
|
304
|
+
"cardinality": "0..1",
|
|
305
|
+
"order": 1
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"name": "body",
|
|
309
|
+
"accepts": {},
|
|
310
|
+
"cardinality": "1..*",
|
|
311
|
+
"order": 2
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"name": "description",
|
|
315
|
+
"accepts": {},
|
|
316
|
+
"cardinality": "0..1",
|
|
317
|
+
"order": 3
|
|
318
|
+
}
|
|
319
|
+
],
|
|
301
320
|
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Field\n\nLabeled form-row wrapper: label above, control, one message line below — description normally, error when set (aria-live). Auto-wires id/aria-describedby/aria-invalid into Input and Select (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `label` | `ReactNode` | — | no | Label content; renders a <label> (or <legend> in group mode). |\n| `description` | `ReactNode` | — | no | Helper line under the control; replaced by error when error is set. |\n| `error` | `ReactNode` | — | no | Error content; when truthy it replaces the description and switches the field (and a wrapped Input/Select) into error state. |\n| `required` | `boolean` | false | no | Renders the required marker and flows `required` to the control. |\n| `group` | `boolean` | false | no | Group mode: fieldset/legend wrapping several self-labeled controls (Checkbox/Switch); the message describes the whole group. |\n| `htmlFor` | `string` | — | no | Override the generated control id (pair it with the same id on the control). |\n| `ref` | `Ref<HTMLDivElement \\| HTMLFieldSetElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focus moves to the wrapped control; the label is announced with it. |\n\nWires label association, aria-describedby and aria-invalid into a wrapped Input/Select automatically; the message line is aria-live=polite. Group mode renders fieldset/legend.\n\n## Theming\n\nOverride `--psi-field-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
302
321
|
},
|
|
303
322
|
{
|
|
@@ -982,11 +1001,11 @@
|
|
|
982
1001
|
},
|
|
983
1002
|
"ember": {
|
|
984
1003
|
"oklch": {
|
|
985
|
-
"l": 0.
|
|
1004
|
+
"l": 0.14,
|
|
986
1005
|
"c": 0.011,
|
|
987
1006
|
"h": 72
|
|
988
1007
|
},
|
|
989
|
-
"hex": "#
|
|
1008
|
+
"hex": "#0c0805"
|
|
990
1009
|
}
|
|
991
1010
|
}
|
|
992
1011
|
},
|
|
@@ -1690,6 +1709,44 @@
|
|
|
1690
1709
|
}
|
|
1691
1710
|
}
|
|
1692
1711
|
},
|
|
1712
|
+
{
|
|
1713
|
+
"name": "fillStaticWhite",
|
|
1714
|
+
"formula": "ref(fgStaticWhite)",
|
|
1715
|
+
"values": {
|
|
1716
|
+
"light": {
|
|
1717
|
+
"oklch": {
|
|
1718
|
+
"l": 1,
|
|
1719
|
+
"c": 0,
|
|
1720
|
+
"h": 250
|
|
1721
|
+
},
|
|
1722
|
+
"hex": "#ffffff"
|
|
1723
|
+
},
|
|
1724
|
+
"dark": {
|
|
1725
|
+
"oklch": {
|
|
1726
|
+
"l": 1,
|
|
1727
|
+
"c": 0,
|
|
1728
|
+
"h": 250
|
|
1729
|
+
},
|
|
1730
|
+
"hex": "#ffffff"
|
|
1731
|
+
},
|
|
1732
|
+
"acme": {
|
|
1733
|
+
"oklch": {
|
|
1734
|
+
"l": 1,
|
|
1735
|
+
"c": 0,
|
|
1736
|
+
"h": 80
|
|
1737
|
+
},
|
|
1738
|
+
"hex": "#ffffff"
|
|
1739
|
+
},
|
|
1740
|
+
"ember": {
|
|
1741
|
+
"oklch": {
|
|
1742
|
+
"l": 1,
|
|
1743
|
+
"c": 0,
|
|
1744
|
+
"h": 49
|
|
1745
|
+
},
|
|
1746
|
+
"hex": "#ffffff"
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1693
1750
|
{
|
|
1694
1751
|
"name": "borderFaint",
|
|
1695
1752
|
"formula": "ref(fgPrimary) → alpha(0.08)",
|
|
@@ -1979,6 +2036,219 @@
|
|
|
1979
2036
|
"hex": "#0c0a09"
|
|
1980
2037
|
}
|
|
1981
2038
|
}
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
"name": "bgInverted",
|
|
2042
|
+
"formula": "ref(fgPrimary)",
|
|
2043
|
+
"values": {
|
|
2044
|
+
"light": {
|
|
2045
|
+
"oklch": {
|
|
2046
|
+
"l": 0.3,
|
|
2047
|
+
"c": 0.03,
|
|
2048
|
+
"h": 250
|
|
2049
|
+
},
|
|
2050
|
+
"hex": "#222f3c"
|
|
2051
|
+
},
|
|
2052
|
+
"dark": {
|
|
2053
|
+
"oklch": {
|
|
2054
|
+
"l": 0.93,
|
|
2055
|
+
"c": 0.01,
|
|
2056
|
+
"h": 250
|
|
2057
|
+
},
|
|
2058
|
+
"hex": "#e3e8ee"
|
|
2059
|
+
},
|
|
2060
|
+
"acme": {
|
|
2061
|
+
"oklch": {
|
|
2062
|
+
"l": 0.3,
|
|
2063
|
+
"c": 0.03,
|
|
2064
|
+
"h": 30
|
|
2065
|
+
},
|
|
2066
|
+
"hex": "#3c2825"
|
|
2067
|
+
},
|
|
2068
|
+
"ember": {
|
|
2069
|
+
"oklch": {
|
|
2070
|
+
"l": 0.949,
|
|
2071
|
+
"c": 0.011,
|
|
2072
|
+
"h": 72
|
|
2073
|
+
},
|
|
2074
|
+
"hex": "#f3ede6"
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
"name": "fgOnInverted",
|
|
2080
|
+
"formula": "ref(bgPrimary)",
|
|
2081
|
+
"values": {
|
|
2082
|
+
"light": {
|
|
2083
|
+
"oklch": {
|
|
2084
|
+
"l": 0.95,
|
|
2085
|
+
"c": 0.005,
|
|
2086
|
+
"h": 250
|
|
2087
|
+
},
|
|
2088
|
+
"hex": "#eceff2"
|
|
2089
|
+
},
|
|
2090
|
+
"dark": {
|
|
2091
|
+
"oklch": {
|
|
2092
|
+
"l": 0.15,
|
|
2093
|
+
"c": 0.01,
|
|
2094
|
+
"h": 250
|
|
2095
|
+
},
|
|
2096
|
+
"hex": "#080c0f"
|
|
2097
|
+
},
|
|
2098
|
+
"acme": {
|
|
2099
|
+
"oklch": {
|
|
2100
|
+
"l": 0.96,
|
|
2101
|
+
"c": 0.01,
|
|
2102
|
+
"h": 80
|
|
2103
|
+
},
|
|
2104
|
+
"hex": "#f5f1ea"
|
|
2105
|
+
},
|
|
2106
|
+
"ember": {
|
|
2107
|
+
"oklch": {
|
|
2108
|
+
"l": 0.147,
|
|
2109
|
+
"c": 0.004,
|
|
2110
|
+
"h": 49
|
|
2111
|
+
},
|
|
2112
|
+
"hex": "#0c0a09"
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
],
|
|
2117
|
+
"patterns": [
|
|
2118
|
+
{
|
|
2119
|
+
"id": "destructive-confirm",
|
|
2120
|
+
"intent": "Dialog confirming a destructive action",
|
|
2121
|
+
"match": [
|
|
2122
|
+
"delete confirmation",
|
|
2123
|
+
"are you sure",
|
|
2124
|
+
"destructive dialog",
|
|
2125
|
+
"confirm delete"
|
|
2126
|
+
],
|
|
2127
|
+
"compose": {
|
|
2128
|
+
"component": "Dialog",
|
|
2129
|
+
"slots": {
|
|
2130
|
+
"title": [
|
|
2131
|
+
"{content:title}"
|
|
2132
|
+
],
|
|
2133
|
+
"body": [
|
|
2134
|
+
"{content:consequence}"
|
|
2135
|
+
],
|
|
2136
|
+
"footer": [
|
|
2137
|
+
{
|
|
2138
|
+
"component": "Button",
|
|
2139
|
+
"props": {
|
|
2140
|
+
"variant": "ghost",
|
|
2141
|
+
"size": "{param:size}"
|
|
2142
|
+
},
|
|
2143
|
+
"content": "cancel-label"
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
"component": "Button",
|
|
2147
|
+
"props": {
|
|
2148
|
+
"variant": "danger",
|
|
2149
|
+
"size": "{param:size}"
|
|
2150
|
+
},
|
|
2151
|
+
"content": "confirm-label"
|
|
2152
|
+
}
|
|
2153
|
+
]
|
|
2154
|
+
}
|
|
2155
|
+
},
|
|
2156
|
+
"parameters": [
|
|
2157
|
+
{
|
|
2158
|
+
"key": "size",
|
|
2159
|
+
"ask": "Button size?",
|
|
2160
|
+
"options": [
|
|
2161
|
+
32,
|
|
2162
|
+
40
|
|
2163
|
+
],
|
|
2164
|
+
"default": 32
|
|
2165
|
+
}
|
|
2166
|
+
],
|
|
2167
|
+
"content": {
|
|
2168
|
+
"title": "<Delete the object?>",
|
|
2169
|
+
"consequence": "<what is permanently lost>",
|
|
2170
|
+
"cancel-label": "Cancel",
|
|
2171
|
+
"confirm-label": "<verb the object>"
|
|
2172
|
+
},
|
|
2173
|
+
"gaps": [],
|
|
2174
|
+
"blocked": false,
|
|
2175
|
+
"preset": "<Dialog\n footer={<>\n <Button size={32} variant=\"ghost\">Cancel</Button>\n <Button size={32} variant=\"danger\"><verb the object></Button>\n </>}\n title=\"<Delete the object?>\"\n>\n <what is permanently lost>\n</Dialog>\n"
|
|
2176
|
+
},
|
|
2177
|
+
{
|
|
2178
|
+
"id": "filter-toolbar",
|
|
2179
|
+
"intent": "Toolbar of filter controls: search, category select, active-filter tags",
|
|
2180
|
+
"match": [
|
|
2181
|
+
"filter toolbar",
|
|
2182
|
+
"filter bar",
|
|
2183
|
+
"search and filter",
|
|
2184
|
+
"faceted filters"
|
|
2185
|
+
],
|
|
2186
|
+
"compose": {
|
|
2187
|
+
"component": "Toolbar",
|
|
2188
|
+
"slots": {
|
|
2189
|
+
"body": [
|
|
2190
|
+
{
|
|
2191
|
+
"component": "Input"
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
"component": "Select"
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
"component": "Tag",
|
|
2198
|
+
"content": "example-filter"
|
|
2199
|
+
}
|
|
2200
|
+
]
|
|
2201
|
+
}
|
|
2202
|
+
},
|
|
2203
|
+
"parameters": [],
|
|
2204
|
+
"content": {
|
|
2205
|
+
"example-filter": "<active filter>"
|
|
2206
|
+
},
|
|
2207
|
+
"gaps": [
|
|
2208
|
+
"Toolbar"
|
|
2209
|
+
],
|
|
2210
|
+
"blocked": true,
|
|
2211
|
+
"preset": null
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
"id": "settings-form-row",
|
|
2215
|
+
"intent": "Labeled settings row: a self-labeled control with a group label and helper text",
|
|
2216
|
+
"match": [
|
|
2217
|
+
"settings row",
|
|
2218
|
+
"form row",
|
|
2219
|
+
"labeled switch",
|
|
2220
|
+
"preference toggle",
|
|
2221
|
+
"settings form"
|
|
2222
|
+
],
|
|
2223
|
+
"compose": {
|
|
2224
|
+
"component": "Field",
|
|
2225
|
+
"props": {
|
|
2226
|
+
"group": true
|
|
2227
|
+
},
|
|
2228
|
+
"slots": {
|
|
2229
|
+
"label": [
|
|
2230
|
+
"{content:row-label}"
|
|
2231
|
+
],
|
|
2232
|
+
"body": [
|
|
2233
|
+
{
|
|
2234
|
+
"component": "Switch",
|
|
2235
|
+
"content": "control-label"
|
|
2236
|
+
}
|
|
2237
|
+
],
|
|
2238
|
+
"description": [
|
|
2239
|
+
"{content:row-description}"
|
|
2240
|
+
]
|
|
2241
|
+
}
|
|
2242
|
+
},
|
|
2243
|
+
"parameters": [],
|
|
2244
|
+
"content": {
|
|
2245
|
+
"row-label": "<setting name>",
|
|
2246
|
+
"control-label": "<what turning it on does>",
|
|
2247
|
+
"row-description": "<one-line consequence of the setting>"
|
|
2248
|
+
},
|
|
2249
|
+
"gaps": [],
|
|
2250
|
+
"blocked": false,
|
|
2251
|
+
"preset": "<Field\n description=\"<one-line consequence of the setting>\"\n group={true}\n label=\"<setting name>\"\n>\n <Switch><what turning it on does></Switch>\n</Field>\n"
|
|
1982
2252
|
}
|
|
1983
2253
|
],
|
|
1984
2254
|
"scales": {
|
|
@@ -2042,6 +2312,11 @@
|
|
|
2042
2312
|
"overlay": 1000,
|
|
2043
2313
|
"tooltip": 1100
|
|
2044
2314
|
}
|
|
2315
|
+
},
|
|
2316
|
+
"scopes": {
|
|
2317
|
+
"space": [
|
|
2318
|
+
"gap"
|
|
2319
|
+
]
|
|
2045
2320
|
}
|
|
2046
2321
|
},
|
|
2047
2322
|
"topics": {
|
|
@@ -2258,6 +2533,11 @@
|
|
|
2258
2533
|
"overlay": 1000,
|
|
2259
2534
|
"tooltip": 1100
|
|
2260
2535
|
}
|
|
2536
|
+
},
|
|
2537
|
+
"scopes": {
|
|
2538
|
+
"space": [
|
|
2539
|
+
"gap"
|
|
2540
|
+
]
|
|
2261
2541
|
}
|
|
2262
2542
|
}
|
|
2263
2543
|
}
|
package/dist/server.js
CHANGED
|
@@ -4,8 +4,9 @@ export function createPsiServer(store, version) {
|
|
|
4
4
|
const server = new McpServer({ name: "psi", version });
|
|
5
5
|
server.registerTool("search", {
|
|
6
6
|
title: "Search the Psi design system",
|
|
7
|
-
description: "Keyword search over Psi components, semantic tokens, and
|
|
8
|
-
"Returns compact briefs with ids for the get tool. Empty query
|
|
7
|
+
description: "Keyword search over Psi components, composition patterns, semantic tokens, and " +
|
|
8
|
+
"guidance topics. Returns compact briefs with ids for the get tool. Empty query " +
|
|
9
|
+
"returns an overview.",
|
|
9
10
|
inputSchema: { query: z.string().describe("Keywords, e.g. 'button variants' or 'bg token'") },
|
|
10
11
|
}, async ({ query }) => ({
|
|
11
12
|
content: [{ type: "text", text: JSON.stringify(store.search(query), null, 1) }],
|
|
@@ -13,8 +14,11 @@ export function createPsiServer(store, version) {
|
|
|
13
14
|
server.registerTool("get", {
|
|
14
15
|
title: "Get full Psi detail by id",
|
|
15
16
|
description: "Full detail for one id from search: component (props, defaults, a11y doc), " +
|
|
16
|
-
"
|
|
17
|
-
"
|
|
17
|
+
"pattern (composition recipe: compose tree, clarifying parameters, content, " +
|
|
18
|
+
"gaps/blocked status, and a copy-paste preset when unblocked), token (formula + " +
|
|
19
|
+
"resolved OKLCH/hex in all four themes), or topic (guidance). Accepts " +
|
|
20
|
+
"'component:Button', 'pattern:destructive-confirm', 'token:bgPrimary', " +
|
|
21
|
+
"'topic:variants', or a bare name.",
|
|
18
22
|
inputSchema: { id: z.string().describe("An id from search results, or a bare name") },
|
|
19
23
|
}, async ({ id }) => {
|
|
20
24
|
const detail = store.get(id);
|
package/dist/store.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ComponentEntry, PsiIndex, TokenEntry } from "./types.js";
|
|
1
|
+
import type { ComponentEntry, PatternEntry, PsiIndex, TokenEntry } from "./types.js";
|
|
2
2
|
export interface Brief {
|
|
3
3
|
id: string;
|
|
4
|
-
kind: "component" | "token" | "topic";
|
|
4
|
+
kind: "component" | "token" | "topic" | "pattern";
|
|
5
5
|
title: string;
|
|
6
6
|
summary: string;
|
|
7
7
|
}
|
|
@@ -9,7 +9,9 @@ export type Detail = ({
|
|
|
9
9
|
kind: "component";
|
|
10
10
|
} & ComponentEntry) | ({
|
|
11
11
|
kind: "token";
|
|
12
|
-
} & TokenEntry) | {
|
|
12
|
+
} & TokenEntry) | ({
|
|
13
|
+
kind: "pattern";
|
|
14
|
+
} & PatternEntry) | {
|
|
13
15
|
kind: "topic";
|
|
14
16
|
name: string;
|
|
15
17
|
content: unknown;
|
package/dist/store.js
CHANGED
|
@@ -29,6 +29,12 @@ function componentSummary(c) {
|
|
|
29
29
|
parts.push(`props: ${c.props.map((p) => p.name).slice(0, 6).join(", ")}`);
|
|
30
30
|
return parts.join(" — ").slice(0, 220);
|
|
31
31
|
}
|
|
32
|
+
function patternSummary(p) {
|
|
33
|
+
let summary = `${p.intent} — ${p.match.join(", ")}, ${p.parameters.length} parameters`;
|
|
34
|
+
if (p.blocked)
|
|
35
|
+
summary += `, blocked (gaps: ${p.gaps.join(", ")})`;
|
|
36
|
+
return summary;
|
|
37
|
+
}
|
|
32
38
|
export function createStore(index) {
|
|
33
39
|
const briefs = [
|
|
34
40
|
...index.components.map((c) => ({
|
|
@@ -37,6 +43,12 @@ export function createStore(index) {
|
|
|
37
43
|
title: c.name,
|
|
38
44
|
summary: componentSummary(c),
|
|
39
45
|
})),
|
|
46
|
+
...index.patterns.map((p) => ({
|
|
47
|
+
id: `pattern:${p.id}`,
|
|
48
|
+
kind: "pattern",
|
|
49
|
+
title: p.id,
|
|
50
|
+
summary: patternSummary(p),
|
|
51
|
+
})),
|
|
40
52
|
...Object.keys(index.topics).map((name) => ({
|
|
41
53
|
id: `topic:${name}`,
|
|
42
54
|
kind: "topic",
|
|
@@ -67,7 +79,7 @@ export function createStore(index) {
|
|
|
67
79
|
function search(query) {
|
|
68
80
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
69
81
|
if (terms.length === 0) {
|
|
70
|
-
// Overview: all components
|
|
82
|
+
// Overview: all components, patterns, and topics (tokens are discoverable via query).
|
|
71
83
|
return briefs.filter((b) => b.kind !== "token").slice(0, MAX_RESULTS + 10);
|
|
72
84
|
}
|
|
73
85
|
return briefs
|
|
@@ -91,6 +103,11 @@ export function createStore(index) {
|
|
|
91
103
|
if (t)
|
|
92
104
|
return { kind: "token", ...t };
|
|
93
105
|
}
|
|
106
|
+
if (want("pattern")) {
|
|
107
|
+
const p = index.patterns.find((p) => p.id.toLowerCase() === name);
|
|
108
|
+
if (p)
|
|
109
|
+
return { kind: "pattern", ...p };
|
|
110
|
+
}
|
|
94
111
|
if (want("topic")) {
|
|
95
112
|
const key = Object.keys(index.topics).find((k) => k.toLowerCase() === name);
|
|
96
113
|
if (key)
|
package/dist/types.d.ts
CHANGED
|
@@ -37,11 +37,43 @@ export interface TokenEntry {
|
|
|
37
37
|
hex: string;
|
|
38
38
|
}>;
|
|
39
39
|
}
|
|
40
|
+
/** A single node in a pattern's `compose` tree: a component instance, its
|
|
41
|
+
* props (which may hold `{param:key}` / `{content:key}` placeholders), and
|
|
42
|
+
* its slot fills (nested nodes or string literals / placeholders). Mirrors
|
|
43
|
+
* `packages/react/scripts/patterns.ts`'s `PatternNode` — the shape emitted
|
|
44
|
+
* into `packages/react/dist/patterns.json` (D47). */
|
|
45
|
+
export interface PatternNode {
|
|
46
|
+
component: string;
|
|
47
|
+
props?: Record<string, unknown>;
|
|
48
|
+
slots?: Record<string, Array<PatternNode | string>>;
|
|
49
|
+
content?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface PatternParameter {
|
|
52
|
+
key: string;
|
|
53
|
+
ask: string;
|
|
54
|
+
options: Array<string | number>;
|
|
55
|
+
default?: string | number;
|
|
56
|
+
}
|
|
57
|
+
/** Shape of one entry in `packages/react/dist/patterns.json` (D47/D48):
|
|
58
|
+
* an authored composition recipe plus its build-time validation/render
|
|
59
|
+
* results (`gaps`, `blocked`, `preset`). */
|
|
60
|
+
export interface PatternEntry {
|
|
61
|
+
id: string;
|
|
62
|
+
intent: string;
|
|
63
|
+
match: string[];
|
|
64
|
+
compose: PatternNode;
|
|
65
|
+
parameters: PatternParameter[];
|
|
66
|
+
content: Record<string, string>;
|
|
67
|
+
gaps: string[];
|
|
68
|
+
blocked: boolean;
|
|
69
|
+
preset: string | null;
|
|
70
|
+
}
|
|
40
71
|
export interface PsiIndex {
|
|
41
72
|
version: string;
|
|
42
73
|
themes: string[];
|
|
43
74
|
components: ComponentEntry[];
|
|
44
75
|
tokens: TokenEntry[];
|
|
76
|
+
patterns: PatternEntry[];
|
|
45
77
|
scales: Record<string, unknown>;
|
|
46
78
|
topics: Record<string, unknown>;
|
|
47
79
|
}
|
package/llms.txt
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Two read-only tools over a baked index of the generated artifacts:
|
|
4
4
|
- search(query): briefs {id, kind, title, summary}; empty query = overview
|
|
5
|
-
- get(id): component (props/defaults/a11y doc) | token (formula + OKLCH/hex per theme) | topic (guidance)
|
|
6
|
-
Ids: component:<Name>, token:<name>, topic:<key>; bare names resolve case-insensitively.
|
|
5
|
+
- get(id): component (props/defaults/a11y doc) | token (formula + OKLCH/hex per theme) | topic (guidance) | pattern (compose tree, clarifying parameters, content, gaps/blocked status, preset JSX)
|
|
6
|
+
Ids: component:<Name>, token:<name>, topic:<key>, pattern:<id>; bare names resolve case-insensitively.
|
|
7
7
|
|
|
8
8
|
Connect: https://psi.kurkin.de/mcp (Streamable HTTP) or `npx @handamade/psi-mcp` (stdio).
|
|
9
9
|
Agent context: `npx @handamade/psi-mcp init [--claude]` writes a marker-delimited block into AGENTS.md/CLAUDE.md (D44).
|
|
10
|
-
The index bakes packages/react/dist/manifest.json + packages/tokens/dist/{guidance.json,resolved/*.json} at publish; lockstep versioning keeps it truthful (D43).
|
|
10
|
+
The index bakes packages/react/dist/{manifest.json,patterns.json} (patterns.json: 3 composition patterns with clarifying parameters, blocked/gaps flags, preset JSX) + packages/tokens/dist/{guidance.json,resolved/*.json} at publish; lockstep versioning keeps it truthful (D43).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handamade/psi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "MCP server and agent-context generator for the Psi (Ψ) design system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"tsx": "^4.22.4",
|
|
33
33
|
"typescript": "^5.7.0",
|
|
34
34
|
"vitest": "^3.2.6",
|
|
35
|
-
"@handamade/psi-react": "0.
|
|
36
|
-
"@handamade/psi-tokens": "0.
|
|
35
|
+
"@handamade/psi-react": "0.6.0",
|
|
36
|
+
"@handamade/psi-tokens": "0.6.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|