Create Your First MCP App
Source: Dev.to
TL;DR
MCP Apps bring interactive UIs to conversational agents and other MCP clients. This tutorial shows how to create a simple yet powerful app (source code here) that can serve as a template for larger projects. The app will display the last flights that arrived at an airport.
We’ll build the app in three steps:
- Create an MCP server
- Register a tool
- Register a resource and connect it to the tool
Step 1 – Initialize the Project
npm init --yes && npm pkg set type="module"
tsc --init
Create a .gitignore file:
echo -e "node_modules\ndist" > .gitignore
Install the required dependencies:
npm i @modelcontextprotocol/sdk @modelcontextprotocol/ext-apps express zod \
&& npm i -D @types/express nodemon
Step 2 – Create the MCP Server
Create server.ts:
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { registerGetFlightsTool } from "./tools/get-flights.js";
import { registerFlightCardResource } from "./resources/flight-card/flight-card.js";
const server = new McpServer({
name: "My First MCP App",
version: "0.0.1",
});
// Register tools and resources.
registerGetFlightsTool(server);
registerFlightCardResource(server);
// Set up Express server to handle MCP requests.
const app = express();
app.use(express.json());
app.use("/mcp", async (req, res, next) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
res.on("close", () => {
transport.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body).catch(next);
});
// Start the server.
app.listen(3000, () => {
console.log("MCP server listening on http://localhost:3000/mcp");
});
Development workflow with nodemon
Create nodemon.json:
{
"watch": ["."],
"ext": "ts,html,css",
"ignore": ["dist", "node_modules"],
"exec": "tsc && node dist/server.js"
}
Add a dev script to package.json:
{
"scripts": {
"dev": "nodemon"
}
}
Make sure the compiled files go to the dist folder by uncommenting (or adding) the following line in tsconfig.json:
{
"compilerOptions": {
"outDir": "./dist"
}
}
Run the server:
npm run dev
Test the server with MCP Jam
npx @mcpjam/inspector@latest
Add a server with URL http://localhost:3000/mcp. The connection should be successful.
Step 3 – Create the Get Flights Tool
Create tools/get-flights.ts:
// tools/get-flights.ts
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import z from "zod";
import { flightCardResourceUri } from "../resources/flight-card/flight-card.js";
export function registerGetFlightsTool(server: McpServer) {
server.registerTool(
"get-flights",
{
description: "Retrieves flight arrivals for a given airport code",
inputSchema: {
code: z.string().describe("The ICAO airport code, e.g. 'KJFK'"),
},
},
async (input: { code: string }) => {
// Mock flight data – replace with a real API call in production.
const mockFlights = [
{ flightNumber: "AA100", airline: "American Airlines" },
{ flightNumber: "DL200", airline: "Delta Airlines" },
{ flightNumber: "UA300", airline: "United Airlines" },
];
return {
content: [{ type: "text", text: JSON.stringify(mockFlights, null, 2) }],
structuredContent: { flights: mockFlights },
};
}
);
}
The tool is already registered in server.ts (see Step 2). After restarting the server, the tool can be invoked from any MCP‑compatible chat playground.
Step 4 – Add a UI with Vite and the Flight‑Card Resource
Install additional dependencies
npm i vite vite-plugin-singlefile glob
Vite configuration
Create vite.config.js:
// vite.config.js
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [viteSingleFile()],
build: {
outDir: "dist",
emptyOutDir: true,
rollupOptions: {
input: {
"flight-card": "resources/flight-card/mcp-app/flight-card-mcp-app.html",
// Add more HTML resources here as needed
},
},
},
});
Update nodemon.json to build the UI before starting the server:
{
"watch": ["."],
"ext": "ts,html,css",
"ignore": ["dist", "node_modules"],
"exec": "vite build && tsc && node dist/server.js"
}
Flight‑Card resource implementation
Create resources/flight-card/flight-card.ts:
// resources/flight-card/flight-card.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import fs from "node:fs/promises";
import path from "node:path";
export const flightCardResourceUri = "ui://flight-card.html";
export function registerFlightCardResource(server: McpServer) {
server.registerResource(
flightCardResourceUri,
flightCardResourceUri,
{},
async () => {
const html = await fs.readFile(
path.join(import.meta.dirname, "mcp-app/flight-card-mcp-app.html"),
"utf-8"
);
return {
contents: [
{
uri: flightCardResourceUri,
mimeType: "text/html;profile=mcp-app",
text: html,
},
],
};
}
);
}
The resource is already registered in server.ts (see Step 2).
Flight‑Card HTML template
Create resources/flight-card/mcp-app/flight-card-mcp-app.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flight Cards</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f0f2f5;
padding: 40px;
}
.container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.card {
background: #fff;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.flight-number {
font-weight: bold;
font-size: 1.2rem;
}
.airline {
color: #555;
}
</style>
</head>
<body>
<h1>Flight Cards</h1>
<div id="cards" class="container"></div>
<script>
// Placeholder script – replace with real rendering logic.
const mockFlights = [
{ flightNumber: "AA100", airline: "American Airlines" },
{ flightNumber: "DL200", airline: "Delta Airlines" },
{ flightNumber: "UA300", airline: "United Airlines" },
];
const container = document.getElementById("cards");
mockFlights.forEach((f) => {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<div class="flight-number">${f.flightNumber}</div>
<div class="airline">${f.airline}</div>
`;
container?.appendChild(card);
});
</script>
</body>
</html>
(You can replace the inline script with a separate flight-card-mcp-app.ts module if desired.)
Final Check
-
Run the development server:
npm run dev -
Open MCP Jam (
npx @mcpjam/inspector@latest) and connect tohttp://localhost:3000/mcp. -
Invoke the
get‑flightstool with an airport code (e.g.,KJFK). -
The tool returns mock flight data, and the UI defined in the flight‑card resource renders it as interactive cards.
Congratulations! You’ve built a functional MCP app with a backend tool and a front‑end UI. Use this project as a foundation for more complex integrations.