Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ Find and download icons at [dashboardicons.com](https://dashboardicons.com):
3. Choose your preferred format
4. Download or copy the direct link

### MCP Server (AI Assistants)

Connect Cursor or other MCP clients to search icons and get CDN URLs programmatically.
See [MCP documentation](web/docs/MCP.md).

### Direct Links

Use icons from CDN with this pattern:
Expand Down
3 changes: 2 additions & 1 deletion web/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-alpine AS base
FROM node:24-alpine AS base

# Install dependencies only when needed
FROM base AS deps
Expand Down Expand Up @@ -32,6 +32,7 @@ ENV NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
ENV NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST}
ENV NEXT_PUBLIC_DISABLE_POSTHOG=${NEXT_PUBLIC_DISABLE_POSTHOG}
ENV NEXT_PUBLIC_POCKETBASE_URL=${NEXT_PUBLIC_POCKETBASE_URL}
ENV PB_URL=${NEXT_PUBLIC_POCKETBASE_URL}
ENV POSTHOG_API_KEY=${POSTHOG_API_KEY}
ENV CI_MODE=${CI_MODE}

Expand Down
37 changes: 18 additions & 19 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A web application to browse, search, and download icons from the
- **User authentication** - Sign in with email/password or GitHub OAuth
- **Submit icons** - Authenticated users can submit new icons to the collection
- **Admin dashboard** - Admins can approve, reject, and manage icon submissions
- **MCP server** - HTTP MCP endpoint for AI assistants to search icons, fetch metadata, and resolve CDN URLs

## Tech Stack

Expand All @@ -24,38 +25,36 @@ A web application to browse, search, and download icons from the
- **Shadcn UI** - Reusable components built with Radix UI and Tailwind
- **PocketBase** - Backend for authentication and data storage
- **PostHog** - Product analytics and user tracking
- **MCP (Model Context Protocol)** - HTTP transport via `mcp-handler` for AI tool integrations

## Project Structure

```
src/
├── app/ # Next.js App Router
│ ├── api/ # API routes
│ │ └── icons/ # Icons browsing and detail pages
│ │ ├── [icon]/ # Dynamic icon detail page
│ │ │ ├── components/ # Icon-specific components
│ │ │ ├── error.tsx # Error handling
│ │ │ ├── loading.tsx # Loading state
│ │ │ └── page.tsx # Icon detail page
│ │ ├── components/ # Icons page components
│ │ ├── loading.tsx # Loading state
│ │ └── page.tsx # Icons browse page
│ │ ├── icons/ # Icon search API
│ │ └── mcp/ # MCP HTTP endpoint
│ ├── icons/ # Icons browsing and detail pages
│ │ ├── [icon]/ # Dynamic icon detail page
│ │ └── page.tsx # Icons browse page
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Homepage
│ └── theme-provider.tsx # Theme provider component
│ └── page.tsx # Homepage
├── components/ # Shared components
│ ├── ui/ # UI components (from shadcn/ui)
│ ├── header.tsx # App header
│ └── theme-switcher.tsx # Theme switcher
├── lib/ # Utility functions
│ ├── api.ts # API utilities
│ └── utils.ts # General utilities
├── lib/
│ ├── api.ts # App-level icon API helpers
│ ├── icon-url.ts # Icon URL resolution
│ └── icons/ # Icon service, search, validation, rate limiting
├── mcp/ # MCP handler and tool registration
└── types/ # TypeScript type definitions
├── icons.ts # Icon-related types
└── index.ts # Type exports
```

## MCP Server

The app exposes a native-icons MCP server over HTTP at `/api/mcp`. AI clients can search the collection, fetch icon metadata, and resolve CDN URLs without scraping the website.
See [docs/MCP.md](./docs/MCP.md) for endpoints, client setup, tool schemas, environment variables, analytics, and rate limits.

## Development

### Prerequisites
Expand Down
108 changes: 108 additions & 0 deletions web/docs/MCP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Dashboard Icons MCP Server

Connect AI assistants to [dashboardicons.com](https://dashboardicons.com) via the Model Context Protocol (MCP).

## Endpoint

```
https://dashboardicons.com/api/mcp
```

Local development:

```
http://localhost:3005/api/mcp
```

## Cursor configuration

```json
{
"mcpServers": {
"dashboard-icons": {
"url": "https://dashboardicons.com/api/mcp"
}
}
}
```

### Stdio-only clients

Use [mcp-remote](https://www.npmjs.com/package/mcp-remote):

```json
{
"mcpServers": {
"dashboard-icons": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://dashboardicons.com/api/mcp"]
}
}
}
```

## Tools

### `search_icons`

Search icons by name, alias, or category.

| Parameter | Type | Default | Max |
|-----------|------|---------|-----|
| `query` | string | required | 100 chars |
| `limit` | number | 20 | 50 |
| `category` | string | optional | 50 chars |

### `get_icon`

Full metadata and CDN URLs for one icon.

| Parameter | Type |
|-----------|------|
| `name` | kebab-case slug |

### `get_icon_url`

Direct CDN URL for one icon.

| Parameter | Type | Default |
|-----------|------|---------|
| `name` | string | required |
| `format` | `svg` \| `png` \| `webp` | `svg` |
| `theme` | `default` \| `light` \| `dark` | `default` |

### `suggest_icon`

Fuzzy match from a natural service name (e.g. `"Plex media server"` → `plex`).

| Parameter | Type | Default | Max |
|-----------|------|---------|-----|
| `service_name` | string | required | 100 chars |
| `limit` | number | 5 | 20 |

## Rate limits

| Scope | Limit |
|-------|-------|
| All MCP requests | 60 per minute per IP |
| Tool calls (`tools/call`) | 30 per minute per IP |

When exceeded, the server returns HTTP `429` with a `Retry-After` header.

## Environment variables

| Variable | Description |
|----------|-------------|
| `MCP_RATE_LIMIT_ENABLED` | Set to `false` to disable rate limiting (local dev only) |
| `MCP_VERBOSE_LOGS` | Set to `true` for verbose MCP handler logs |
| `MCP_WARM_CACHE` | Set to `true` to preload metadata on server start |
| `DASHBOARD_ICONS_METADATA_PATH` | Local `metadata.json` path (development only; blocked in production) |
| `NEXT_PUBLIC_POSTHOG_KEY` | Enables PostHog MCP analytics using the existing project key |
| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog ingestion host (defaults to the EU cloud endpoint) |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| `NEXT_PUBLIC_DISABLE_POSTHOG` | Set to `true` to disable all PostHog capture |

When enabled, the official `@posthog/mcp` SDK captures MCP lifecycle events, tool calls, latency, responses, and errors. Analytics are flushed after each serverless invocation.

## Scope

v1 covers **native icons** from `metadata.json` only. External sources (selfh.st, LobeHub) are not included.
36 changes: 35 additions & 1 deletion web/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import path from "node:path"
import { fileURLToPath } from "node:url"
import type { NextConfig } from "next";
import { withPostHogConfig } from "@posthog/nextjs-config";

const projectRoot = path.dirname(fileURLToPath(import.meta.url))

const securityHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
Expand All @@ -10,15 +14,25 @@ const securityHeaders = [
];

const nextConfig: NextConfig = {
turbopack: {
root: projectRoot,
},
cacheComponents: false,
images: {
unoptimized: true,
remotePatterns: [
{
protocol: "https",
hostname: "cdn.simpleicons.org",
port: "",
pathname: "/**",
search: "",
},
{
protocol: "https",
hostname: "cdn.jsdelivr.net",
port: "",
pathname: "/gh/selfhst/icons/**",
pathname: "/gh/homarr-labs/dashboard-icons/**",
search: "",
},
{
Expand All @@ -35,6 +49,18 @@ const nextConfig: NextConfig = {
pathname: "/lobehub/lobe-icons/**",
search: "",
},
{
protocol: "http",
hostname: "127.0.0.1",
port: "8090",
pathname: "/api/files/**",
},
{
protocol: "http",
hostname: "localhost",
port: "8090",
pathname: "/api/files/**",
},
],
},
output: "standalone",
Expand All @@ -59,6 +85,14 @@ const nextConfig: NextConfig = {
source: "/(.*)",
headers: securityHeaders,
},
{
source: "/api/mcp/:path*",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Cache-Control", value: "no-store" },
{ key: "X-Frame-Options", value: "DENY" },
],
},
{
source: "/:path*.png",
headers: [
Expand Down
40 changes: 30 additions & 10 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@opentelemetry/api-logs": "^0.216.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.216.0",
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-logs": "^0.216.0",
"@posthog/nextjs-config": "^1.9.16",
"@modelcontextprotocol/server": "^2.0.0",
"@opentelemetry/api-logs": "^0.221.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.221.0",
"@opentelemetry/resources": "^2.10.0",
"@opentelemetry/sdk-logs": "^0.221.0",
"@posthog/mcp": "0.10.1",
"@posthog/nextjs-config": "^1.9.68",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
Expand Down Expand Up @@ -71,11 +73,12 @@
"framer-motion": "^12.38.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.553.0",
"mcp-handler": "^2.0.1",
"motion": "^12.38.0",
"next": "16.2.3",
"next": "16.2.12",
"next-themes": "^0.4.6",
"pocketbase": "^0.26.8",
"posthog-js": "^1.372.6",
"posthog-js": "^1.408.2",
"posthog-node": "^5.32.1",
"radix-ui": "^1.4.3",
"react": "^19.2.5",
Expand All @@ -85,6 +88,7 @@
"react-hook-form": "^7.74.0",
"react-resizable-panels": "^3.0.6",
"recharts": "^3.8.1",
"server-only": "^0.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss-motion": "^1.1.1",
Expand All @@ -101,16 +105,32 @@
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitest/coverage-v8": "^4.1.10",
"baseline-browser-mapping": "^2.10.25",
"dotenv": "^17.4.2",
"jsdom": "^29.1.1",
"jsdom": "^30.0.1",
"tailwindcss": "^4.2.4",
"tsx": "^4.21.0",
"tsx": "^4.23.1",
"typescript": "^5.9.3",
"vitest": "^4.1.5"
"vite": "^8.1.5",
"vitest": "^4.1.10"
},
"packageManager": "pnpm@10.18.2",
"pnpm": {
"overrides": {
"@opentelemetry/core": "^2.10.0",
"axios": "^1.19.0",
"brace-expansion": "^5.0.9",
"dompurify": "^3.4.12",
"esbuild": "^0.28.1",
"fast-uri": "^3.1.4",
"form-data": "^4.0.6",
"postcss": "^8.5.25",
"protobufjs": "^8.7.1",
"sharp": "^0.35.3",
"undici": "^8.9.0",
"vite": "8.1.5"
},
"onlyBuiltDependencies": [
"@biomejs/biome",
"@posthog/cli",
Expand Down
Loading