@tattva/mcp 0.0.0-stage → 0.11.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/LICENSE.md +49 -0
- package/README.md +16 -2
- package/mcp-guide.mjs +289 -0
- package/mcp-server.mjs +319 -0
- package/package.json +26 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Tattva licence
|
|
2
|
+
|
|
3
|
+
> **Draft.** This explains in plain words how Tattva may be used. It is not legal advice. Have it reviewed before you sell the paid plans.
|
|
4
|
+
|
|
5
|
+
Tattva comes in three plans: **Free**, **Pro** and **Team**. Plans and prices are on the [pricing page](https://lkb00.github.io/tattva/#pricing).
|
|
6
|
+
|
|
7
|
+
## Free
|
|
8
|
+
|
|
9
|
+
The free core covers:
|
|
10
|
+
- the documentation, patterns, case studies and guides
|
|
11
|
+
- the design tokens
|
|
12
|
+
- the parts marked **Free** on the site: the basic building blocks and the parts of a plain chat
|
|
13
|
+
|
|
14
|
+
You may use the free core in any project, personal or commercial, and change it as you like. Keep this licence notice with any copy of the source you share.
|
|
15
|
+
|
|
16
|
+
## Pro and Team (paid)
|
|
17
|
+
|
|
18
|
+
A paid licence covers:
|
|
19
|
+
- every part, marked **Pro** on the site
|
|
20
|
+
- the page templates and whole screens
|
|
21
|
+
- the AI-agent kit: the MCP server, `components.json` and the Markdown page for each part
|
|
22
|
+
|
|
23
|
+
**Who may use it**
|
|
24
|
+
- **Pro:** one person.
|
|
25
|
+
- **Team:** up to 10 people at one company.
|
|
26
|
+
|
|
27
|
+
**What you may do**
|
|
28
|
+
- Use the parts in unlimited projects, including work for clients and products you sell.
|
|
29
|
+
- Change the parts to fit your product.
|
|
30
|
+
|
|
31
|
+
**What you may not do**
|
|
32
|
+
- Resell, share or give away the parts, the templates or the AI-agent kit, whether on their own or as part of another UI kit, design system, template or theme.
|
|
33
|
+
- Share your licence key or the download with people it does not cover.
|
|
34
|
+
|
|
35
|
+
**Updates**
|
|
36
|
+
- You get updates for one year from the date you buy.
|
|
37
|
+
- Everything you received in that year is yours to keep using after that.
|
|
38
|
+
|
|
39
|
+
**Payment and refunds**
|
|
40
|
+
- Lemon Squeezy, the seller of record, handles payment, receipts and sales tax or VAT.
|
|
41
|
+
- Refunds follow the policy shown at checkout.
|
|
42
|
+
|
|
43
|
+
## No warranty
|
|
44
|
+
|
|
45
|
+
Tattva is provided as it is, without any warranty. The authors are not liable for any claim or damage that arises from using it.
|
|
46
|
+
|
|
47
|
+
## Questions
|
|
48
|
+
|
|
49
|
+
Open an issue on the repository: https://github.com/LKB00/tattva/issues
|
package/README.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tattva/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The [Tattva](https://lkb00.github.io/tattva/) MCP server. It lets an AI coding tool look up every Tattva part, plan a build, and check the code it writes. Read-only, no dependencies, needs Node 18 or newer.
|
|
4
|
+
|
|
5
|
+
Claude Code:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
claude mcp add tattva -- npx -y @tattva/mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Cursor, Claude Desktop and others (MCP settings):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{ "mcpServers": { "tattva": { "command": "npx", "args": ["-y", "@tattva/mcp"] } } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Pro and Team: set `TATTVA_API` and `TATTVA_LICENSE_KEY` (from your receipt) to unlock the full code of Pro parts. More: https://lkb00.github.io/tattva/connect-ai/
|
package/mcp-guide.mjs
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Guidance for the Tattva MCP server: the starting questions, the build recipes and the code checker.
|
|
2
|
+
// Pure functions, no dependencies. mcp-server.mjs imports this file, so keep the two files together.
|
|
3
|
+
|
|
4
|
+
/** The questions an agent asks the person before building. Every question has a default, so "defaults" is a valid answer. */
|
|
5
|
+
export const QUESTIONS = [
|
|
6
|
+
{ id: "product", question: "What are you building?", default: "chat-assistant",
|
|
7
|
+
options: [
|
|
8
|
+
{ value: "chat-assistant", label: "A chat assistant (a thread, a message box, sources and confidence)" },
|
|
9
|
+
{ value: "agent-console", label: "An agent console (plans, steps, approvals, diffs, a needs-you inbox)" },
|
|
10
|
+
{ value: "creative-tool", label: "A creative AI tool (prompt, variations, canvas, history)" },
|
|
11
|
+
{ value: "generative-ui", label: "An app where the AI builds interface on the fly" },
|
|
12
|
+
{ value: "ai-dashboard", label: "A dashboard for an AI product (usage, cost, charts, tables)" },
|
|
13
|
+
] },
|
|
14
|
+
{ id: "stack", question: "Is this a new project or an existing app, and which framework?", default: "new-vite",
|
|
15
|
+
options: [
|
|
16
|
+
{ value: "new-vite", label: "New project with React and Vite" },
|
|
17
|
+
{ value: "new-next", label: "New project with Next.js" },
|
|
18
|
+
{ value: "existing", label: "Add it to an existing React app that uses Tailwind CSS v4" },
|
|
19
|
+
{ value: "no-code", label: "A no-code AI builder such as Lovable, v0 or Bolt (nothing to install)" },
|
|
20
|
+
] },
|
|
21
|
+
{ id: "theme", question: "Which colour theme?", default: "lime",
|
|
22
|
+
options: [
|
|
23
|
+
{ value: "lime", label: "Lime: warm paper, fresh lime" },
|
|
24
|
+
{ value: "teal", label: "Teal: cool paper, calm teal" },
|
|
25
|
+
{ value: "indigo", label: "Indigo: soft violet-blue" },
|
|
26
|
+
{ value: "harbor", label: "Harbor: steady, trusted blue" },
|
|
27
|
+
{ value: "plum", label: "Plum: warm berry pink" },
|
|
28
|
+
{ value: "graphite", label: "Graphite: no colour, the AI is marked by label and icon" },
|
|
29
|
+
{ value: "terminal", label: "Terminal: near-black and dense, with a sky-blue AI colour, for trading and data screens" },
|
|
30
|
+
] },
|
|
31
|
+
{ id: "mode", question: "Light, dark, or both?", default: "both",
|
|
32
|
+
options: [{ value: "both", label: "Both, following the person's device" }, { value: "light", label: "Light only" }, { value: "dark", label: "Dark only" }] },
|
|
33
|
+
{ id: "trust", question: "Which trust behaviours does the product need? (pick any)", default: ["sources", "feedback"], multiple: true,
|
|
34
|
+
options: [
|
|
35
|
+
{ value: "sources", label: "Show sources and citations" },
|
|
36
|
+
{ value: "confidence", label: "Show how sure the AI is" },
|
|
37
|
+
{ value: "approvals", label: "Ask before the AI acts (approvals)" },
|
|
38
|
+
{ value: "undo", label: "Undo and checkpoints" },
|
|
39
|
+
{ value: "memory", label: "Memory the person can see and edit" },
|
|
40
|
+
{ value: "disclosure", label: "Clear AI labels and disclosure" },
|
|
41
|
+
{ value: "feedback", label: "Thumbs up and down feedback" },
|
|
42
|
+
{ value: "errors", label: "Calm error, refusal and rate-limit states" },
|
|
43
|
+
] },
|
|
44
|
+
{ id: "plan", question: "Which plan are you on?", default: "free",
|
|
45
|
+
options: [{ value: "free", label: "Free: the basic building blocks and a plain chat" }, { value: "pro", label: "Pro or Team: every part" }] },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** Parts per product, in the order to build them. `core` is always used; `why` is one plain line. Ids are checked against components.json. */
|
|
49
|
+
export const RECIPES = {
|
|
50
|
+
"chat-assistant": {
|
|
51
|
+
title: "Chat assistant",
|
|
52
|
+
screen: "thread-template",
|
|
53
|
+
core: [["message-list", "The scrolling thread."], ["message", "One turn: the person's bubble or the assistant's text."], ["composer", "The message box."], ["markdown", "Renders the assistant's formatting safely."], ["thinking-block", "Shows the assistant is working, and what it did."], ["typing-indicator", "A quiet sign a reply is on the way."], ["suggestion-chips", "Starter prompts and follow-ups."], ["message-actions", "Copy, retry and rate under a reply."], ["conversation-list", "Past chats in the sidebar."], ["model-picker", "Lets the person choose a model."]],
|
|
54
|
+
patterns: ["source", "uncertain", "undo", "mistake"],
|
|
55
|
+
},
|
|
56
|
+
"agent-console": {
|
|
57
|
+
title: "Agent console",
|
|
58
|
+
screen: "split-canvas-template",
|
|
59
|
+
core: [["agent-session-list", "All runs, with what each needs."], ["plan-card", "The plan, before the agent starts."], ["step-timeline", "What the agent did, in order."], ["tool-call-card", "One tool call and its result."], ["permission-prompt", "Asks before an action that matters."], ["needs-you-inbox", "Everything waiting on a person, in one place."], ["agent-question-card", "The agent asks a question mid-task."], ["takeover-bar", "Lets the person take control."], ["diff-view", "Shows exactly what changed."], ["checkpoint-list", "Go back to an earlier point."], ["budget-control", "Limits cost and time."], ["re-entry-recap", "Catches the person up when they return."]],
|
|
60
|
+
patterns: ["approval", "ask before", "undo", "work as it happens"],
|
|
61
|
+
},
|
|
62
|
+
"creative-tool": {
|
|
63
|
+
title: "Creative AI tool",
|
|
64
|
+
screen: "split-canvas-template",
|
|
65
|
+
core: [["prompt-box", "Where the person describes what they want."], ["variant-grid", "Several options to choose from."], ["canvas-panel", "The working area."], ["parameter-panel", "Controls such as size, style and strength."], ["generation-progress", "Honest progress while it makes the result."], ["version-history", "Every earlier result, so nothing is lost."], ["variation-actions", "More like this, upscale, edit."], ["suggestion-bar", "Ideas to try next."], ["media-grid", "A library of results."]],
|
|
66
|
+
patterns: ["undo", "mistake", "work as it happens"],
|
|
67
|
+
},
|
|
68
|
+
"generative-ui": {
|
|
69
|
+
title: "Generative interface",
|
|
70
|
+
screen: "thread-template",
|
|
71
|
+
core: [["gen-ui-renderer", "Draws interface the AI describes."], ["generated-surface", "The frame generated content lives in."], ["sandboxed-frame", "Keeps untrusted generated content safe."], ["message-list", "The thread beside it."], ["composer", "The message box."], ["markdown", "Formatted text."], ["ai-label", "Marks what the AI made."]],
|
|
72
|
+
patterns: ["ask before", "undo"],
|
|
73
|
+
},
|
|
74
|
+
"ai-dashboard": {
|
|
75
|
+
title: "AI product dashboard",
|
|
76
|
+
screen: "app-shell",
|
|
77
|
+
core: [["stat-tile", "One headline number."], ["usage-meter", "How much has been used."], ["context-meter", "How full the AI's memory window is."], ["line-chart", "A trend over time."], ["bar-chart", "Compare amounts."], ["data-table", "Rows people can sort and read."], ["chart-frame", "Title, caption and a table view for every chart."], ["timeline", "Events in order."]],
|
|
78
|
+
patterns: ["uncertain"],
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Extra parts for each trust behaviour the person asked for. */
|
|
83
|
+
export const TRUST = {
|
|
84
|
+
sources: [["citation-marker", "The small numbered marker in the text."], ["source-list", "The sources behind an answer."], ["source-card", "One source."]],
|
|
85
|
+
confidence: [["confidence-indicator", "Shows how sure the AI is, with a word as well as a colour."]],
|
|
86
|
+
approvals: [["permission-prompt", "Asks before an action that matters."], ["approval-prompt", "A simple yes or no on one step."]],
|
|
87
|
+
undo: [["revert-toggle", "Switch back to the earlier version."], ["checkpoint-list", "Return to an earlier point."], ["version-pager", "Step between versions of a reply."], ["action-log", "A record of what the agent did, with a timed undo."]],
|
|
88
|
+
memory: [["memory-notice", "Tells the person something was remembered."], ["memory-list", "See, edit and delete memories."]],
|
|
89
|
+
disclosure: [["ai-label", "Marks AI-made content."], ["ai-disclosure", "A plain statement that the person is talking to an AI."], ["ai-badge", "A small badge for AI features."]],
|
|
90
|
+
feedback: [["feedback-bar", "Thumbs up and down with a reason."]],
|
|
91
|
+
errors: [["error-state", "Says what went wrong and what to do."], ["refusal-notice", "Explains a refusal kindly."], ["rate-limit-notice", "Explains a limit and when it lifts."], ["callout", "A calm note inside a page."]],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const SITE = "https://lkb00.github.io/tattva";
|
|
95
|
+
const FONTS = `<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400..600&display=swap" rel="stylesheet" />`;
|
|
96
|
+
|
|
97
|
+
const setupSteps = (a) => {
|
|
98
|
+
const steps = [];
|
|
99
|
+
const stack = a.stack;
|
|
100
|
+
if (stack === "no-code") {
|
|
101
|
+
steps.push("Paste the prompt into your AI builder. It carries your choices and the rules, so you do not need to explain the design system.");
|
|
102
|
+
steps.push(`If the builder cannot install packages, give it the one-file source of every free part: ${SITE}/downloads/tattva-free-source.md, and ask it to copy the parts it needs into the project.`);
|
|
103
|
+
steps.push(`Add the colours: ${SITE}/downloads/tokens.css goes in the project's global stylesheet.${a.theme && a.theme !== "lime" ? ` Then set data-palette="${a.theme}" on the html element.` : ""}`);
|
|
104
|
+
steps.push("Pro parts cannot be installed by a builder. If the plan lists them, use the free parts or ask for a plain stand-in and swap in the Pro part later.");
|
|
105
|
+
return steps;
|
|
106
|
+
}
|
|
107
|
+
if (stack === "new-vite") steps.push("Create the app: `npm create vite@latest my-app -- --template react-ts`, then add Tailwind CSS v4: `npm install tailwindcss @tailwindcss/vite` and add the plugin to vite.config.");
|
|
108
|
+
else if (stack === "new-next") steps.push("Create the app: `npx create-next-app@latest` (TypeScript and Tailwind CSS v4 on), then keep the Tattva files under `src/`.");
|
|
109
|
+
else steps.push("Check the app uses React 18+ and Tailwind CSS v4. Tattva's tokens are written for v4.");
|
|
110
|
+
if (a.plan === "free") steps.push(`Install the free parts and the tokens from npm: \`npm install @tattva/ui\`. Create \`src/tattva.css\` holding \`@import "@tattva/ui/tokens.css";\` and \`@source "../node_modules/@tattva/ui/dist";\` (the tokens load Tailwind, so this replaces your own \`@import "tailwindcss";\`; the @source line lets Tailwind see the classes the parts use) and import it once at the top of the app entry (\`import "./tattva.css";\`).`);
|
|
111
|
+
else steps.push(`Add the tokens: download ${SITE}/downloads/tokens.css, save it as \`src/tokens/tokens.css\` and import it once at the top of the app entry (\`import "./tokens/tokens.css";\`).`);
|
|
112
|
+
steps.push(`Load the fonts (Geist and Geist Mono) by adding this to the page head: ${FONTS}`);
|
|
113
|
+
if (a.theme && a.theme !== "lime") steps.push(`Set the colour theme on the root element: \`<html data-palette="${a.theme}">\`.`);
|
|
114
|
+
if (a.mode === "dark") steps.push('Dark only: add `class="dark"` to the `<html>` element.');
|
|
115
|
+
else if (a.mode === "both") steps.push('Light and dark: toggle `class="dark"` on `<html>`, starting from `matchMedia("(prefers-color-scheme: dark)")` so it follows the device.');
|
|
116
|
+
if (a.plan === "free") steps.push("Import every part from the package only (`import { Message } from \"@tattva/ui\"`), never from a deep path. Pro parts are not in the package; they come with a Pro or Team plan.");
|
|
117
|
+
else steps.push("Install all the parts from the Pro zip: the person got `tattva-pro.zip` when they bought Pro or Team (check their email or the store's download page). Ask them for its path; do not look for it online. Unzip it so that its `tattva` folder lands in `src/` and its `tokens.css` replaces `src/tokens/tokens.css`. Import from that folder only (`import { ApprovalPrompt } from \"./tattva\"`, adjusting the relative path).");
|
|
118
|
+
return steps;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** Files an agent can write as they are, so setup does not depend on it remembering the steps. */
|
|
122
|
+
const setupFiles = (a, brief) => {
|
|
123
|
+
const files = [{ path: "tattva.md", why: "The project brief. Read it first in every session.", content: brief + "\n" }];
|
|
124
|
+
if (a.stack === "new-vite") {
|
|
125
|
+
const dark = a.mode === "dark" ? ' class="dark"' : "";
|
|
126
|
+
const palette = a.theme && a.theme !== "lime" ? ` data-palette="${a.theme}"` : "";
|
|
127
|
+
const script = a.mode === "both" ? `
|
|
128
|
+
<script>
|
|
129
|
+
(function () {
|
|
130
|
+
if (matchMedia("(prefers-color-scheme: dark)").matches) document.documentElement.classList.add("dark");
|
|
131
|
+
})();
|
|
132
|
+
</script>` : "";
|
|
133
|
+
files.push(
|
|
134
|
+
{ path: "vite.config.ts", why: "React and Tailwind CSS v4.", content: `import { defineConfig } from "vite";\nimport react from "@vitejs/plugin-react";\nimport tailwindcss from "@tailwindcss/vite";\n\nexport default defineConfig({ plugins: [react(), tailwindcss()] });\n` },
|
|
135
|
+
{ path: "index.html", why: `Fonts, ${a.theme === "lime" ? "the default theme" : `the ${a.theme} theme`}${a.mode === "both" ? " and light/dark that follows the device" : a.mode === "dark" ? " and dark mode" : ""}.`, content: `<!doctype html>\n<html lang="en"${dark}${palette}>\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <title>My app</title>\n <link rel="preconnect" href="https://fonts.googleapis.com" />\n ${FONTS}${script}\n </head>\n <body>\n <div id="root"></div>\n <script type="module" src="/src/main.tsx"></script>\n </body>\n</html>\n` },
|
|
136
|
+
{ path: "src/main.tsx", why: "Loads the tokens and renders App. You write src/App.tsx.", content: `import { StrictMode } from "react";\nimport { createRoot } from "react-dom/client";\nimport "${a.plan === "free" ? "./tattva.css" : "./tokens/tokens.css"}";\nimport { App } from "./App";\n\ncreateRoot(document.getElementById("root")!).render(<StrictMode><App /></StrictMode>);\n` },
|
|
137
|
+
);
|
|
138
|
+
if (a.plan === "free") files.push({ path: "src/tattva.css", why: "The Tattva tokens (they load Tailwind) and the package's classes.", content: `@import "@tattva/ui/tokens.css";\n@source "../node_modules/@tattva/ui/dist";\n` });
|
|
139
|
+
}
|
|
140
|
+
return files;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const setupCommands = (a) => a.stack === "no-code" ? [] : [
|
|
144
|
+
...(a.stack === "new-vite" ? ["npm create vite@latest my-app -- --template react-ts", "cd my-app && npm install && npm install tailwindcss @tailwindcss/vite"] : []),
|
|
145
|
+
...(a.plan === "free" ? ["npm install @tattva/ui"] : [`mkdir -p src/tokens && curl -fsSL ${SITE}/downloads/tokens.css -o src/tokens/tokens.css`, "unzip -oq <path to tattva-pro.zip> 'tattva/*' -d src && unzip -oqj <path to tattva-pro.zip> tokens.css -d src/tokens"]),
|
|
146
|
+
...(a.stack === "new-vite" ? ["rm -f src/index.css src/App.css"] : []),
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
/** Builds the plan for an agent: setup, parts in order (with Pro marks), patterns to read and a brief to save. */
|
|
150
|
+
export function planBuild(rawAnswers, components) {
|
|
151
|
+
const byId = new Map(components.map((c) => [c.id, c]));
|
|
152
|
+
const a = { product: "chat-assistant", stack: "new-vite", theme: "lime", mode: "both", trust: ["sources", "feedback"], plan: "free", ...Object.fromEntries(Object.entries(rawAnswers ?? {}).filter(([, v]) => v !== undefined && v !== null && v !== "")) };
|
|
153
|
+
if (typeof a.trust === "string") a.trust = a.trust.split(/[,\s]+/).filter(Boolean);
|
|
154
|
+
const recipe = RECIPES[a.product];
|
|
155
|
+
if (!recipe) return { error: `Unknown product "${a.product}". Use one of: ${Object.keys(RECIPES).join(", ")}.` };
|
|
156
|
+
const warnings = [];
|
|
157
|
+
const themes = ["lime", "teal", "indigo", "harbor", "plum", "graphite", "terminal"];
|
|
158
|
+
if (!themes.includes(a.theme)) { warnings.push(`Unknown theme "${a.theme}"; using lime. Themes: ${themes.join(", ")}.`); a.theme = "lime"; }
|
|
159
|
+
const unknownTrust = a.trust.filter((t) => !TRUST[t]);
|
|
160
|
+
if (unknownTrust.length) warnings.push(`Ignored unknown trust behaviours: ${unknownTrust.join(", ")}. Known: ${Object.keys(TRUST).join(", ")}.`);
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
const parts = [];
|
|
163
|
+
const add = (list, group) => {
|
|
164
|
+
for (let [id, why] of list) {
|
|
165
|
+
if (seen.has(id)) continue; seen.add(id);
|
|
166
|
+
let c = byId.get(id);
|
|
167
|
+
if (!c) { warnings.push(`The part "${id}" was not found in this copy of Tattva, so it is left out.`); continue; }
|
|
168
|
+
const stage = c.lifecycle?.stage ?? c.status ?? "stable";
|
|
169
|
+
if ((stage === "deprecated" || stage === "retired") && c.lifecycle?.replacedBy && byId.get(c.lifecycle.replacedBy)) {
|
|
170
|
+
warnings.push(`${c.name} is ${stage}; using ${byId.get(c.lifecycle.replacedBy).name} instead.`);
|
|
171
|
+
c = byId.get(c.lifecycle.replacedBy); id = c.id;
|
|
172
|
+
if (seen.has(id)) continue; seen.add(id);
|
|
173
|
+
}
|
|
174
|
+
parts.push({ id, name: c.name, group, why, plan: c.plan ?? "free", stage: c.lifecycle?.stage ?? c.status ?? "stable", importLine: c.importPath });
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
add([[recipe.screen, `The page layout (${recipe.title}).`]], "Screen");
|
|
178
|
+
add(recipe.core, recipe.title);
|
|
179
|
+
for (const t of a.trust.filter((x) => TRUST[x])) add(TRUST[t], `Trust: ${t}`);
|
|
180
|
+
const pro = parts.filter((p) => p.plan === "pro");
|
|
181
|
+
if (a.plan === "free" && pro.length) warnings.push(`${pro.length} of ${parts.length} parts are Pro (${pro.slice(0, 6).map((p) => p.name).join(", ")}${pro.length > 6 ? ", ..." : ""}). Their previews and guidance are free, but their code comes with Pro or Team: ${SITE}/#pricing. Tell the person before relying on them, and offer the free parts first.`);
|
|
182
|
+
const brief = [
|
|
183
|
+
"# Tattva project brief", "",
|
|
184
|
+
"Saved by the Tattva MCP server. Read this first in every session so the product stays consistent.", "",
|
|
185
|
+
`- Product: ${recipe.title}`, `- Stack: ${a.stack}`, `- Theme: ${a.theme}${a.theme === "lime" ? " (default)" : ` (data-palette="${a.theme}")`}`, `- Mode: ${a.mode}`,
|
|
186
|
+
`- Trust behaviours: ${a.trust.join(", ") || "none"}`, `- Plan: ${a.plan}`, "",
|
|
187
|
+
"## Parts", ...parts.map((p) => `- ${p.name}${p.plan === "pro" ? " (Pro)" : ""}: ${p.why}`), "",
|
|
188
|
+
"## Rules", "- Import from the package index only.", "- Use tokens (`var(--surface)`), never hex values.", "- The AI colour (`--lime`) is a fill, never text. Amber means a person has to act.",
|
|
189
|
+
"- Every control has an accessible name. Status is never colour alone.", "- Run `review_code` on each file before finishing.",
|
|
190
|
+
].join("\n");
|
|
191
|
+
return { answers: a, recipe: { product: a.product, title: recipe.title, screen: recipe.screen }, setup: setupSteps(a), commands: setupCommands(a), files: setupFiles(a, brief), parts, patterns: recipe.patterns, warnings, brief };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The one prompt a person pastes into an AI tool, from a plan. Written to the AI, not to the person.
|
|
196
|
+
* The Build brief page and the tests use this same function.
|
|
197
|
+
*/
|
|
198
|
+
export function buildPrompt(plan) {
|
|
199
|
+
const a = plan.answers;
|
|
200
|
+
const noCode = a.stack === "no-code";
|
|
201
|
+
const pro = plan.parts.filter((p) => p.plan === "pro");
|
|
202
|
+
const setup = noCode
|
|
203
|
+
? [
|
|
204
|
+
`If you can install packages, run npm install @tattva/ui, put @import "@tattva/ui/tokens.css"; and @source "../node_modules/@tattva/ui/dist"; at the top of the main stylesheet (they replace @import "tailwindcss";), and import every part from "@tattva/ui". If you cannot install packages, read ${SITE}/downloads/tattva-free-source.md, which holds the source of every free part in one file, recreate the parts you need under src/tattva/ with the same file names, and import from there.`,
|
|
205
|
+
`If you used the source file, add the colours from ${SITE}/downloads/tokens.css to the project's global stylesheet.${a.theme && a.theme !== "lime" ? ` Set data-palette="${a.theme}" on the html element.` : ""}${a.mode === "both" ? " Toggle the dark class on the html element to follow the device's colour scheme." : a.mode === "dark" ? " Add the dark class to the html element." : ""}`,
|
|
206
|
+
]
|
|
207
|
+
: plan.setup;
|
|
208
|
+
return [
|
|
209
|
+
`I want to build: ${plan.recipe.title}.`,
|
|
210
|
+
"",
|
|
211
|
+
`Use the Tattva design system. Read its guide first: ${SITE}/llms.txt (it links to a page for every part). Theme: ${a.theme}. Mode: ${a.mode === "both" ? "light and dark, following the device" : a.mode}.`,
|
|
212
|
+
"",
|
|
213
|
+
"Setup:",
|
|
214
|
+
...setup.map((x, i) => `${i + 1}. ${x}`),
|
|
215
|
+
"",
|
|
216
|
+
"Use these parts, in this order:",
|
|
217
|
+
...plan.parts.map((p) => `- ${p.name}${p.plan === "pro" ? " (Pro)" : ""}: ${p.why}`),
|
|
218
|
+
...(pro.length ? ["", `Parts marked (Pro) have a free preview and guidance, but their code is not available to you. Build the screen from the free parts, and for each Pro part use a simple plain stand-in built from free parts, with a comment naming the Pro part so I can swap it in later.`] : []),
|
|
219
|
+
"",
|
|
220
|
+
"Rules:",
|
|
221
|
+
"- Import parts and icons only from the tattva folder, never from a deep path (icons are named like SendIcon and CopyIcon). Never write hex colours; use the tokens (var(--surface), var(--fg)).",
|
|
222
|
+
"- The AI colour marks AI only, and is never used as text. Amber means a person has to act.",
|
|
223
|
+
"- Every button and icon has a clear name. Status is never colour alone.",
|
|
224
|
+
"- If no part fits something I ask for, tell me and ask. Do not invent parts, props or colours.",
|
|
225
|
+
"",
|
|
226
|
+
"Project brief (save it as tattva.md and read it first next time):",
|
|
227
|
+
plan.brief,
|
|
228
|
+
].join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The checks run by review_code. Each has a rule name, how serious it is, a test for one line, and how to fix it. */
|
|
232
|
+
const CHECKS = [
|
|
233
|
+
{ rule: "no-hex", level: "error", test: (l) => /(?<![\w&])#[0-9a-fA-F]{3,8}\b/.test(l.replace(/\b(?:href|to|xlinkHref)=["']#[^"']*["']/g, "").replace(/url\(#[^)]*\)/g, "")) && !/^\s*(\/\/|\*|\/\*)/.test(l) && /(color|bg-|text-|border|fill|stroke|background|style|className|#[0-9a-fA-F]{6}\b)/.test(l), fix: "Use a token: `var(--surface)`, `var(--fg)` or a class such as `bg-surface` and `text-fg-muted`. Ask get_tokens for the right name." },
|
|
234
|
+
{ rule: "no-color-function", level: "error", test: (l) => /\b(rgb|rgba|hsl|hsla|oklch)\(/.test(l) && !/var\(|--/.test(l) && !/^\s*(\/\/|\*)/.test(l), fix: "Use a token instead of a raw colour function." },
|
|
235
|
+
{ rule: "no-default-palette", level: "error", test: (l) => /\b(?:bg|text|border|ring|fill|stroke|from|to|via)-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\d{2,3}\b/.test(l), fix: "Tailwind's default palette is not part of Tattva. Use classes built from tokens, such as `bg-surface`, `text-fg`, `border-line`, `bg-lime-soft`." },
|
|
236
|
+
{ rule: "no-deep-import", level: "error", test: (l) => /from\s+["'][^"']*\/(?:atoms|molecules|organisms|templates|pages)\/[^"']*["']/.test(l), fix: "Import from the package index: `import { Message } from \"./index\"`." },
|
|
237
|
+
{ rule: "no-div-click", level: "error", test: (l) => /<(?:div|span)\b[^>]*\bonClick=/.test(l) && !/role=["']button["']/.test(l), fix: "Use a real `<button>` (or Tattva's Button). A clickable div cannot be reached with the keyboard." },
|
|
238
|
+
{ rule: "lime-as-text", level: "error", test: (l) => /\btext-lime\b|color:\s*var\(--lime\)/.test(l), fix: "The AI colour is a fill, never text colour. Put dark text on it (`text-on-lime`)." },
|
|
239
|
+
{ rule: "focus-removed", level: "warn", test: (l) => /\b(?:focus:)?outline-none\b|outline:\s*none/.test(l) && !/focus-visible|ring/.test(l), fix: "Do not remove the focus outline. Tattva already draws one; keep it, or replace it with a visible ring." },
|
|
240
|
+
{ rule: "vague-destructive", level: "warn", test: (l) => />\s*(?:Delete|Remove|Discard|Reset)\s*</.test(l), fix: "Say the effect: \"Delete project Atlas\", not \"Delete\"." },
|
|
241
|
+
{ rule: "raw-html", level: "warn", test: (l) => /dangerouslySetInnerHTML/.test(l), fix: "Avoid raw HTML from the AI. Use Markdown, or put untrusted content in SandboxedFrame." },
|
|
242
|
+
{ rule: "amber-check", level: "info", test: (l) => /\b(?:text|bg|border)-attention\b|var\(--attention/.test(l), fix: "Amber means a person has to act. Use it only for a \"needs you\" state, not for warnings or decoration." },
|
|
243
|
+
{ rule: "colour-only-status", level: "info", test: (l) => /\b(?:text|bg)-(?:success|danger)\b/.test(l) && !/<(?:Icon|svg)|aria-label|sr-only/.test(l), fix: "Status must not rely on colour alone. Pair it with an icon and a word." },
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
/** Checks a piece of code against Tattva's rules. Returns findings with line numbers. */
|
|
247
|
+
export function reviewCode(code, components = [], filename = "") {
|
|
248
|
+
const lines = String(code).split("\n");
|
|
249
|
+
const findings = [];
|
|
250
|
+
lines.forEach((line, i) => {
|
|
251
|
+
for (const c of CHECKS) if (c.test(line)) findings.push({ line: i + 1, rule: c.rule, level: c.level, text: line.trim().slice(0, 140), fix: c.fix });
|
|
252
|
+
});
|
|
253
|
+
// Tag checks: read the whole opening tag, so props on several lines are seen.
|
|
254
|
+
const src = String(code);
|
|
255
|
+
const tagOf = (from) => { let depth = 0; for (let k = from; k < src.length; k++) { const ch = src[k]; if (ch === "{") depth++; else if (ch === "}") depth--; else if (ch === ">" && depth === 0) return src.slice(from, k + 1); } return src.slice(from, from + 400); };
|
|
256
|
+
for (const [re, attr, rule, fix] of [
|
|
257
|
+
[/<IconButton\b/g, /\blabel=/, "icon-button-label", "Every IconButton needs a `label` that names what it does, such as `label=\"Copy reply\"`."],
|
|
258
|
+
[/<img\b/g, /\balt=/, "img-alt", "Give every image an `alt`, or `alt=\"\"` if it is decoration."],
|
|
259
|
+
]) for (const m of src.matchAll(re)) { const tag = tagOf(m.index); if (!attr.test(tag) && !/\{\.\.\.\w+\}/.test(tag)) findings.push({ line: src.slice(0, m.index).split("\n").length, rule, level: "error", text: tag.split("\n")[0].trim().slice(0, 140), fix }); }
|
|
260
|
+
// Parts that are on their way out.
|
|
261
|
+
for (const c of components) {
|
|
262
|
+
const l = c.lifecycle; if (!l || (l.stage !== "deprecated" && l.stage !== "retired")) continue;
|
|
263
|
+
const re = new RegExp(`<${c.name}\\b`, "g");
|
|
264
|
+
for (const m of src.matchAll(re)) findings.push({ line: src.slice(0, m.index).split("\n").length, rule: `${l.stage}-part`, level: l.stage === "retired" ? "error" : "warn", text: `<${c.name}>`, fix: `${c.name} is ${l.stage}${l.removeIn ? ` (removed in ${l.removeIn})` : ""}. ${l.note ?? ""} Use ${components.find((x) => x.id === l.replacedBy)?.name ?? l.replacedBy} instead.` });
|
|
265
|
+
}
|
|
266
|
+
// Names imported from the package index must exist.
|
|
267
|
+
const names = new Set(components.map((c) => c.name));
|
|
268
|
+
const ids = components.map((c) => c.id);
|
|
269
|
+
for (const m of String(code).matchAll(/import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g)) {
|
|
270
|
+
if (!/(^|\/)(index|tattva)$/.test(m[2]) && !/tattva/.test(m[2])) continue;
|
|
271
|
+
const at = String(code).slice(0, m.index).split("\n").length;
|
|
272
|
+
for (const raw of m[1].split(",")) {
|
|
273
|
+
const n = raw.trim().split(/\s+as\s+/)[0];
|
|
274
|
+
if (!n || names.has(n) || !/^[A-Z]/.test(n)) continue;
|
|
275
|
+
const near = ids.map((id) => ({ id, d: lev(n.toLowerCase(), id.replace(/-/g, "")) })).sort((x, y) => x.d - y.d)[0];
|
|
276
|
+
findings.push({ line: at, rule: "unknown-part", level: "error", text: raw.trim(), fix: `"${n}" is not a Tattva part.${near && near.d <= 6 ? ` Did you mean "${components.find((c) => c.id === near.id).name}"?` : ""} Call search_components to find the right one.` });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
findings.sort((x, y) => x.line - y.line);
|
|
280
|
+
const order = { error: 0, warn: 1, info: 2 };
|
|
281
|
+
return { filename, findings, counts: { error: findings.filter((f) => f.level === "error").length, warn: findings.filter((f) => f.level === "warn").length, info: findings.filter((f) => f.level === "info").length }, order };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function lev(a, b) {
|
|
285
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
286
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
287
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
288
|
+
return d[a.length][b.length];
|
|
289
|
+
}
|
package/mcp-server.mjs
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tattva MCP server. Zero dependencies. Speaks newline-delimited JSON-RPC 2.0 over stdio.
|
|
3
|
+
// Read-only: it reads public/components.json, public/tokens.dtcg.json, public/themes.json and public/llms.txt and never writes.
|
|
4
|
+
// Run `node scripts/export-assets.mjs` first so those files exist.
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { QUESTIONS, planBuild, reviewCode } from "./mcp-guide.mjs";
|
|
10
|
+
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PROTOCOL = "2025-06-18";
|
|
13
|
+
const SITE = "https://lkb00.github.io/tattva/";
|
|
14
|
+
// Settings are read as TATTVA_*; the names from before the rename (SANDSTONE_*) still work.
|
|
15
|
+
for (const k of ["DATA", "API", "LICENSE_KEY"]) if (!process.env[`TATTVA_${k}`] && process.env[`SANDSTONE_${k}`]) process.env[`TATTVA_${k}`] = process.env[`SANDSTONE_${k}`];
|
|
16
|
+
// Where the data files come from, in this order: a folder named in TATTVA_DATA; a `data` folder next to this file
|
|
17
|
+
// (the Pro download ships one, with the full code); `../public` (inside the Tattva repo); the live site (the small
|
|
18
|
+
// public download needs nothing else, so the free parts and every part's guidance still work).
|
|
19
|
+
const DATA_DIRS = [process.env.TATTVA_DATA, join(here, "data"), join(here, "..", "public")].filter(Boolean);
|
|
20
|
+
const FILES = ["components.json", "tokens.dtcg.json", "themes.json", "llms.txt"];
|
|
21
|
+
const data = {};
|
|
22
|
+
const dataDir = DATA_DIRS.find((d) => FILES.every((f) => existsSync(join(d, f))));
|
|
23
|
+
for (const f of FILES) {
|
|
24
|
+
if (dataDir) { data[f] = readFileSync(join(dataDir, f), "utf8"); continue; }
|
|
25
|
+
try {
|
|
26
|
+
const r = await fetch(SITE + f);
|
|
27
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
28
|
+
data[f] = await r.text();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
process.stderr.write(`Tattva MCP: could not read ${f} locally or from ${SITE} (${e.message}). Check your internet connection, or set TATTVA_DATA to a folder holding the four data files.\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Pro and Team buyers can add their licence key instead of keeping the Pro download's data folder:
|
|
35
|
+
// TATTVA_LICENSE_KEY=<key from the receipt email>, and TATTVA_API=<the Tattva API address>. The API checks the
|
|
36
|
+
// key and sends every part with its full code. If the check fails, the server says why and carries on with the free data.
|
|
37
|
+
if (process.env.TATTVA_LICENSE_KEY && process.env.TATTVA_API && !process.env.TATTVA_DATA) {
|
|
38
|
+
try {
|
|
39
|
+
const r = await fetch(new URL("v1/pro/components.json", process.env.TATTVA_API.replace(/\/?$/, "/")), { headers: { authorization: `Bearer ${process.env.TATTVA_LICENSE_KEY.trim()}` } });
|
|
40
|
+
const body = await r.text();
|
|
41
|
+
if (r.ok) data["components.json"] = body;
|
|
42
|
+
else process.stderr.write(`Tattva MCP: Pro code stays locked. ${JSON.parse(body).message ?? r.status}\n`);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
process.stderr.write(`Tattva MCP: could not reach the Tattva API (${e.message}), so Pro code stays locked. The free parts and all guidance still work.\n`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const readJson = (rel) => JSON.parse(data[rel.replace(/^public\//, "")]);
|
|
48
|
+
const components = () => readJson("public/components.json").components;
|
|
49
|
+
|
|
50
|
+
const lev = (a, b) => {
|
|
51
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
52
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
53
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
54
|
+
return d[a.length][b.length];
|
|
55
|
+
};
|
|
56
|
+
const norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
57
|
+
const suggest = (q, list) => {
|
|
58
|
+
const n = norm(q);
|
|
59
|
+
return list.map((c) => ({ c, s: norm(c.id).includes(n) || n.includes(norm(c.id)) ? 0 : lev(n, norm(c.id)) })).sort((a, b) => a.s - b.s).slice(0, 4).map((x) => x.c.id);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const fail = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
63
|
+
const ok = (text, structured) => ({ content: [{ type: "text", text }], structuredContent: structured });
|
|
64
|
+
|
|
65
|
+
const tools = [
|
|
66
|
+
{
|
|
67
|
+
name: "search_components",
|
|
68
|
+
description: "Find Tattva components by what they are for. Searches names, summaries and descriptions. Use this first when you do not know the component id. Returns up to 15 matches with id, name, category, status and a one-line summary. Read-only.",
|
|
69
|
+
inputSchema: { type: "object", properties: {
|
|
70
|
+
query: { type: "string", description: "Words describing what you need, for example \"ask before deleting\" or \"chart\". Leave empty to list everything in a category." },
|
|
71
|
+
category: { type: "string", description: "Optional. One of: conversation, agents, trust, creating, numbers, states, controls, screens." },
|
|
72
|
+
status: { type: "string", enum: ["stable", "experimental"], description: "Optional. Experimental parts rest on design reasoning only and may change." },
|
|
73
|
+
}, additionalProperties: false },
|
|
74
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "get_component",
|
|
78
|
+
description: "Get everything needed to use one Tattva component correctly: props with types and defaults, when to use it, what to avoid, accessibility rules, the exact import line, tokens it uses and one example. Call it with an id from search_components. Read-only.",
|
|
79
|
+
inputSchema: { type: "object", properties: {
|
|
80
|
+
id: { type: "string", description: "Component id in kebab-case, for example \"permission-prompt\". The component name (PermissionPrompt) also works." },
|
|
81
|
+
all_examples: { type: "boolean", description: "Return every example instead of the first one. Default false." },
|
|
82
|
+
}, required: ["id"], additionalProperties: false },
|
|
83
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "list_patterns",
|
|
87
|
+
description: "List Tattva's patterns: short answers to situations like \"the assistant wants to do something that cannot be undone\". Each has a question, an answer and a page link. Use this to choose an approach before choosing components. Read-only.",
|
|
88
|
+
inputSchema: { type: "object", properties: { query: { type: "string", description: "Optional word filter, for example \"source\" or \"undo\"." } }, additionalProperties: false },
|
|
89
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "start_project",
|
|
93
|
+
description: "Call this FIRST when a person wants to build something with Tattva, unless a tattva.md brief already exists in their project (then read it instead). Returns a short list of questions to ask the person, each with options and a default. Ask them in plain words, skip any the person has already answered, and accept \"defaults\". Then call plan_build with the answers. Read-only.",
|
|
94
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
95
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "plan_build",
|
|
99
|
+
description: "Turn the person's answers into a build plan: setup steps, the parts to use in build order (marking which are Pro), patterns to read, warnings, and a project brief to save as tattva.md in their project. Call it after start_project. Missing answers fall back to defaults. Read-only.",
|
|
100
|
+
inputSchema: { type: "object", properties: {
|
|
101
|
+
product: { type: "string", enum: ["chat-assistant", "agent-console", "creative-tool", "generative-ui", "ai-dashboard"], description: "What is being built." },
|
|
102
|
+
stack: { type: "string", enum: ["new-vite", "new-next", "existing"], description: "New or existing project, and framework." },
|
|
103
|
+
theme: { type: "string", enum: ["lime", "teal", "indigo", "harbor", "plum", "graphite"], description: "Colour theme. Default lime." },
|
|
104
|
+
mode: { type: "string", enum: ["both", "light", "dark"], description: "Light, dark or both. Default both." },
|
|
105
|
+
trust: { type: "array", items: { type: "string", enum: ["sources", "confidence", "approvals", "undo", "memory", "disclosure", "feedback", "errors"] }, description: "Trust behaviours the product needs." },
|
|
106
|
+
plan: { type: "string", enum: ["free", "pro"], description: "The person's plan. Default free." },
|
|
107
|
+
}, additionalProperties: false },
|
|
108
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "review_code",
|
|
112
|
+
description: "Check code you wrote against Tattva's rules before you finish: typed hex colours, raw colour functions, Tailwind's default palette, deep imports, clickable divs, icon buttons with no label, images with no alt, lime used as text, removed focus outlines, vague destructive buttons, and parts that do not exist. Pass the file's code. Fix every error and re-run until none are left. Read-only; nothing is written.",
|
|
113
|
+
inputSchema: { type: "object", properties: {
|
|
114
|
+
code: { type: "string", description: "The full source of one file (TSX, JSX, CSS or HTML)." },
|
|
115
|
+
filename: { type: "string", description: "Optional file name, shown in the result." },
|
|
116
|
+
}, required: ["code"], additionalProperties: false },
|
|
117
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "get_themes",
|
|
121
|
+
description: "List Tattva's colour themes (lime, teal, indigo, harbor, plum, graphite, terminal) or get one in full: its AI color in light and dark, paper and ink, the two soft wash colors, how to switch to it, and every token that differs from the default. A theme changes the greys and the AI color only; amber and the status colors never change. Read-only.",
|
|
122
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "Optional theme id, for example \"indigo\". Omit to list all themes." } }, additionalProperties: false },
|
|
123
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "get_tokens",
|
|
127
|
+
description: "Get Tattva design tokens (colors, timing, sizes, shadows) with what each is for. Use token names in CSS as var(--name) or as Tailwind classes. Never type a hex value in generated code. Read-only.",
|
|
128
|
+
inputSchema: { type: "object", properties: {
|
|
129
|
+
group: { type: "string", description: "Optional group: surfaces, lines, text, actions, attention, status, direction, reserved, code, charts, size, motion, shadow. Omit to list the group names with counts." },
|
|
130
|
+
mode: { type: "string", enum: ["light", "dark"], description: "Light or dark values. Default light." },
|
|
131
|
+
theme: { type: "string", description: "Optional colour theme id (see get_themes), for example \"indigo\". Returns that theme's colors. Default lime." },
|
|
132
|
+
}, additionalProperties: false },
|
|
133
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const handlers = {
|
|
138
|
+
search_components({ query = "", category, status } = {}) {
|
|
139
|
+
const all = components();
|
|
140
|
+
const cats = [...new Set(all.map((c) => c.category).filter(Boolean))];
|
|
141
|
+
if (category && !cats.includes(category)) return fail(`Unknown category "${category}". Use one of: ${cats.join(", ")}.`);
|
|
142
|
+
// Plain-language search: drop filler words, match word stems, and let rare words count for more than common ones.
|
|
143
|
+
const STOP = new Set("a an the and or of to for in on at by with from into is are be it its this that these those i we you your my our me us user users person people need needs want wants let lets make makes show shows showing display displays place way something thing things when while before after so can should would could will do does how what which where who ai".split(" "));
|
|
144
|
+
const stem = (w) => w.replace(/(ing|ed|es|s|ly)$/, (m, x, i) => (i > 3 ? "" : m));
|
|
145
|
+
const tok = (t) => String(t).toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter(Boolean);
|
|
146
|
+
const SAME = { got: "came", get: "come", gets: "comes", citation: "source", citations: "sources", cite: "source", reference: "source", references: "sources" };
|
|
147
|
+
const words = [...new Set(tok(query).filter((w) => !STOP.has(w)).map((w) => stem(SAME[w] ?? w)))].filter((w) => w.length > 1);
|
|
148
|
+
const field = (c) => ({
|
|
149
|
+
name: tok(`${c.id} ${c.name}`).map(stem),
|
|
150
|
+
head: tok(`${c.summary} ${c.purpose ?? ""} ${(c.useFor ?? []).join(" ")}`).map(stem),
|
|
151
|
+
body: tok(`${c.description} ${c.whenToUse.join(" ")}`).map(stem),
|
|
152
|
+
});
|
|
153
|
+
const pool = all.filter((c) => (!category || c.category === category) && (!status || c.status === status));
|
|
154
|
+
const fields = new Map(pool.map((c) => [c, field(c)]));
|
|
155
|
+
const df = (w) => pool.filter((c) => { const f = fields.get(c); return f.name.includes(w) || f.head.includes(w) || f.body.includes(w); }).length;
|
|
156
|
+
const idf = new Map(words.map((w) => [w, Math.log(1 + pool.length / (1 + df(w)))]));
|
|
157
|
+
const scored = pool.map((c) => {
|
|
158
|
+
const f = fields.get(c);
|
|
159
|
+
let s = 0;
|
|
160
|
+
for (const w of words) s += idf.get(w) * ((f.name.includes(w) ? 5 : 0) + (f.head.includes(w) ? 3 : 0) + (f.body.includes(w) ? 1 : 0));
|
|
161
|
+
return { c, s };
|
|
162
|
+
}).filter((x) => !words.length || x.s > 0).sort((a, b) => b.s - a.s);
|
|
163
|
+
const results = scored.slice(0, 15).map(({ c }) => ({ id: c.id, name: c.name, category: c.category, status: c.status, summary: c.summary }));
|
|
164
|
+
if (!results.length) return ok(`No components match "${query}". Try fewer or simpler words, or filter by category: ${cats.join(", ")}.`, { results: [], total: 0 });
|
|
165
|
+
const text = `${scored.length} match${scored.length === 1 ? "" : "es"}${scored.length > 15 ? " (showing 15)" : ""}:\n` + results.map((r) => `- ${r.id} (${r.name}, ${r.category}, ${r.status}): ${r.summary}`).join("\n") + "\nNext: call get_component with an id.";
|
|
166
|
+
return ok(text, { results, total: scored.length });
|
|
167
|
+
},
|
|
168
|
+
get_component({ id, all_examples = false } = {}) {
|
|
169
|
+
if (!id) return fail("Missing `id`. Call search_components first to find one, for example id: \"button\".");
|
|
170
|
+
const all = components();
|
|
171
|
+
const n = norm(id);
|
|
172
|
+
let c = all.find((x) => x.id === id) ?? all.find((x) => norm(x.id) === n || norm(x.name) === n);
|
|
173
|
+
if (!c) return fail(`No component called "${id}". Did you mean: ${suggest(id, all).join(", ")}? Call search_components to browse.`);
|
|
174
|
+
// The docs are written for the Tattva repo (./index). In a person's project free parts come from the npm package
|
|
175
|
+
// and Pro parts from the tattva folder of the Pro download.
|
|
176
|
+
const toKit = (x) => String(x).replace(/from "\.\/index"/g, c.plan === "pro" ? 'from "./tattva"' : 'from "@tattva/ui"');
|
|
177
|
+
c = { ...c, importPath: toKit(c.importPath) };
|
|
178
|
+
const examples = (all_examples ? c.examples : c.examples.slice(0, 1)).map((e) => ({ ...e, code: toKit(e.code) }));
|
|
179
|
+
const data = {
|
|
180
|
+
id: c.id, name: c.name, level: c.level, category: c.category, status: c.status, lifecycle: c.lifecycle ?? null, states: c.states ?? [], summary: c.summary, description: c.description,
|
|
181
|
+
importPath: c.importPath, sourcePath: c.sourcePath,
|
|
182
|
+
props: c.props, variantsHint: c.variantsHint,
|
|
183
|
+
purpose: c.purpose ?? null, useFor: c.useFor ?? [], notFor: c.notFor ?? [],
|
|
184
|
+
whenToUse: c.whenToUse, avoid: c.avoid, accessibility: c.accessibility, accessibilityYouDo: c.accessibilityYouDo ?? [],
|
|
185
|
+
tokens: c.tokens, related: c.related, composedOf: c.composedOf, examples, pageUrl: c.pageUrl, markdownUrl: c.markdownUrl,
|
|
186
|
+
};
|
|
187
|
+
const list = (a) => a.map((x) => `- ${x}`).join("\n");
|
|
188
|
+
const text = [
|
|
189
|
+
`${c.name} (${c.id}, ${c.level}, ${c.status})`, c.summary,
|
|
190
|
+
c.lifecycle && (c.lifecycle.stage === "deprecated" || c.lifecycle.stage === "retired") ? `WARNING: ${c.name} is ${c.lifecycle.stage}. ${c.lifecycle.note ?? ""} Use ${c.lifecycle.replacedBy} instead.` : "",
|
|
191
|
+
"", c.importPath, "",
|
|
192
|
+
"Props:", ...c.props.map((p) => `- ${p.name}${p.required ? " (required)" : ""}: ${p.type}${p.default ? ` = ${p.default}` : ""}. ${p.description}`), "",
|
|
193
|
+
c.purpose ? `When to use it: ${c.purpose}` : "", c.notFor?.length ? `Not for:\n${list(c.notFor.map((n) => n.use ? `${n.text}: use ${n.use}` : n.text))}` : "",
|
|
194
|
+
"Do:", list(c.whenToUse), "Avoid:", list(c.avoid), "Accessibility (built in):", list(c.accessibility),
|
|
195
|
+
c.accessibilityYouDo?.length ? `Accessibility (you must):\n${list(c.accessibilityYouDo)}` : "",
|
|
196
|
+
c.states?.length ? `States:\n${list(c.states.map((x) => `${x.name}: ${x.note}`))}` : "",
|
|
197
|
+
c.related.length ? `Related: ${c.related.join(", ")}` : "",
|
|
198
|
+
"", ...examples.flatMap((e) => [`Example: ${e.title}`, "```tsx", e.code, "```"]),
|
|
199
|
+
].filter((x) => x !== "").join("\n");
|
|
200
|
+
return ok(text, data);
|
|
201
|
+
},
|
|
202
|
+
list_patterns({ query = "" } = {}) {
|
|
203
|
+
const txt = data["llms.txt"];
|
|
204
|
+
const sec = txt.split(/^## /m).find((s) => s.startsWith("Patterns")) ?? "";
|
|
205
|
+
let patterns = sec.split("\n").filter((l) => l.startsWith("- ")).map((l) => {
|
|
206
|
+
const m = l.match(/^- ([^:]+): (.*) \((https?:\/\/[^)]+)\)$/);
|
|
207
|
+
if (!m) return null;
|
|
208
|
+
const url = m[3]; const q = m[2].match(/^(.*?\?)\s*(.*)$/);
|
|
209
|
+
return { id: url.split("#pattern-")[1] ?? "", title: m[1], question: q ? q[1] : "", answer: q ? q[2] : m[2], url };
|
|
210
|
+
}).filter(Boolean);
|
|
211
|
+
const words = String(query).toLowerCase().split(/\s+/).filter(Boolean);
|
|
212
|
+
if (words.length) patterns = patterns.filter((p) => words.some((w) => `${p.title} ${p.question} ${p.answer}`.toLowerCase().includes(w)));
|
|
213
|
+
if (!patterns.length) return ok(`No patterns match "${query}". Call list_patterns with no query to see all of them.`, { patterns: [] });
|
|
214
|
+
return ok(patterns.map((p) => `- ${p.title}: ${p.question} ${p.answer}\n ${p.url}`).join("\n"), { patterns });
|
|
215
|
+
},
|
|
216
|
+
start_project() {
|
|
217
|
+
const lines = QUESTIONS.map((q, i) => `${i + 1}. ${q.question}\n${q.options.map((o) => ` - ${o.value}: ${o.label}${(Array.isArray(q.default) ? q.default.includes(o.value) : q.default === o.value) ? " [default]" : ""}`).join("\n")}`);
|
|
218
|
+
return ok(["Before building, ask the person these questions in plain words (one message, not one at a time). Skip any they have already answered. If they say \"defaults\", use the defaults.", "", ...lines, "", "Then call plan_build with the answers. If the project already has a tattva.md brief, read it and only ask what is missing."].join("\n"), { questions: QUESTIONS });
|
|
219
|
+
},
|
|
220
|
+
plan_build(args = {}) {
|
|
221
|
+
const r = planBuild(args, components());
|
|
222
|
+
if (r.error) return fail(r.error);
|
|
223
|
+
const groups = [...new Set(r.parts.map((p) => p.group))];
|
|
224
|
+
const text = [
|
|
225
|
+
`Plan: ${r.recipe.title}. Theme ${r.answers.theme}, ${r.answers.mode} mode, ${r.answers.plan} plan.`, "",
|
|
226
|
+
"Setup:", ...r.setup.map((x, i) => `${i + 1}. ${x}`), "",
|
|
227
|
+
"Run these commands, in order:", ...r.commands.map((c) => ` ${c}`), "",
|
|
228
|
+
"Write these files exactly as given (overwrite the ones the template created):", ...r.files.flatMap((f) => ["", `--- ${f.path} (${f.why}) ---`, f.content.trimEnd()]), "",
|
|
229
|
+
"Setup is not finished until the commands have run and every file above is written. Then you write src/App.tsx, importing parts from \"./tattva\". On the free plan the kit holds the free parts only: if the plan lists Pro parts, their code is not installed, so tell the person and do not pretend they are there. On Pro or Team, ask the person where their tattva-pro.zip is.", "",
|
|
230
|
+
"Parts, in build order:", ...groups.flatMap((g) => [`${g}:`, ...r.parts.filter((p) => p.group === g).map((p) => `- ${p.name} (${p.id})${p.plan === "pro" ? " [Pro]" : ""}: ${p.why}`)]), "",
|
|
231
|
+
`Patterns to read first: call list_patterns with the words ${r.patterns.map((x) => `"${x}"`).join(", ")}.`,
|
|
232
|
+
...(r.warnings.length ? ["", "Heads up:", ...r.warnings.map((w) => `- ${w}`)] : []),
|
|
233
|
+
"", "Next: for each part call get_component for its props and an example, then build. When a file is done, call review_code on it and fix every error.", "", "(The brief is written to tattva.md above.)",
|
|
234
|
+
].join("\n");
|
|
235
|
+
return ok(text, r);
|
|
236
|
+
},
|
|
237
|
+
review_code({ code, filename = "" } = {}) {
|
|
238
|
+
if (typeof code !== "string" || !code.trim()) return fail("Pass the file's source as `code`.");
|
|
239
|
+
const r = reviewCode(code, components(), filename);
|
|
240
|
+
const label = filename ? ` in ${filename}` : "";
|
|
241
|
+
if (!r.findings.length) return ok(`No problems found${label}. The code follows the Tattva rules this checker can see. It cannot judge layout or wording, so still check the result on screen.`, r);
|
|
242
|
+
const text = [`${r.counts.error} error${r.counts.error === 1 ? "" : "s"}, ${r.counts.warn} warning${r.counts.warn === 1 ? "" : "s"}, ${r.counts.info} note${r.counts.info === 1 ? "" : "s"}${label}. Fix the errors and run review_code again.`, "", ...r.findings.map((f) => `line ${f.line} [${f.level}] ${f.rule}: ${f.text}\n Fix: ${f.fix}`)].join("\n");
|
|
243
|
+
return ok(text, r);
|
|
244
|
+
},
|
|
245
|
+
get_themes({ id } = {}) {
|
|
246
|
+
const all = readJson("public/themes.json").themes;
|
|
247
|
+
if (!id) {
|
|
248
|
+
const lines = all.map((t) => `- ${t.id}${t.default ? " (default)" : ""}: ${t.name}, ${t.note}. AI color ${t.ai.light} (light) / ${t.ai.dark} (dark). Use: ${t.use}`);
|
|
249
|
+
return ok(`Tattva colour themes. A theme changes the greys and the AI color (--lime) in light and dark. Amber and the status colors never change. Switch with data-palette on the html element.\n${lines.join("\n")}\nCall get_themes with an id for the full set of values.`, { themes: all.map(({ colors, ...t }) => t) });
|
|
250
|
+
}
|
|
251
|
+
const t = all.find((x) => x.id === String(id).toLowerCase());
|
|
252
|
+
if (!t) return fail(`No theme called "${id}". Use one of: ${all.map((x) => x.id).join(", ")}.`);
|
|
253
|
+
const n = (o) => Object.keys(o).length;
|
|
254
|
+
const lines = [`${t.name}${t.default ? " (default)" : ""}: ${t.note}`, `Use: ${t.use}`, `AI color (--lime): ${t.ai.light} light, ${t.ai.dark} dark`, `Paper (--bg): ${t.paper.light} light, ${t.paper.dark} dark`, `Ink (--fg): ${t.ink.light} light, ${t.ink.dark} dark`, `Soft wash (--ai-wash-1, --ai-wash-2): ${t.wash.light?.join(", ")} light${t.wash.dark ? `; ${t.wash.dark.join(", ")} dark` : ""}`, `Tokens that differ from the default: ${n(t.colors.light)} light, ${n(t.colors.dark)} dark. Call get_tokens with theme "${t.id}" to read them. In code, keep using var(--name); never type these hex values.`];
|
|
255
|
+
return ok(lines.join("\n"), t);
|
|
256
|
+
},
|
|
257
|
+
get_tokens({ group, mode = "light", theme } = {}) {
|
|
258
|
+
if (!["light", "dark"].includes(mode)) return fail(`Unknown mode "${mode}". Use "light" or "dark".`);
|
|
259
|
+
const t = readJson("public/tokens.dtcg.json");
|
|
260
|
+
let themeColors = null;
|
|
261
|
+
if (theme) {
|
|
262
|
+
const th = readJson("public/themes.json").themes.find((x) => x.id === String(theme).toLowerCase());
|
|
263
|
+
if (!th) return fail(`No theme called "${theme}". Call get_themes to see the ids.`);
|
|
264
|
+
themeColors = th.colors[mode];
|
|
265
|
+
}
|
|
266
|
+
const cols = t.color[mode];
|
|
267
|
+
const names = { surfaces: "color", lines: "color", text: "color", actions: "color", attention: "color", status: "color", code: "color", charts: "color", other: "color", size: "size", fontSize: "fontSize", motion: "duration", shadow: "shadow" };
|
|
268
|
+
const flat = (g, kind, o) => Object.entries(o).filter(([k]) => !k.startsWith("$")).map(([k, v]) => {
|
|
269
|
+
const val = v.$type === "color" ? v.$value.hex : v.$type === "duration" || v.$type === "dimension" ? `${v.$value.value}${v.$value.unit}` : v.$type === "cubicBezier" ? `cubic-bezier(${v.$value.join(", ")})` : v.$type === "shadow" ? (v.$extensions?.css ?? JSON.stringify(v.$value)) : JSON.stringify(v.$value);
|
|
270
|
+
return { name: `--${k}`, type: v.$type, value: val, description: v.$description ?? "", group: g };
|
|
271
|
+
});
|
|
272
|
+
const groups = {};
|
|
273
|
+
for (const [g, o] of Object.entries(cols)) groups[g] = flat(g, "color", o).map((x) => (themeColors?.[x.name] ? { ...x, value: themeColors[x.name] } : x));
|
|
274
|
+
groups.size = [...flat("size", "dimension", t.size ?? {}), ...flat("size", "dimension", t.fontSize ?? {})];
|
|
275
|
+
groups.motion = [...flat("motion", "duration", t.duration ?? {}), ...flat("motion", "cubicBezier", t.easing ?? {})];
|
|
276
|
+
groups.shadow = flat("shadow", "shadow", t.shadow?.[mode] ?? {});
|
|
277
|
+
if (!group) {
|
|
278
|
+
const summary = Object.entries(groups).map(([g, a]) => ({ group: g, count: a.length }));
|
|
279
|
+
return ok(`Token groups (${mode}): ${summary.map((s) => `${s.group} (${s.count})`).join(", ")}.\nCall get_tokens with a group to see names and values. Use names as var(--name). Never write hex values in components.`, { mode, groups: summary });
|
|
280
|
+
}
|
|
281
|
+
if (!groups[group]) return fail(`No token group called "${group}". Use one of: ${Object.keys(groups).join(", ")}.`);
|
|
282
|
+
return ok(groups[group].map((x) => `${x.name} = ${x.value}${x.description ? ` (${x.description})` : ""}`).join("\n"), { mode, group, tokens: groups[group] });
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const send = (m) => process.stdout.write(JSON.stringify(m) + "\n");
|
|
287
|
+
const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
288
|
+
const error = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
289
|
+
|
|
290
|
+
function handle(msg) {
|
|
291
|
+
const { id, method, params } = msg;
|
|
292
|
+
const isNote = id === undefined || id === null;
|
|
293
|
+
if (method === "initialize") {
|
|
294
|
+
return reply(id, { protocolVersion: params?.protocolVersion ?? PROTOCOL, capabilities: { tools: { listChanged: false }, prompts: { listChanged: false } }, serverInfo: { name: "tattva", version: "0.1.0" },
|
|
295
|
+
instructions: "Tattva is a design system for AI products. To build something: (1) if the project has a tattva.md brief, read it; otherwise call start_project and ask the person its questions, then call plan_build, then run its commands and write its files exactly as given before anything else. (2) For each part call get_component, then import from the package index. (3) Use tokens, never hex values. The AI colour (--lime) is a fill, never text; amber means a person has to act. (4) If nothing fits what the person asked for, say so and ask; never invent a part, a prop or a colour. (5) Before finishing, call review_code on every file you wrote and fix all errors. Parts marked Pro have free previews but code that needs a Pro or Team plan; tell the person before using them." });
|
|
296
|
+
}
|
|
297
|
+
if (method === "notifications/initialized" || method?.startsWith("notifications/")) return;
|
|
298
|
+
if (method === "ping") return reply(id, {});
|
|
299
|
+
if (method === "tools/list") return reply(id, { tools });
|
|
300
|
+
if (method === "prompts/list") return reply(id, { prompts: [{ name: "build", title: "Build with Tattva", description: "Start a guided build: a few questions, then a plan, then a check of the code.", arguments: [{ name: "idea", description: "Optional: what you want to build, in a sentence.", required: false }] }] });
|
|
301
|
+
if (method === "prompts/get") {
|
|
302
|
+
if (params?.name !== "build") return error(id, -32602, `Unknown prompt "${params?.name}". Available: build.`);
|
|
303
|
+
const idea = params?.arguments?.idea ? ` The person wants to build: ${params.arguments.idea}.` : "";
|
|
304
|
+
return reply(id, { description: "Build with Tattva", messages: [{ role: "user", content: { type: "text", text: `Help me build a product with the Tattva design system.${idea} Start by calling the start_project tool and ask me its questions, then call plan_build, then build it part by part, and run review_code on every file before you finish.` } }] });
|
|
305
|
+
}
|
|
306
|
+
if (method === "tools/call") {
|
|
307
|
+
const h = handlers[params?.name];
|
|
308
|
+
if (!h) return reply(id, fail(`Unknown tool "${params?.name}". Available tools: ${tools.map((t) => t.name).join(", ")}.`));
|
|
309
|
+
try { return reply(id, h(params.arguments ?? {})); } catch (e) { return reply(id, fail(String(e.message ?? e))); }
|
|
310
|
+
}
|
|
311
|
+
if (!isNote) error(id, -32601, `Method not found: ${method}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
createInterface({ input: process.stdin }).on("line", (line) => {
|
|
315
|
+
if (!line.trim()) return;
|
|
316
|
+
let msg;
|
|
317
|
+
try { msg = JSON.parse(line); } catch { return error(null, -32700, "Parse error: send one JSON-RPC message per line."); }
|
|
318
|
+
try { handle(msg); } catch (e) { if (msg.id !== undefined) error(msg.id, -32603, String(e.message ?? e)); }
|
|
319
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tattva/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"description": "The Tattva MCP server: lets Claude Code, Cursor and other AI tools look up Tattva parts, plan a build and check code.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
|
+
"homepage": "https://lkb00.github.io/tattva/connect-ai/",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mcp",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"design-system",
|
|
11
|
+
"tattva",
|
|
12
|
+
"claude",
|
|
13
|
+
"cursor"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"tattva-mcp": "mcp-server.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"mcp-server.mjs",
|
|
21
|
+
"mcp-guide.mjs",
|
|
22
|
+
"LICENSE.md",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
}
|
|
28
|
+
}
|