Skip to content

Commit 0e4028c

Browse files
valebearzottimdottaviofveiraswwwclaude
authored
v0.5.7 (#411)
* feat: introducing customHome config option (#397) * fix: include tsx files in tsconfig for gpt-apps and mcp-apps default templates The default templates for both GPT apps and MCP apps contained .tsx files but their tsconfig.json was only including .ts files. Added jsx compiler option and .tsx file pattern to match the tailwind template configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * unit tests for updateTsConfig function and support updating path aliases on setup (#399) * feat: skip type check on nextjs adapter mode (#400) * feat: upd adapter mode tool gen syntax (#401) * feat: add CSS module type declarations for zero-config ts support * feat: docs sidebar w/ toggleable func (#403) * apply font-family once on body * add CSS documentation to core concepts * improve CSS docs and ordering * feat: example initialization via cli (#406) * feat: example initialization via cli * remove templates parent directory + upd next steps * rmv auto-detection * next steps to handle directory * add globals.css auto-detection and update docs * ignore example metadata folder (#408) * add engineering category * add blog post on MCP UI components (#409) * add blog post on MCP UI components * always bet on react * copywriting nitpicks * upload image + upd date --------- Co-authored-by: Francisco Veiras <74626997+francisco-svg761@users.noreply.github.com> Co-authored-by: valebearzotti <valebearzotti1@gmail.com> * downloadTarStream handle download status (#412) * add safety checks to the --example path (#413) --------- Co-authored-by: Mauricio Dottavio <dottavio@gmail.com> Co-authored-by: Francisco Veiras <74626997+francisco-svg761@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: Francisco Veiras <74626997+fveiraswww@users.noreply.github.com>
2 parents c5bd27d + 121d1ee commit 0e4028c

File tree

37 files changed

+1597
-68
lines changed

37 files changed

+1597
-68
lines changed

apps/website/components/layout/docs.tsx

Lines changed: 195 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,52 @@
11
"use client";
22
import type * as PageTree from "fumadocs-core/page-tree";
3-
import { type ReactNode, useMemo } from "react";
3+
import { type ReactNode, useEffect, useMemo, useState } from "react";
44
import { cn } from "../../lib/cn";
55
import { TreeContextProvider, useTreeContext } from "fumadocs-ui/contexts/tree";
66
import Link from "fumadocs-core/link";
77
import { useSidebar } from "fumadocs-ui/contexts/sidebar";
88
import { cva } from "class-variance-authority";
99
import { usePathname } from "fumadocs-core/framework";
10+
import {
11+
getSeparatorId,
12+
useSidebarOpenState,
13+
} from "@/hooks/use-sidebar-open-state";
14+
import { Icons } from "../icons";
15+
16+
type SeparatorNode = Extract<PageTree.Node, { type: "separator" }>;
17+
type GroupedItem =
18+
| { type: "group"; separator: SeparatorNode; items: PageTree.Node[] }
19+
| { type: "item"; item: PageTree.Node };
20+
21+
function groupBySeparator(items: PageTree.Node[]): GroupedItem[] {
22+
const grouped: GroupedItem[] = [];
23+
let currentGroup: {
24+
separator: SeparatorNode;
25+
items: PageTree.Node[];
26+
} | null = null;
27+
28+
for (const item of items) {
29+
if (item.type === "separator") {
30+
if (currentGroup) {
31+
grouped.push({ type: "group", ...currentGroup });
32+
}
33+
currentGroup = { separator: item, items: [] };
34+
continue;
35+
}
36+
37+
if (currentGroup) {
38+
currentGroup.items.push(item);
39+
} else {
40+
grouped.push({ type: "item", item });
41+
}
42+
}
43+
44+
if (currentGroup) {
45+
grouped.push({ type: "group", ...currentGroup });
46+
}
47+
48+
return grouped;
49+
}
1050

1151
export interface DocsLayoutProps {
1252
tree: PageTree.Root;
@@ -30,20 +70,54 @@ export function DocsLayout({ tree, children }: DocsLayoutProps) {
3070
function Sidebar() {
3171
const { root } = useTreeContext();
3272
const { open } = useSidebar();
73+
const { openState, toggle, initialOpenSnapshot } = useSidebarOpenState(root);
3374

3475
const children = useMemo(() => {
3576
function renderItems(items: PageTree.Node[]) {
36-
return items.map((item) => {
77+
return groupBySeparator(items).map((entry, index) => {
78+
if (entry.type === "group") {
79+
const separatorId = getSeparatorId(entry.separator, index);
80+
const isOpen = openState[separatorId] ?? false;
81+
const disableInitialAnimation =
82+
initialOpenSnapshot[separatorId] ?? false;
83+
84+
const childContent = renderItems(entry.items);
85+
86+
return (
87+
<SidebarSeparator
88+
key={separatorId}
89+
item={entry.separator}
90+
itemId={separatorId}
91+
isOpen={isOpen}
92+
onToggle={toggle}
93+
disableInitialAnimation={disableInitialAnimation}
94+
>
95+
{childContent}
96+
</SidebarSeparator>
97+
);
98+
}
99+
100+
const item = entry.item;
101+
const itemId =
102+
item.$id ?? `${item.type}-${index}-${String(item.name ?? "item")}`;
103+
const isOpen = openState[itemId] ?? false;
104+
37105
return (
38-
<SidebarItem key={item.$id} item={item}>
106+
<SidebarItem
107+
key={itemId}
108+
item={item}
109+
itemId={itemId}
110+
isOpen={isOpen}
111+
onToggle={toggle}
112+
>
39113
{item.type === "folder" ? renderItems(item.children) : null}
40114
</SidebarItem>
41115
);
42116
});
43117
}
44118

45119
return renderItems(root.children);
46-
}, [root]);
120+
}, [openState, root, toggle, initialOpenSnapshot]);
47121
return (
48122
<aside
49123
className={cn(
@@ -72,9 +146,15 @@ const linkVariants = cva(
72146
function SidebarItem({
73147
item,
74148
children,
149+
itemId,
150+
isOpen,
151+
onToggle,
75152
}: {
76153
item: PageTree.Node;
77154
children: ReactNode;
155+
itemId: string;
156+
isOpen?: boolean;
157+
onToggle?: (id: string) => void;
78158
}) {
79159
const pathname = usePathname();
80160

@@ -86,40 +166,129 @@ function SidebarItem({
86166
active: pathname === item.url,
87167
})}
88168
>
89-
{item.icon}
90169
{item.name}
91170
</Link>
92171
);
93172
}
94173

95174
if (item.type === "separator") {
96-
return (
97-
<p className="text-brand-white mt-6 mb-1 first:mt-0 text-sm font-medium">
98-
{item.icon}
99-
{item.name}
100-
</p>
101-
);
175+
return null;
102176
}
103177

104178
return (
105179
<div>
106-
{item.index ? (
107-
<Link
108-
className={linkVariants({
109-
active: pathname === item.index.url,
110-
})}
111-
href={item.index.url}
180+
<div className="flex items-center gap-2">
181+
<button
182+
type="button"
183+
aria-label={isOpen ? "Collapse section" : "Expand section"}
184+
aria-expanded={isOpen}
185+
onClick={() => onToggle?.(itemId)}
186+
className="rounded px-1 text-xs text-brand-neutral-100 hover:text-brand-white"
112187
>
113-
{item.index.icon}
114-
{item.index.name}
115-
</Link>
116-
) : (
117-
<p className={cn(linkVariants(), "text-start")}>
118-
{item.icon}
119-
{item.name}
120-
</p>
188+
<Icons.arrowDown
189+
className={cn(
190+
"size-4 transition-transform duration-200 ease-in-out group-hover:rotate-180",
191+
isOpen ? "rotate-0" : "-rotate-90"
192+
)}
193+
/>
194+
</button>
195+
{item.index ? (
196+
<Link
197+
className={linkVariants({
198+
active: pathname === item.index.url,
199+
})}
200+
href={item.index.url}
201+
>
202+
{item.index.icon}
203+
{item.index.name}
204+
</Link>
205+
) : (
206+
<p className={cn(linkVariants(), "text-start")}>
207+
{item.icon}
208+
{item.name}
209+
</p>
210+
)}
211+
</div>
212+
{isOpen ? (
213+
<div className="pl-4 border-l flex flex-col">{children}</div>
214+
) : null}
215+
</div>
216+
);
217+
}
218+
219+
function SidebarSeparator({
220+
item,
221+
children,
222+
itemId,
223+
isOpen,
224+
onToggle,
225+
disableInitialAnimation,
226+
}: {
227+
item: Extract<PageTree.Node, { type: "separator" }>;
228+
children: ReactNode;
229+
itemId: string;
230+
isOpen?: boolean;
231+
onToggle?: (id: string) => void;
232+
disableInitialAnimation?: boolean;
233+
}) {
234+
return (
235+
<div className="mt-4 first:mt-0">
236+
<button
237+
type="button"
238+
aria-label={isOpen ? "Collapse section" : "Expand section"}
239+
aria-expanded={isOpen}
240+
onClick={() => onToggle?.(itemId)}
241+
className="group flex w-full items-center gap-2 text-start text-sm font-medium text-brand-white cursor-pointer"
242+
>
243+
<span className="rounded text-xs text-brand-neutral-100 transition-colors duration-200 group-hover:text-brand-white">
244+
<Icons.arrowDown
245+
className={cn(
246+
"size-4 transition-transform duration-200 ease-in-out",
247+
isOpen ? "rotate-0" : "-rotate-90"
248+
)}
249+
/>
250+
</span>
251+
<span className="mt-[2px]">{item.name}</span>
252+
</button>
253+
<AnimatedGroup
254+
isOpen={isOpen}
255+
disableInitialAnimation={disableInitialAnimation}
256+
>
257+
<div className="mt-1 pl-8 flex flex-col gap-1">{children}</div>
258+
</AnimatedGroup>
259+
</div>
260+
);
261+
}
262+
263+
function AnimatedGroup({
264+
isOpen,
265+
children,
266+
disableInitialAnimation,
267+
}: {
268+
isOpen?: boolean;
269+
children: ReactNode;
270+
disableInitialAnimation?: boolean;
271+
}) {
272+
const [initialized, setInitialized] = useState(!disableInitialAnimation);
273+
274+
useEffect(() => {
275+
if (!initialized) {
276+
setInitialized(true);
277+
}
278+
}, [initialized]);
279+
280+
return (
281+
<div
282+
aria-hidden={!isOpen}
283+
className={cn(
284+
"grid transition-[grid-template-rows,opacity] duration-200 ease-in-out",
285+
disableInitialAnimation && !initialized && isOpen
286+
? "transition-none"
287+
: null,
288+
isOpen ? "opacity-100 grid-rows-[1fr]" : "opacity-0 grid-rows-[0fr]"
121289
)}
122-
<div className="pl-4 border-l flex flex-col">{children}</div>
290+
>
291+
<div className="overflow-hidden">{children}</div>
123292
</div>
124293
);
125294
}

apps/website/components/layout/navigation-link.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Link from "next/link";
22
import { cn } from "@/lib/cn";
3-
import { Icons } from "../icons";
43
import type * as PageTree from "fumadocs-core/page-tree";
54

65
export function NavigationLink({
@@ -22,9 +21,7 @@ export function NavigationLink({
2221
)}
2322
>
2423
<span className="text-sm text-brand-neutral-100 flex items-center gap-2">
25-
{isPrevious && <Icons.arrowDown className="size-3 rotate-90" />}
2624
{label}
27-
{!isPrevious && <Icons.arrowDown className="size-3 rotate-270" />}
2825
</span>
2926
<span className="text-brand-white font-medium group-hover:text-fd-primary transition-colors">
3027
{item.name}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
title: "When tools need UIs"
3+
description: "Our philosophy on bringing UI components to MCP tools."
4+
summary: "Our philosophy on bringing UI components to MCP tools."
5+
category: "engineering"
6+
date: "2026-01-06"
7+
order: 2
8+
previewImage: "/blog/react-client-components.png"
9+
authors:
10+
- fveiras_
11+
---
12+
13+
In recent years, modern frameworks have brought backend and frontend together. The client and server environments have different capabilities,
14+
but the developer experience feels monolithic.
15+
16+
xmcp started on the server side: that's where MCP began. But things are evolving fast. Our mission is to lower the barrier to entry for developers to build and ship MCP servers without thinking about all the complexity and friction that comes with it.
17+
18+
With the latest [MCP apps](https://modelcontextprotocol.github.io/ext-apps/api/) proposal, we're getting closer to a more unified experience for developers: a standard inspired by [MCP-UI](https://mcpui.dev/) and [OpenAI's Apps SDK](https://developers.openai.com/apps-sdk/) to allow MCP Servers to display interactive UI elements in conversational MCP clients and chatbots.
19+
20+
You shouldn't need to think about complex config or set up. Instead, you can use [template literals](/docs/core-concepts/tools#2-template-literal-handlers) or [React Client Components](/docs/core-concepts/tools#3-react-component-handlers) directly, and it just works.
21+
22+
## Always bet on React
23+
24+
It's the most popular JavaScript UI framework out there, massive community, extensive ecosystem,
25+
and widespread adoption by major companies. When you already know React, you shouldn't have to learn something new just to return UI from your tools.
26+
27+
We aimed to make it as easy as possible to return React Client components. You only need to convert your handlers from `.ts` to `.tsx`.
28+
We prioritize speed and keeping the framework lightweight, so for this reason React is an optional peer dependency. You only install it if you need it.
29+
30+
![File Extension Diagram - converting .ts to .tsx](https://j2fbnka41vq9pfap.public.blob.vercel-storage.com/images/diagram-1-169%20%281%29.svg)
31+
32+
## Simplicity over complexity
33+
34+
For client-side components, React is all you need to get up and running quickly and efficiently.
35+
36+
For cases where you need to manage complex interactions between client and server components, like hydration or caching, you can use a framework
37+
like Next.js with our [adapter](/docs/adapters/nextjs) that would also allow you to convert Next.js application into compatible MCP apps.
38+
39+
<Callout variant="info">
40+
Check out our [ChatGPT App with Next.js](/blog/doom-with-xmcp) that runs DOOM
41+
for an example of how to use Next.js with xmcp.
42+
</Callout>
43+
44+
## Styling that just works
45+
46+
Under the hood, xmcp compiles using [rspack](https://rspack.rs/), a fast Rust-based web bundler.
47+
When the framework detects JSX, it bundles CSS alongside your JavaScript into the dist directory.
48+
Tailwind, CSS Modules, and standard CSS imports work out of the box.
49+
50+
![Styling Diagram - CSS, CSS Modules, and Tailwind](https://j2fbnka41vq9pfap.public.blob.vercel-storage.com/images/diagram-2-169.svg)
51+
52+
If you want to learn more, you can find additional details and how to get started [here](/docs/core-concepts/css).
53+
54+
## Looking forward
55+
56+
The protocol and ext-apps specification will continue evolving, and sometimes this natural progression brings additional complexity.
57+
Our commitment is to reduce that complexity as much as we can, by actively contributing to the protocol development, carefully following all the latest updates,
58+
and ensuring our framework abstracts away unnecessary complications while maintaining full compatibility with the standard.
59+
60+
If you're building your MCP server using our [Next.js adapter](/docs/adapters/nextjs) or [React Client Components](/docs/core-concepts/tools#3-react-component-handlers), we'd love to hear about your experience. Join us on [Discord](https://discord.gg/d9a7JBBxV9).

0 commit comments

Comments
 (0)