ext-apps/examples/basic-server-svelte/src/App.svelte
Olivier Chafik fcd243a6f3
fix(e2e): make server-time element fill width for consistent mask (#283)
* fix(e2e): make server-time element fill width for consistent mask

- Add flex layout to .action p to make server-time stretch
- Add #server-time flex: 1 rule across all basic-server variants
- Update golden screenshots with full-width masked time field

This ensures the Playwright mask covers a consistent width regardless
of the timestamp content, fixing screenshot comparison flakiness.

* fix(e2e): wait for select to be enabled in screenshot generator

Match the logic in servers.spec.ts - wait for select to be enabled
(servers connected) instead of just visible.

* chore: trigger CI re-run
2026-01-19 17:01:55 +00:00

212 lines
5 KiB
Svelte
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { onMount } from "svelte";
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;
}
let app = $state<App | null>(null);
let hostContext = $state<McpUiHostContext | undefined>();
let serverTime = $state("Loading...");
let messageText = $state("This is message text.");
let logText = $state("This is log text.");
let linkUrl = $state("https://modelcontextprotocol.io/");
// Apply host styles reactively when hostContext changes
$effect(() => {
if (hostContext?.theme) {
applyDocumentTheme(hostContext.theme);
}
if (hostContext?.styles?.variables) {
applyHostStyleVariables(hostContext.styles.variables);
}
if (hostContext?.styles?.css?.fonts) {
applyHostFonts(hostContext.styles.css.fonts);
}
});
onMount(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 = extractTime(result);
};
instance.ontoolcancelled = (params) => {
console.info("Tool call cancelled:", params.reason);
};
instance.onerror = console.error;
instance.onhostcontextchanged = (params) => {
hostContext = { ...hostContext, ...params };
};
await instance.connect();
app = instance;
hostContext = instance.getHostContext();
});
async function handleGetTime() {
if (!app) return;
try {
console.info("Calling get-time tool...");
const result = await app.callServerTool({ name: "get-time", arguments: {} });
console.info("get-time result:", result);
serverTime = extractTime(result);
} catch (e) {
console.error(e);
serverTime = "[ERROR]";
}
}
async function handleSendMessage() {
if (!app) return;
const signal = AbortSignal.timeout(5000);
try {
console.info("Sending message text to Host:", messageText);
const { isError } = await 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() {
if (!app) return;
console.info("Sending log text to Host:", logText);
await app.sendLog({ level: "info", data: logText });
}
async function handleOpenLink() {
if (!app) return;
console.info("Sending open link request to Host:", linkUrl);
const { isError } = await app.openLink({ url: linkUrl });
console.info("Open link request", isError ? "rejected" : "accepted");
}
</script>
<main
class="main"
style:padding={hostContext?.safeAreaInsets && `${hostContext.safeAreaInsets.top}px ${hostContext.safeAreaInsets.right}px ${hostContext.safeAreaInsets.bottom}px ${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 onclick={handleGetTime}>Get Server Time</button>
</div>
<div class="action">
<textarea bind:value={messageText}></textarea>
<button onclick={handleSendMessage}>Send Message</button>
</div>
<div class="action">
<input type="text" bind:value={logText}>
<button onclick={handleSendLog}>Send Log</button>
</div>
<div class="action">
<input type="url" bind:value={linkUrl}>
<button onclick={handleOpenLink}>Open Link</button>
</div>
</main>
<style>
.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;
}
/* Server time row: flex layout for consistent mask width in E2E tests */
> p {
display: flex;
align-items: baseline;
gap: 0.25em;
}
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;
}
}
/* Server time fills remaining width for consistent E2E screenshot masking */
:global(#server-time) {
flex: 1;
min-width: 0;
}
</style>