Add basic-host and basic-server-react examples

Introduce new example implementations to replace simple-host and
simple-server:

- basic-host: Reference host implementation with double-iframe sandbox
  pattern for secure UI isolation (outer iframe validates and relays
  messages, inner iframe renders untrusted tool UI via srcdoc)

- basic-server-react: MCP server example demonstrating tool registration
  with linked UI resources and React UI using the useApp() hook

Build infrastructure updates:
- Add build:all script and simplify CI/pre-commit to use it
- Add examples:dev for watch-mode development workflow

Also change PostMessageTransport logging from info to debug level.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan Hefner 2025-12-02 13:11:17 -06:00
commit 97310d50c5
4 changed files with 1808 additions and 25 deletions

View file

@ -0,0 +1,12 @@
* {
box-sizing: border-box;
}
html, body {
font-family: system-ui, -apple-system, sans-serif;
font-size: 1rem;
}
code {
font-size: 1em;
}

View file

@ -0,0 +1,63 @@
.main {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-notice-bg: #eff6ff;
min-width: 425px;
> * {
margin-top: 0;
margin-bottom: 0;
}
> * + * {
margin-top: 1.5rem;
}
}
.action {
> * {
margin-top: 0;
margin-bottom: 0;
width: 100%;
}
> * + * {
margin-top: 0.5rem;
}
/* Consistent font for form inputs (inherits from global.css) */
textarea,
input {
font-family: inherit;
font-size: inherit;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
background-color: var(--color-primary);
cursor: pointer;
&:hover,
&:focus-visible {
background-color: var(--color-primary-hover);
}
}
}
.notice {
padding: 0.5rem 0.75rem;
color: var(--color-primary);
text-align: center;
font-style: italic;
background-color: var(--color-notice-bg);
&::before {
content: "ℹ️ ";
font-style: normal;
}
}

View file

@ -0,0 +1,141 @@
/**
* @file App that demonstrates a few features using MCP Apps SDK + React.
*/
import type { App } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import styles from "./mcp-app.module.css";
const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" };
const log = {
info: console.log.bind(console, "[APP]"),
warn: console.warn.bind(console, "[APP]"),
error: console.error.bind(console, "[APP]"),
};
function extractTime(callToolResult: CallToolResult): string {
const { time } = (callToolResult.structuredContent as { time?: string }) ?? {};
return time ?? "[ERROR]";
}
function GetTimeApp() {
const [toolResult, setToolResult] = useState<CallToolResult | null>(null);
const { app, error } = useApp({
appInfo: IMPLEMENTATION,
capabilities: {},
onAppCreated: (app) => {
app.ontoolinput = async (input) => {
log.info("Received tool call input:", input);
};
app.ontoolresult = async (result) => {
log.info("Received tool call result:", result);
setToolResult(result);
};
app.onerror = log.error;
},
});
if (error) return <div><strong>ERROR:</strong> {error.message}</div>;
if (!app) return <div>Connecting...</div>;
return <GetTimeAppInner app={app} toolResult={toolResult} />;
}
interface GetTimeAppInnerProps {
app: App;
toolResult: CallToolResult | null;
}
function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) {
const [serverTime, setServerTime] = useState("Loading...");
const [messageText, setMessageText] = useState("This is message text.");
const [logText, setLogText] = useState("This is log text.");
const [linkUrl, setLinkUrl] = useState("https://modelcontextprotocol.io/");
useEffect(() => {
if (toolResult) {
setServerTime(extractTime(toolResult));
}
}, [toolResult]);
const handleGetTime = useCallback(async () => {
try {
log.info("Calling get-time tool...");
const result = await app.callServerTool({ name: "get-time", arguments: {} });
log.info("get-time result:", result);
setServerTime(extractTime(result));
} catch (e) {
log.error(e);
setServerTime("[ERROR]");
}
}, [app]);
const handleSendMessage = useCallback(async () => {
const signal = AbortSignal.timeout(5000);
try {
log.info("Sending message text to Host:", messageText);
const { isError } = await app.sendMessage(
{ role: "user", content: [{ type: "text", text: messageText }] },
{ signal },
);
log.info("Message", isError ? "rejected" : "accepted");
} catch (e) {
log.error("Message send error:", signal.aborted ? "timed out" : e);
}
}, [app, messageText]);
const handleSendLog = useCallback(async () => {
log.info("Sending log text to Host:", logText);
await app.sendLog({ level: "info", data: logText });
}, [app, logText]);
const handleOpenLink = useCallback(async () => {
log.info("Sending open link request to Host:", linkUrl);
const { isError } = await app.sendOpenLink({ url: linkUrl });
log.info("Open link request", isError ? "rejected" : "accepted");
}, [app, linkUrl]);
return (
<main className={styles.main}>
<p className={styles.notice}>Watch activity in the DevTools console!</p>
<div className={styles.action}>
<p>
<strong>Server Time:</strong> <code>{serverTime}</code>
</p>
<button onClick={handleGetTime}>Get Server Time</button>
</div>
<div className={styles.action}>
<textarea value={messageText} onChange={(e) => setMessageText(e.target.value)} />
<button onClick={handleSendMessage}>Send Message</button>
</div>
<div className={styles.action}>
<input type="text" value={logText} onChange={(e) => setLogText(e.target.value)} />
<button onClick={handleSendLog}>Send Log</button>
</div>
<div className={styles.action}>
<input type="url" value={linkUrl} onChange={(e) => setLinkUrl(e.target.value)} />
<button onClick={handleOpenLink}>Open Link</button>
</div>
</main>
);
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<GetTimeApp />
</StrictMode>,
);

View file

@ -0,0 +1 @@
/// <reference types="vite/client" />