From bd0609a28571197589fccf6613c0a36b00b00c3e Mon Sep 17 00:00:00 2001 From: x x Date: Mon, 7 Sep 2026 15:29:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcpapps):=20=E5=85=B1=E4=BA=AB=20MCP=20App?= =?UTF-8?q?s=20=E6=B8=B2=E6=9F=93=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mcpapps/app.html | 749 +++++++++++++++++++++++++++++++++++++++++++ mcpapps/apps.go | 15 + mcpapps/apps_test.go | 51 +++ 3 files changed, 815 insertions(+) create mode 100644 mcpapps/app.html create mode 100644 mcpapps/apps.go create mode 100644 mcpapps/apps_test.go diff --git a/mcpapps/app.html b/mcpapps/app.html new file mode 100644 index 0000000..7548859 --- /dev/null +++ b/mcpapps/app.html @@ -0,0 +1,749 @@ + + + + + + +{{TITLE}} + + + +
Waiting for tool output…
+ + + \ No newline at end of file diff --git a/mcpapps/apps.go b/mcpapps/apps.go new file mode 100644 index 0000000..4a571d6 --- /dev/null +++ b/mcpapps/apps.go @@ -0,0 +1,15 @@ +package mcpapps + +import ( + _ "embed" + "strings" +) + +//go:embed app.html +var appHTMLTemplate string + +// HTML renders one shared MCP App document for a known AgentDock view. +// View and title are internal constants owned by AgentDock/NexusDock, not user input. +func HTML(view, title string) string { + return strings.NewReplacer("{{VIEW}}", view, "{{TITLE}}", title).Replace(appHTMLTemplate) +} diff --git a/mcpapps/apps_test.go b/mcpapps/apps_test.go new file mode 100644 index 0000000..6cb893f --- /dev/null +++ b/mcpapps/apps_test.go @@ -0,0 +1,51 @@ +package mcpapps + +import ( + "strings" + "testing" +) + +func TestHTMLRendersSharedMCPAppTemplate(t *testing.T) { + html := HTML("recall", "Recall") + for _, marker := range []string{ + `Recall`, + `expectedView="recall"`, + `rpcRequest("ui/initialize"`, + `protocolVersion:"2026-01-26"`, + `rpcNotify("ui/notifications/initialized"`, + `message.method==="ui/notifications/tool-result"`, + } { + if !strings.Contains(html, marker) { + t.Fatalf("shared MCP App missing marker %q", marker) + } + } + for _, forbidden := range []string{"{{VIEW}}", "{{TITLE}}", "window.openai", "openai/widget", "openai/outputTemplate"} { + if strings.Contains(html, forbidden) { + t.Fatalf("shared MCP App contains forbidden marker %q", forbidden) + } + } +} + +func TestHTMLUsesStandardHostContextThemeAndSemanticTokens(t *testing.T) { + html := HTML("workflow", "Workflow") + for _, marker := range []string{ + `:root{color-scheme:light dark;`, + `:root[data-theme="dark"]{color-scheme:dark;`, + `@media(prefers-color-scheme:dark)`, + `--ad-text-primary:`, + `var(--ad-text-primary)`, + `const initialized=await rpcRequest("ui/initialize"`, + `applyHostContext(initialized&&initialized.hostContext)`, + `message.method==="ui/notifications/host-context-changed"`, + `applyHostContext(message.params)`, + `context.styles.variables`, + `window.matchMedia("(prefers-color-scheme: dark)")`, + } { + if !strings.Contains(html, marker) { + t.Fatalf("shared MCP App missing theme marker %q", marker) + } + } + if strings.Contains(html, `:root{color-scheme:light;font:`) { + t.Fatal("shared MCP App still forces a light-only color scheme") + } +}