@handamade/psi-mcp 0.4.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 +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +15 -0
- package/dist/http.d.ts +10 -0
- package/dist/http.js +24 -0
- package/dist/index-builder.d.ts +15 -0
- package/dist/index-builder.js +55 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/init.d.ts +9 -0
- package/dist/init.js +60 -0
- package/dist/psi-index.json +2018 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +30 -0
- package/dist/store.d.ts +21 -0
- package/dist/store.js +99 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +1 -0
- package/llms.txt +10 -0
- package/package.json +41 -0
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export function createPsiServer(store, version) {
|
|
4
|
+
const server = new McpServer({ name: "psi", version });
|
|
5
|
+
server.registerTool("search", {
|
|
6
|
+
title: "Search the Psi design system",
|
|
7
|
+
description: "Keyword search over Psi components, semantic tokens, and guidance topics. " +
|
|
8
|
+
"Returns compact briefs with ids for the get tool. Empty query returns an overview.",
|
|
9
|
+
inputSchema: { query: z.string().describe("Keywords, e.g. 'button variants' or 'bg token'") },
|
|
10
|
+
}, async ({ query }) => ({
|
|
11
|
+
content: [{ type: "text", text: JSON.stringify(store.search(query), null, 1) }],
|
|
12
|
+
}));
|
|
13
|
+
server.registerTool("get", {
|
|
14
|
+
title: "Get full Psi detail by id",
|
|
15
|
+
description: "Full detail for one id from search: component (props, defaults, a11y doc), " +
|
|
16
|
+
"token (formula + resolved OKLCH/hex in all four themes), or topic (guidance). " +
|
|
17
|
+
"Accepts 'component:Button', 'token:bgPrimary', 'topic:variants', or a bare name.",
|
|
18
|
+
inputSchema: { id: z.string().describe("An id from search results, or a bare name") },
|
|
19
|
+
}, async ({ id }) => {
|
|
20
|
+
const detail = store.get(id);
|
|
21
|
+
if (!detail) {
|
|
22
|
+
return {
|
|
23
|
+
isError: true,
|
|
24
|
+
content: [{ type: "text", text: `Unknown id "${id}". Use the search tool to discover ids.` }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { content: [{ type: "text", text: JSON.stringify(detail, null, 1) }] };
|
|
28
|
+
});
|
|
29
|
+
return server;
|
|
30
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentEntry, PsiIndex, TokenEntry } from "./types.js";
|
|
2
|
+
export interface Brief {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: "component" | "token" | "topic";
|
|
5
|
+
title: string;
|
|
6
|
+
summary: string;
|
|
7
|
+
}
|
|
8
|
+
export type Detail = ({
|
|
9
|
+
kind: "component";
|
|
10
|
+
} & ComponentEntry) | ({
|
|
11
|
+
kind: "token";
|
|
12
|
+
} & TokenEntry) | {
|
|
13
|
+
kind: "topic";
|
|
14
|
+
name: string;
|
|
15
|
+
content: unknown;
|
|
16
|
+
};
|
|
17
|
+
export interface Store {
|
|
18
|
+
search(query: string): Brief[];
|
|
19
|
+
get(id: string): Detail | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function createStore(index: PsiIndex): Store;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const MAX_RESULTS = 20;
|
|
2
|
+
const TOPIC_SUMMARIES = {
|
|
3
|
+
variants: "Variant intent and typical use for all 8 flat variants",
|
|
4
|
+
rules: "House rules: accent budget, sizing, color binding",
|
|
5
|
+
states: "hover/active/disabled/focus derivation formulas",
|
|
6
|
+
typographyDefaults: "Default type combos per role",
|
|
7
|
+
fonts: "Font roles and brand font stacks",
|
|
8
|
+
motion: "Durations, easings, reduced-motion behavior, recipes",
|
|
9
|
+
layout: "Breakpoints, container, z-index",
|
|
10
|
+
recipes: "Composition recipes (mediaTint, sectionHeader)",
|
|
11
|
+
tags: "Tag/badge rules incl. the accent exemption (D40)",
|
|
12
|
+
"getting-started": "Install, the four required CSS imports, theme attribute, core rules",
|
|
13
|
+
};
|
|
14
|
+
function componentSummary(c) {
|
|
15
|
+
const axis = (name) => c.props.find((p) => p.name === name)?.type;
|
|
16
|
+
const parts = [];
|
|
17
|
+
if (c.description)
|
|
18
|
+
parts.push(c.description);
|
|
19
|
+
const variants = axis("variant");
|
|
20
|
+
const sizes = axis("size");
|
|
21
|
+
if (variants)
|
|
22
|
+
parts.push(`variants: ${variants.replaceAll('"', "")}`);
|
|
23
|
+
if (sizes)
|
|
24
|
+
parts.push(`sizes: ${sizes}`);
|
|
25
|
+
if (!variants && !sizes)
|
|
26
|
+
parts.push(`props: ${c.props.map((p) => p.name).slice(0, 6).join(", ")}`);
|
|
27
|
+
return parts.join(" — ").slice(0, 220);
|
|
28
|
+
}
|
|
29
|
+
export function createStore(index) {
|
|
30
|
+
const briefs = [
|
|
31
|
+
...index.components.map((c) => ({
|
|
32
|
+
id: `component:${c.name}`,
|
|
33
|
+
kind: "component",
|
|
34
|
+
title: c.name,
|
|
35
|
+
summary: componentSummary(c),
|
|
36
|
+
})),
|
|
37
|
+
...Object.keys(index.topics).map((name) => ({
|
|
38
|
+
id: `topic:${name}`,
|
|
39
|
+
kind: "topic",
|
|
40
|
+
title: name,
|
|
41
|
+
summary: TOPIC_SUMMARIES[name] ?? "Guidance topic",
|
|
42
|
+
})),
|
|
43
|
+
...index.tokens.map((t) => ({
|
|
44
|
+
id: `token:${t.name}`,
|
|
45
|
+
kind: "token",
|
|
46
|
+
title: t.name,
|
|
47
|
+
summary: t.formula,
|
|
48
|
+
})),
|
|
49
|
+
];
|
|
50
|
+
function score(brief, terms) {
|
|
51
|
+
let s = 0;
|
|
52
|
+
const title = brief.title.toLowerCase();
|
|
53
|
+
const summary = brief.summary.toLowerCase();
|
|
54
|
+
for (const term of terms) {
|
|
55
|
+
if (title === term)
|
|
56
|
+
s += 10;
|
|
57
|
+
else if (title.includes(term))
|
|
58
|
+
s += 5;
|
|
59
|
+
if (summary.includes(term))
|
|
60
|
+
s += 1;
|
|
61
|
+
}
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
64
|
+
function search(query) {
|
|
65
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
66
|
+
if (terms.length === 0) {
|
|
67
|
+
// Overview: all components + all topics (tokens are discoverable via query).
|
|
68
|
+
return briefs.filter((b) => b.kind !== "token").slice(0, MAX_RESULTS + 10);
|
|
69
|
+
}
|
|
70
|
+
return briefs
|
|
71
|
+
.map((b) => ({ b, s: score(b, terms) }))
|
|
72
|
+
.filter((x) => x.s > 0)
|
|
73
|
+
.sort((a, z) => z.s - a.s)
|
|
74
|
+
.slice(0, MAX_RESULTS)
|
|
75
|
+
.map((x) => x.b);
|
|
76
|
+
}
|
|
77
|
+
function get(id) {
|
|
78
|
+
const [prefix, rest] = id.includes(":") ? id.split(/:(.*)/s) : [null, id];
|
|
79
|
+
const name = (rest ?? id).toLowerCase();
|
|
80
|
+
const want = (kind) => prefix === null || prefix === kind;
|
|
81
|
+
if (want("component")) {
|
|
82
|
+
const c = index.components.find((c) => c.name.toLowerCase() === name);
|
|
83
|
+
if (c)
|
|
84
|
+
return { kind: "component", ...c };
|
|
85
|
+
}
|
|
86
|
+
if (want("token")) {
|
|
87
|
+
const t = index.tokens.find((t) => t.name.toLowerCase() === name);
|
|
88
|
+
if (t)
|
|
89
|
+
return { kind: "token", ...t };
|
|
90
|
+
}
|
|
91
|
+
if (want("topic")) {
|
|
92
|
+
const key = Object.keys(index.topics).find((k) => k.toLowerCase() === name);
|
|
93
|
+
if (key)
|
|
94
|
+
return { kind: "topic", name: key, content: index.topics[key] };
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return { search, get };
|
|
99
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface PropDoc {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
required: boolean;
|
|
5
|
+
default: unknown;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ComponentEntry {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
props: PropDoc[];
|
|
12
|
+
/** Full generated markdown doc (includes the a11y/keyboard table). */
|
|
13
|
+
doc: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Oklch {
|
|
16
|
+
l: number;
|
|
17
|
+
c: number;
|
|
18
|
+
h: number;
|
|
19
|
+
alpha?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface TokenEntry {
|
|
22
|
+
name: string;
|
|
23
|
+
formula: string;
|
|
24
|
+
values: Record<string, {
|
|
25
|
+
oklch: Oklch;
|
|
26
|
+
hex: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export interface PsiIndex {
|
|
30
|
+
version: string;
|
|
31
|
+
themes: string[];
|
|
32
|
+
components: ComponentEntry[];
|
|
33
|
+
tokens: TokenEntry[];
|
|
34
|
+
scales: Record<string, unknown>;
|
|
35
|
+
topics: Record<string, unknown>;
|
|
36
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/llms.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @handamade/psi-mcp — MCP access to Psi (Ψ)
|
|
2
|
+
|
|
3
|
+
Two read-only tools over a baked index of the generated artifacts:
|
|
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.
|
|
7
|
+
|
|
8
|
+
Connect: https://psi.kurkin.de/mcp (Streamable HTTP) or `npx @handamade/psi-mcp` (stdio).
|
|
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).
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@handamade/psi-mcp",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "MCP server and agent-context generator for the Psi (Ψ) design system",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"psi-mcp": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./http": {
|
|
17
|
+
"types": "./dist/http.d.ts",
|
|
18
|
+
"import": "./dist/http.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"llms.txt"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.16.0",
|
|
28
|
+
"zod": "^3.25.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"tsx": "^4.22.4",
|
|
33
|
+
"typescript": "^5.7.0",
|
|
34
|
+
"vitest": "^3.2.6",
|
|
35
|
+
"@handamade/psi-react": "0.4.0",
|
|
36
|
+
"@handamade/psi-tokens": "0.4.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|
|
40
|
+
}
|
|
41
|
+
}
|