Skip to content

Commit 610eba0

Browse files
committed
servers, command menu, new header
1 parent 0dd02f4 commit 610eba0

File tree

17 files changed

+3639
-30
lines changed

17 files changed

+3639
-30
lines changed

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,24 @@
1313
"prepare": "husky"
1414
},
1515
"dependencies": {
16+
"@modelcontextprotocol/sdk": "^1.20.0",
1617
"@radix-ui/react-dialog": "^1.1.15",
1718
"@radix-ui/react-separator": "^1.1.7",
1819
"@radix-ui/react-slot": "^1.2.3",
1920
"@radix-ui/react-tooltip": "^1.2.8",
2021
"@vercel/analytics": "^1.5.0",
22+
"ai": "^5.0.72",
2123
"class-variance-authority": "^0.7.1",
2224
"clsx": "^2.1.1",
25+
"cmdk": "^1.1.1",
2326
"framer-motion": "^12.23.24",
2427
"lucide-react": "^0.545.0",
2528
"next": "15.5.5",
2629
"next-themes": "^0.4.6",
2730
"react": "19.1.0",
2831
"react-dom": "19.1.0",
29-
"tailwind-merge": "^3.3.1"
32+
"tailwind-merge": "^3.3.1",
33+
"zod": "^3.25.76"
3034
},
3135
"devDependencies": {
3236
"@biomejs/biome": "2.2.6",

pnpm-lock.yaml

Lines changed: 834 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/globals.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@
117117
* {
118118
@apply border-border outline-ring/50;
119119
}
120+
html {
121+
@apply bg-background;
122+
overscroll-behavior-x: none;
123+
}
120124
body {
121125
@apply bg-background text-foreground;
122126
}

src/app/layout.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Geist, Geist_Mono } from "next/font/google";
33
import "./globals.css";
44
import { Analytics } from "@vercel/analytics/next";
55
import localFont from "next/font/local";
6+
import { CommandMenu } from "@/components/command-menu";
7+
import { KeyboardShortcutsHandler } from "@/components/keyboard-shortcuts-handler";
68
import { ThemeProvider } from "@/components/theme-provider";
79
import { TopNav } from "@/components/top-nav";
810
import { BreakpointIndicator } from "@/components/ui/breakpoint-indicator";
@@ -36,16 +38,22 @@ export default function RootLayout({
3638
<body
3739
className={`${geistSans.variable} ${geistMono.variable} ${foundryPlek.variable} font-sans antialiased`}
3840
>
41+
<div className="pointer-events-none fixed inset-0 bg-gradient-to-b from-primary/10 via-transparent to-transparent" />
3942
<ThemeProvider
4043
attribute="class"
4144
defaultTheme="system"
4245
disableTransitionOnChange
4346
enableSystem
4447
>
48+
<KeyboardShortcutsHandler />
49+
<CommandMenu />
4550
<TopNav />
4651
<BreakpointIndicator className="fixed bottom-0 left-0" />
47-
48-
{children}
52+
<main className="min-h-screen">
53+
<div className="mx-auto min-h-screen w-full max-w-6xl py-32">
54+
{children}
55+
</div>
56+
</main>
4957
</ThemeProvider>
5058
<Analytics />
5159
</body>

src/app/servers/actions.ts

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
"use server";
2+
3+
import { z } from "zod";
4+
5+
import type { McpServerConfig, McpServerSummary } from "@/lib/mcp/manager";
6+
import { getMcpManager } from "@/lib/mcp/registry";
7+
8+
export type ActionResponse<T> =
9+
| {
10+
success: true;
11+
data: T;
12+
message?: string;
13+
}
14+
| {
15+
success: false;
16+
error: string;
17+
};
18+
19+
export type ServerDetailsSnapshot = {
20+
summary: McpServerSummary;
21+
tools: Array<{
22+
name: string;
23+
description?: string;
24+
schema?: unknown;
25+
}>;
26+
resources: Array<{
27+
uri: string;
28+
name?: string;
29+
description?: string;
30+
}>;
31+
resourceTemplates: Array<{
32+
name: string;
33+
description?: string;
34+
}>;
35+
};
36+
37+
const httpServerSchema = z.object({
38+
id: z.string().min(1),
39+
transport: z.literal("http"),
40+
url: z.string().url(),
41+
timeoutMs: z.number().int().positive().optional(),
42+
preferSse: z.boolean().optional(),
43+
allowSseFallback: z.boolean().optional(),
44+
autoConnect: z.boolean().optional(),
45+
});
46+
47+
const sseServerSchema = z.object({
48+
id: z.string().min(1),
49+
transport: z.literal("sse"),
50+
url: z.string().url(),
51+
timeoutMs: z.number().int().positive().optional(),
52+
autoConnect: z.boolean().optional(),
53+
});
54+
55+
const stdioServerSchema = z.object({
56+
id: z.string().min(1),
57+
transport: z.literal("stdio"),
58+
command: z.string().min(1),
59+
args: z.array(z.string().min(1)).optional(),
60+
cwd: z.string().min(1).optional(),
61+
timeoutMs: z.number().int().positive().optional(),
62+
autoConnect: z.boolean().optional(),
63+
});
64+
65+
const addServerSchema = z.discriminatedUnion("transport", [
66+
httpServerSchema,
67+
sseServerSchema,
68+
stdioServerSchema,
69+
]);
70+
71+
export type AddServerPayload = z.infer<typeof addServerSchema>;
72+
73+
export async function fetchServerSummariesAction(): Promise<
74+
McpServerSummary[]
75+
> {
76+
return await Promise.resolve(getMcpManager().getServerSummaries());
77+
}
78+
79+
export async function fetchServerDetailsAction(
80+
serverId: string
81+
): Promise<ActionResponse<ServerDetailsSnapshot>> {
82+
const manager = getMcpManager();
83+
try {
84+
const summary = manager
85+
.getServerSummaries()
86+
.find((entry) => entry.id === serverId);
87+
if (!summary) {
88+
return {
89+
success: false,
90+
error: `Server "${serverId}" not found.`,
91+
};
92+
}
93+
const [toolsResult, resourcesResult, templatesResult] = await Promise.all([
94+
safeRequest(() => manager.listTools(serverId), { tools: [] }),
95+
safeRequest(() => manager.listResources(serverId), { resources: [] }),
96+
safeRequest(() => manager.listResourceTemplates(serverId), {
97+
resourceTemplates: [],
98+
}),
99+
]);
100+
return {
101+
success: true,
102+
data: {
103+
summary,
104+
tools: (toolsResult.tools ?? []).map((tool) => ({
105+
name: tool.name,
106+
description: tool.description ?? undefined,
107+
schema: tool.inputSchema ?? undefined,
108+
})),
109+
resources: (resourcesResult.resources ?? []).map((resource) => ({
110+
uri: resource.uri,
111+
name: resource.name ?? undefined,
112+
description: resource.description ?? undefined,
113+
})),
114+
resourceTemplates: (templatesResult.resourceTemplates ?? []).map(
115+
(template) => ({
116+
name: template.name,
117+
description: template.description ?? undefined,
118+
})
119+
),
120+
},
121+
};
122+
} catch (error) {
123+
return {
124+
success: false,
125+
error: formatError(error),
126+
};
127+
}
128+
}
129+
130+
export async function addServerAction(
131+
payload: unknown
132+
): Promise<ActionResponse<McpServerSummary[]>> {
133+
const parsedResult = addServerSchema.safeParse(payload);
134+
if (!parsedResult.success) {
135+
return {
136+
success: false,
137+
error: "Invalid server configuration.",
138+
};
139+
}
140+
141+
const manager = getMcpManager();
142+
const serverConfig = buildServerConfig(parsedResult.data);
143+
manager.registerServer(parsedResult.data.id, serverConfig);
144+
145+
if (parsedResult.data.autoConnect) {
146+
try {
147+
await manager.connectServer(parsedResult.data.id);
148+
} catch (error) {
149+
return {
150+
success: false,
151+
error: formatError(error),
152+
};
153+
}
154+
}
155+
156+
return {
157+
success: true,
158+
data: manager.getServerSummaries(),
159+
message: parsedResult.data.autoConnect
160+
? "Server added and connection attempted."
161+
: "Server added.",
162+
};
163+
}
164+
165+
export async function connectServerAction(
166+
serverId: string
167+
): Promise<ActionResponse<McpServerSummary[]>> {
168+
const manager = getMcpManager();
169+
try {
170+
await manager.connectServer(serverId);
171+
return {
172+
success: true,
173+
data: manager.getServerSummaries(),
174+
};
175+
} catch (error) {
176+
return {
177+
success: false,
178+
error: formatError(error),
179+
};
180+
}
181+
}
182+
183+
export async function disconnectServerAction(
184+
serverId: string
185+
): Promise<ActionResponse<McpServerSummary[]>> {
186+
const manager = getMcpManager();
187+
try {
188+
await manager.disconnectServer(serverId);
189+
return {
190+
success: true,
191+
data: manager.getServerSummaries(),
192+
};
193+
} catch (error) {
194+
return {
195+
success: false,
196+
error: formatError(error),
197+
};
198+
}
199+
}
200+
201+
export async function removeServerAction(
202+
serverId: string
203+
): Promise<ActionResponse<McpServerSummary[]>> {
204+
const manager = getMcpManager();
205+
try {
206+
await manager.removeServer(serverId);
207+
return {
208+
success: true,
209+
data: manager.getServerSummaries(),
210+
message: "Server removed.",
211+
};
212+
} catch (error) {
213+
return {
214+
success: false,
215+
error: formatError(error),
216+
};
217+
}
218+
}
219+
220+
export async function refreshServersAction(): Promise<
221+
ActionResponse<McpServerSummary[]>
222+
> {
223+
try {
224+
const data = await Promise.resolve(getMcpManager().getServerSummaries());
225+
return {
226+
success: true,
227+
data,
228+
};
229+
} catch (error) {
230+
return {
231+
success: false,
232+
error: formatError(error),
233+
};
234+
}
235+
}
236+
237+
function buildServerConfig(descriptor: AddServerPayload): McpServerConfig {
238+
if (descriptor.transport === "http") {
239+
return {
240+
transport: "http",
241+
url: descriptor.url,
242+
timeoutMs: descriptor.timeoutMs,
243+
preferSse: descriptor.preferSse,
244+
allowSseFallback: descriptor.allowSseFallback ?? true,
245+
};
246+
}
247+
if (descriptor.transport === "sse") {
248+
return {
249+
transport: "sse",
250+
url: descriptor.url,
251+
timeoutMs: descriptor.timeoutMs,
252+
};
253+
}
254+
const args = descriptor.args?.filter((item) => item.trim().length > 0);
255+
return {
256+
transport: "stdio",
257+
command: descriptor.command,
258+
args,
259+
cwd: descriptor.cwd,
260+
timeoutMs: descriptor.timeoutMs,
261+
};
262+
}
263+
264+
function formatError(error: unknown): string {
265+
if (error instanceof Error) {
266+
return error.message;
267+
}
268+
if (typeof error === "string") {
269+
return error;
270+
}
271+
try {
272+
return JSON.stringify(error);
273+
} catch {
274+
return String(error);
275+
}
276+
}
277+
278+
async function safeRequest<T>(
279+
request: () => Promise<T>,
280+
fallback: T
281+
): Promise<T> {
282+
try {
283+
return await request();
284+
} catch (error) {
285+
if (isMethodNotFoundError(error)) {
286+
return fallback;
287+
}
288+
throw error;
289+
}
290+
}
291+
292+
function isMethodNotFoundError(error: unknown): boolean {
293+
if (!(error instanceof Error)) {
294+
return false;
295+
}
296+
const message = error.message.toLowerCase();
297+
return message.includes("method not found") || message.includes("-32601");
298+
}

src/app/servers/page.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
import { getMcpManager } from "@/lib/mcp/registry";
2+
import ServersClient from "./servers-client";
3+
14
export default function ServersPage() {
5+
const initialServers = getMcpManager().getServerSummaries();
6+
27
return (
3-
<main className="flex min-h-screen flex-col items-center justify-center p-24">
4-
<div className="flex flex-col items-center gap-6 rounded-3xl border border-border/40 bg-primary-foreground/60 p-12 backdrop-blur-xl">
5-
<h1 className="font-plek font-semibold text-4xl">Servers</h1>
6-
<p className="text-center text-muted-foreground text-sm">
7-
Server management and configuration will go here.
8-
</p>
9-
</div>
10-
</main>
8+
<div className="px-1">
9+
<ServersClient initialServers={initialServers} />
10+
</div>
1111
);
1212
}

0 commit comments

Comments
 (0)