A design system in a box. hip-ui.tngl.io/docs/introduction
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fold server into main cli package

+285 -312
+1 -1
.cursor/mcp.json
··· 2 2 "mcpServers": { 3 3 "hip-ui": { 4 4 "command": "node", 5 - "args": ["packages/mcp/dist/index.js"] 5 + "args": ["packages/hip-ui/dist/cli/bin.js", "mcp"] 6 6 } 7 7 } 8 8 }
+6 -2
packages/hip-ui/package.json
··· 14 14 "./*": "./dist/*.js" 15 15 }, 16 16 "scripts": { 17 - "build": "tsc --build tsconfig.build.json", 17 + "build": "tsc --build tsconfig.build.json && node ./scripts/copy-mcp-docs.mjs", 18 18 "lint": "oxlint .", 19 19 "generate:component": "turbo gen react-component", 20 20 "generate:component-configs": "tsx scripts/generate-component-configs.ts", ··· 56 56 }, 57 57 "dependencies": { 58 58 "@inkjs/ui": "^2.0.0", 59 + "@tmcp/adapter-valibot": "^0.1.5", 60 + "@tmcp/transport-stdio": "latest", 59 61 "command-line-application": "^0.10.1", 60 62 "ink": "^6.3.1", 61 - "lilconfig": "^3.1.3" 63 + "lilconfig": "^3.1.3", 64 + "tmcp": "latest", 65 + "valibot": "^1.1.0" 62 66 } 63 67 }
+28 -14
packages/hip-ui/src/cli/bin.ts
··· 2 2 3 3 import { app } from "command-line-application"; 4 4 5 + import { startMcpServer } from "../mcp/index.js"; 5 6 import { installComponent } from "./install.js"; 6 7 7 8 const install: Command = { ··· 24 25 ], 25 26 }; 26 27 28 + const mcp: Command = { 29 + name: "mcp", 30 + description: "Start the Hip UI MCP server", 31 + }; 32 + 27 33 const hip: MultiCommand = { 28 - name: "hip", 34 + name: "hip-ui", 29 35 description: "Scaffold hip components into your project", 30 - commands: [install], 36 + commands: [install, mcp], 31 37 }; 32 38 33 39 const args = app(hip); 34 40 35 - if (args?._command === "install") { 36 - const component = ( 37 - Array.isArray(args.component) 38 - ? args.component 39 - : args.component 40 - ? [args.component] 41 - : [] 42 - ) as Array<string>; 41 + async function main() { 42 + if (args?._command === "install") { 43 + const component = ( 44 + Array.isArray(args.component) 45 + ? args.component 46 + : args.component 47 + ? [args.component] 48 + : [] 49 + ) as Array<string>; 43 50 44 - void installComponent({ 45 - component, 46 - all: args.all as boolean, 47 - }); 51 + await installComponent({ 52 + component, 53 + all: args.all as boolean, 54 + }); 55 + } 56 + 57 + if (args?._command === "mcp") { 58 + startMcpServer(); 59 + } 48 60 } 61 + 62 + void main();
+237
packages/hip-ui/src/mcp/index.ts
··· 1 + import { ValibotJsonSchemaAdapter } from "@tmcp/adapter-valibot"; 2 + import { StdioTransport } from "@tmcp/transport-stdio"; 3 + import { McpServer } from "tmcp"; 4 + import { resource, tool } from "tmcp/utils"; 5 + import * as v from "valibot"; 6 + 7 + import { getAllSections } from "./docs-index.js"; 8 + 9 + const llmTipsResourceUri = "hip-ui://tips-and-tricks-for-llms"; 10 + const llmTipsResourceName = "tips & tricks for LLMs"; 11 + const llmTipsResourceContent = `# Tips & Tricks for LLMs 12 + 13 + - Flex should always be used when possible. Use Grid when you need complex 2 dimensional layout. Always have gaps. 14 + - All the text uses text-box-trim: trim-both with text-box-edge: cap alphabetic. This means that line height is not used for the first and last line of text. In practice this means that we need to increase the flex gap size used for text. 15 + - Use Card sparingly 16 + `; 17 + 18 + const getDocumentationSchema = v.object({ 19 + section: v.pipe( 20 + v.union([v.string(), v.array(v.string())]), 21 + v.description( 22 + "The section name(s) to retrieve. Can search by title, docs slug path, or /docs URL path. Supports a single string or array of strings.", 23 + ), 24 + ), 25 + }); 26 + 27 + function createMcpServer() { 28 + const server = new McpServer( 29 + { 30 + name: "hip-ui-docs", 31 + version: "0.0.0", 32 + }, 33 + { 34 + adapter: new ValibotJsonSchemaAdapter(), 35 + capabilities: { 36 + tools: { 37 + listChanged: true, 38 + }, 39 + resources: { 40 + subscribe: true, 41 + listChanged: true, 42 + }, 43 + }, 44 + instructions: `Always load the resource "${llmTipsResourceName}" at "${llmTipsResourceUri}" first, and keep it in context while using this server. Then use list-sections to discover available Hip UI docs pages, and get-documentation to retrieve complete markdown for one or more sections.`, 45 + }, 46 + ); 47 + 48 + server.resource( 49 + { 50 + name: llmTipsResourceName, 51 + description: "Persistent context and usage guidance for this MCP server.", 52 + uri: llmTipsResourceUri, 53 + mimeType: "text/markdown", 54 + }, 55 + async () => 56 + resource.text( 57 + llmTipsResourceUri, 58 + llmTipsResourceContent, 59 + "text/markdown", 60 + ), 61 + ); 62 + 63 + server.tool( 64 + { 65 + name: "list-sections", 66 + description: "Lists all available Hip UI docs markdown sections.", 67 + annotations: { 68 + readOnlyHint: true, 69 + openWorldHint: false, 70 + destructiveHint: false, 71 + title: "List Sections", 72 + }, 73 + }, 74 + async () => { 75 + const sectionsList = await formatSectionsList(); 76 + return tool.text(sectionsList); 77 + }, 78 + ); 79 + 80 + server.tool( 81 + { 82 + name: "get-documentation", 83 + description: 84 + "Retrieves full markdown documentation for one or more Hip UI docs sections.", 85 + schema: getDocumentationSchema, 86 + annotations: { 87 + readOnlyHint: true, 88 + openWorldHint: false, 89 + destructiveHint: false, 90 + title: "Get Documentation", 91 + }, 92 + }, 93 + async ({ section }: { section: string | Array<string> }) => { 94 + const requestedSections = normalizeInputToArray(section); 95 + const availableSections = await getAllSections(); 96 + 97 + const results = requestedSections.map((requestedSection) => { 98 + const requestedKey = normalizeSectionKey(requestedSection); 99 + const exactMatch = availableSections.find((availableSection) => { 100 + const matchesTitle = 101 + availableSection.title.toLowerCase() === 102 + requestedSection.toLowerCase(); 103 + const matchesSlug = 104 + normalizeSectionKey(availableSection.slug) === requestedKey; 105 + const matchesUrlPath = 106 + normalizeSectionKey(availableSection.urlPath) === requestedKey; 107 + return matchesTitle || matchesSlug || matchesUrlPath; 108 + }); 109 + 110 + if (!exactMatch) { 111 + return { 112 + requestedSection, 113 + success: false as const, 114 + }; 115 + } 116 + 117 + return { 118 + requestedSection, 119 + success: true as const, 120 + content: `## ${exactMatch.title}\n\n${exactMatch.markdown}`, 121 + }; 122 + }); 123 + 124 + const successfulResults = results.filter((result) => result.success); 125 + const failedSections = results 126 + .filter((result) => !result.success) 127 + .map((result) => result.requestedSection); 128 + 129 + if (successfulResults.length > 0 && failedSections.length === 0) { 130 + return tool.text( 131 + successfulResults.map((result) => result.content).join("\n\n---\n\n"), 132 + ); 133 + } 134 + 135 + const responseParts: Array<string> = []; 136 + 137 + if (successfulResults.length > 0) { 138 + responseParts.push( 139 + successfulResults.map((result) => result.content).join("\n\n---\n\n"), 140 + ); 141 + } 142 + 143 + const fuzzyMatches = failedSections.map((failedSection) => { 144 + const failedSectionKey = normalizeSectionKey(failedSection); 145 + const similarSections = availableSections.filter((availableSection) => { 146 + const title = availableSection.title.toLowerCase(); 147 + const slug = availableSection.slug.toLowerCase(); 148 + const urlPath = availableSection.urlPath.toLowerCase(); 149 + return ( 150 + title.includes(failedSectionKey) || 151 + slug.includes(failedSectionKey) || 152 + urlPath.includes(failedSectionKey) || 153 + failedSectionKey.includes(slug.split("/").at(-1) ?? "") 154 + ); 155 + }); 156 + return { failedSection, similarSections }; 157 + }); 158 + 159 + const hasFuzzyMatches = fuzzyMatches.some( 160 + (fuzzyMatch) => fuzzyMatch.similarSections.length > 0, 161 + ); 162 + 163 + if (successfulResults.length === 0 && !hasFuzzyMatches) { 164 + responseParts.push(await formatSectionsList()); 165 + } 166 + 167 + for (const fuzzyMatch of fuzzyMatches) { 168 + if (fuzzyMatch.similarSections.length > 0) { 169 + const similarSectionsList = fuzzyMatch.similarSections 170 + .map( 171 + (similarSection) => 172 + `- title: ${similarSection.title}, section: ${similarSection.slug}`, 173 + ) 174 + .join("\n"); 175 + responseParts.push( 176 + `${fuzzyMatch.similarSections.length} similar result${fuzzyMatch.similarSections.length > 1 ? "s" : ""} for "${fuzzyMatch.failedSection}":\n${similarSectionsList}`, 177 + ); 178 + } 179 + 180 + responseParts.push(`Section not found: "${fuzzyMatch.failedSection}".`); 181 + } 182 + 183 + return tool.text(responseParts.join("\n\n---\n\n")); 184 + }, 185 + ); 186 + 187 + return server; 188 + } 189 + 190 + function normalizeInputToArray(value: string | Array<string>) { 191 + if (Array.isArray(value)) { 192 + return value.filter((item): item is string => typeof item === "string"); 193 + } 194 + 195 + const trimmed = value.trim(); 196 + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { 197 + try { 198 + const parsedValue = JSON.parse(trimmed); 199 + if (Array.isArray(parsedValue)) { 200 + return parsedValue.filter( 201 + (item): item is string => typeof item === "string", 202 + ); 203 + } 204 + } catch { 205 + return [value]; 206 + } 207 + } 208 + 209 + return [value]; 210 + } 211 + 212 + function normalizeSectionKey(value: string) { 213 + const trimmed = value.trim(); 214 + const withoutLeadingSlash = trimmed.startsWith("/") 215 + ? trimmed.slice(1) 216 + : trimmed; 217 + const withoutDocsPrefix = withoutLeadingSlash.startsWith("docs/") 218 + ? withoutLeadingSlash.slice("docs/".length) 219 + : withoutLeadingSlash; 220 + const withoutMarkdownSuffix = withoutDocsPrefix.endsWith(".md") 221 + ? withoutDocsPrefix.slice(0, -".md".length) 222 + : withoutDocsPrefix; 223 + return withoutMarkdownSuffix.toLowerCase(); 224 + } 225 + 226 + function formatSectionsList() { 227 + return getAllSections().then((sections) => 228 + sections 229 + .map((section) => `- title: ${section.title}, section: ${section.slug}`) 230 + .join("\n"), 231 + ); 232 + } 233 + 234 + export function startMcpServer() { 235 + const server = createMcpServer(); 236 + new StdioTransport(server).listen(); 237 + }
-29
packages/mcp/package.json
··· 1 - { 2 - "name": "mcp", 3 - "version": "0.0.0", 4 - "private": true, 5 - "type": "module", 6 - "description": "TMCP server for Hip UI docs markdown exports.", 7 - "bin": { 8 - "hip-ui-mcp": "./dist/index.js" 9 - }, 10 - "files": [ 11 - "dist" 12 - ], 13 - "scripts": { 14 - "build": "tsc --build tsconfig.build.json && node ./scripts/copy-docs.mjs", 15 - "check-types": "tsc --noEmit", 16 - "lint": "oxlint ." 17 - }, 18 - "dependencies": { 19 - "@tmcp/adapter-valibot": "^0.1.5", 20 - "@tmcp/transport-stdio": "latest", 21 - "tmcp": "latest", 22 - "valibot": "^1.1.0" 23 - }, 24 - "devDependencies": { 25 - "@repo/typescript-config": "workspace:*", 26 - "@types/node": "catalog:", 27 - "typescript": "catalog:" 28 - } 29 - }
+1 -1
packages/mcp/scripts/copy-docs.mjs packages/hip-ui/scripts/copy-mcp-docs.mjs
··· 7 7 packageRoot, 8 8 "../../apps/docs/dist/client/docs", 9 9 ); 10 - const docsTargetDirectory = path.resolve(packageRoot, "dist/docs"); 10 + const docsTargetDirectory = path.resolve(packageRoot, "dist/mcp/docs"); 11 11 12 12 async function exists(targetPath) { 13 13 try {
packages/mcp/src/docs-index.ts packages/hip-ui/src/mcp/docs-index.ts
-226
packages/mcp/src/index.ts
··· 1 - import { ValibotJsonSchemaAdapter } from "@tmcp/adapter-valibot"; 2 - import { StdioTransport } from "@tmcp/transport-stdio"; 3 - import { McpServer } from "tmcp"; 4 - import { resource, tool } from "tmcp/utils"; 5 - import * as v from "valibot"; 6 - 7 - import { getAllSections } from "./docs-index.js"; 8 - 9 - const llmTipsResourceUri = "hip-ui://tips-and-tricks-for-llms"; 10 - const llmTipsResourceName = "tips & tricks for LLMs"; 11 - const llmTipsResourceContent = `# Tips & Tricks for LLMs 12 - 13 - - Flex should always be used when possible. Use Grid when you need complex 2 dimentional layout. Always have gaps. 14 - - All the text uses text-box-trim: trim-both with text-box-edge: cap alphabetic. This means that line height is not used for the first and last line of text. In practive this means that we need to increase the flex gap size used for text. 15 - - Use Card sparingly 16 - `; 17 - 18 - const server = new McpServer( 19 - { 20 - name: "hip-ui-docs", 21 - version: "0.0.0", 22 - }, 23 - { 24 - adapter: new ValibotJsonSchemaAdapter(), 25 - capabilities: { 26 - tools: { 27 - listChanged: true, 28 - }, 29 - resources: { 30 - subscribe: true, 31 - listChanged: true, 32 - }, 33 - }, 34 - instructions: `Always load the resource "${llmTipsResourceName}" at "${llmTipsResourceUri}" first, and keep it in context while using this server. Then use list-sections to discover available Hip UI docs pages, and get-documentation to retrieve complete markdown for one or more sections.`, 35 - }, 36 - ); 37 - 38 - const getDocumentationSchema = v.object({ 39 - section: v.pipe( 40 - v.union([v.string(), v.array(v.string())]), 41 - v.description( 42 - "The section name(s) to retrieve. Can search by title, docs slug path, or /docs URL path. Supports a single string or array of strings.", 43 - ), 44 - ), 45 - }); 46 - 47 - function normalizeInputToArray(value: string | Array<string>) { 48 - if (Array.isArray(value)) { 49 - return value.filter((item): item is string => typeof item === "string"); 50 - } 51 - 52 - const trimmed = value.trim(); 53 - if (trimmed.startsWith("[") && trimmed.endsWith("]")) { 54 - try { 55 - const parsedValue = JSON.parse(trimmed); 56 - if (Array.isArray(parsedValue)) { 57 - return parsedValue.filter( 58 - (item): item is string => typeof item === "string", 59 - ); 60 - } 61 - } catch { 62 - return [value]; 63 - } 64 - } 65 - 66 - return [value]; 67 - } 68 - 69 - function normalizeSectionKey(value: string) { 70 - const trimmed = value.trim(); 71 - const withoutLeadingSlash = trimmed.startsWith("/") 72 - ? trimmed.slice(1) 73 - : trimmed; 74 - const withoutDocsPrefix = withoutLeadingSlash.startsWith("docs/") 75 - ? withoutLeadingSlash.slice("docs/".length) 76 - : withoutLeadingSlash; 77 - const withoutMarkdownSuffix = withoutDocsPrefix.endsWith(".md") 78 - ? withoutDocsPrefix.slice(0, -".md".length) 79 - : withoutDocsPrefix; 80 - return withoutMarkdownSuffix.toLowerCase(); 81 - } 82 - 83 - function formatSectionsList() { 84 - return getAllSections().then((sections) => 85 - sections 86 - .map((section) => `- title: ${section.title}, section: ${section.slug}`) 87 - .join("\n"), 88 - ); 89 - } 90 - 91 - server.resource( 92 - { 93 - name: llmTipsResourceName, 94 - description: "Persistent context and usage guidance for this MCP server.", 95 - uri: llmTipsResourceUri, 96 - mimeType: "text/markdown", 97 - }, 98 - async () => 99 - resource.text(llmTipsResourceUri, llmTipsResourceContent, "text/markdown"), 100 - ); 101 - 102 - server.tool( 103 - { 104 - name: "list-sections", 105 - description: "Lists all available Hip UI docs markdown sections.", 106 - annotations: { 107 - readOnlyHint: true, 108 - openWorldHint: false, 109 - destructiveHint: false, 110 - title: "List Sections", 111 - }, 112 - }, 113 - async () => { 114 - const sectionsList = await formatSectionsList(); 115 - return tool.text(sectionsList); 116 - }, 117 - ); 118 - 119 - server.tool( 120 - { 121 - name: "get-documentation", 122 - description: 123 - "Retrieves full markdown documentation for one or more Hip UI docs sections.", 124 - schema: getDocumentationSchema, 125 - annotations: { 126 - readOnlyHint: true, 127 - openWorldHint: false, 128 - destructiveHint: false, 129 - title: "Get Documentation", 130 - }, 131 - }, 132 - async ({ section }) => { 133 - const requestedSections = normalizeInputToArray(section); 134 - const availableSections = await getAllSections(); 135 - 136 - const results = requestedSections.map((requestedSection) => { 137 - const requestedKey = normalizeSectionKey(requestedSection); 138 - const exactMatch = availableSections.find((availableSection) => { 139 - const matchesTitle = 140 - availableSection.title.toLowerCase() === 141 - requestedSection.toLowerCase(); 142 - const matchesSlug = 143 - normalizeSectionKey(availableSection.slug) === requestedKey; 144 - const matchesUrlPath = 145 - normalizeSectionKey(availableSection.urlPath) === requestedKey; 146 - return matchesTitle || matchesSlug || matchesUrlPath; 147 - }); 148 - 149 - if (!exactMatch) { 150 - return { 151 - requestedSection, 152 - success: false as const, 153 - }; 154 - } 155 - 156 - return { 157 - requestedSection, 158 - success: true as const, 159 - content: `## ${exactMatch.title}\n\n${exactMatch.markdown}`, 160 - }; 161 - }); 162 - 163 - const successfulResults = results.filter((result) => result.success); 164 - const failedSections = results 165 - .filter((result) => !result.success) 166 - .map((result) => result.requestedSection); 167 - 168 - if (successfulResults.length > 0 && failedSections.length === 0) { 169 - return tool.text( 170 - successfulResults.map((result) => result.content).join("\n\n---\n\n"), 171 - ); 172 - } 173 - 174 - const responseParts: Array<string> = []; 175 - 176 - if (successfulResults.length > 0) { 177 - responseParts.push( 178 - successfulResults.map((result) => result.content).join("\n\n---\n\n"), 179 - ); 180 - } 181 - 182 - const fuzzyMatches = failedSections.map((failedSection) => { 183 - const failedSectionKey = normalizeSectionKey(failedSection); 184 - const similarSections = availableSections.filter((availableSection) => { 185 - const title = availableSection.title.toLowerCase(); 186 - const slug = availableSection.slug.toLowerCase(); 187 - const urlPath = availableSection.urlPath.toLowerCase(); 188 - return ( 189 - title.includes(failedSectionKey) || 190 - slug.includes(failedSectionKey) || 191 - urlPath.includes(failedSectionKey) || 192 - failedSectionKey.includes(slug.split("/").at(-1) ?? "") 193 - ); 194 - }); 195 - return { failedSection, similarSections }; 196 - }); 197 - 198 - const hasFuzzyMatches = fuzzyMatches.some( 199 - (fuzzyMatch) => fuzzyMatch.similarSections.length > 0, 200 - ); 201 - 202 - if (successfulResults.length === 0 && !hasFuzzyMatches) { 203 - responseParts.push(await formatSectionsList()); 204 - } 205 - 206 - for (const fuzzyMatch of fuzzyMatches) { 207 - if (fuzzyMatch.similarSections.length > 0) { 208 - const similarSectionsList = fuzzyMatch.similarSections 209 - .map( 210 - (similarSection) => 211 - `- title: ${similarSection.title}, section: ${similarSection.slug}`, 212 - ) 213 - .join("\n"); 214 - responseParts.push( 215 - `${fuzzyMatch.similarSections.length} similar result${fuzzyMatch.similarSections.length > 1 ? "s" : ""} for "${fuzzyMatch.failedSection}":\n${similarSectionsList}`, 216 - ); 217 - } 218 - 219 - responseParts.push(`Section not found: "${fuzzyMatch.failedSection}".`); 220 - } 221 - 222 - return tool.text(responseParts.join("\n\n---\n\n")); 223 - }, 224 - ); 225 - 226 - new StdioTransport(server).listen();
-5
packages/mcp/tsconfig.build.json
··· 1 - { 2 - "extends": "./tsconfig.json", 3 - "include": ["src"], 4 - "exclude": ["node_modules", "dist"] 5 - }
-1
packages/mcp/tsconfig.build.tsbuildinfo
··· 1 - {"root":["./src/docs-index.ts","./src/index.ts"],"version":"5.9.3"}
-8
packages/mcp/tsconfig.json
··· 1 - { 2 - "extends": "@repo/typescript-config/base.json", 3 - "compilerOptions": { 4 - "outDir": "dist", 5 - "rootDir": "src", 6 - "types": ["node"] 7 - } 8 - }
+12 -25
pnpm-lock.yaml
··· 373 373 '@inkjs/ui': 374 374 specifier: ^2.0.0 375 375 version: 2.0.0(ink@6.3.1(@types/react@19.2.0)(react@19.2.0)) 376 + '@tmcp/adapter-valibot': 377 + specifier: ^0.1.5 378 + version: 0.1.5(tmcp@1.19.3(typescript@5.9.3))(valibot@1.3.1(typescript@5.9.3)) 379 + '@tmcp/transport-stdio': 380 + specifier: latest 381 + version: 0.4.2(tmcp@1.19.3(typescript@5.9.3)) 376 382 command-line-application: 377 383 specifier: ^0.10.1 378 384 version: 0.10.1 ··· 382 388 lilconfig: 383 389 specifier: ^3.1.3 384 390 version: 3.1.3 391 + tmcp: 392 + specifier: latest 393 + version: 1.19.3(typescript@5.9.3) 394 + valibot: 395 + specifier: ^1.1.0 396 + version: 1.3.1(typescript@5.9.3) 385 397 devDependencies: 386 398 '@origin-space/image-cropper': 387 399 specifier: ^0.1.9 ··· 470 482 web-haptics: 471 483 specifier: ^0.0.6 472 484 version: 0.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 473 - 474 - packages/mcp: 475 - dependencies: 476 - '@tmcp/adapter-valibot': 477 - specifier: ^0.1.5 478 - version: 0.1.5(tmcp@1.19.3(typescript@5.9.3))(valibot@1.3.1(typescript@5.9.3)) 479 - '@tmcp/transport-stdio': 480 - specifier: latest 481 - version: 0.4.2(tmcp@1.19.3(typescript@5.9.3)) 482 - tmcp: 483 - specifier: latest 484 - version: 1.19.3(typescript@5.9.3) 485 - valibot: 486 - specifier: ^1.1.0 487 - version: 1.3.1(typescript@5.9.3) 488 - devDependencies: 489 - '@repo/typescript-config': 490 - specifier: workspace:* 491 - version: link:../typescript-config 492 - '@types/node': 493 - specifier: 'catalog:' 494 - version: 24.9.1 495 - typescript: 496 - specifier: 'catalog:' 497 - version: 5.9.3 498 485 499 486 packages/typescript-config: {} 500 487