diff --git a/build-all.mts b/build-all.mts
index abd9832..7d6a8b2 100644
--- a/build-all.mts
+++ b/build-all.mts
@@ -15,6 +15,7 @@ const PER_ENTRY_CSS_IGNORE = "**/*.module.*".split(",").map((s) => s.trim());
const GLOBAL_CSS_LIST = [path.resolve("src/index.css")];
const targets: string[] = [
+ "test",
"todo",
"solar-system",
"pizzaz",
diff --git a/src/test/index.jsx b/src/test/index.jsx
new file mode 100644
index 0000000..156fb9a
--- /dev/null
+++ b/src/test/index.jsx
@@ -0,0 +1,7 @@
+import { createRoot } from "react-dom/client";
+import App from "./test";
+
+createRoot(document.getElementById("test-root")).render();
+
+export { App };
+export default App;
diff --git a/src/test/test.css b/src/test/test.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/test/test.jsx b/src/test/test.jsx
new file mode 100644
index 0000000..7c50703
--- /dev/null
+++ b/src/test/test.jsx
@@ -0,0 +1,120 @@
+import React from "react";
+import { useWidgetProps } from "../use-widget-props";
+import { useWidgetState } from "../use-widget-state";
+import { useDisplayMode } from "../use-display-mode";
+import { useEffect } from "react";
+
+const ExpandIcon = () => {
+ return (
+
+ );
+};
+
+export function App() {
+ const widgetProps = useWidgetProps() || {};
+ const { title_text = 'hi' } = widgetProps;
+ const displayMode = useDisplayMode();
+ const maxHeight = "100vh";
+
+ const [titleText, setTitleText] = useWidgetState(title_text);
+ const [isLoading, setIsLoading] = useWidgetState(false);
+
+ useEffect(() => {
+ setTitleText(title_text);
+ }, [title_text, setTitleText]);
+
+ const helloAgain = async () => {
+ //await window.openai.sendFollowUpMessage({ "prompt": "can you show the test app again with the title 'hello again.'" });
+ setIsLoading(true);
+ try {
+ const reply = await window.openai.callTool("test-tool", {
+ "title_text": "hi again. :)"
+ });
+ if (reply?.structuredContent?.title_text) {
+ setTitleText(reply.structuredContent.title_text);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const gotoDoc = () => {
+ window.openai.openExternal({ href: "https://developers.openai.com/apps-sdk" });
+ };
+
+ return (
+
+ {displayMode !== "fullscreen" && (
+
+
+
+ )}
+
+
+
{titleText || 'hi'}
+
+
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/test_server_python/main.py b/test_server_python/main.py
new file mode 100644
index 0000000..6fd34ef
--- /dev/null
+++ b/test_server_python/main.py
@@ -0,0 +1,284 @@
+"""Test demo MCP server implemented with the Python FastMCP helper.
+
+Each handler returns the HTML shell via an MCP resource and renders the title text.
+The module also wires the handlers into an HTTP/SSE stack so you can
+run the server with uvicorn on port 8000."""
+
+from __future__ import annotations
+
+import os
+from copy import deepcopy
+from dataclasses import dataclass
+from typing import Any, Dict, List
+
+import mcp.types as types
+from mcp.server.fastmcp import FastMCP
+from pydantic import BaseModel, ConfigDict, Field, ValidationError
+
+# Get base URL from environment or use local static files
+BASE_URL = os.getenv("STATIC_BASE_URL", "/static")
+
+@dataclass(frozen=True)
+class TestWidget:
+ identifier: str
+ title: str
+ template_uri: str
+ invoking: str
+ invoked: str
+ html: str
+ response_text: str
+
+
+widgets: List[TestWidget] = [
+ TestWidget(
+ identifier="test-tool",
+ title="Show Test",
+ template_uri="ui://widget/test.html",
+ invoking="Displaying test",
+ invoked="Rendered test",
+ html=(
+ f"\n"
+ f"/static/test-2d2b.css\">\n"
+ f""
+ ),
+ response_text="Rendered test component!",
+ ),
+]
+
+
+MIME_TYPE = "text/html+skybridge"
+
+
+WIDGETS_BY_ID: Dict[str, TestWidget] = {widget.identifier: widget for widget in widgets}
+WIDGETS_BY_URI: Dict[str, TestWidget] = {widget.template_uri: widget for widget in widgets}
+
+
+class TestInput(BaseModel):
+ """Schema for Test tools."""
+
+ title_text: str = Field(
+ ...,
+ alias="titleText",
+ description="Title text used when rendering the test widget.",
+ )
+
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
+
+
+mcp = FastMCP(
+ name="test-server-python",
+ sse_path="/mcp",
+ message_path="/mcp/messages",
+ stateless_http=True,
+)
+
+
+TOOL_INPUT_SCHEMA: Dict[str, Any] = {
+ "type": "object",
+ "properties": {
+ "titleText": {
+ "type": "string",
+ "description": "Title text used when rendering the test widget.",
+ }
+ },
+ "required": ["titleText"],
+ "additionalProperties": False,
+}
+
+
+def _resource_description(widget: TestWidget) -> str:
+ return f"{widget.title} widget markup"
+
+
+def _tool_meta(widget: TestWidget) -> Dict[str, Any]:
+ return {
+ "openai/outputTemplate": widget.template_uri,
+ "openai/toolInvocation/invoking": widget.invoking,
+ "openai/toolInvocation/invoked": widget.invoked,
+ "openai/widgetAccessible": True,
+ "openai/widgetPrefersBorder": True,
+ "openai/resultCanProduceWidget": True,
+ "annotations": {
+ "destructiveHint": False,
+ "openWorldHint": False,
+ "readOnlyHint": True,
+ }
+ }
+
+
+def _embedded_widget_resource(widget: TestWidget) -> types.EmbeddedResource:
+ return types.EmbeddedResource(
+ type="resource",
+ resource=types.TextResourceContents(
+ uri=widget.template_uri,
+ mimeType=MIME_TYPE,
+ text=widget.html,
+ title=widget.title,
+ ),
+ )
+
+
+@mcp._mcp_server.list_tools()
+async def _list_tools() -> List[types.Tool]:
+ return [
+ types.Tool(
+ name=widget.identifier,
+ title=widget.title,
+ description=widget.title,
+ inputSchema=deepcopy(TOOL_INPUT_SCHEMA),
+ _meta=_tool_meta(widget),
+ )
+ for widget in widgets
+ ]
+
+
+@mcp._mcp_server.list_resources()
+async def _list_resources() -> List[types.Resource]:
+ return [
+ types.Resource(
+ name=widget.title,
+ title=widget.title,
+ uri=widget.template_uri,
+ description=_resource_description(widget),
+ mimeType=MIME_TYPE,
+ _meta=_tool_meta(widget),
+ )
+ for widget in widgets
+ ]
+
+
+@mcp._mcp_server.list_resource_templates()
+async def _list_resource_templates() -> List[types.ResourceTemplate]:
+ return [
+ types.ResourceTemplate(
+ name=widget.title,
+ title=widget.title,
+ uriTemplate=widget.template_uri,
+ description=_resource_description(widget),
+ mimeType=MIME_TYPE,
+ _meta=_tool_meta(widget),
+ )
+ for widget in widgets
+ ]
+
+
+async def _handle_read_resource(req: types.ReadResourceRequest) -> types.ServerResult:
+ widget = WIDGETS_BY_URI.get(str(req.params.uri))
+ if widget is None:
+ return types.ServerResult(
+ types.ReadResourceResult(
+ contents=[],
+ _meta={"error": f"Unknown resource: {req.params.uri}"},
+ )
+ )
+
+ contents = [
+ types.TextResourceContents(
+ uri=widget.template_uri,
+ mimeType=MIME_TYPE,
+ text=widget.html,
+ _meta=_tool_meta(widget),
+ )
+ ]
+
+ return types.ServerResult(types.ReadResourceResult(contents=contents))
+
+
+async def _call_tool_request(req: types.CallToolRequest) -> types.ServerResult:
+ widget = WIDGETS_BY_ID.get(req.params.name)
+ if widget is None:
+ return types.ServerResult(
+ types.CallToolResult(
+ content=[
+ types.TextContent(
+ type="text",
+ text=f"Unknown tool: {req.params.name}",
+ )
+ ],
+ isError=True,
+ )
+ )
+
+ arguments = req.params.arguments or {}
+ try:
+ payload = TestInput.model_validate(arguments)
+ except ValidationError as exc:
+ return types.ServerResult(
+ types.CallToolResult(
+ content=[
+ types.TextContent(
+ type="text",
+ text=f"Input validation error: {exc.errors()}",
+ )
+ ],
+ isError=True,
+ )
+ )
+
+ titleText = payload.title_text
+
+ widget_resource = _embedded_widget_resource(widget)
+ meta: Dict[str, Any] = {
+ "openai.com/widget": widget_resource.model_dump(mode="json"),
+ "openai/outputTemplate": widget.template_uri,
+ "openai/toolInvocation/invoking": widget.invoking,
+ "openai/toolInvocation/invoked": widget.invoked,
+ "openai/widgetAccessible": True,
+ "openai/resultCanProduceWidget": True,
+ }
+
+ structured = {
+ "title_text": titleText
+ }
+
+ return types.ServerResult(
+ types.CallToolResult(
+ content=[
+ types.TextContent(
+ type="text",
+ text=widget.response_text,
+ )
+ ],
+ structuredContent=structured,
+ _meta=meta,
+ )
+ )
+
+
+mcp._mcp_server.request_handlers[types.CallToolRequest] = _call_tool_request
+mcp._mcp_server.request_handlers[types.ReadResourceRequest] = _handle_read_resource
+
+
+app = mcp.streamable_http_app()
+
+# Add static file serving
+try:
+ from starlette.staticfiles import StaticFiles
+ import os
+
+ # Get the directory where this script is located
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ static_dir = os.path.join(script_dir, "static")
+
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
+except Exception:
+ pass
+
+try:
+ from starlette.middleware.cors import CORSMiddleware
+
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+ allow_credentials=False,
+ )
+except Exception:
+ pass
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ uvicorn.run("main:app", host="0.0.0.0", port=8000)
diff --git a/test_server_python/static/copy-assets-files-here.txt b/test_server_python/static/copy-assets-files-here.txt
new file mode 100644
index 0000000..3389200
--- /dev/null
+++ b/test_server_python/static/copy-assets-files-here.txt
@@ -0,0 +1 @@
+See README.md
\ No newline at end of file