Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 28 additions & 23 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ A simple app that fetches the current server time and displays it in a clickable

## Prerequisites

- Node.js 18+
- Familiarity with MCP concepts, especially [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) and [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources)
- Familiarity with the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- Node.js 18+

> [!TIP]
> New to building MCP servers? Start with the [official MCP quickstart guide](https://modelcontextprotocol.io/docs/develop/build-server) to learn the core concepts first.

## 1. Project Setup

Expand All @@ -30,7 +34,7 @@ npm init -y
Install dependencies:

```bash
npm install github:modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx
```

Expand Down Expand Up @@ -99,63 +103,64 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
RESOURCE_MIME_TYPE,
type McpUiToolMeta,
RESOURCE_URI_META_KEY,
} from "@modelcontextprotocol/ext-apps";
import cors from "cors";
import express from "express";
import fs from "node:fs/promises";
import path from "node:path";
import * as z from "zod";

const server = new McpServer({
name: "My MCP App Server",
version: "1.0.0",
});

// Two-part registration: tool + resource
// Two-part registration: tool + resource, tied together by the resource URI.
const resourceUri = "ui://get-time/mcp-app.html";

// Register a tool with UI metadata. When the host calls this tool, it reads
// `_meta[RESOURCE_URI_META_KEY]` to know which resource to fetch and render as
// an interactive UI.
server.registerTool(
"get-time",
{
title: "Get Time",
description: "Returns the current server time.",
inputSchema: {},
outputSchema: { time: z.string() },
_meta: { ui: { resourceUri } as McpUiToolMeta }, // Links tool to UI
_meta: { ui: { resourceUri } } as const,
},
async () => {
const time = new Date().toISOString();
return {
content: [{ type: "text", text: time }],
structuredContent: { time },
};
},
);

// Register the resource, which returns the bundled HTML/JavaScript for the UI.
server.registerResource(
resourceUri,
resourceUri,
{ mimeType: "text/html;profile=mcp-app" },
{ mimeType: RESOURCE_MIME_TYPE },
async () => {
const html = await fs.readFile(
path.join(import.meta.dirname, "dist", "mcp-app.html"),
"utf-8",
);
return {
contents: [
{ uri: resourceUri, mimeType: "text/html;profile=mcp-app", text: html },
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
},
);

// Express server for MCP endpoint
const app = express();
app.use(cors());
app.use(express.json());
// Start an Express server that exposes the MCP endpoint.
const expressApp = express();
expressApp.use(cors());
expressApp.use(express.json());

app.post("/mcp", async (req, res) => {
expressApp.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
Expand All @@ -165,7 +170,7 @@ app.post("/mcp", async (req, res) => {
await transport.handleRequest(req, res, req.body);
});

app.listen(3001, (err) => {
expressApp.listen(3001, (err) => {
if (err) {
console.error("Error starting server:", err);
process.exit(1);
Expand All @@ -180,7 +185,7 @@ app.listen(3001, (err) => {
Then, verify your server compiles:

```bash
npx tsc --noEmit server.ts
npx tsc --noEmit
```

No output means success. If you see errors, check for typos in `server.ts`.
Expand Down Expand Up @@ -220,30 +225,30 @@ const app = new App({ name: "Get Time App", version: "1.0.0" });

// Register handlers BEFORE connecting
app.ontoolresult = (result) => {
const { time } = (result.structuredContent as { time?: string }) ?? {};
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
};

// Wire up button click
getTimeBtn.addEventListener("click", async () => {
const result = await app.callServerTool({ name: "get-time", arguments: {} });
const { time } = (result.structuredContent as { time?: string }) ?? {};
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
});

// Connect to host
app.connect(new PostMessageTransport(window.parent));
app.connect();
```

> [!NOTE]
> **Full files:** [`mcp-app.html`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/mcp-app.html), [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/src/mcp-app.ts)

Build the UI:

```bash
npm run build
```

> [!NOTE]
> **Full files:** [`mcp-app.html`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/mcp-app.html), [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/src/mcp-app.ts)

This produces `dist/mcp-app.html` which contains your bundled app:

```console
Expand Down
Loading