From f8dfa90fba990d6be48cd2ef39722ab98841aff5 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 25 Feb 2026 16:02:43 -0700 Subject: [PATCH] Add live product catalog to docs site Espressif product data fetched at build time via Content Layer API and rendered as browsable Starlight pages. Alpine.js provides client-side filtering by series, type, connectivity, and status. - Products content collection with inline API loader + Zod schema - Filterable browse page at /products/ with card grid - Individual spec sheet pages at /products/[id]/ (94 products) - Separate CSS with dark/light theme support - Sidebar section linking to catalog - Cross-references to MCP tool reference docs --- docs-site/astro.config.mjs | 14 +- docs-site/package-lock.json | 43 ++ docs-site/package.json | 2 + docs-site/src/content.config.ts | 135 +++++- .../src/content/docs/reference/index.mdx | 10 + .../docs/reference/product-catalog.mdx | 326 +++++++++++++ .../src/content/docs/reference/resources.mdx | 64 ++- docs-site/src/pages/products/[id].astro | 238 ++++++++++ docs-site/src/pages/products/index.astro | 246 ++++++++++ docs-site/src/styles/products.css | 438 ++++++++++++++++++ 10 files changed, 1513 insertions(+), 3 deletions(-) create mode 100644 docs-site/src/content/docs/reference/product-catalog.mdx create mode 100644 docs-site/src/pages/products/[id].astro create mode 100644 docs-site/src/pages/products/index.astro create mode 100644 docs-site/src/styles/products.css diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index fa58cb2..a2c7e45 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -1,5 +1,6 @@ import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; +import alpinejs from "@astrojs/alpinejs"; import tailwindcss from "@tailwindcss/vite"; import sitemap from "@astrojs/sitemap"; @@ -8,6 +9,7 @@ export default defineConfig({ telemetry: false, devToolbar: { enabled: false }, integrations: [ + alpinejs(), starlight({ title: "mcesptool", description: @@ -20,7 +22,7 @@ export default defineConfig({ href: "https://git.supported.systems/MCP/mcesptool", }, ], - customCss: ["./src/styles/global.css"], + customCss: ["./src/styles/global.css", "./src/styles/products.css"], sidebar: [ { label: "Tutorials", @@ -82,12 +84,22 @@ export default defineConfig({ label: "Server Tools", slug: "reference/server-tools", }, + { + label: "Product Catalog", + slug: "reference/product-catalog", + }, ], }, { label: "Resources", slug: "reference/resources" }, { label: "Configuration", slug: "reference/configuration" }, ], }, + { + label: "Product Catalog", + items: [ + { label: "Browse Products", link: "/products/" }, + ], + }, { label: "Explanation", autogenerate: { directory: "explanation" }, diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json index 7da90c2..35e991a 100644 --- a/docs-site/package-lock.json +++ b/docs-site/package-lock.json @@ -8,14 +8,26 @@ "name": "mcesptool-docs", "version": "0.0.1", "dependencies": { + "@astrojs/alpinejs": "^0.4.9", "@astrojs/sitemap": "^3.3.1", "@astrojs/starlight": "^0.37.6", "@tailwindcss/vite": "^4.1.3", + "alpinejs": "^3.15.8", "astro": "^5.7.10", "sharp": "^0.33.0", "tailwindcss": "^4.1.3" } }, + "node_modules/@astrojs/alpinejs": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@astrojs/alpinejs/-/alpinejs-0.4.9.tgz", + "integrity": "sha512-fvKBAugn7yIngEKfdk6vL3ZlcVKtQvFXCZznG28OikGanKN5W+PkRPIdKaW/0gThRU2FyCemgzyHgyFjsH8dTA==", + "license": "MIT", + "peerDependencies": { + "@types/alpinejs": "^3.0.0", + "alpinejs": "^3.0.0" + } + }, "node_modules/@astrojs/compiler": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz", @@ -2026,6 +2038,13 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@types/alpinejs": { + "version": "3.13.11", + "resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.11.tgz", + "integrity": "sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2125,6 +2144,21 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2146,6 +2180,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/alpinejs": { + "version": "3.15.8", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.8.tgz", + "integrity": "sha512-zxIfCRTBGvF1CCLIOMQOxAyBuqibxSEwS6Jm1a3HGA9rgrJVcjEWlwLcQTVGAWGS8YhAsTRLVrtQ5a5QT9bSSQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", diff --git a/docs-site/package.json b/docs-site/package.json index 132cb4a..c82e5a2 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -9,9 +9,11 @@ "astro": "astro" }, "dependencies": { + "@astrojs/alpinejs": "^0.4.9", "@astrojs/sitemap": "^3.3.1", "@astrojs/starlight": "^0.37.6", "@tailwindcss/vite": "^4.1.3", + "alpinejs": "^3.15.8", "astro": "^5.7.10", "sharp": "^0.33.0", "tailwindcss": "^4.1.3" diff --git a/docs-site/src/content.config.ts b/docs-site/src/content.config.ts index 7fbcf2c..b72a77b 100644 --- a/docs-site/src/content.config.ts +++ b/docs-site/src/content.config.ts @@ -1,7 +1,140 @@ -import { defineCollection } from "astro:content"; +import { defineCollection, z } from "astro:content"; import { docsLoader } from "@astrojs/starlight/loaders"; import { docsSchema } from "@astrojs/starlight/schema"; +function parseMemoryField(value: string | number | null | undefined): [number, string] { + if (typeof value === "number") return [value, ""]; + if (!value || ["N/A", "NA", "-", "\u2014"].includes(value)) return [0, ""]; + const parts = value.split(",").map((s) => s.trim()); + const size = parseInt(parts[0], 10); + return [isNaN(size) ? 0 : size, parts[1] || ""]; +} + +function parseSramKb(value: string | number | null | undefined): number { + if (typeof value === "number") return value; + if (!value || ["N/A", "NA", "-", "\u2014"].includes(value)) return 0; + const n = parseInt(value.split(",")[0].trim(), 10); + return isNaN(n) ? 0 : n; +} + +function hasCapability(value: string | null | undefined): boolean { + if (!value) return false; + const v = value.trim().toLowerCase(); + return !["", "n/a", "na", "-", "\u2014", "no", "0"].includes(v); +} + +function normalizeStatus(value: string): string { + const mapping: Record = { + "mass production": "Mass Production", + nrnd: "NRND", + eol: "EOL", + replaced: "Replaced", + sample: "Sample", + }; + return mapping[value.trim().toLowerCase()] || value.trim(); +} + +const products = defineCollection({ + loader: async () => { + const resp = await fetch( + "https://products.espressif.com/api/user/products?language=en", + ); + if (!resp.ok) { + throw new Error(`Espressif API returned ${resp.status}`); + } + const data = await resp.json(); + const results: any[] = data.results || []; + + return results.map((p: any) => { + const [flashMb, flashType] = parseMemoryField(p.flash); + const [psramMb, psramType] = parseMemoryField(p.psram); + + const id = (p.name || "unknown") + .toLowerCase() + .replace(/[^a-z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); + + return { + id, + name: p.name || "", + name_new: p.nameNew || "", + type: p.type || "", + series: p.seriesName || "", + status: normalizeStatus(p.status || ""), + mpn: p.mpn || "", + dimensions: p.dimensions || "", + wifi: p.wifi || "", + wifi6: p.wifi6 || "", + bluetooth: p.bluetooth || "", + thread_zigbee: p.threadZigbee || "", + pins: String(p.pins || ""), + frequency_mhz: String(p.freq || ""), + sram_kb: parseSramKb(p.sram), + rom_kb: String(p.rom || ""), + flash_mb: flashMb, + flash_type: flashType, + psram_mb: psramMb, + psram_type: psramType, + gpio: parseInt(String(p.gpio || 0), 10) || 0, + operating_temp: p.operatingTemp || "", + voltage_range: p.voltageRange || "", + antenna: p.antenna || "", + size_type: p.sizeType || "", + release_time: p.releaseTime || "", + idf_supports: p.idfSupports || "", + spq: String(p.spq || ""), + moq: String(p.moq || ""), + lead_time: p.leadTime || "", + eccn_code: p.eccnCode || "", + hs_code: p.hsCode || "", + replaced_by: p.replacedByName || "", + has_wifi: hasCapability(p.wifi), + has_bluetooth: hasCapability(p.bluetooth), + has_thread_zigbee: hasCapability(p.threadZigbee), + }; + }); + }, + schema: z.object({ + name: z.string(), + name_new: z.string(), + type: z.string(), + series: z.string(), + status: z.string(), + mpn: z.string(), + dimensions: z.string(), + wifi: z.string(), + wifi6: z.string(), + bluetooth: z.string(), + thread_zigbee: z.string(), + pins: z.string(), + frequency_mhz: z.string(), + sram_kb: z.number(), + rom_kb: z.string(), + flash_mb: z.number(), + flash_type: z.string(), + psram_mb: z.number(), + psram_type: z.string(), + gpio: z.number(), + operating_temp: z.string(), + voltage_range: z.string(), + antenna: z.string(), + size_type: z.string(), + release_time: z.string(), + idf_supports: z.string(), + spq: z.string(), + moq: z.string(), + lead_time: z.string(), + eccn_code: z.string(), + hs_code: z.string(), + replaced_by: z.string(), + has_wifi: z.boolean(), + has_bluetooth: z.boolean(), + has_thread_zigbee: z.boolean(), + }), +}); + export const collections = { docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), + products, }; diff --git a/docs-site/src/content/docs/reference/index.mdx b/docs-site/src/content/docs/reference/index.mdx index c95830f..bee2537 100644 --- a/docs-site/src/content/docs/reference/index.mdx +++ b/docs-site/src/content/docs/reference/index.mdx @@ -103,6 +103,16 @@ All tools follow a consistent pattern: they return a JSON object with a `success | `esp_list_tools` | List all tools by category | | `esp_health_check` | Environment health check | +### Product Catalog (5 tools) + +| Tool | Description | +|------|-------------| +| `esp_product_search` | Search products by series, type, connectivity, memory, GPIO, temp, status | +| `esp_product_info` | Full spec sheet for a product (fuzzy-matched by name or MPN) | +| `esp_chip_compare` | Side-by-side comparison of 2-4 products | +| `esp_product_recommend` | Recommendations based on use case description | +| `esp_product_availability` | Filter by production status, lead time, and MOQ | + ## Additional References diff --git a/docs-site/src/content/docs/reference/product-catalog.mdx b/docs-site/src/content/docs/reference/product-catalog.mdx new file mode 100644 index 0000000..4ba2521 --- /dev/null +++ b/docs-site/src/content/docs/reference/product-catalog.mdx @@ -0,0 +1,326 @@ +--- +title: Product Catalog +description: Chip and module discovery, comparison, and procurement planning +--- + +import { Aside } from '@astrojs/starlight/components'; + +The Product Catalog component provides 5 tools for discovering Espressif chips and modules, comparing specifications, getting recommendations, and checking availability. Data is fetched from [Espressif's public product API](https://products.espressif.com/) and cached in-memory. + + + + + +--- + +## esp_product_search + +Search the Espressif product catalog by specs, capabilities, or keyword. All filters are optional and combine with AND logic. Returns matching products with key specs. + +### Parameters + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `series` | `str \| None` | `None` | Chip family filter, e.g. `"ESP32-S3"`, `"ESP32-C6"`. | +| `product_type` | `str \| None` | `None` | `"SoC"` or `"Module"`. | +| `wifi` | `bool \| None` | `None` | Filter to products with (or without) Wi-Fi. | +| `bluetooth` | `bool \| None` | `None` | Filter to products with (or without) Bluetooth. | +| `thread_zigbee` | `bool \| None` | `None` | Filter to products with (or without) Thread/Zigbee (802.15.4). | +| `min_flash_mb` | `int \| None` | `None` | Minimum flash size in MB. | +| `min_psram_mb` | `int \| None` | `None` | Minimum PSRAM size in MB. | +| `min_sram_kb` | `int \| None` | `None` | Minimum SRAM size in KB. | +| `min_gpio` | `int \| None` | `None` | Minimum GPIO pin count. | +| `max_temp_c` | `int \| None` | `None` | Required upper operating temperature (e.g. `105` for industrial). | +| `status` | `str \| None` | `None` | Production status: `"Mass Production"`, `"NRND"`, `"EOL"`, `"Sample"`. | +| `antenna` | `str \| None` | `None` | Antenna type: `"PCB"`, `"IPEX"`, etc. | +| `keyword` | `str \| None` | `None` | Fuzzy search on product name or MPN. | + +### Example + +```python +result = await client.call_tool("esp_product_search", { + "series": "ESP32-S3", + "product_type": "Module", + "wifi": True, + "min_flash_mb": 8, + "status": "Mass Production" +}) +``` + +### Return Value + +```json +{ + "count": 5, + "filters_applied": { + "series": "ESP32-S3", + "product_type": "Module", + "wifi": true, + "min_flash_mb": 8, + "status": "Mass Production" + }, + "products": [ + { + "name": "ESP32-S3-WROOM-1-N8", + "type": "Module", + "series": "ESP32-S3", + "status": "Mass Production", + "wifi": true, + "bluetooth": true, + "thread_zigbee": false, + "flash_mb": 8, + "psram_mb": 0, + "sram_kb": 512, + "gpio": 36, + "antenna": "PCB" + } + ] +} +``` + +The `filters_applied` object echoes back only the filters that were actually provided, making it easy to confirm what was searched. + +--- + +## esp_product_info + +Get the full specification sheet for a specific Espressif product. The product is looked up by name or MPN using fuzzy matching. + +### Parameters + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `name` | `str` | -- | Product name or MPN, e.g. `"ESP32-S3-WROOM-1"`. **Required**. Fuzzy-matched. | + +### Example + +```python +result = await client.call_tool("esp_product_info", { + "name": "ESP32-S3-WROOM-1" +}) +``` + +### Return Value + +```json +{ + "name": "ESP32-S3-WROOM-1-N16R8", + "name_new": "", + "type": "Module", + "series": "ESP32-S3", + "status": "Mass Production", + "mpn": "ESP32-S3-WROOM-1-N16R8", + "dimensions": "18x25.5x3.1", + "wifi": "802.11 b/g/n", + "wifi6": "", + "bluetooth": "BLE 5.0", + "thread_zigbee": "", + "pins": "38", + "frequency_mhz": "240", + "sram_kb": 512, + "rom_kb": "384", + "flash_mb": 16, + "flash_type": "Quad", + "psram_mb": 8, + "psram_type": "Octal", + "gpio": 36, + "operating_temp": "-40~85C", + "voltage_range": "3.0~3.6V", + "antenna": "PCB", + "size_type": "18x25.5", + "release_time": "2021-12", + "idf_supports": ">=v4.4", + "spq": "1400", + "moq": "1400", + "lead_time": "8-10 weeks", + "eccn_code": "5A992.c", + "ccatsz_code": "", + "hs_code": "8542390000", + "pre_firmware": "", + "replaced_by": "" +} +``` + +If no product matches, the response includes an `error` field and a `suggestion` to try `esp_product_search` with a keyword. + +--- + +## esp_chip_compare + +Compare 2-4 Espressif products side-by-side. The response highlights fields that differ between the products and groups fields that are identical, making it easy to spot the key differences between chip families or module variants. + +### Parameters + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `products` | `list[str]` | -- | List of 2-4 product names to compare. **Required**. Each name is fuzzy-matched. | + +### Example + +```python +result = await client.call_tool("esp_chip_compare", { + "products": ["ESP32-S3-WROOM-1-N16R8", "ESP32-C6-WROOM-1-N8"] +}) +``` + +### Return Value + +```json +{ + "products_compared": ["ESP32-S3-WROOM-1-N16R8", "ESP32-C6-WROOM-1-N8"], + "differences": { + "series": { + "ESP32-S3-WROOM-1-N16R8": "ESP32-S3", + "ESP32-C6-WROOM-1-N8": "ESP32-C6" + }, + "flash_mb": { + "ESP32-S3-WROOM-1-N16R8": 16, + "ESP32-C6-WROOM-1-N8": 8 + }, + "thread_zigbee": { + "ESP32-S3-WROOM-1-N16R8": "", + "ESP32-C6-WROOM-1-N8": "802.15.4" + } + }, + "common": { + "type": "Module", + "status": "Mass Production", + "antenna": "PCB" + }, + "full_specs": { } +} +``` + +The `differences` object maps each differing field to a dict of `{product_name: value}`. The `common` object contains fields where all compared products have the same value. `full_specs` includes the complete detail record for each product. + +--- + +## esp_product_recommend + +Get chip or module recommendations for a use case. Describe what you're building and any constraints, and this tool filters the catalog to matching products ranked by fit. + +### Parameters + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `use_case` | `str` | -- | What you're building, e.g. `"battery-powered BLE sensor"`. **Required**. | +| `constraints` | `str \| None` | `None` | Technical constraints, e.g. `"needs PSRAM, temp to 105C"`. | +| `prefer_module` | `bool` | `True` | Prefer modules over bare SoCs. Falls back to SoCs if no modules match. | + +### Example + +```python +result = await client.call_tool("esp_product_recommend", { + "use_case": "battery-powered BLE sensor with 8MB flash", + "constraints": "needs PSRAM, industrial temperature" +}) +``` + +### Return Value + +```json +{ + "use_case": "battery-powered BLE sensor with 8MB flash", + "constraints_parsed": { + "wifi": false, + "bluetooth": true, + "thread_zigbee": false, + "psram_required": true, + "industrial_temp": true, + "min_flash_mb": 8, + "min_psram_mb": null, + "prefer_module": true + }, + "count": 3, + "recommendations": [ + { + "name": "ESP32-S3-WROOM-1-N8R8", + "type": "Module", + "series": "ESP32-S3", + "status": "Mass Production", + "wifi": true, + "bluetooth": true, + "thread_zigbee": false, + "flash_mb": 8, + "psram_mb": 8, + "sram_kb": 512, + "gpio": 36, + "antenna": "PCB" + } + ] +} +``` + +The `constraints_parsed` object shows how the natural-language inputs were interpreted into filter criteria. Up to 15 recommendations are returned. + + + +--- + +## esp_product_availability + +Check product availability and procurement details. Filter by production status, lead time, and minimum order quantity to find products you can actually source. + +### Parameters + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `series` | `str \| None` | `None` | Chip family filter, e.g. `"ESP32-S3"`. | +| `status` | `str` | `"Mass Production"` | Production status filter. | +| `max_lead_weeks` | `int \| None` | `None` | Maximum acceptable lead time in weeks. | +| `max_moq` | `int \| None` | `None` | Maximum acceptable minimum order quantity. | + +### Example + +```python +result = await client.call_tool("esp_product_availability", { + "series": "ESP32-C3", + "max_lead_weeks": 12, + "max_moq": 500 +}) +``` + +### Return Value + +```json +{ + "count": 4, + "filters": { + "series": "ESP32-C3", + "status": "Mass Production", + "max_lead_weeks": 12, + "max_moq": 500 + }, + "products": [ + { + "name": "ESP32-C3-WROOM-02-N4", + "type": "Module", + "series": "ESP32-C3", + "status": "Mass Production", + "wifi": true, + "bluetooth": true, + "thread_zigbee": false, + "flash_mb": 4, + "psram_mb": 0, + "sram_kb": 400, + "gpio": 22, + "antenna": "PCB", + "lead_time": "8-10 weeks", + "moq": "200", + "spq": "200", + "mpn": "ESP32-C3-WROOM-02-N4", + "replaced_by": "" + } + ] +} +``` + + diff --git a/docs-site/src/content/docs/reference/resources.mdx b/docs-site/src/content/docs/reference/resources.mdx index c5991f4..e35845e 100644 --- a/docs-site/src/content/docs/reference/resources.mdx +++ b/docs-site/src/content/docs/reference/resources.mdx @@ -5,7 +5,7 @@ description: Real-time MCP resources exposed by mcesptool import { Aside } from '@astrojs/starlight/components'; -mcesptool exposes 3 MCP resources that provide real-time server state without requiring tool invocations. Resources are read-only and can be accessed by any MCP client using the `read_resource` protocol method. +mcesptool exposes 4 MCP resources that provide real-time server state without requiring tool invocations. Resources are read-only and can be accessed by any MCP client using the `read_resource` protocol method.