skills/smithery-homepage/SKILL.md
Build and edit the Smithery homepage app -- a TanStack Start + shadcn/ui web app at ~/.smithery/homepage that connects to MCP servers through the Smithery Connect API. Use this skill whenever the user wants to create, modify, or add features to the Smithery homepage, build pages that display data from MCP tools (Linear issues, Gmail, Notion, etc.), or asks about editing anything in ~/.smithery/homepage. Also triggers for requests like 'add a page to the homepage', 'show my Linear issues on the homepage', 'update the homepage UI', or any task involving the ~/.smithery/homepage project.
npx skillsauth add smithery-ai/cli smithery-homepageInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The Smithery homepage is a TanStack Start app at ~/.smithery/homepage that serves as a personal dashboard connecting to MCP servers via the Smithery Connect API.
If ~/.smithery/homepage does not exist, scaffold it from scratch:
mkdir -p ~/.smithery
cd ~/.smithery && npx shadcn@latest init --preset b1FSjVe3E --template start --name homepage
cd ~/.smithery/homepage
npm install @smithery/api @modelcontextprotocol/sdk @tanstack/react-query @tanstack/react-query-devtools
git init && git add -A && git commit -m "feat: initial commit".env with the user's Smithery API key and namespace (read from ~/Library/Application Support/smithery/settings.json on macOS — fields apiKey and namespace)If ~/.smithery/homepage already exists, work within the existing project — read the current code before making changes.
@tanstack/react-query (React Query) — ALL API requests MUST use React Query@smithery/api + @modelcontextprotocol/sdkcreateServerFn from @tanstack/react-start for server-side MCP callsEvery API request in the app MUST use React Query (@tanstack/react-query). Do not use raw fetch, useEffect + useState, or route loaders alone for data fetching. React Query provides caching, background refetching, loading/error states, and stale-while-revalidate — all of which are essential for a good dashboard UX.
The QueryClient must be configured in the router and provided at the root layout. The scaffold generates getRouter() — update it to add the QueryClient:
// src/router.tsx
import { QueryClient } from "@tanstack/react-query"
import { createRouter as createTanStackRouter } from "@tanstack/react-router"
import { routeTree } from "./routeTree.gen"
export function getRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
refetchOnWindowFocus: true,
},
},
})
return createTanStackRouter({
routeTree,
context: { queryClient },
scrollRestoration: true,
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
})
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof getRouter>
}
}
The scaffold generates __root.tsx with createRootRoute and a shellComponent for the HTML document wrapper. Replace createRootRoute with createRootRouteWithContext to pass QueryClient, keep the shellComponent, and add a component with QueryClientProvider:
// src/routes/__root.tsx
import {
HeadContent,
Outlet,
Scripts,
createRootRouteWithContext,
} from "@tanstack/react-router"
import { QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import type { QueryClient } from "@tanstack/react-query"
import appCss from "../styles.css?url"
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "Dashboard" },
],
links: [{ rel: "stylesheet", href: appCss }],
}),
component: RootComponent,
shellComponent: RootDocument,
})
function RootComponent() {
const { queryClient } = Route.useRouteContext()
return (
<QueryClientProvider client={queryClient}>
<Outlet />
<ReactQueryDevtools />
</QueryClientProvider>
)
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}
~/.smithery/homepage/
├── src/
│ ├── routes/ # File-based routes (TanStack Router)
│ │ ├── __root.tsx # Root layout with QueryClientProvider
│ │ └── index.tsx # Home page
│ ├── components/ui/ # shadcn components
│ ├── lib/ # Server-side helpers (MCP tool callers)
│ │ └── schemas/ # Cached Zod schemas copied from ~/.smithery/
│ ├── router.tsx # Router setup with QueryClient
│ ├── routeTree.gen.ts # Auto-generated route tree
│ └── styles.css # Tailwind + shadcn theme
├── .env # SMITHERY_API_KEY
├── components.json # shadcn config
├── package.json
├── tsconfig.json
└── vite.config.ts # (if present)
The app uses @smithery/api/mcp to create MCP connections through Smithery Connect. This runs server-side via TanStack Start server functions — the API key never reaches the browser.
Create a shared src/lib/mcp.ts that handles MCP connections for any server:
// src/lib/mcp.ts
import { createConnection } from "@smithery/api/mcp"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
const NAMESPACE = process.env.SMITHERY_NAMESPACE ?? ""
export async function callMcpTool(
connectionId: string,
toolName: string,
args: Record<string, unknown>,
): Promise<unknown> {
const { transport } = await createConnection({
namespace: NAMESPACE,
connectionId,
})
const client = new Client({ name: "homepage", version: "1.0.0" })
await client.connect(transport)
try {
const result = await client.callTool({ name: toolName, arguments: args })
const content = result.content
if (!Array.isArray(content)) return null
const textBlock = content.find(
(c): c is { type: "text"; text: string } => c.type === "text",
)
return textBlock?.text ? JSON.parse(textBlock.text) : null
} finally {
await client.close()
}
}
CRITICAL: Always use cached tool schemas for type safety. The Smithery CLI caches Zod schemas for tool inputs and outputs at ~/.smithery/<connection>__<tool_name>.ts after each successful tool call. These schemas contain the real input and output types — never hand-write or guess tool response types. Never use as type casting on tool results. Always parse results through the cached outputSchema to get proper types at runtime.
Before writing a tool caller module, check if the cached schema file exists at ~/.smithery/<connection>__<tool_name>.ts. If it does NOT exist, you MUST run the tool first to generate it:
smithery mcp call <connection> <tool_name> [--args '{}']
This creates the schema file with accurate inputSchema, Input, outputSchema, and Output types inferred from the real tool response. You must always run the tool to generate the schema before writing code that depends on it — do not guess or hallucinate tool response shapes.
Copy the cached schema file into the homepage's src/lib/schemas/ directory so it's part of the project and available to the TypeScript compiler:
mkdir -p ~/.smithery/homepage/src/lib/schemas
cp ~/.smithery/<connection>__<tool_name>.ts ~/.smithery/homepage/src/lib/schemas/
Also ensure zod is installed in the homepage project (npm install zod if needed).
For each MCP server, create a helper in src/lib/ that imports the shared MCP helper AND the cached schemas. Import outputSchema (the Zod schema) to parse results, and type Input for compile-time argument checking:
// src/lib/linear.ts
import { createServerFn } from "@tanstack/react-start"
import { queryOptions } from "@tanstack/react-query"
import { callMcpTool } from "./mcp"
import type { Input as ListIssuesInput } from "./schemas/linear__list_issues"
import { outputSchema as listIssuesOutputSchema } from "./schemas/linear__list_issues"
// Server functions (called by React Query from the client)
export const fetchIssues = createServerFn({ method: "GET" }).handler(
async () => {
const raw = await callMcpTool("linear", "list_issues", {
assignee: "me",
includeArchived: false,
} satisfies ListIssuesInput)
const data = listIssuesOutputSchema.parse(raw)
return data.issues.filter(
(i) => i.statusType !== "canceled" && i.statusType !== "completed"
)
},
)
// Query options factory — use this in routes and components
export const issuesQueryOptions = () =>
queryOptions({
queryKey: ["linear", "issues"],
queryFn: () => fetchIssues(),
staleTime: 1000 * 60 * 2, // 2 minutes
})
Key points:
satisfies Input on the args object to catch invalid tool inputs at compile timeoutputSchema.parse(raw) — this validates the data at runtime AND gives you the correct TypeScript type. Never use as casting on tool results.smithery mcp call ...) to regenerate itIMPORTANT: .validator() does NOT exist in TanStack Start. For server functions that accept input:
For GET server functions, type the handler's { data } parameter directly:
export const fetchItem = createServerFn({ method: "GET" }).handler(
async ({ data }: { data: { id: string } }) => {
return callMcpTool("service", "get_item", { item_id: data.id })
},
)
// Call: fetchItem({ data: { id: "123" } })
For POST mutations, use .inputValidator() (not .validator()):
export const createItem = createServerFn({ method: "POST" })
.inputValidator((data: { title: string }) => data)
.handler(async ({ data }) => {
return callMcpTool("service", "create_item", { title: data.title })
})
Use queryOptions + useQuery for data fetching. Prefetch in the route loader for instant navigation, then read via React Query in the component. Always destructure isLoading, error, and refetch alongside data to handle all states.
// src/routes/issues.tsx
import { createFileRoute } from "@tanstack/react-router"
import { useQuery } from "@tanstack/react-query"
import { issuesQueryOptions } from "@/lib/linear"
export const Route = createFileRoute("/issues")({
loader: ({ context: { queryClient } }) =>
// Use void to fire-and-forget — don't block navigation on data
void queryClient.ensureQueryData(issuesQueryOptions()),
component: IssuesPage,
})
function IssuesPage() {
const { data: issues, isLoading, error, refetch } = useQuery(issuesQueryOptions())
// Handle all three states: loading, error, success
return (/* render issues */)
}
Every component that uses useQuery MUST handle loading and error states. Never render only the success case.
Use isLoading and error from useQuery, plus a skeleton loader and an error component with retry:
function DataSection() {
const { data, isLoading, error, refetch } = useQuery(dataQueryOptions())
return (
<Card>
<CardContent>
{isLoading ? (
<LoadingSkeleton />
) : error ? (
<ErrorState message="Failed to load data" onRetry={() => refetch()} />
) : (
<div>{/* render data */}</div>
)}
</CardContent>
</Card>
)
}
Always include a retry button so users can recover from transient failures:
function ErrorState({ message, onRetry }: { message: string; onRetry?: () => void }) {
return (
<div className="flex flex-col items-center gap-2 p-6 text-center">
<AlertCircle className="text-destructive h-5 w-5" />
<p className="text-destructive text-sm">{message}</p>
{onRetry && (
<Button variant="outline" size="sm" onClick={onRetry}>
Retry
</Button>
)}
</div>
)
}
Update card descriptions to show loading/error/count:
<CardDescription>
{isLoading ? "Loading..." : error ? "Error" : `${data?.length ?? 0} items`}
</CardDescription>
Show inline error messages for mutations (e.g., inside a dialog):
{mutation.error && (
<p className="text-destructive text-sm">
Failed to create item. Please try again.
</p>
)}
For actions that modify data (creating issues, updating status, etc.), use useMutation:
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { createIssue } from "@/lib/linear"
function CreateIssueButton() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (input: { title: string }) => createIssue(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["linear", "issues"] })
},
})
return (
<button onClick={() => mutation.mutate({ title: "New issue" })}>
{mutation.isPending ? "Creating..." : "Create Issue"}
</button>
)
}
For data that should stay fresh (e.g., notifications), use refetchInterval:
const { data } = useQuery({
queryKey: ["notifications"],
queryFn: () => fetchNotifications(),
refetchInterval: 1000 * 30, // poll every 30 seconds
})
Organize query keys hierarchically by service and resource:
["linear", "issues"] — all issues["linear", "issues", issueId] — single issue["gmail", "messages", { label: "inbox" }] — filtered messages["notion", "pages"] — all pagesThis enables targeted invalidation: queryClient.invalidateQueries({ queryKey: ["linear"] }) invalidates all Linear queries.
The Smithery settings file on macOS is at:
~/Library/Application Support/smithery/settings.json
It contains:
{
"apiKey": "smry_...",
"namespace": "..."
}
The .env file should have both values:
SMITHERY_API_KEY=<the apiKey from settings>
SMITHERY_NAMESPACE=<the namespace from settings>
The @smithery/api client reads SMITHERY_API_KEY automatically. SMITHERY_NAMESPACE is used by the MCP helper to identify the user's namespace for connections.
To find which MCP connections are available, use the Smithery CLI:
smithery mcp list
Or check a specific connection's tools:
smithery tool list <connection-id> --flat --limit 100
smithery tool get <connection-id> <tool-name>
cd ~/.smithery/homepage
npx shadcn@latest add <component-name>
Components install to src/components/ui/. Import as @/components/ui/<name>.
~/.smithery/<connection>__<tool>.ts exists. If not, run smithery mcp call <connection> <tool> to generate it, then copy it to src/lib/schemas/.src/routes/<page-name>.tsx with createFileRoute("/<page-name>")queryOptions in the relevant src/lib/ module, importing types from ./schemas/loader with queryClient.ensureQueryData(...)useSuspenseQuery(...)__root.tsx if neededAfter making changes, always commit:
cd ~/.smithery/homepage
git add -A
git commit -m "<descriptive commit message>"
Use conventional commit prefixes: feat:, fix:, style:, refactor:.
The homepage runs as a background daemon via the Smithery CLI. Before making changes, ensure the daemon is running:
smithery homepage up # Start the daemon (idempotent — safe to call if already running)
smithery homepage status # Check if it's running
smithery homepage down # Stop the daemon
smithery homepage up auto-installs portless if needed, starts Vite in the background, and serves the app at https://smithery.localhost with automatic HTTPS.
The daemon uses Vite's dev server, so file changes auto-reload via HMR — no restart needed after editing code. Logs are written to ~/.smithery/homepage.log.
After making changes to the homepage, tell the user to visit https://smithery.localhost to see the result. If the daemon is not running, start it with smithery homepage up first.
For direct REST calls (alternative to the SDK), see references/connect-api.md.
tools
Find, connect, and use MCP tools and skills via the Smithery CLI. Use when the user searches for new tools or skills, wants to discover integrations, connect to an MCP, install a skill, or wants to interact with an external service (email, Slack, Discord, GitHub, Jira, Notion, databases, cloud APIs, monitoring, etc.).
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------