Model Context Protocol is a standard way for an AI host to discover and use capabilities exposed by a server: tools, resources, and prompts.
stdio — the host launches a local command and communicates through stdin/stdout.
Streamable HTTP — the host connects to an independent HTTP server, whether it is on localhost or the internet.
For this talk, we'll focus on remote MCP servers over Streamable HTTP.
server.registerTool(
"echo",
{
title: "Echo",
description: "Echo back the provided message",
inputSchema: z.object({
message: z.string().describe("The message to echo back"),
}),
outputSchema: z.object({
echo: z.string().describe("The echoed message"),
}),
},
(args, ctx) => echo(args, ctx),
);
async function echo(args: { message: string }) {
return createTextResult({ echo: args.message });
}
export function createTextResult(data: unknown): CallToolResult {
const safeData = data === undefined ? null : data;
const result: CallToolResult = {
content: [
{ type: "text", text: JSON.stringify(safeData, null, 2) },
],
};
if (safeData !== null && typeof safeData === "object") {
result.structuredContent = safeData as Record<string, unknown>;
}
return result;
}
// Request
{
"method": "tools/call",
"params": {
"name": "echo",
"arguments": { "message": "Hello World!" }
}
}
// Response (result)
{
"content": [
{ "type": "text", "text": "{\n \"echo\": \"Hello World!\"\n}" }
],
"structuredContent": { "echo": "Hello World!" }
}
A remote MCP server is:
localhost in dev, HTTPS once a host has to reach itSame server from here on. We're about to give it a face.
The host still calls a tool. Instead of a blob of JSON, you get something you can actually use in the chat.

An MCP app is an MCP server where a tool's result comes with a UI.
Bind the echo tool to a UI resource:
registerAppTool(server, "echo", {
title: "Echo",
description: "Echoes back the user's message in an interactive view",
inputSchema: EchoToolInputSchema.shape,
_meta: {
ui: { resourceUri: "ui://echo" },
},
}, handler);
The resource is HTML. MIME type is text/html;profile=mcp-app. Hosts look for it.
Without _meta.ui, the echo tool is text. With it, the host fetches ui://echo and renders that HTML.
postMessage via the MCP Apps App APIHosts nest it so your HTML can't touch the chat. That's why you declare a CSP.
HTML, CSS, React — whatever you already ship on the web.
<iframe> showed up in IE 3 in 1996. postMessage has been in browsers since 2008. That's ~30 years and almost 20 years. Not a new runtime.
MCP Apps didn't invent a UI stack. This tech just found a new place to be useful: a widget in the chat.
The new bit is the host contract on top: ui://, the MIME type, and CSP.
const result = await app.callServerTool({
name: "echo",
arguments: { message: "Hello from the widget!" },
});
This isn't a screenshot of the first response. Once it's on screen, it can keep calling the same remote server.
Widgets run under a strict Content Security Policy. An <img> can 200 and still not render. You never listed that origin.
_meta: {
ui: {
csp: {
resourceDomains: ["https://images.ctfassets.net"],
// connectDomains: ["https://api.example.com"], // fetch / XHR / WS
},
},
}
resourceDomains — images, fonts, scriptsconnectDomains — fetch / XHR / WebSocketcommityourcode.com doesn't expose a speaker or schedule API.
So the Commit Your Code app reads JSON we committed. catalog.ts. Don't scrape the site at request time.
data/speakers.json
data/sessions.json
data/schedule.json
data/events.json
data/tracks.json
Same remote MCP shape as the echo tool. Tools for speakers, schedule, and events.
They need a public HTTPS URL.
Pomerium is tunneling that and handling OAuth 2.1. Open-core MCP gateway, among other things. The app has no auth code.
Full disclosure: I work at Pomerium. I'd still use it for this.
MCPJam can hit localhost, so that's where we'll look first.
_meta.ui.resourceUripostMessage_meta.ui.csp.resourceDomainsSpeaker note: 25 min hard stop (2:30–2:55). ~13 min slides, ~12 min live. Talk-and-show, not a code-along. Say that up front. We're staying on remote MCP — ChatGPT, Claude, MCPJam over HTTPS. Local stdio is a different talk. Everything is open source. Links at the end, including a free MCP course that uses MCPJam (no paid AI sub) and open-core Pomerium. Don't linger. PREP (before walking on): - `npm run dev` green (server :8080, widgets :4444) - MCPJam on `http://localhost:8080/mcp`, client = **MCPJam** (not Claude), one successful `list_speakers` - Pomerium already signed in — no first-time signup on stage - Tunnel command copied (`ssh -R 0 pom.run`; second tunnel to :4444 if the host loads Vite assets) - Dev `/mcp` and prod `/mcp` on the clipboard - ChatGPT **prod** connector pre-added; live-add is the **tunnel** into Claude and ChatGPT - Assets in `images/`: `excalidraw-mcp.gif`, `qr-nickyt.png`, `qr-cyc-app.png`. Cover uses the Echo widget HTML from the MCP app template. - Present from Marp HTML, not PDF (GIF dies on frame one) - One ready prompt in the pocket. At most one. Hallway for questions — no Q&A block. Not a Pomerium talk; when the tunnel comes up, full disclosure, I'd still use it. Don't flash the CYC speakers widget. That's the live bit.
Speaker note: Keep this simple. MCP gives AI hosts one consistent way to discover what a server offers and use it. Servers can expose tools the model can call, resources the application can add to context, and reusable prompts. We're starting with tools, then adding a UI. [Sources] - https://modelcontextprotocol.io/specification/2025-06-18/architecture - https://modelcontextprotocol.io/specification/2025-06-18/server/index [/Sources]
Speaker note: One protocol, two ways to carry its JSON-RPC messages. With stdio, the host launches a CLI-style program as a child process. It isn't an HTTP server: there is no URL or port. MCP messages travel through stdin and stdout. With Streamable HTTP, the server is an independent process behind one HTTP endpoint. During development that endpoint can be localhost; deploy the same server publicly and it becomes a remote MCP server. We're going to focus on remote MCPs for this talk. ChatGPT, Claude, and MCPJam connect to our published server over HTTPS. [Sources] - https://modelcontextprotocol.io/specification/2025-11-25/basic/transports - https://modelcontextprotocol.io/registry/remote-servers [/Sources]
Speaker note: echo from nickytonline/mcp-typescript-template, `src/tools.ts`. Don't read the URL. Point at the four pieces — don't read the code: 1. Name: `"echo"` 2. Input schema: `{ message: string }` 3. Output schema: `{ echo: string }` 4. Handler: `(args, ctx) => echo(args, ctx)` That's a remote function the model can call. No UI, no iframe, no apps spec. Next slide is the handler itself.
Speaker note: three lines. That's the whole handler. Boring on purpose. `createTextResult` is the helper — next slide is that function. Don't linger here. Skip elicitation, logging, annotations. They're in the template (`elicit_echo`). Not this talk.
Speaker note: `createTextResult` from nickytonline/mcp-typescript-template, `src/lib/utils.ts`. Don't read the URL. Point at the two fields — don't read the function: 1. `content` — JSON as text, for any client 2. `structuredContent` — the object, for clients that read the schema Next two slides are that going over the wire.
Speaker note: this is the whole request. `tools/call`, the tool name, and the arguments the model filled in from the input schema. The model picked `"Hello World!"`. Nothing else here is ours. Next slide is what comes back.
Speaker note: same result, two shapes. That's `createTextResult` from the last slide doing its job. Point at the two fields — don't read the JSON: 1. `content` — the JSON serialized into a text block 2. `structuredContent` — the actual object Why both: MCP 2025-06-18 says if you return `structuredContent` you SHOULD also return the serialized JSON as a text block. That's why the helper emits both — not because we're being cute. Don't live-call echo. Save the live host for CYC. Say it out loud instead of showing it: this is all you get today — text and JSON, no widget. That's the setup for the next slide.
Speaker note: you've shown them MCP. Now spoil it. Don't say "HTTPS" flatly. The transport is Streamable HTTP. You'll be on `http://localhost:8080/mcp` in MCPJam in a few minutes; TLS is what the remote hosts require, not what the spec calls a transport.
Speaker note: this is MCP Apps. First official MCP extension (announced Jan 2026). Claude, ChatGPT, VS Code, Goose, MCPJam, and more. Don't say Pomerium invented this. The spec is MCP Apps. Next slide is the Excalidraw GIF. Let it play. Don't live-connect that MCP on stage.
Speaker note: ~30–45 seconds. This is my go-to MCP App, not a second live demo. Say: - Same idea we'll wire with echo: a tool call, then a UI in the host. - I use this constantly. - Don't read the link out loud. The GIF is the point. Don't narrate every stroke, don't open Excalidraw in Claude, skip Excaliclaw / OpenClaw. If the GIF is long, talk over one loop and advance. Present from Marp HTML, not PDF. File: `images/excalidraw-mcp.gif`
Speaker note: "app" is not a new runtime, host, or protocol. Extension on the server you already have. If the host can't render UI, you still return text. Excalidraw is the version I actually use. Echo is the one you can read in eight lines.
Speaker note: from pomerium/mcp-app-typescript-template, `server/src/server.ts`. Point at `ui://echo`. Next slide is the MIME type.
Speaker note: say out loud: 1. URI scheme is `ui://` 2. That MIME type has to be exact. Wrong string, no widget. Don't walk the whole registerResource call unless someone asks. No echo-widget screenshot. Excalidraw already showed what this looks like. Don't spoil CYC.
Speaker note: people think there's a special widget runtime. It's an iframe and JSON-RPC. One sentence on isolation, then stop. Don't go into the double-iframe architecture (outer proxy, no `allow-same-origin`, inner srcdoc). Host-builder stuff. Q&A: hosts nest two frames so untrusted HTML can't reach `window.top` or the chat DOM. Template uses React because that's what the starter ships. Say so, then drop it. If asked why Claude and ChatGPT load widgets differently: some hosts run static `<script src>`; Claude often wants fully inlined HTML. The template handles that per request. Not the talk.
Speaker note: hammer this. CYC is full of web folks. Old platform, new place. Say it out loud: iframes, postMessage, HTML, CSS, JS — you've been doing this. It just lives in Claude and ChatGPT now. Dates if someone pushes: - iframe: Microsoft, IE 3, 1996. HTML 4.0, 1997. ~30 years. - postMessage: HTML5 cross-document messaging. Firefox 3 / Safari / Opera around 2008, IE 8 in 2009. Opera had an earlier version ~2005. Say 2008. Almost 20 years. - Before postMessage, people hacked it with hash fragments. Don't go there unless asked. Linger one beat. Then callServerTool, which *is* new — it's a wrapper, not a new browser API.
Speaker note: `App` is from `@modelcontextprotocol/ext-apps`. Also `ontoolresult`, `openLink`, `sendMessage`, `updateModelContext`. Live half: they click a speaker, `get_speaker` fires.
Speaker note: linger. This used to be a live broken-image beat. Slides are more reliable. Next slide is the three rules.
Speaker note: say: - Not a bug in your `<img>`. - CYC speaker photos are on Contentful (`images.ctfassets.net`). There's a second host on Vercel blob — list that too. - Trap: `picsum.photos` redirects to `fastly.picsum.photos`. You'd have to allow both. Don't use picsum on stage. - Data URIs always work. That's why local Vite images survive inlining. Don't show the CYC speakers grid. Q&A only: Google Fonts get added automatically when we inline HTML.
Speaker note: one beat, then let's look at it. Don't tour every tool. Seven tools: `list_speakers`, `get_speaker`, `get_schedule`, `view_schedule_item`, `search_sessions`, `list_events`, `list_tracks`. Widgets call the detail tools via `callServerTool`. Repo is private for now (`nickytonline/cyc2026-mcp-app`). Demos will be public — last slide.
Speaker note: one beat. Then MCPJam. We'll start the tunnel on stage after the local prove. Don't open a config. Don't do first-time Pomerium signup. Course and links are on the last slide.
Speaker note: old web platform, new place to be useful. Next slide is the rest.
Speaker note: hit CSP and the iframe. Resources next — point people to the links without reading every line.
Speaker note: API reference, two ways to inspect and test servers, the MCP App starter used in this talk, the plain MCP server starter, and the free hands-on course. The links are clickable in the HTML deck. [Sources] - https://apps.extensions.modelcontextprotocol.io/api - https://github.com/modelcontextprotocol/inspector - https://mcpjam.com - https://github.com/pomerium/mcp-app-typescript-template - https://github.com/nickytonline/mcp-typescript-template - https://labs.iximiuz.com/courses/securing-mcp-servers-and-mcp-apps-with-pomerium-2d28fcaa [/Sources]
Speaker note: leave this up. Don't read the URLs. Point at the two cards: my site on the left, the live CYC app on the right. Double iframe Q&A: hosts nest your HTML so it can't touch the chat. CSP is the allowlist for that inner frame. That's the whole answer. Everything you saw is open source. I'll share the demo repos. Pomerium is an open-core MCP gateway, among other things — github.com/pomerium/pomerium