diff --git a/README.md b/README.md index f1332e6..3cc3da6 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ pnpm start python -m venv .venv source .venv/bin/activate pip install -r pizzaz_server_python/requirements.txt -uvicorn pizzaz_server_python.main:app --port 8000 +uv run uvicorn pizzaz_server_python.main:app --port 8000 ``` ### Solar system Python server @@ -101,10 +101,10 @@ uvicorn pizzaz_server_python.main:app --port 8000 python -m venv .venv source .venv/bin/activate pip install -r solar-system_server_python/requirements.txt -uvicorn solar-system_server_python.main:app --port 8000 +uv run uvicorn solar-system_server_python.main:app --port 8000 ``` -You can reuse the same virtual environment for all Python servers—install the dependencies once and run whichever entry point you need. +**Important**: Python servers must be run from the project root directory using the module path format (e.g., `pizzaz_server_python.main:app`), not from within their own directories. You can reuse the same virtual environment for all Python servers—install the dependencies once and run whichever entry point you need. ## Testing in ChatGPT diff --git a/build-all.mts b/build-all.mts index abd9832..25bbf02 100644 --- a/build-all.mts +++ b/build-all.mts @@ -3,8 +3,7 @@ import react from "@vitejs/plugin-react"; import fg from "fast-glob"; import path from "path"; import fs from "fs"; -import crypto from "crypto"; -import pkg from "./package.json" with { type: "json" }; +import { getVersionHash } from "./src/version.js"; import tailwindcss from "@tailwindcss/vite"; const entries = fg.sync("src/**/index.{tsx,jsx}"); @@ -145,11 +144,7 @@ const outputs = fs const renamed = []; -const h = crypto - .createHash("sha256") - .update(pkg.version, "utf8") - .digest("hex") - .slice(0, 4); +const h = getVersionHash(); console.group("Hashing outputs"); for (const out of outputs) { diff --git a/pizzaz_server_node/src/server.ts b/pizzaz_server_node/src/server.ts index cdef68f..46a15a4 100644 --- a/pizzaz_server_node/src/server.ts +++ b/pizzaz_server_node/src/server.ts @@ -19,6 +19,7 @@ import { type Tool } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; +import { generateWidgetHtml } from "./version.js"; type PizzazWidget = { id: string; @@ -47,11 +48,7 @@ const widgets: PizzazWidget[] = [ templateUri: "ui://widget/pizza-map.html", invoking: "Hand-tossing a map", invoked: "Served a fresh map", - html: ` -
- - - `.trim(), + html: generateWidgetHtml("pizzaz"), responseText: "Rendered a pizza map!" }, { @@ -60,11 +57,7 @@ const widgets: PizzazWidget[] = [ templateUri: "ui://widget/pizza-carousel.html", invoking: "Carousel some spots", invoked: "Served a fresh carousel", - html: ` - - - - `.trim(), + html: generateWidgetHtml("pizzaz-carousel"), responseText: "Rendered a pizza carousel!" }, { @@ -73,11 +66,7 @@ const widgets: PizzazWidget[] = [ templateUri: "ui://widget/pizza-albums.html", invoking: "Hand-tossing an album", invoked: "Served a fresh album", - html: ` -
- - - `.trim(), + html: generateWidgetHtml("pizzaz-albums"), responseText: "Rendered a pizza album!" }, { @@ -86,11 +75,7 @@ const widgets: PizzazWidget[] = [ templateUri: "ui://widget/pizza-list.html", invoking: "Hand-tossing a list", invoked: "Served a fresh list", - html: ` -
- - - `.trim(), + html: generateWidgetHtml("pizzaz-list"), responseText: "Rendered a pizza list!" }, { @@ -99,11 +84,7 @@ const widgets: PizzazWidget[] = [ templateUri: "ui://widget/pizza-video.html", invoking: "Hand-tossing a video", invoked: "Served a fresh video", - html: ` -
- - - `.trim(), + html: generateWidgetHtml("pizzaz-video"), responseText: "Rendered a pizza video!" } ]; diff --git a/pizzaz_server_node/src/version.ts b/pizzaz_server_node/src/version.ts new file mode 100644 index 0000000..3430efb --- /dev/null +++ b/pizzaz_server_node/src/version.ts @@ -0,0 +1,80 @@ +import fs from "fs"; +import path from "path"; +import crypto from "crypto"; + +/** + * Centralized version management for the Apps SDK examples + * This module provides a single source of truth for version hashes + * and asset URLs to avoid hardcoded version references across the codebase. + */ + +// Read package.json version +const packageJsonPath = path.resolve("../package.json"); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); +const version = packageJson.version; + +// Generate version hash (same logic as build-all.mts) +export function getVersionHash(): string { + return crypto + .createHash("sha256") + .update(version, "utf8") + .digest("hex") + .slice(0, 4); +} + +// Asset URL configuration +export interface AssetConfig { + /** Base URL for built assets */ + baseUrl: string; + + /** Whether to use local development mode */ + isDevelopment?: boolean; +} + +// Default configuration +export const DEFAULT_CONFIG: AssetConfig = { + baseUrl: "https://persistent.oaistatic.com/ecosystem-built-assets", + isDevelopment: false +}; + +/** + * Get asset URL for a specific widget + */ +export function getAssetUrl(widgetName: string, assetType: "css" | "js" | "html", config: AssetConfig = DEFAULT_CONFIG): string { + const versionHash = getVersionHash(); + const filename = `${widgetName}-${versionHash}.${assetType}`; + + if (config.isDevelopment) { + // For local development, use relative paths + return `/assets/${filename}`; + } + + // For production, use the configured base URL + return `${config.baseUrl}/${filename}`; +} + +/** + * Generate HTML markup for a widget + */ +export function generateWidgetHtml(widgetName: string, config: AssetConfig = DEFAULT_CONFIG): string { + const cssUrl = getAssetUrl(widgetName, "css", config); + const jsUrl = getAssetUrl(widgetName, "js", config); + const rootId = `${widgetName}-root`; + + return ` +
+ + + `.trim(); +} + +/** + * Get current version information + */ +export function getVersionInfo() { + return { + version, + hash: getVersionHash(), + timestamp: new Date().toISOString() + }; +} \ No newline at end of file diff --git a/pizzaz_server_python/main.py b/pizzaz_server_python/main.py index 4d8534b..0a89e4e 100644 --- a/pizzaz_server_python/main.py +++ b/pizzaz_server_python/main.py @@ -16,6 +16,14 @@ import mcp.types as types from mcp.server.fastmcp import FastMCP from pydantic import BaseModel, ConfigDict, Field, ValidationError +try: + from version import generate_widget_html +except ImportError: + # Fallback for when running from the pizzaz_server_python directory + import sys + import os + sys.path.insert(0, os.path.dirname(__file__)) + from version import generate_widget_html @dataclass(frozen=True) @@ -36,13 +44,7 @@ class PizzazWidget: template_uri="ui://widget/pizza-map.html", invoking="Hand-tossing a map", invoked="Served a fresh map", - html=( - "
\n" - "\n" - "" - ), + html=generate_widget_html("pizzaz"), response_text="Rendered a pizza map!", ), PizzazWidget( @@ -51,13 +53,7 @@ class PizzazWidget: template_uri="ui://widget/pizza-carousel.html", invoking="Carousel some spots", invoked="Served a fresh carousel", - html=( - "\n" - "\n" - "" - ), + html=generate_widget_html("pizzaz-carousel"), response_text="Rendered a pizza carousel!", ), PizzazWidget( @@ -66,13 +62,7 @@ class PizzazWidget: template_uri="ui://widget/pizza-albums.html", invoking="Hand-tossing an album", invoked="Served a fresh album", - html=( - "
\n" - "\n" - "" - ), + html=generate_widget_html("pizzaz-albums"), response_text="Rendered a pizza album!", ), PizzazWidget( @@ -81,13 +71,7 @@ class PizzazWidget: template_uri="ui://widget/pizza-list.html", invoking="Hand-tossing a list", invoked="Served a fresh list", - html=( - "
\n" - "\n" - "" - ), + html=generate_widget_html("pizzaz-list"), response_text="Rendered a pizza list!", ), PizzazWidget( @@ -96,13 +80,7 @@ class PizzazWidget: template_uri="ui://widget/pizza-video.html", invoking="Hand-tossing a video", invoked="Served a fresh video", - html=( - "
\n" - "\n" - "" - ), + html=generate_widget_html("pizzaz-video"), response_text="Rendered a pizza video!", ), ] diff --git a/pizzaz_server_python/version.py b/pizzaz_server_python/version.py new file mode 100644 index 0000000..634fc1d --- /dev/null +++ b/pizzaz_server_python/version.py @@ -0,0 +1,84 @@ +""" +Centralized version management for the Apps SDK examples +This module provides a single source of truth for version hashes +and asset URLs to avoid hardcoded version references across the codebase. +""" + +import hashlib +import json +import os +from pathlib import Path +from typing import Dict, Literal, Optional + + +# Read package.json version +package_json_path = Path(__file__).parent.parent / "package.json" +with open(package_json_path, "r") as f: + package_json = json.load(f) +version = package_json["version"] + + +# Generate version hash (same logic as build-all.mts) +def get_version_hash() -> str: + """Generate version hash from package.json version.""" + return hashlib.sha256(version.encode("utf-8")).hexdigest()[:4] + + +# Asset URL configuration +class AssetConfig: + """Configuration for asset URLs.""" + + def __init__( + self, + base_url: str = "https://persistent.oaistatic.com/ecosystem-built-assets", + is_development: bool = False, + ): + self.base_url = base_url + self.is_development = is_development + + +# Default configuration +DEFAULT_CONFIG = AssetConfig() + + +def get_asset_url( + widget_name: str, + asset_type: Literal["css", "js", "html"], + config: AssetConfig = DEFAULT_CONFIG, +) -> str: + """Get asset URL for a specific widget.""" + version_hash = get_version_hash() + filename = f"{widget_name}-{version_hash}.{asset_type}" + + if config.is_development: + # For local development, use relative paths + return f"/assets/{filename}" + + # For production, use the configured base URL + return f"{config.base_url}/{filename}" + + +def generate_widget_html( + widget_name: str, config: AssetConfig = DEFAULT_CONFIG +) -> str: + """Generate HTML markup for a widget.""" + css_url = get_asset_url(widget_name, "css", config) + js_url = get_asset_url(widget_name, "js", config) + root_id = f"{widget_name}-root" + + return f''' +
+ + + '''.strip() + + +def get_version_info() -> Dict[str, str]: + """Get current version information.""" + from datetime import datetime + + return { + "version": version, + "hash": get_version_hash(), + "timestamp": datetime.now().isoformat(), + } \ No newline at end of file diff --git a/solar-system_server_python/main.py b/solar-system_server_python/main.py index 42c06c6..2428b3a 100644 --- a/solar-system_server_python/main.py +++ b/solar-system_server_python/main.py @@ -8,6 +8,14 @@ import mcp.types as types from mcp.server.fastmcp import FastMCP from pydantic import BaseModel, ConfigDict, Field, ValidationError +try: + from version import generate_widget_html +except ImportError: + # Fallback for when running from the solar-system_server_python directory + import sys + import os + sys.path.insert(0, os.path.dirname(__file__)) + from version import generate_widget_html MIME_TYPE = "text/html+skybridge" PLANETS = [ @@ -62,13 +70,7 @@ class SolarWidget: template_uri="ui://widget/solar-system.html", invoking="Charting the solar system", invoked="Solar system ready", - html=( - "
\n" - "\n" - "" - ), + html=generate_widget_html("solar-system"), response_text="Solar system ready", ) diff --git a/solar-system_server_python/version.py b/solar-system_server_python/version.py new file mode 100644 index 0000000..634fc1d --- /dev/null +++ b/solar-system_server_python/version.py @@ -0,0 +1,84 @@ +""" +Centralized version management for the Apps SDK examples +This module provides a single source of truth for version hashes +and asset URLs to avoid hardcoded version references across the codebase. +""" + +import hashlib +import json +import os +from pathlib import Path +from typing import Dict, Literal, Optional + + +# Read package.json version +package_json_path = Path(__file__).parent.parent / "package.json" +with open(package_json_path, "r") as f: + package_json = json.load(f) +version = package_json["version"] + + +# Generate version hash (same logic as build-all.mts) +def get_version_hash() -> str: + """Generate version hash from package.json version.""" + return hashlib.sha256(version.encode("utf-8")).hexdigest()[:4] + + +# Asset URL configuration +class AssetConfig: + """Configuration for asset URLs.""" + + def __init__( + self, + base_url: str = "https://persistent.oaistatic.com/ecosystem-built-assets", + is_development: bool = False, + ): + self.base_url = base_url + self.is_development = is_development + + +# Default configuration +DEFAULT_CONFIG = AssetConfig() + + +def get_asset_url( + widget_name: str, + asset_type: Literal["css", "js", "html"], + config: AssetConfig = DEFAULT_CONFIG, +) -> str: + """Get asset URL for a specific widget.""" + version_hash = get_version_hash() + filename = f"{widget_name}-{version_hash}.{asset_type}" + + if config.is_development: + # For local development, use relative paths + return f"/assets/{filename}" + + # For production, use the configured base URL + return f"{config.base_url}/{filename}" + + +def generate_widget_html( + widget_name: str, config: AssetConfig = DEFAULT_CONFIG +) -> str: + """Generate HTML markup for a widget.""" + css_url = get_asset_url(widget_name, "css", config) + js_url = get_asset_url(widget_name, "js", config) + root_id = f"{widget_name}-root" + + return f''' +
+ + + '''.strip() + + +def get_version_info() -> Dict[str, str]: + """Get current version information.""" + from datetime import datetime + + return { + "version": version, + "hash": get_version_hash(), + "timestamp": datetime.now().isoformat(), + } \ No newline at end of file diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..24f57bc --- /dev/null +++ b/src/version.ts @@ -0,0 +1,95 @@ +import fs from "fs"; +import path from "path"; +import crypto from "crypto"; + +/** + * Centralized version management for the Apps SDK examples + * This module provides a single source of truth for version hashes + * and asset URLs to avoid hardcoded version references across the codebase. + */ + +// Read package.json version +const packageJsonPath = path.resolve("package.json"); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); +const version = packageJson.version; + +// Generate version hash (same logic as build-all.mts) +export function getVersionHash(): string { + return crypto + .createHash("sha256") + .update(version, "utf8") + .digest("hex") + .slice(0, 4); +} + +// Asset URL configuration +export interface AssetConfig { + /** Base URL for built assets */ + baseUrl: string; + + /** Whether to use local development mode */ + isDevelopment?: boolean; +} + +// Default configuration +export const DEFAULT_CONFIG: AssetConfig = { + baseUrl: "https://persistent.oaistatic.com/ecosystem-built-assets", + isDevelopment: false +}; + +/** + * Get asset URL for a specific widget + */ +export function getAssetUrl(widgetName: string, assetType: "css" | "js" | "html", config: AssetConfig = DEFAULT_CONFIG): string { + const versionHash = getVersionHash(); + const filename = `${widgetName}-${versionHash}.${assetType}`; + + if (config.isDevelopment) { + // For local development, use relative paths + return `/assets/${filename}`; + } + + // For production, use the configured base URL + return `${config.baseUrl}/${filename}`; +} + +/** + * Get all available widget names + */ +export function getWidgetNames(): string[] { + return [ + "pizzaz", + "pizzaz-carousel", + "pizzaz-albums", + "pizzaz-list", + "pizzaz-video", + "solar-system", + "todo" + ]; +} + +/** + * Generate HTML markup for a widget + */ +export function generateWidgetHtml(widgetName: string, config: AssetConfig = DEFAULT_CONFIG): string { + const cssUrl = getAssetUrl(widgetName, "css", config); + const jsUrl = getAssetUrl(widgetName, "js", config); + const rootId = `${widgetName}-root`; + + return ` +
+ + + `.trim(); +} + +/** + * Get current version information + */ +export function getVersionInfo() { + return { + version, + hash: getVersionHash(), + timestamp: new Date().toISOString() + }; +} \ No newline at end of file