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.

mcp first pass

+1474 -44
+8
.cursor/mcp.json
··· 1 + { 2 + "mcpServers": { 3 + "hip-ui": { 4 + "command": "node", 5 + "args": ["packages/mcp/dist/index.js"] 6 + } 7 + } 8 + }
+4 -1
.vscode/settings.json
··· 7 7 "files.watcherExclude": {"**/routeTree.gen.ts": true}, 8 8 "files.readonlyInclude": {"**/routeTree.gen.ts": true}, 9 9 "typescript.experimental.useTsgo": false, 10 - "typescript.tsdk": "node_modules/typescript/lib" 10 + "typescript.tsdk": "node_modules/typescript/lib", 11 + "cSpell.words": [ 12 + "llms" 13 + ] 11 14 }
+63
apps/docs/src/docs/ai.mdx
··· 1 + --- 2 + title: AI Usage 3 + description: How to use AI to enhance building with Hip UI. 4 + --- 5 + 6 + import { MCPInstallationGuide } from "../lib/MCPInstallationGuide"; 7 + 8 + Hip UI interfaces with AI in two main ways: 9 + 10 + 1. **llms.txt/per page markdown** - The docs as static markdown files that can be used as context for AI chats like OpenAi or Claude. 11 + 2. **MCP server** - A MCP server that AI can use to get guidance on how to use Hip UI. 12 + 13 + ## MCP Server 14 + 15 + The MCP server exposes the following tools: 16 + 17 + - **list-sections** - Lists all available Hip UI docs sections. 18 + - **get-documentation** - Retrieves the full markdown documentation for a given section. 19 + 20 + It also exposes some lightweight instructions and rules around using and composing components. 21 + 22 + ### Setup 23 + 24 + To use the MCP server you need to add it to your AI tool of choice. 25 + 26 + #### Cursor 27 + 28 + ```json 29 + { 30 + "mcpServers": { 31 + "hip-ui": { 32 + "command": "npx", 33 + "args": ["hip-ui", "mcp"] 34 + } 35 + } 36 + } 37 + ``` 38 + 39 + #### VS Code 40 + 41 + ```bash 42 + code --add-mcp '{"name":"hip-ui","command":"npx","args":["hip-ui", "mcp"]}' 43 + ``` 44 + 45 + #### Claude Code 46 + 47 + ```bash 48 + claude mcp add hip-ui npx hip-ui mcp 49 + ``` 50 + 51 + #### Codex 52 + 53 + ```yaml 54 + [mcp_servers.hip-ui] 55 + command = "npx" 56 + args = ["hip-ui", "mcp"] 57 + ``` 58 + 59 + #### Gemini CLI 60 + 61 + ```bash 62 + gemini mcp add hip-ui npx hip-ui mcp 63 + ```
+4 -4
apps/docs/src/docs/showcase/music.mdx
··· 1 1 --- 2 - title: Music App 3 - description: An example showing what building a music app looks like with Hip. 2 + title: Music Cards 3 + description: A collection of music-related cards built with Hip components. 4 4 --- 5 5 6 - import { Music } from "../../showcases/music"; 6 + import { MusicCards } from "../../showcases/music"; 7 7 8 - <Music /> 8 + <MusicCards />
+8
apps/docs/src/docs/showcase/spotify.mdx
··· 1 + --- 2 + title: Spotify App 3 + description: A Spotify-style streaming app showcase built with Hip components. 4 + --- 5 + 6 + import { SpotifyApp } from "../../showcases/spotify"; 7 + 8 + <SpotifyApp />
+69 -38
apps/docs/src/routes/docs.$.tsx
··· 9 9 useMatches, 10 10 } from "@tanstack/react-router"; 11 11 import { allDocs } from "content-collections"; 12 - import { Copy, Moon, Sun } from "lucide-react"; 12 + import { Moon, Sun } from "lucide-react"; 13 13 import { useEffect, useState } from "react"; 14 14 import { modules, pages } from "virtual:content"; 15 15 ··· 43 43 UnorderedList, 44 44 } from "@/components/typography"; 45 45 import { Text } from "@/components/typography/text"; 46 + import { CopyForLLMButton } from "@/lib/CopyForLLM"; 46 47 import { MarkdownExportContext } from "@/lib/MarkdownExportContext"; 47 48 import { ThemePicker } from "@/lib/ThemePicker"; 48 49 import { UnShikiCode } from "@/lib/UnShiki"; ··· 52 53 size as sizeSpace, 53 54 verticalSpace, 54 55 } from "../components/theme/semantic-spacing.stylex"; 55 - import { CopyForLLMButton } from "@/lib/CopyForLLM"; 56 56 57 57 declare global { 58 58 // eslint-disable-next-line @typescript-eslint/no-namespace ··· 81 81 82 82 interface SidebarGroupEntry { 83 83 id: string; 84 - label: string; 84 + label?: string; 85 85 items: Array<SidebarLeafEntry | SidebarSectionEntry>; 86 86 } 87 87 ··· 92 92 ); 93 93 const foundationDocs = allDocs.filter((doc) => 94 94 doc._meta.directory.startsWith("foundations"), 95 + ); 96 + const showCaseDocs = allDocs.filter((doc) => 97 + doc._meta.directory.startsWith("showcase"), 95 98 ); 96 99 97 100 // oxlint-disable-next-line eslint-plugin-unicorn(no-array-reduce) ··· 131 134 132 135 const sidebarItems: Array<SidebarTopLevelEntry> = [ 133 136 { 134 - id: "introduction", 135 - label: "Introduction", 136 - to: "/docs/$", 137 - params: { _splat: "introduction" }, 137 + id: "getting-started", 138 + items: [ 139 + { 140 + id: "introduction", 141 + label: "Introduction", 142 + to: "/docs/$", 143 + params: { _splat: "introduction" }, 144 + }, 145 + { 146 + id: "ai", 147 + label: "AI Usage", 148 + to: "/docs/$", 149 + params: { _splat: "ai" }, 150 + }, 151 + ], 138 152 }, 139 153 { 140 154 id: "foundations", ··· 150 164 id: "components", 151 165 label: "Components", 152 166 items: componentItems, 167 + }, 168 + { 169 + id: "showcases", 170 + label: "Showcases", 171 + items: showCaseDocs.map( 172 + (doc): SidebarLeafEntry => ({ 173 + id: doc._meta.path, 174 + label: doc.title, 175 + to: "/docs/$", 176 + params: { _splat: doc._meta.path }, 177 + }), 178 + ), 153 179 }, 154 180 ]; 155 181 ··· 338 364 ); 339 365 } 340 366 341 - return ( 342 - <SidebarGroup title={item.label} key={item.id}> 343 - {item.items.map((subItem) => { 344 - if ("items" in subItem) { 345 - return ( 346 - <SidebarSection key={subItem.id} title={subItem.label}> 347 - {subItem.items.map((leafItem) => ( 348 - <SidebarItemLink 349 - key={leafItem.id} 350 - to={leafItem.to} 351 - params={leafItem.params} 352 - isActive={currentItem?.id === leafItem.id} 353 - > 354 - {leafItem.label} 355 - </SidebarItemLink> 356 - ))} 357 - </SidebarSection> 358 - ); 359 - } 360 - 361 - return ( 362 - <SidebarSection key={subItem.id}> 367 + const contents = item.items.map((subItem) => { 368 + if ("items" in subItem) { 369 + return ( 370 + <> 371 + {subItem.items.map((leafItem) => ( 363 372 <SidebarItemLink 364 - to={subItem.to} 365 - params={subItem.params} 366 - isActive={currentItem?.id === subItem.id} 373 + key={leafItem.id} 374 + to={leafItem.to} 375 + params={leafItem.params} 376 + isActive={currentItem?.id === leafItem.id} 367 377 > 368 - {subItem.label} 378 + {leafItem.label} 369 379 </SidebarItemLink> 370 - </SidebarSection> 371 - ); 372 - })} 373 - </SidebarGroup> 374 - ); 380 + ))} 381 + </> 382 + ); 383 + } 384 + 385 + return ( 386 + <SidebarItemLink 387 + key={subItem.id} 388 + to={subItem.to} 389 + params={subItem.params} 390 + isActive={currentItem?.id === subItem.id} 391 + > 392 + {subItem.label} 393 + </SidebarItemLink> 394 + ); 395 + }); 396 + 397 + if (item.label) { 398 + return ( 399 + <SidebarGroup title={item.label} key={item.id}> 400 + <SidebarSection>{contents}</SidebarSection> 401 + </SidebarGroup> 402 + ); 403 + } 404 + 405 + return <SidebarSection key={item.id}>{contents}</SidebarSection>; 375 406 })} 376 407 </Sidebar> 377 408 );
+1 -1
apps/docs/src/showcases/music.tsx
··· 775 775 ); 776 776 } 777 777 778 - export function Music() { 778 + export function MusicCards() { 779 779 return ( 780 780 <Flex gap="2xl" style={styles.main}> 781 781 <Flex direction="column" gap="2xl" style={styles.skinny}>
+820
apps/docs/src/showcases/spotify.tsx
··· 1 + import * as stylex from "@stylexjs/stylex"; 2 + import { 3 + Album, 4 + ArrowDownToLine, 5 + Heart, 6 + ListMusic, 7 + Mic2, 8 + MoreHorizontal, 9 + Play, 10 + Plus, 11 + Search, 12 + SkipBack, 13 + SkipForward, 14 + Sparkles, 15 + Volume2, 16 + } from "lucide-react"; 17 + 18 + import { AspectRatio, AspectRatioImage } from "@/components/aspect-ratio"; 19 + import { Avatar } from "@/components/avatar"; 20 + import { Badge } from "@/components/badge"; 21 + import { Button } from "@/components/button"; 22 + import { 23 + Card, 24 + CardBody, 25 + CardHeader, 26 + CardHeaderAction, 27 + CardTitle, 28 + } from "@/components/card"; 29 + import { Flex } from "@/components/flex"; 30 + import { Grid } from "@/components/grid"; 31 + import { IconButton } from "@/components/icon-button"; 32 + import { SearchField } from "@/components/search-field"; 33 + import { Separator } from "@/components/separator"; 34 + import { 35 + Sidebar, 36 + SidebarHeader, 37 + SidebarItem, 38 + SidebarSection, 39 + } from "@/components/sidebar"; 40 + import { SidebarLayout } from "@/components/sidebar-layout"; 41 + import { Slider } from "@/components/slider"; 42 + import { Text } from "@/components/typography/text"; 43 + 44 + import { primaryColor, uiColor } from "../components/theme/color.stylex"; 45 + import { radius } from "../components/theme/radius.stylex"; 46 + import { 47 + gap, 48 + horizontalSpace, 49 + size as sizeSpace, 50 + verticalSpace, 51 + } from "../components/theme/semantic-spacing.stylex"; 52 + 53 + const spotlightPlaylist = { 54 + title: "Late Night Signals", 55 + description: 56 + "A moody blend of indie electronic, soft house, and slow-burn alt-pop for after-hours focus.", 57 + image: 58 + "https://images.unsplash.com/photo-1511379938547-c1f69419868d?auto=format&fit=crop&w=900&q=80", 59 + }; 60 + 61 + const featuredPlaylists = [ 62 + { 63 + title: "Daily Mix 01", 64 + subtitle: "Khruangbin, Jungle, Men I Trust", 65 + image: 66 + "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?auto=format&fit=crop&w=600&q=80", 67 + }, 68 + { 69 + title: "Fresh Finds", 70 + subtitle: "New releases for your commute", 71 + image: 72 + "https://images.unsplash.com/photo-1501612780327-45045538702b?auto=format&fit=crop&w=600&q=80", 73 + }, 74 + { 75 + title: "Chill Instrumentals", 76 + subtitle: "Low-key beats and spacious keys", 77 + image: 78 + "https://images.unsplash.com/photo-1516280440614-37939bbacd81?auto=format&fit=crop&w=600&q=80", 79 + }, 80 + { 81 + title: "Release Radar", 82 + subtitle: "Brand new from artists you follow", 83 + image: 84 + "https://images.unsplash.com/photo-1507838153414-b4b713384a76?auto=format&fit=crop&w=600&q=80", 85 + }, 86 + ] as const; 87 + 88 + const playlistTracks = [ 89 + { 90 + number: "1", 91 + title: "Biscuit Town", 92 + artist: "King Krule", 93 + album: "The OOZ", 94 + duration: "3:48", 95 + image: 96 + "https://workos.imgix.net/images/e35b46dc-4384-43d1-932c-24fa44e212cd.png?auto=format&fit=clip&q=80", 97 + }, 98 + { 99 + number: "2", 100 + title: "The Less I Know the Better", 101 + artist: "Tame Impala", 102 + album: "Currents", 103 + duration: "3:39", 104 + image: 105 + "https://workos.imgix.net/images/79645741-51e0-47fc-bb40-2fa66cf9f68e.png?auto=format&fit=clip&q=80&w=192", 106 + }, 107 + { 108 + number: "3", 109 + title: "Pieces", 110 + artist: "Villagers", 111 + album: "Becoming a Jackal", 112 + duration: "5:25", 113 + image: 114 + "https://workos.imgix.net/images/95ff9b99-36f3-46d8-a3fe-9387fd7c3c32.png?auto=format&fit=clip&q=80&w=192", 115 + }, 116 + { 117 + number: "4", 118 + title: "Cola", 119 + artist: "Arlo Parks", 120 + album: "Super Sad Generation", 121 + duration: "3:50", 122 + image: 123 + "https://workos.imgix.net/images/945c66a9-afd9-4b1c-8eb0-4ce3992731ca.png?auto=format&fit=clip&q=80&w=192", 124 + }, 125 + { 126 + number: "5", 127 + title: "Do the Astral Plane", 128 + artist: "Flying Lotus", 129 + album: "Cosmogramma", 130 + duration: "3:58", 131 + image: 132 + "https://workos.imgix.net/images/3d9075e4-c232-4fb5-a1a4-b0a33d669192.png?auto=format&fit=clip&q=80&w=192", 133 + }, 134 + ] as const; 135 + 136 + const queueTracks = [ 137 + { 138 + title: "Left Hand Free", 139 + artist: "Alt-J", 140 + duration: "2:54", 141 + image: 142 + "https://workos.imgix.net/images/8d431b64-ebe8-41be-b986-2f59cb5c567d.png?auto=format&fit=clip&q=80&w=192", 143 + }, 144 + { 145 + title: "Harvey", 146 + artist: "Her's", 147 + duration: "4:26", 148 + image: 149 + "https://images.unsplash.com/photo-1499364615650-ec38552f4f34?auto=format&fit=crop&w=240&q=80", 150 + }, 151 + { 152 + title: "Border Line", 153 + artist: "King Krule", 154 + duration: "3:18", 155 + image: 156 + "https://images.unsplash.com/photo-1510915361894-db8b60106cb1?auto=format&fit=crop&w=240&q=80", 157 + }, 158 + ] as const; 159 + 160 + const friendActivity = [ 161 + { 162 + name: "Sofia", 163 + track: "Cherry", 164 + artist: "Jungle", 165 + image: 166 + "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80", 167 + }, 168 + { 169 + name: "Noah", 170 + track: "Moon Undah Water", 171 + artist: "Puma Blue", 172 + image: 173 + "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=200&q=80", 174 + }, 175 + { 176 + name: "Ava", 177 + track: "Seigfried", 178 + artist: "Frank Ocean", 179 + image: 180 + "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=200&q=80", 181 + }, 182 + ] as const; 183 + 184 + const styles = stylex.create({ 185 + showcase: { 186 + minHeight: 1100, 187 + width: 1600, 188 + }, 189 + sidebar: { 190 + height: "100%", 191 + }, 192 + sidebarBrand: { 193 + gap: horizontalSpace.md, 194 + alignItems: "center", 195 + display: "flex", 196 + }, 197 + brandMark: { 198 + borderRadius: { 199 + default: radius.md, 200 + "@supports (corner-shape: squircle)": radius["3xl"], 201 + }, 202 + cornerShape: "squircle", 203 + alignItems: "center", 204 + backgroundColor: primaryColor.solid1, 205 + color: primaryColor.textContrast, 206 + display: "flex", 207 + justifyContent: "center", 208 + height: sizeSpace["3xl"], 209 + width: sizeSpace["3xl"], 210 + }, 211 + pageColumn: { 212 + minWidth: 0, 213 + paddingBottom: 110, 214 + }, 215 + grow: { 216 + flexGrow: 1, 217 + minWidth: 0, 218 + }, 219 + wrap: { 220 + flexWrap: "wrap", 221 + }, 222 + heroArt: { 223 + width: 360, 224 + }, 225 + shelfArt: { 226 + width: "100%", 227 + }, 228 + rowArt: { 229 + height: 48, 230 + width: 48, 231 + }, 232 + nowPlayingArt: { 233 + width: "100%", 234 + }, 235 + rightSidebar: { 236 + boxSizing: "border-box", 237 + paddingBottom: 120, 238 + paddingLeft: horizontalSpace["2xl"], 239 + paddingRight: horizontalSpace["2xl"], 240 + paddingTop: verticalSpace["4xl"], 241 + width: 340, 242 + }, 243 + surface: { 244 + borderColor: uiColor.border1, 245 + borderRadius: { 246 + default: radius.xl, 247 + "@supports (corner-shape: squircle)": radius["3xl"], 248 + }, 249 + borderStyle: "solid", 250 + borderWidth: 1, 251 + cornerShape: "squircle", 252 + backgroundColor: uiColor.bgSubtle, 253 + paddingBottom: verticalSpace["4xl"], 254 + paddingLeft: horizontalSpace["4xl"], 255 + paddingRight: horizontalSpace["4xl"], 256 + paddingTop: verticalSpace["4xl"], 257 + }, 258 + surfaceSm: { 259 + borderColor: uiColor.border1, 260 + borderRadius: { 261 + default: radius.lg, 262 + "@supports (corner-shape: squircle)": radius["3xl"], 263 + }, 264 + borderStyle: "solid", 265 + borderWidth: 1, 266 + cornerShape: "squircle", 267 + backgroundColor: uiColor.bgSubtle, 268 + paddingBottom: verticalSpace["3xl"], 269 + paddingLeft: horizontalSpace["3xl"], 270 + paddingRight: horizontalSpace["3xl"], 271 + paddingTop: verticalSpace["3xl"], 272 + }, 273 + sectionHeader: { 274 + flexWrap: "wrap", 275 + }, 276 + playerRail: { 277 + pointerEvents: "none", 278 + position: "fixed", 279 + transform: "translateX(-50%)", 280 + zIndex: 2, 281 + bottom: 0, 282 + left: "50%", 283 + width: 1600, 284 + }, 285 + playerRailSpacer: { 286 + flexShrink: 0, 287 + width: sizeSpace["11xl"], 288 + }, 289 + playerDock: { 290 + gap: gap["4xl"], 291 + alignItems: "center", 292 + backgroundColor: uiColor.bg, 293 + flexGrow: 1, 294 + pointerEvents: "auto", 295 + borderTopColor: uiColor.border1, 296 + borderTopStyle: "solid", 297 + borderTopWidth: 1, 298 + minWidth: 0, 299 + paddingBottom: verticalSpace["2xl"], 300 + paddingLeft: horizontalSpace["3xl"], 301 + paddingRight: horizontalSpace["3xl"], 302 + paddingTop: verticalSpace["2xl"], 303 + }, 304 + playerInfo: { 305 + flexBasis: 280, 306 + flexShrink: 0, 307 + minWidth: 0, 308 + }, 309 + playerCenter: { 310 + flexBasis: "0%", 311 + flexGrow: 1, 312 + flexShrink: 1, 313 + minWidth: 0, 314 + }, 315 + playerCenterControls: { 316 + justifyContent: "center", 317 + }, 318 + playerProgress: { 319 + alignItems: "center", 320 + }, 321 + playerRight: { 322 + flexBasis: 240, 323 + flexShrink: 0, 324 + justifyContent: "flex-end", 325 + }, 326 + playerVolume: { 327 + width: 120, 328 + }, 329 + queueArt: { 330 + height: 40, 331 + width: 40, 332 + }, 333 + statSurface: { 334 + borderColor: uiColor.border1, 335 + borderRadius: { 336 + default: radius.md, 337 + "@supports (corner-shape: squircle)": radius["3xl"], 338 + }, 339 + borderStyle: "solid", 340 + borderWidth: 1, 341 + cornerShape: "squircle", 342 + backgroundColor: uiColor.bgSubtle, 343 + paddingBottom: verticalSpace["2xl"], 344 + paddingLeft: horizontalSpace["2xl"], 345 + paddingRight: horizontalSpace["2xl"], 346 + paddingTop: verticalSpace["2xl"], 347 + }, 348 + }); 349 + 350 + function SidebarNavItem({ 351 + icon, 352 + label, 353 + isActive = false, 354 + }: { 355 + icon: React.ReactNode; 356 + label: string; 357 + isActive?: boolean; 358 + }) { 359 + return ( 360 + <SidebarItem isActive={isActive}> 361 + <Flex align="center" gap="md"> 362 + {icon} 363 + <Text weight={isActive ? "semibold" : "medium"}>{label}</Text> 364 + </Flex> 365 + </SidebarItem> 366 + ); 367 + } 368 + 369 + function SpotifySidebar() { 370 + return ( 371 + <Sidebar style={styles.sidebar}> 372 + <SidebarHeader 373 + action={ 374 + <IconButton label="Create playlist" variant="outline"> 375 + <Plus /> 376 + </IconButton> 377 + } 378 + > 379 + <div {...stylex.props(styles.sidebarBrand)}> 380 + <div {...stylex.props(styles.brandMark)}> 381 + <Play size={18} fill="currentColor" /> 382 + </div> 383 + <Text weight="bold">Hipify</Text> 384 + </div> 385 + </SidebarHeader> 386 + 387 + <SidebarSection title="Browse"> 388 + <SidebarNavItem icon={<Album size={16} />} label="Home" isActive /> 389 + <SidebarNavItem icon={<Search size={16} />} label="Search" /> 390 + <SidebarNavItem icon={<ListMusic size={16} />} label="Your Library" /> 391 + </SidebarSection> 392 + 393 + <SidebarSection title="Playlists"> 394 + <SidebarNavItem icon={<Sparkles size={16} />} label="Discover Weekly" /> 395 + <SidebarNavItem icon={<Heart size={16} />} label="Liked Songs" /> 396 + <SidebarNavItem icon={<Album size={16} />} label="Late Night Signals" /> 397 + <SidebarNavItem icon={<ListMusic size={16} />} label="Indie Arrival" /> 398 + </SidebarSection> 399 + </Sidebar> 400 + ); 401 + } 402 + 403 + function AppToolbar() { 404 + return ( 405 + <Flex align="center" gap="xl" style={[styles.surfaceSm, styles.wrap]}> 406 + <SearchField 407 + aria-label="Search music" 408 + placeholder="What do you want to play?" 409 + style={styles.grow} 410 + variant="secondary" 411 + /> 412 + <Flex align="center" gap="md" style={styles.wrap}> 413 + <Button size="sm">Playlists</Button> 414 + <Button size="sm" variant="secondary"> 415 + Albums 416 + </Button> 417 + <Button size="sm" variant="secondary"> 418 + Podcasts 419 + </Button> 420 + </Flex> 421 + <Avatar 422 + alt="Vlad Moroz" 423 + fallback="VM" 424 + size="md" 425 + src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=200&q=80" 426 + /> 427 + </Flex> 428 + ); 429 + } 430 + 431 + function HeroCard() { 432 + return ( 433 + <Flex gap="5xl" align="center" style={[styles.surface]}> 434 + <AspectRatio aspectRatio={1} style={styles.heroArt}> 435 + <AspectRatioImage 436 + src={spotlightPlaylist.image} 437 + alt={spotlightPlaylist.title} 438 + /> 439 + </AspectRatio> 440 + 441 + <Flex direction="column" gap="4xl"> 442 + <Flex direction="column" gap="3xl"> 443 + <Badge size="sm">Playlist</Badge> 444 + <Text size="5xl" weight="bold"> 445 + {spotlightPlaylist.title} 446 + </Text> 447 + <Text variant="secondary" leading="lg"> 448 + {spotlightPlaylist.description} 449 + </Text> 450 + </Flex> 451 + 452 + <Flex gap="2xl" style={styles.wrap}> 453 + <Flex direction="column" gap="md" style={styles.statSurface}> 454 + <Text weight="semibold">45 songs</Text> 455 + <Text variant="secondary" size="sm"> 456 + 2 hr 33 min 457 + </Text> 458 + </Flex> 459 + <Flex direction="column" gap="md" style={styles.statSurface}> 460 + <Text weight="semibold">1.2M saves</Text> 461 + <Text variant="secondary" size="sm"> 462 + Updated every Friday 463 + </Text> 464 + </Flex> 465 + <Flex direction="column" gap="md" style={styles.statSurface}> 466 + <Text weight="semibold">Perfect for</Text> 467 + <Text variant="secondary" size="sm"> 468 + Deep work and late drives 469 + </Text> 470 + </Flex> 471 + </Flex> 472 + 473 + <Flex align="center" gap="md" style={styles.wrap}> 474 + <Button> 475 + <Play size={16} fill="currentColor" /> 476 + Play now 477 + </Button> 478 + <Button variant="secondary">Save to library</Button> 479 + <IconButton label="Like playlist" variant="outline"> 480 + <Heart /> 481 + </IconButton> 482 + <IconButton label="More actions" variant="outline"> 483 + <MoreHorizontal /> 484 + </IconButton> 485 + </Flex> 486 + </Flex> 487 + </Flex> 488 + ); 489 + } 490 + 491 + function PlaylistShelfCard({ 492 + title, 493 + description, 494 + items, 495 + }: { 496 + title: string; 497 + description: string; 498 + items: typeof featuredPlaylists; 499 + }) { 500 + return ( 501 + <Flex direction="column" gap="3xl" style={styles.surface}> 502 + <Flex direction="column" gap="md"> 503 + <Text weight="bold" size="xl"> 504 + {title} 505 + </Text> 506 + <Text variant="secondary">{description}</Text> 507 + </Flex> 508 + <Grid 509 + columns="repeat(4, minmax(0, 1fr))" 510 + columnGap="2xl" 511 + alignItems="start" 512 + > 513 + {items.map((item) => ( 514 + <Flex direction="column" gap="md" key={item.title}> 515 + <AspectRatio aspectRatio={1} style={styles.shelfArt}> 516 + <AspectRatioImage src={item.image} alt={item.title} /> 517 + </AspectRatio> 518 + <Flex direction="column" gap="sm"> 519 + <Text weight="semibold">{item.title}</Text> 520 + <Text size="sm" variant="secondary"> 521 + {item.subtitle} 522 + </Text> 523 + </Flex> 524 + </Flex> 525 + ))} 526 + </Grid> 527 + </Flex> 528 + ); 529 + } 530 + 531 + function TrackListCard() { 532 + return ( 533 + <Flex direction="column" gap="3xl" style={styles.surface}> 534 + <Flex 535 + align="center" 536 + gap="xl" 537 + justify="between" 538 + style={styles.sectionHeader} 539 + > 540 + <Flex direction="column" gap="md"> 541 + <Text weight="bold" size="xl"> 542 + Track list 543 + </Text> 544 + <Text variant="secondary"> 545 + Everything in the playlist, ready to queue. 546 + </Text> 547 + </Flex> 548 + <Flex align="center" gap="md"> 549 + <Button size="sm" variant="tertiary"> 550 + Share 551 + </Button> 552 + <Button size="sm" variant="tertiary"> 553 + Download 554 + </Button> 555 + </Flex> 556 + </Flex> 557 + 558 + <Flex direction="column" gap="xl"> 559 + {playlistTracks.map((track, index) => ( 560 + <Flex direction="column" gap="xl" key={track.title}> 561 + <Grid 562 + columns="32px minmax(0, 1.8fr) minmax(0, 1fr) auto auto" 563 + columnGap="xl" 564 + alignItems="center" 565 + > 566 + <Text variant="secondary" size="sm"> 567 + {track.number} 568 + </Text> 569 + <Flex align="center" gap="xl" style={styles.grow}> 570 + <AspectRatio aspectRatio={1} style={styles.rowArt}> 571 + <AspectRatioImage src={track.image} alt={track.title} /> 572 + </AspectRatio> 573 + <Flex direction="column" gap="sm" style={styles.grow}> 574 + <Text weight="medium">{track.title}</Text> 575 + <Text variant="secondary" size="sm"> 576 + {track.artist} 577 + </Text> 578 + </Flex> 579 + </Flex> 580 + <Text variant="secondary" size="sm"> 581 + {track.album} 582 + </Text> 583 + <Text variant="secondary" size="sm"> 584 + {track.duration} 585 + </Text> 586 + <IconButton label={`Like ${track.title}`} variant="tertiary"> 587 + <Heart /> 588 + </IconButton> 589 + </Grid> 590 + {index < playlistTracks.length - 1 && <Separator />} 591 + </Flex> 592 + ))} 593 + </Flex> 594 + </Flex> 595 + ); 596 + } 597 + 598 + function PlayerCard() { 599 + const currentTrack = playlistTracks[0]; 600 + 601 + return ( 602 + <Flex style={styles.playerRail}> 603 + <div {...stylex.props(styles.playerRailSpacer)} /> 604 + <Flex align="center" style={styles.playerDock}> 605 + <Flex align="center" gap="xl" style={styles.playerInfo}> 606 + <AspectRatio aspectRatio={1} style={styles.rowArt}> 607 + <AspectRatioImage 608 + src={currentTrack.image} 609 + alt={currentTrack.title} 610 + /> 611 + </AspectRatio> 612 + <Flex direction="column" gap="sm" style={styles.grow}> 613 + <Text weight="semibold" hasEllipsis> 614 + {currentTrack.title} 615 + </Text> 616 + <Text variant="secondary" size="sm" hasEllipsis> 617 + {currentTrack.artist} 618 + </Text> 619 + </Flex> 620 + <IconButton label="Save track" variant="tertiary"> 621 + <Heart /> 622 + </IconButton> 623 + </Flex> 624 + 625 + <Flex direction="column" gap="sm" style={styles.playerCenter}> 626 + <Flex align="center" gap="md" style={styles.playerCenterControls}> 627 + <IconButton label="Previous track" variant="tertiary"> 628 + <SkipBack /> 629 + </IconButton> 630 + <IconButton label="Pause" variant="secondary"> 631 + <Play fill="currentColor" /> 632 + </IconButton> 633 + <IconButton label="Next track" variant="tertiary"> 634 + <SkipForward /> 635 + </IconButton> 636 + </Flex> 637 + 638 + <Flex gap="md" style={styles.playerProgress}> 639 + <Text size="xs" variant="secondary"> 640 + 1:18 641 + </Text> 642 + <Slider 643 + aria-label="Playback progress" 644 + defaultValue={34} 645 + maxValue={100} 646 + minValue={0} 647 + showValueLabel={false} 648 + step={1} 649 + style={styles.grow} 650 + /> 651 + <Text size="xs" variant="secondary"> 652 + 3:48 653 + </Text> 654 + </Flex> 655 + </Flex> 656 + 657 + <Flex align="center" gap="md" style={styles.playerRight}> 658 + <IconButton label="Lyrics" variant="tertiary"> 659 + <Mic2 /> 660 + </IconButton> 661 + <IconButton label="Queue" variant="tertiary"> 662 + <ListMusic /> 663 + </IconButton> 664 + <Volume2 size={16} /> 665 + <Slider 666 + aria-label="Volume" 667 + defaultValue={68} 668 + maxValue={100} 669 + minValue={0} 670 + showValueLabel={false} 671 + step={1} 672 + style={styles.playerVolume} 673 + /> 674 + </Flex> 675 + </Flex> 676 + </Flex> 677 + ); 678 + } 679 + 680 + function NowPlayingSidebar() { 681 + const currentTrack = playlistTracks[0]; 682 + 683 + return ( 684 + <Card> 685 + <CardHeader> 686 + <CardTitle>Now playing</CardTitle> 687 + <CardHeaderAction> 688 + <Badge size="sm" variant="success"> 689 + Live 690 + </Badge> 691 + </CardHeaderAction> 692 + </CardHeader> 693 + <CardBody> 694 + <Flex direction="column" gap="3xl"> 695 + <AspectRatio aspectRatio={1} style={styles.nowPlayingArt}> 696 + <AspectRatioImage 697 + src={currentTrack.image} 698 + alt={currentTrack.title} 699 + /> 700 + </AspectRatio> 701 + 702 + <Flex direction="column" gap="sm"> 703 + <Text weight="bold" size="xl"> 704 + {currentTrack.title} 705 + </Text> 706 + <Text variant="secondary">{currentTrack.artist}</Text> 707 + </Flex> 708 + 709 + <Flex align="center" justify="between" gap="md"> 710 + <Button size="sm"> 711 + <Play size={16} fill="currentColor" /> 712 + Playing 713 + </Button> 714 + <IconButton label="Download track" variant="outline"> 715 + <ArrowDownToLine /> 716 + </IconButton> 717 + </Flex> 718 + </Flex> 719 + </CardBody> 720 + </Card> 721 + ); 722 + } 723 + 724 + function QueueSidebar() { 725 + return ( 726 + <Flex direction="column" gap="3xl" style={styles.surfaceSm}> 727 + <Flex direction="column" gap="md"> 728 + <Text weight="bold" size="lg"> 729 + Up next 730 + </Text> 731 + <Text variant="secondary" size="sm"> 732 + Your queue for the next 12 minutes. 733 + </Text> 734 + </Flex> 735 + <Flex direction="column" gap="xl"> 736 + {queueTracks.map((track) => ( 737 + <Flex align="center" gap="xl" key={track.title}> 738 + <AspectRatio aspectRatio={1} style={styles.queueArt}> 739 + <AspectRatioImage src={track.image} alt={track.title} /> 740 + </AspectRatio> 741 + <Flex direction="column" gap="sm" style={styles.grow}> 742 + <Text weight="medium" size="sm"> 743 + {track.title} 744 + </Text> 745 + <Text size="xs" variant="secondary"> 746 + {track.artist} 747 + </Text> 748 + </Flex> 749 + <Text size="xs" variant="secondary"> 750 + {track.duration} 751 + </Text> 752 + </Flex> 753 + ))} 754 + </Flex> 755 + </Flex> 756 + ); 757 + } 758 + 759 + function FriendActivityCard() { 760 + return ( 761 + <Flex direction="column" gap="3xl" style={styles.surfaceSm}> 762 + <Text weight="bold" size="lg"> 763 + Friend activity 764 + </Text> 765 + <Flex direction="column" gap="xl"> 766 + {friendActivity.map((friend) => ( 767 + <Flex align="center" gap="xl" key={friend.name}> 768 + <Avatar 769 + alt={friend.name} 770 + fallback={friend.name.charAt(0)} 771 + size="md" 772 + src={friend.image} 773 + /> 774 + <Flex direction="column" gap="sm" style={styles.grow}> 775 + <Text weight="medium" size="sm"> 776 + {friend.name} 777 + </Text> 778 + <Text size="xs" variant="secondary"> 779 + {friend.track} by {friend.artist} 780 + </Text> 781 + </Flex> 782 + </Flex> 783 + ))} 784 + </Flex> 785 + </Flex> 786 + ); 787 + } 788 + 789 + export function SpotifyApp() { 790 + return ( 791 + <SidebarLayout.Root style={styles.showcase}> 792 + <SidebarLayout.NavigationSidebar> 793 + <SpotifySidebar /> 794 + </SidebarLayout.NavigationSidebar> 795 + 796 + <SidebarLayout.Page> 797 + <Flex direction="column" gap="3xl" style={styles.pageColumn}> 798 + <AppToolbar /> 799 + <HeroCard /> 800 + <PlaylistShelfCard 801 + title="Made for you" 802 + description="A Spotify-style discovery shelf rendered with Hip cards, grid, and media components." 803 + items={featuredPlaylists} 804 + /> 805 + <TrackListCard /> 806 + </Flex> 807 + </SidebarLayout.Page> 808 + 809 + <SidebarLayout.InconsequentialSidebar visible="lg"> 810 + <Flex direction="column" gap="2xl" style={styles.rightSidebar}> 811 + <NowPlayingSidebar /> 812 + <QueueSidebar /> 813 + <FriendActivityCard /> 814 + </Flex> 815 + </SidebarLayout.InconsequentialSidebar> 816 + 817 + <PlayerCard /> 818 + </SidebarLayout.Root> 819 + ); 820 + }
+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 + }
+39
packages/mcp/scripts/copy-docs.mjs
··· 1 + import { cp, mkdir, rm, stat } from "node:fs/promises"; 2 + import path from "node:path"; 3 + import process from "node:process"; 4 + 5 + const packageRoot = path.resolve(import.meta.dirname, ".."); 6 + const docsSourceDirectory = path.resolve( 7 + packageRoot, 8 + "../../apps/docs/dist/client/docs", 9 + ); 10 + const docsTargetDirectory = path.resolve(packageRoot, "dist/docs"); 11 + 12 + async function exists(targetPath) { 13 + try { 14 + await stat(targetPath); 15 + return true; 16 + } catch { 17 + return false; 18 + } 19 + } 20 + 21 + async function main() { 22 + const hasSource = await exists(docsSourceDirectory); 23 + 24 + if (!hasSource) { 25 + throw new Error( 26 + `Missing docs markdown source at ${docsSourceDirectory}. Run \`pnpm --filter docs build\` first.`, 27 + ); 28 + } 29 + 30 + await rm(docsTargetDirectory, { force: true, recursive: true }); 31 + await mkdir(path.dirname(docsTargetDirectory), { recursive: true }); 32 + await cp(docsSourceDirectory, docsTargetDirectory, { recursive: true }); 33 + 34 + process.stdout.write( 35 + `Copied docs markdown to ${path.relative(packageRoot, docsTargetDirectory)}\n`, 36 + ); 37 + } 38 + 39 + await main();
+93
packages/mcp/src/docs-index.ts
··· 1 + import { readFile, readdir } from "node:fs/promises"; 2 + import path from "node:path"; 3 + 4 + export interface DocSection { 5 + slug: string; 6 + title: string; 7 + markdown: string; 8 + urlPath: string; 9 + } 10 + 11 + const MARKDOWN_EXTENSION = ".md"; 12 + 13 + let cachedSections: Array<DocSection> | undefined; 14 + 15 + function toPosixPath(filePath: string) { 16 + return filePath.split(path.sep).join(path.posix.sep); 17 + } 18 + 19 + function extractTitle(markdown: string, fallbackSlug: string) { 20 + const lines = markdown.split(/\r?\n/); 21 + 22 + for (const line of lines) { 23 + const headingMatch = line.match(/^#\s+(.+?)\s*$/); 24 + 25 + if (headingMatch?.[1]) { 26 + return headingMatch[1].trim(); 27 + } 28 + } 29 + 30 + const slugLeaf = fallbackSlug.split("/").at(-1) ?? fallbackSlug; 31 + return slugLeaf 32 + .split("-") 33 + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) 34 + .join(" "); 35 + } 36 + 37 + async function collectMarkdownFiles(directory: string) { 38 + const entries = await readdir(directory, { withFileTypes: true }); 39 + const filePaths: Array<string> = []; 40 + 41 + await Promise.all( 42 + entries.map(async (entry) => { 43 + const entryPath = path.join(directory, entry.name); 44 + 45 + if (entry.isDirectory()) { 46 + filePaths.push(...(await collectMarkdownFiles(entryPath))); 47 + return; 48 + } 49 + 50 + if (entry.isFile() && entry.name.endsWith(MARKDOWN_EXTENSION)) { 51 + filePaths.push(entryPath); 52 + } 53 + }), 54 + ); 55 + 56 + return filePaths; 57 + } 58 + 59 + function getDocsRootDirectory() { 60 + return path.resolve(import.meta.dirname, "docs"); 61 + } 62 + 63 + async function loadSectionsFromDisk() { 64 + const docsRootDirectory = getDocsRootDirectory(); 65 + const markdownFiles = await collectMarkdownFiles(docsRootDirectory); 66 + 67 + const sections = await Promise.all( 68 + markdownFiles.map(async (markdownFilePath) => { 69 + const markdown = await readFile(markdownFilePath, "utf8"); 70 + const relativePath = toPosixPath( 71 + path.relative(docsRootDirectory, markdownFilePath), 72 + ); 73 + const slug = relativePath.replace(/\.md$/, ""); 74 + const urlPath = `/docs/${slug}`; 75 + 76 + return { 77 + slug, 78 + title: extractTitle(markdown, slug), 79 + markdown, 80 + urlPath, 81 + } satisfies DocSection; 82 + }), 83 + ); 84 + 85 + return sections.toSorted((a, b) => 86 + a.title.localeCompare(b.title, undefined, { sensitivity: "base" }), 87 + ); 88 + } 89 + 90 + export async function getAllSections() { 91 + cachedSections ??= await loadSectionsFromDisk(); 92 + return cachedSections; 93 + }
+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 + }
+96
pnpm-lock.yaml
··· 471 471 specifier: ^0.0.6 472 472 version: 0.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 473 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 + 474 499 packages/typescript-config: {} 475 500 476 501 packages: ··· 3654 3679 3655 3680 '@tldraw/validate@4.1.2': 3656 3681 resolution: {integrity: sha512-SqaCVkm2T2E9WtF9LeYs2/aLVdd0FitL1+4UM9WuzhR524A2eDvM8P4a7PNaApNUAfVmbTJnZ2eJRHDH6QyRAA==} 3682 + 3683 + '@tmcp/adapter-valibot@0.1.5': 3684 + resolution: {integrity: sha512-9P2wrVYPngemNK0UvPb/opC722/jfd09QxXmme1TRp/wPsl98vpSk/MXt24BCMqBRv4Dvs0xxJH4KHDcjXW52Q==} 3685 + peerDependencies: 3686 + tmcp: ^1.17.0 3687 + valibot: ^1.1.0 3688 + 3689 + '@tmcp/transport-stdio@0.4.2': 3690 + resolution: {integrity: sha512-OLVLJzUXAKsCvenkjPf5ygli9ZcbEv3Lcei/ry+DB4T1NzvDc1oU3m41zYtHhAmbES1h6om3T9f/zonBSDFMRQ==} 3691 + peerDependencies: 3692 + tmcp: ^1.16.3 3657 3693 3658 3694 '@tybys/wasm-util@0.10.1': 3659 3695 resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} ··· 3900 3936 peerDependencies: 3901 3937 react: '>= 16.8.0' 3902 3938 3939 + '@valibot/to-json-schema@1.6.0': 3940 + resolution: {integrity: sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A==} 3941 + peerDependencies: 3942 + valibot: ^1.3.0 3943 + 3903 3944 '@vitejs/plugin-react@5.0.2': 3904 3945 resolution: {integrity: sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw==} 3905 3946 engines: {node: ^20.19.0 || >=22.12.0} ··· 5491 5532 json-parse-even-better-errors@2.3.1: 5492 5533 resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 5493 5534 5535 + json-rpc-2.0@1.7.1: 5536 + resolution: {integrity: sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==} 5537 + 5494 5538 json-schema-traverse@0.4.1: 5495 5539 resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 5496 5540 ··· 6714 6758 sprintf-js@1.0.3: 6715 6759 resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} 6716 6760 6761 + sqids@0.3.0: 6762 + resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==} 6763 + 6717 6764 srvx@0.11.13: 6718 6765 resolution: {integrity: sha512-oknN6qduuMPafxKtHucUeG32Q963pjriA5g3/Bl05cwEsUe5VVbIU4qR9LrALHbipSCyBe+VmfDGGydqazDRkw==} 6719 6766 engines: {node: '>=20.16.0'} ··· 6911 6958 resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} 6912 6959 hasBin: true 6913 6960 6961 + tmcp@1.19.3: 6962 + resolution: {integrity: sha512-plz/TLKNFrdfQN32LjCTN6ULy6pynfGPgHcU7KGCI5dBrxQ9Mub99SmcYuzxEkLjJooQuOD3gosSwZEl1htOtw==} 6963 + 6914 6964 tmp@0.2.5: 6915 6965 resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} 6916 6966 engines: {node: '>=14.14'} ··· 7094 7144 uri-js@4.4.1: 7095 7145 resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 7096 7146 7147 + uri-template-matcher@1.1.2: 7148 + resolution: {integrity: sha512-uZc1h12jdO3m/R77SfTEOuo6VbMhgWznaawKpBjRGSJb7i91x5PgI37NQJtG+Cerxkk0yr1pylBY2qG1kQ+aEQ==} 7149 + 7097 7150 use-callback-ref@1.3.3: 7098 7151 resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} 7099 7152 engines: {node: '>=10'} ··· 7122 7175 uuid@9.0.1: 7123 7176 resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} 7124 7177 hasBin: true 7178 + 7179 + valibot@1.3.1: 7180 + resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} 7181 + peerDependencies: 7182 + typescript: '>=5' 7183 + peerDependenciesMeta: 7184 + typescript: 7185 + optional: true 7125 7186 7126 7187 vfile-message@4.0.3: 7127 7188 resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} ··· 11210 11271 '@tldraw/validate@4.1.2': 11211 11272 dependencies: 11212 11273 '@tldraw/utils': 4.1.2 11274 + 11275 + '@tmcp/adapter-valibot@0.1.5(tmcp@1.19.3(typescript@5.9.3))(valibot@1.3.1(typescript@5.9.3))': 11276 + dependencies: 11277 + '@standard-schema/spec': 1.1.0 11278 + '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@5.9.3)) 11279 + tmcp: 1.19.3(typescript@5.9.3) 11280 + valibot: 1.3.1(typescript@5.9.3) 11281 + 11282 + '@tmcp/transport-stdio@0.4.2(tmcp@1.19.3(typescript@5.9.3))': 11283 + dependencies: 11284 + tmcp: 1.19.3(typescript@5.9.3) 11213 11285 11214 11286 '@tybys/wasm-util@0.10.1': 11215 11287 dependencies: ··· 11441 11513 '@use-gesture/core': 10.3.1 11442 11514 react: 19.2.0 11443 11515 11516 + '@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@5.9.3))': 11517 + dependencies: 11518 + valibot: 1.3.1(typescript@5.9.3) 11519 + 11444 11520 '@vitejs/plugin-react@5.0.2(vite@7.1.5(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.8.1))': 11445 11521 dependencies: 11446 11522 '@babel/core': 7.28.5 ··· 13384 13460 json-buffer@3.0.1: {} 13385 13461 13386 13462 json-parse-even-better-errors@2.3.1: {} 13463 + 13464 + json-rpc-2.0@1.7.1: {} 13387 13465 13388 13466 json-schema-traverse@0.4.1: {} 13389 13467 ··· 15165 15243 15166 15244 sprintf-js@1.0.3: {} 15167 15245 15246 + sqids@0.3.0: {} 15247 + 15168 15248 srvx@0.11.13: {} 15169 15249 15170 15250 stable-hash-x@0.2.0: {} ··· 15395 15475 dependencies: 15396 15476 tldts-core: 7.0.17 15397 15477 15478 + tmcp@1.19.3(typescript@5.9.3): 15479 + dependencies: 15480 + '@standard-schema/spec': 1.1.0 15481 + json-rpc-2.0: 1.7.1 15482 + sqids: 0.3.0 15483 + uri-template-matcher: 1.1.2 15484 + valibot: 1.3.1(typescript@5.9.3) 15485 + transitivePeerDependencies: 15486 + - typescript 15487 + 15398 15488 tmp@0.2.5: {} 15399 15489 15400 15490 to-regex-range@5.0.1: ··· 15622 15712 dependencies: 15623 15713 punycode: 2.3.1 15624 15714 15715 + uri-template-matcher@1.1.2: {} 15716 + 15625 15717 use-callback-ref@1.3.3(@types/react@19.2.0)(react@19.2.0): 15626 15718 dependencies: 15627 15719 react: 19.2.0 ··· 15642 15734 react: 19.2.0 15643 15735 15644 15736 uuid@9.0.1: {} 15737 + 15738 + valibot@1.3.1(typescript@5.9.3): 15739 + optionalDependencies: 15740 + typescript: 5.9.3 15645 15741 15646 15742 vfile-message@4.0.3: 15647 15743 dependencies: