Search & MCP Architecture
This document covers how pattern search works across three surfaces: the site topbar, the chatbot widget, and the MCP endpoint used by AI agents.
Search index
All three surfaces consume the same pre-built file:
public/search-index.json
Generated at build time by:
npm run build:search
# → scripts/build-search-index.mjs
Each entry has the shape:
{
id: string;
title: string;
description: string;
tags: string[];
type: "pattern" | "flows" | "components";
slug: string; // e.g. /ux-patterns/drag-and-drop
searchText: string; // concatenated title + description + tags
coverImage?: string;
}
The index is fetched from https://patttterns.com/search-index.json at runtime by the MCP edge function, and from /search-index.json by the client-side search.
Normalization — the shared rule
All search surfaces apply the same normalization before comparing strings so that user input like drag drop, drag & drop, and drag and drop all match the same patterns.
Algorithm (4 steps):
1. lowercase
2. replace & → " and "
3. replace all punctuation / non-word chars → " "
4. collapse multiple spaces → single space, trim
Then the query is token-split: every whitespace-separated token must appear in the target field (AND logic). A single-word query behaves identically to the old includes() approach.
Implementations
| Location | Function | Language |
|---|---|---|
netlify/edge-functions/mcp.ts | normalize(s) | TypeScript (Deno) |
src/lib/search.ts | normalizeQuery(s) | TypeScript (Node/browser) |
⚠️ These two functions must stay identical. They operate on the same index, so drift between them produces inconsistent results across surfaces. When you change one, grep for normalize in both files and update the other.
Surface 1 — Site topbar search
- Entry point:
src/lib/search.ts→searchPatterns(query, options) - Index source:
/search-index.json(fetched client-side, cached in memory) - Relevance scoring: weighted — exact title match (100 pts) › title prefix (50) › contains (25) › description (10) › tag match (30/15)
- Token-split: yes (as of April 2026)
- Normalization:
normalizeQuery()insrc/lib/search.ts
Surface 2 — Chatbot widget (MCP mode)
The chatbot widget (src/chatbot/chatbot.js / public/chatbot.js) sends all queries to:
POST https://patttterns.com/.netlify/functions/chatbot-proxy
Body: { "query": "..." }
The proxy (netlify/functions/chatbot-proxy.mts) calls the MCP edge function internally via fetchPatterns(), then optionally passes results to Gemini for an AI answer. The widget receives an SSE stream:
data: {"type":"patterns","patterns":[...]}
data: {"type":"delta","text":"..."} ← streamed Gemini answer
data: {"type":"done"}
If ENABLE_AI_CHAT is not "true" or GEMINI_API_KEY is absent, the proxy falls back to MCP-only mode (patterns cards, no AI text).
Surface 3 — MCP endpoint (AI agents / external tools)
POST https://patttterns.com/mcp
Implements MCP Streamable HTTP (spec 2025-03-26). Single-response mode only (no SSE).
Tools
| Tool | Description |
|---|---|
search_patterns | Full-text search. Args: query (required), limit (default 10, max 50). Returns pattern_id, catalog_number, tags, hasGeneratedComponent. |
list_categories | All categories with URL and pattern count |
get_pattern | Single pattern by slug (e.g. /ux-patterns/drag-and-drop) |
search_components | Find patterns with fetchable TSX; query/category; includes tags + catalog identity |
get_component | Hybrid payload: metadata (incl. tags), usage_rules, component_code, resource. pattern_id accepts UUID, #123, or slug. Default version = active/last generation. |
PRD: PATTTTERNS MCP Server PRD.
Example request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_component",
"arguments": { "pattern_id": "#413" }
}
}
Search logic
normalize(query)applied to the query- Token-split: all tokens must match at least one of
searchText,title,description, or (for component search)tags - No relevance scoring — order follows index order (Notion source order);
search_componentsprefers hits with code - Results capped at
limit(hard max 50) - Component TSX is fetched from the Components API (
op=active/op=code); MCP never mutates the artifact
Usage visibility
Each MCP initialize / tools/call records a structured [mcp-usage] edge log (Netlify → Edge Functions logs) and increments a monthly Blobs counter via mcp-usage-log:
| Field | Where | Notes |
|---|---|---|
tool, status, duration_ms | logs + Blobs | e.g. get_component / ok / version_not_found |
pattern_id, catalog_number, version_id | logs + Blobs | component fetches |
component_bytes | logs + Blobs | size only — never TSX body |
ip | Netlify logs only | short retention; ops anomaly review |
ip_hash | Blobs monthly | stable anonymized client key |
agent_hint, user_agent | logs + Blobs | soft identity (Cursor/Claude/…); spoofable |
country, request_id | logs (+ country in persist) | Netlify context.geo / requestId |
Blob store: patttterns-mcp-usage → key monthly/YYYY-MM.json (browse in Netlify Blobs UI).
v1 is visibility only — no rate limits.
Chatbot build pipeline
Source files live in src/chatbot/:
| File | Purpose |
|---|---|
src/chatbot/chatbot.js | Widget logic (source of truth) |
src/chatbot/chatbot.css | Widget styles (source of truth) |
public/chatbot.js— served by Next.js / Netlify devdocs/assets/js/chatbot.js— consumed by Jekyll / GH Pages docs site
Never edit the output files directly. Edit src/chatbot/ and rebuild.
Environment variables
| Variable | Required | Effect |
|---|---|---|
GEMINI_API_KEY | No | Enables AI answers in the proxy. Without it, proxy returns MCP results only. |
ENABLE_AI_CHAT | No | Must be "true" to activate AI path. Any other value → MCP-only. |
Both must be set in Netlify environment variables for full AI mode. The chatbot widget on docs.patttterns.com requires no environment variables — it calls the production proxy directly.