| | <script setup lang="ts"> |
| | import { ref, onMounted, watchEffect } from "vue"; |
| | import { |
| | App, |
| | applyDocumentTheme, |
| | applyHostFonts, |
| | applyHostStyleVariables, |
| | type McpUiHostContext, |
| | } from "@modelcontextprotocol/ext-apps"; |
| | import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; |
| | |
| | function extractTime(result: CallToolResult): string { |
| | const { text } = result.content?.find((c) => c.type === "text")!; |
| | return text; |
| | } |
| | |
| | |
| | const app = ref<App | null>(null); |
| | const hostContext = ref<McpUiHostContext | undefined>(); |
| | const serverTime = ref("Loading..."); |
| | const messageText = ref("This is message text."); |
| | const logText = ref("This is log text."); |
| | const linkUrl = ref("https://modelcontextprotocol.io/"); |
| | |
| | |
| | watchEffect(() => { |
| | const ctx = hostContext.value; |
| | if (ctx?.theme) { |
| | applyDocumentTheme(ctx.theme); |
| | } |
| | if (ctx?.styles?.variables) { |
| | applyHostStyleVariables(ctx.styles.variables); |
| | } |
| | if (ctx?.styles?.css?.fonts) { |
| | applyHostFonts(ctx.styles.css.fonts); |
| | } |
| | }); |
| | |
| | onMounted(async () => { |
| | const instance = new App({ name: "Get Time App", version: "1.0.0" }); |
| | |
| | instance.ontoolinput = (params) => { |
| | console.info("Received tool call input:", params); |
| | }; |
| | |
| | instance.ontoolresult = (result) => { |
| | console.info("Received tool call result:", result); |
| | serverTime.value = extractTime(result); |
| | }; |
| | |
| | instance.ontoolcancelled = (params) => { |
| | console.info("Tool call cancelled:", params.reason); |
| | }; |
| | |
| | instance.onerror = console.error; |
| | |
| | instance.onhostcontextchanged = (params) => { |
| | hostContext.value = { ...hostContext.value, ...params }; |
| | }; |
| | |
| | await instance.connect(); |
| | app.value = instance; |
| | hostContext.value = instance.getHostContext(); |
| | }); |
| | |
| | async function handleGetTime() { |
| | if (!app.value) return; |
| | try { |
| | console.info("Calling get-time tool..."); |
| | const result = await app.value.callServerTool({ name: "get-time", arguments: {} }); |
| | console.info("get-time result:", result); |
| | serverTime.value = extractTime(result); |
| | } catch (e) { |
| | console.error(e); |
| | serverTime.value = "[ERROR]"; |
| | } |
| | } |
| | |
| | async function handleSendMessage() { |
| | if (!app.value) return; |
| | const signal = AbortSignal.timeout(5000); |
| | try { |
| | console.info("Sending message text to Host:", messageText.value); |
| | const { isError } = await app.value.sendMessage( |
| | { role: "user", content: [{ type: "text", text: messageText.value }] }, |
| | { signal }, |
| | ); |
| | console.info("Message", isError ? "rejected" : "accepted"); |
| | } catch (e) { |
| | console.error("Message send error:", signal.aborted ? "timed out" : e); |
| | } |
| | } |
| | |
| | async function handleSendLog() { |
| | if (!app.value) return; |
| | console.info("Sending log text to Host:", logText.value); |
| | await app.value.sendLog({ level: "info", data: logText.value }); |
| | } |
| | |
| | async function handleOpenLink() { |
| | if (!app.value) return; |
| | console.info("Sending open link request to Host:", linkUrl.value); |
| | const { isError } = await app.value.openLink({ url: linkUrl.value }); |
| | console.info("Open link request", isError ? "rejected" : "accepted"); |
| | } |
| | </script> |
| |
|
| | <template> |
| | <main |
| | class="main" |
| | :style="hostContext?.safeAreaInsets && { |
| | paddingTop: hostContext.safeAreaInsets.top + 'px', |
| | paddingRight: hostContext.safeAreaInsets.right + 'px', |
| | paddingBottom: hostContext.safeAreaInsets.bottom + 'px', |
| | paddingLeft: hostContext.safeAreaInsets.left + 'px', |
| | }" |
| | > |
| | <p class="notice">Watch activity in the DevTools console!</p> |
| |
|
| | <div class="action"> |
| | <p><strong>Server Time:</strong> <code id="server-time">{{ serverTime }}</code></p> |
| | <button @click="handleGetTime">Get Server Time</button> |
| | </div> |
| |
|
| | <div class="action"> |
| | <textarea v-model="messageText"></textarea> |
| | <button @click="handleSendMessage">Send Message</button> |
| | </div> |
| |
|
| | <div class="action"> |
| | <input type="text" v-model="logText"> |
| | <button @click="handleSendLog">Send Log</button> |
| | </div> |
| |
|
| | <div class="action"> |
| | <input type="url" v-model="linkUrl"> |
| | <button @click="handleOpenLink">Open Link</button> |
| | </div> |
| | </main> |
| | </template> |
| |
|
| | <style scoped> |
| | .main { |
| | --color-primary: #2563eb; |
| | --color-primary-hover: #1d4ed8; |
| | --color-notice-bg: #eff6ff; |
| | |
| | width: 100%; |
| | max-width: 425px; |
| | box-sizing: border-box; |
| | |
| | > * { |
| | margin-top: 0; |
| | margin-bottom: 0; |
| | } |
| | |
| | > * + * { |
| | margin-top: 1.5rem; |
| | } |
| | } |
| | |
| | .action { |
| | > * { |
| | margin-top: 0; |
| | margin-bottom: 0; |
| | width: 100%; |
| | } |
| | |
| | > * + * { |
| | margin-top: 0.5rem; |
| | } |
| | |
| | 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; |
| | } |
| | } |
| | </style> |
| |
|