This document provides essential context for AI coding assistants (Claude, GitHub Copilot, Cursor, etc.) working on this codebase.
IMPORTANT: Before making any code changes, read:
- This file (AGENTS.md) - Project overview and key patterns
CLAUDE.md- Detailed guidelines for AI assistantsREADME.md- Setup and deployment instructionsdocs/mocks.md- MSW auto-mocker, fixtures, and dev server
ToolHive Cloud UI is a Next.js 16 application for visualizing MCP (Model Context Protocol) servers in user infrastructure.
- Repository: https://github.com/stacklok/toolhive-cloud-ui
- Backend API: https://github.com/stacklok/toolhive-registry-server
- License: Apache 2.0 (Open Source)
| Category | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript (strict mode) |
| UI Library | React 19 + React Compiler |
| Styling | Tailwind CSS 4 |
| Components | shadcn/ui |
| Auth | Better Auth (OIDC, stateless) |
| API Client | hey-api + React Query |
| Testing | Vitest + Testing Library |
| Linting | Biome |
| Package Manager | pnpm |
- Server Components First - Use
'use client'only when necessary - Generated API Client - Never write manual fetch logic, use hey-api functions in server actions
- Server Actions for API Calls - Client components fetch data via server actions, not direct API calls
- Async/Await Only - No
.then()promise chains - 🚫 NEVER USE
any- STRICTLY FORBIDDEN. Useunknownwith type guards or proper types - Stateless Auth - JWT tokens, no server-side sessions
IMPORTANT: This is a Next.js 16 App Router application. Before suggesting any code:
- Check Next.js Documentation - Verify your approach is correct
- Validate your suggestion - Ensure it follows App Router conventions
- Consider Server vs Client - Default to Server Components, only use Client when needed
- Use Next.js built-ins - File-system routing, caching, revalidation, etc.
Common Next.js patterns that AI agents often get wrong:
- ❌ Using old Pages Router patterns in App Router
- ❌ Creating custom routing when file-system routing should be used
- ❌ Not understanding Server vs Client Component boundaries
- ❌ Missing
'use client'or adding it unnecessarily - ❌ Not leveraging Next.js caching and revalidation
src/
├── app/ # Next.js App Router (pages, layouts, routes)
├── components/ui/ # shadcn/ui components (DO NOT EDIT)
├── lib/ # Utilities, auth config
├── generated/ # hey-api output (DO NOT EDIT)
└── hooks/ # Custom React hooks
src/mocks/ # MSW auto-mocker, handlers, fixtures, and dev server
dev-auth/ # Development OIDC mock
helm/ # Kubernetes deployment
scripts/ # Build scripts
# Development
pnpm dev # Start dev server + OIDC mock
pnpm mock:server # Start standalone MSW mock server (default: http://localhost:9090)
pnpm dev:next # Start only Next.js
pnpm oidc # Start only OIDC mock
# Code Quality
pnpm lint # Run linter
pnpm format # Auto-format code
pnpm type-check # TypeScript validation
pnpm test # Run tests
# API Client
pnpm generate-client # Regenerate from backend API-
Schema-based mocks are generated automatically. To create a new mock for an endpoint, run a Vitest test (or the app in dev) that calls that endpoint. The first call writes a fixture under
src/mocks/fixtures/<sanitized-path>/<method>.ts. -
To adjust the payload, edit the generated fixture file. Prefer this over adding a non-schema mock when you only need more realistic sample data.
-
Non-schema mocks live in
src/mocks/customHandlersand take precedence over schema-based mocks. Use these for behavior overrides or endpoints without schema. -
Global test setup: Add common mocks to
vitest.setup.ts(e.g.,next/headers,next/navigation,next/image,sonner, auth client). Before copying a mock into a test file, check if it can be centralized globally. Reset all mocks globally between tests.
- Use file-system routing - Routes are defined by folder structure
- Naming conventions:
page.tsx(route),layout.tsx(shared UI),loading.tsx(loading states),error.tsx(error boundaries) - Don't create custom routing logic - Use Next.js conventions
app/
├── page.tsx # / route
├── layout.tsx # Root layout
├── dashboard/
│ ├── page.tsx # /dashboard route
│ ├── layout.tsx # Dashboard layout
│ └── servers/
│ ├── page.tsx # /dashboard/servers
│ └── [name]/
│ └── page.tsx # /dashboard/servers/:name (dynamic)
Server Components (default):
- Fetch data directly with async/await
- Access backend resources
- No event handlers, no browser APIs, no hooks
- Faster, reduced bundle size
Client Components ('use client'):
- Interactive elements (onClick, onChange)
- Browser APIs (window, localStorage, clipboard)
- React hooks (useState, useEffect, useContext)
- hey-api React Query hooks
Server Component:
async function ServerList() {
const response = await fetch("/registry/v0.1/servers", {
next: { revalidate: 3600 }, // In dev, Next rewrites proxy to mock server
});
const data = await response.json();
return <ServerList servers={data} />;
}Client Component:
"use client";
import { useGetApiV0Servers } from "@/generated/client/@tanstack/react-query.gen";
function ServerList() {
const { data, isLoading } = useGetApiV0Servers();
return <div>{data?.map(...)}</div>;
}Prefer Server Actions over API routes for mutations:
"use server";
import { revalidatePath } from "next/cache";
export async function createServer(formData: FormData) {
await db.server.create({ data: formData });
revalidatePath("/servers"); // Revalidate cache
return { success: true };
}next: { revalidate: 3600 }- Time-based revalidationnext: { tags: ['servers'] }- Tag-based cacherevalidatePath('/servers')- On-demand revalidationrevalidateTag('servers')- Invalidate tagged cache
// app/servers/page.tsx
async function ServersPage() {
const response = await fetch("http://api/servers", {
next: { revalidate: 3600 },
});
const servers = await response.json();
return <ServerList servers={servers} />;
}"use client";
import { useGetApiV0Servers } from "@/generated/client/@tanstack/react-query.gen";
function ServerList() {
const { data, isLoading, error } = useGetApiV0Servers();
if (isLoading) return <Skeleton />;
if (error) return <ErrorDisplay error={error} />;
return <div>{data?.map(server => ...)}</div>;
}"use server";
import { revalidatePath } from "next/cache";
export async function createServer(formData: FormData) {
try {
await db.server.create({ data: formData });
revalidatePath("/servers");
return { success: true };
} catch (error) {
return { error: "Failed to create server" };
}
}- Use Server Components by default
- Use hey-api generated hooks for API calls
- Use
async/await(never.then()) - Use shadcn/ui components
- Handle errors with user-friendly messages
- Add JSDoc for complex functions (explain why, not what)
- Check TypeScript and linter before committing
- Follow existing patterns in codebase
- Use
anytype - STRICTLY FORBIDDEN. Useunknown+ type guards or proper types - Edit files in
src/generated/*- Auto-generated, will be overwritten on regeneration - Use
'use client'on every component - Create manual fetch logic in components
- Use
.then()promise chains - Create custom Button/Dialog/Card components
- Ignore TypeScript or linting errors
- Mass refactor without clear reason
- Create API routes for simple mutations (use Server Actions)
GET /api/v0/servers- List MCP serversGET /api/v0/servers/{name}- Server detailsGET /api/v0/deployed- Deployed instancesGET /api/v0/deployed/{name}- Instance details
- Never edit files in
src/generated/*** - they are auto-generated and will be overwritten - Always use server actions - Client components should not call the API directly
- The API client is server-side only (no
NEXT_PUBLIC_env vars needed)
Example: Server Action:
// src/app/catalog/actions.ts
"use server";
import { getRegistryV01Servers } from "@/generated/sdk.gen";
export async function getServersSummary() {
try {
const resp = await getRegistryV01Servers();
const data = resp.data;
// Process data...
return { count: data?.servers?.length ?? 0, ... };
} catch (error) {
console.error("Failed to fetch servers:", error);
return { count: 0, ... };
}
}Example: Server Component:
// src/app/catalog/page.tsx
import { getServersSummary } from "./actions";
export default async function CatalogPage() {
const summary = await getServersSummary();
return <div>{summary.count} servers</div>;
}When Backend Changes:
pnpm generate-client # Fetch swagger.json and regenerate- OIDC provider agnostic
- Stateless JWT authentication
- Environment variables:
OIDC_ISSUER_URL- OIDC provider URLOIDC_CLIENT_ID- OAuth2 client IDOIDC_CLIENT_SECRET- OAuth2 client secretOIDC_PROVIDER_ID- Provider identifier (e.g., "okta", "oidc") - Required, server-side only.BETTER_AUTH_URL- Application base URLBETTER_AUTH_SECRET- Secret for token encryption
- Mock OIDC provider (runs via
pnpm dev) - MSW for API mocking
- No real authentication needed
import { render, screen, waitFor } from "@testing-library/react";
import { describe, it, expect } from "vitest";
describe("Component", () => {
it("does something", async () => {
render(<Component />);
await waitFor(() => {
expect(screen.getByText("Expected")).toBeVisible();
});
});
});- User interactions
- Authentication flows
- Error scenarios
- Loading states
- Accessibility
- Prefer
toBeVisible()overtoBeInTheDocument()-toBeVisible()checks that an element is actually visible to the user (not hidden via CSS,aria-hidden, etc.), whiletoBeInTheDocument()only checks DOM presence. UsetoBeVisible()for positive assertions and.not.toBeInTheDocument()for absence checks.
// ❌ BAD
"use client";
function Page() {
return <div>Static content</div>;
}
// ✅ GOOD
function Page() {
return <div>Static content</div>;
}// ❌ BAD
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api")
.then((r) => r.json())
.then(setData);
}, []);
// ✅ GOOD
const { data } = useGetApiV0Servers();// ❌ BAD
fetch("/api")
.then((r) => r.json())
.then((data) => process(data));
// ✅ GOOD
const response = await fetch("/api");
const data = await response.json();
process(data);// ❌ BAD
function CustomButton({ children, onClick }) {
return (
<button className="..." onClick={onClick}>
{children}
</button>
);
}
// ✅ GOOD
import { Button } from "@/components/ui/button";
<Button onClick={onClick}>{children}</Button>;// ❌ FORBIDDEN - NEVER USE any
function process(data: any) {
return data.value;
}
// ✅ GOOD - Use proper types
interface Data {
value: string;
}
function process(data: Data) {
return data.value;
}
// ✅ GOOD - Use unknown with type guards for truly unknown types
function process(data: unknown) {
if (isData(data)) {
return data.value;
}
throw new Error("Invalid data");
}
function isData(value: unknown): value is Data {
return typeof value === "object" && value != null && "value" in value;
}- TypeScript Errors:
pnpm type-check- Fix errors, don't suppress - Linter Errors:
pnpm lintandpnpm format - API Issues: Check
NEXT_PUBLIC_API_URL, verify backend is running, regenerate client - Auth Issues: Dev - ensure
pnpm devrunning; Prod - checkOIDC_*env vars
- Project Guides: AGENTS.md, CLAUDE.md, copilot-instructions.md (MUST READ)
- Next.js: https://nextjs.org/docs
- Better Auth: https://www.better-auth.com
- hey-api: https://heyapi.vercel.app
- shadcn/ui: https://ui.shadcn.com
- Backend API: https://github.com/stacklok/toolhive-registry-server
- MCP Registry: https://github.com/modelcontextprotocol/registry
When implementing features:
- Check existing patterns - Look for similar code in the codebase
- Server or Client? - Default to Server Component
- API calls? - Use hey-api hooks
- UI components? - Use shadcn/ui
- Mutations? - Prefer Server Actions over API routes
- Uncertain? - Ask before implementing
Before marking a task complete:
- No TypeScript errors
- No linter errors
- Uses hey-api hooks (no manual fetch)
- Server Components by default
- Proper error handling
- Loading states implemented
- Uses
async/await(no.then()) - Follows existing patterns
- JSDoc for complex functions
- Tests pass
- No unnecessary refactoring
This is an open-source project. Write code that:
- Is easy to understand and maintain
- Follows the established patterns
- Is properly tested and documented
- Considers other contributors
Remember: Simple, readable code is better than clever code. When in doubt, check the project documentation (AGENTS.md, CLAUDE.md) and existing codebase patterns.