Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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