| | |
| | |
| | |
| | import { |
| | App, |
| | applyDocumentTheme, |
| | applyHostFonts, |
| | applyHostStyleVariables, |
| | type McpUiHostContext, |
| | } from "@modelcontextprotocol/ext-apps"; |
| | import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; |
| | import { createEffect, createSignal, onMount, Show } from "solid-js"; |
| | import { render } from "solid-js/web"; |
| | import styles from "./mcp-app.module.css"; |
| |
|
| | function extractTime(callToolResult: CallToolResult): string { |
| | const { text } = callToolResult.content?.find((c) => c.type === "text")!; |
| | return text; |
| | } |
| |
|
| |
|
| | function GetTimeApp() { |
| | const [app, setApp] = createSignal<App | null>(null); |
| | const [error, setError] = createSignal<Error | null>(null); |
| | const [toolResult, setToolResult] = createSignal<CallToolResult | null>(null); |
| | const [hostContext, setHostContext] = createSignal<McpUiHostContext | undefined>(); |
| |
|
| | |
| | createEffect(() => { |
| | const ctx = hostContext(); |
| | if (ctx?.theme) { |
| | applyDocumentTheme(ctx.theme); |
| | } |
| | if (ctx?.styles?.variables) { |
| | applyHostStyleVariables(ctx.styles.variables); |
| | } |
| | if (ctx?.styles?.css?.fonts) { |
| | applyHostFonts(ctx.styles.css.fonts); |
| | } |
| | }); |
| |
|
| | onMount(async () => { |
| | const instance = new App({ name: "Get Time App", version: "1.0.0" }); |
| |
|
| | instance.ontoolinput = async (input) => { |
| | console.info("Received tool call input:", input); |
| | }; |
| |
|
| | instance.ontoolresult = async (result) => { |
| | console.info("Received tool call result:", result); |
| | setToolResult(result); |
| | }; |
| |
|
| | instance.ontoolcancelled = (params) => { |
| | console.info("Tool call cancelled:", params.reason); |
| | }; |
| |
|
| | instance.onerror = console.error; |
| |
|
| | instance.onhostcontextchanged = (params) => { |
| | setHostContext((prev) => ({ ...prev, ...params })); |
| | }; |
| |
|
| | try { |
| | await instance.connect(); |
| | setApp(instance); |
| | setHostContext(instance.getHostContext()); |
| | } catch (e) { |
| | setError(e as Error); |
| | } |
| | }); |
| |
|
| | return ( |
| | <Show when={!error()} fallback={<div><strong>ERROR:</strong> {error()!.message}</div>}> |
| | <Show when={app()} fallback={<div>Connecting...</div>}> |
| | <GetTimeAppInner app={app()!} toolResult={toolResult()} hostContext={hostContext()} /> |
| | </Show> |
| | </Show> |
| | ); |
| | } |
| |
|
| |
|
| | interface GetTimeAppInnerProps { |
| | app: App; |
| | toolResult: CallToolResult | null; |
| | hostContext?: McpUiHostContext; |
| | } |
| | function GetTimeAppInner(props: GetTimeAppInnerProps) { |
| | const [serverTime, setServerTime] = createSignal("Loading..."); |
| | const [messageText, setMessageText] = createSignal("This is message text."); |
| | const [logText, setLogText] = createSignal("This is log text."); |
| | const [linkUrl, setLinkUrl] = createSignal("https://modelcontextprotocol.io/"); |
| |
|
| | |
| | createEffect(() => { |
| | if (props.toolResult) { |
| | setServerTime(extractTime(props.toolResult)); |
| | } |
| | }); |
| |
|
| | async function handleGetTime() { |
| | try { |
| | console.info("Calling get-time tool..."); |
| | const result = await props.app.callServerTool({ name: "get-time", arguments: {} }); |
| | console.info("get-time result:", result); |
| | setServerTime(extractTime(result)); |
| | } catch (e) { |
| | console.error(e); |
| | setServerTime("[ERROR]"); |
| | } |
| | } |
| |
|
| | async function handleSendMessage() { |
| | const signal = AbortSignal.timeout(5000); |
| | try { |
| | console.info("Sending message text to Host:", messageText()); |
| | const { isError } = await props.app.sendMessage( |
| | { role: "user", content: [{ type: "text", text: messageText() }] }, |
| | { signal }, |
| | ); |
| | console.info("Message", isError ? "rejected" : "accepted"); |
| | } catch (e) { |
| | console.error("Message send error:", signal.aborted ? "timed out" : e); |
| | } |
| | } |
| |
|
| | async function handleSendLog() { |
| | console.info("Sending log text to Host:", logText()); |
| | await props.app.sendLog({ level: "info", data: logText() }); |
| | } |
| |
|
| | async function handleOpenLink() { |
| | console.info("Sending open link request to Host:", linkUrl()); |
| | const { isError } = await props.app.openLink({ url: linkUrl() }); |
| | console.info("Open link request", isError ? "rejected" : "accepted"); |
| | } |
| |
|
| | return ( |
| | <main |
| | class={styles.main} |
| | style={props.hostContext?.safeAreaInsets ? { |
| | "padding-top": `${props.hostContext.safeAreaInsets.top}px`, |
| | "padding-right": `${props.hostContext.safeAreaInsets.right}px`, |
| | "padding-bottom": `${props.hostContext.safeAreaInsets.bottom}px`, |
| | "padding-left": `${props.hostContext.safeAreaInsets.left}px`, |
| | } : undefined} |
| | > |
| | <p class={styles.notice}>Watch activity in the DevTools console!</p> |
| | |
| | <div class={styles.action}> |
| | <p> |
| | <strong>Server Time:</strong> <code id="server-time">{serverTime()}</code> |
| | </p> |
| | <button onClick={handleGetTime}>Get Server Time</button> |
| | </div> |
| | |
| | <div class={styles.action}> |
| | <textarea value={messageText()} onInput={(e) => setMessageText(e.currentTarget.value)} /> |
| | <button onClick={handleSendMessage}>Send Message</button> |
| | </div> |
| | |
| | <div class={styles.action}> |
| | <input type="text" value={logText()} onInput={(e) => setLogText(e.currentTarget.value)} /> |
| | <button onClick={handleSendLog}>Send Log</button> |
| | </div> |
| | |
| | <div class={styles.action}> |
| | <input type="url" value={linkUrl()} onInput={(e) => setLinkUrl(e.currentTarget.value)} /> |
| | <button onClick={handleOpenLink}>Open Link</button> |
| | </div> |
| | </main> |
| | ); |
| | } |
| |
|
| |
|
| | render(() => <GetTimeApp />, document.getElementById("root")!); |
| |
|