Skip to content

Commit 6d61b3b

Browse files
committed
Add basic-server-angular example
1 parent 0bbbfee commit 6d61b3b

File tree

14 files changed

+7982
-3166
lines changed

14 files changed

+7982
-3166
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
dist/
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Example: Basic Server (Angular)
2+
3+
![Screenshot](screenshot.png)
4+
5+
An MCP App example with an Angular UI.
6+
7+
> [!TIP]
8+
> Looking for a vanilla JavaScript example? See [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs)!
9+
10+
## MCP Client Configuration
11+
12+
Add to your MCP client configuration (stdio transport):
13+
14+
```json
15+
{
16+
"mcpServers": {
17+
"basic-angular": {
18+
"command": "npx",
19+
"args": [
20+
"-y",
21+
"--silent",
22+
"--registry=https://registry.npmjs.org/",
23+
"@modelcontextprotocol/server-basic-angular",
24+
"--stdio"
25+
]
26+
}
27+
}
28+
}
29+
```
30+
31+
### Local Development
32+
33+
To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):
34+
35+
```json
36+
{
37+
"mcpServers": {
38+
"basic-angular": {
39+
"command": "bash",
40+
"args": [
41+
"-c",
42+
"cd ~/code/ext-apps/examples/basic-server-angular && npm run build >&2 && node dist/index.js --stdio"
43+
]
44+
}
45+
}
46+
}
47+
```
48+
49+
## Overview
50+
51+
- Tool registration with a linked UI resource
52+
- Angular UI using signals and zoneless change detection
53+
- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink)
54+
55+
## Key Files
56+
57+
- [`server.ts`](server.ts) - MCP server with tool and resource registration
58+
- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.ts`](src/mcp-app.ts) - Angular standalone component using MCP App SDK
59+
60+
## Getting Started
61+
62+
```bash
63+
npm install
64+
npm run dev
65+
```
66+
67+
## How It Works
68+
69+
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
70+
2. When the tool is invoked, the Host renders the UI from the resource.
71+
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
72+
73+
## Build System
74+
75+
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` and `@analogjs/vite-plugin-angular` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Entry point for running the MCP server.
3+
* Run with: npx @modelcontextprotocol/server-basic-react
4+
* Or: node dist/index.js [--stdio]
5+
*/
6+
7+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
8+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11+
import cors from "cors";
12+
import type { Request, Response } from "express";
13+
import { createServer } from "./server.js";
14+
15+
/**
16+
* Starts an MCP server with Streamable HTTP transport in stateless mode.
17+
*
18+
* @param createServer - Factory function that creates a new McpServer instance per request.
19+
*/
20+
export async function startStreamableHTTPServer(
21+
createServer: () => McpServer,
22+
): Promise<void> {
23+
const port = parseInt(process.env.PORT ?? "3001", 10);
24+
25+
const app = createMcpExpressApp({ host: "0.0.0.0" });
26+
app.use(cors());
27+
28+
app.all("/mcp", async (req: Request, res: Response) => {
29+
const server = createServer();
30+
const transport = new StreamableHTTPServerTransport({
31+
sessionIdGenerator: undefined,
32+
});
33+
34+
res.on("close", () => {
35+
transport.close().catch(() => {});
36+
server.close().catch(() => {});
37+
});
38+
39+
try {
40+
await server.connect(transport);
41+
await transport.handleRequest(req, res, req.body);
42+
} catch (error) {
43+
console.error("MCP error:", error);
44+
if (!res.headersSent) {
45+
res.status(500).json({
46+
jsonrpc: "2.0",
47+
error: { code: -32603, message: "Internal server error" },
48+
id: null,
49+
});
50+
}
51+
}
52+
});
53+
54+
const httpServer = app.listen(port, (err) => {
55+
if (err) {
56+
console.error("Failed to start server:", err);
57+
process.exit(1);
58+
}
59+
console.log(`MCP server listening on http://localhost:${port}/mcp`);
60+
});
61+
62+
const shutdown = () => {
63+
console.log("\nShutting down...");
64+
httpServer.close(() => process.exit(0));
65+
};
66+
67+
process.on("SIGINT", shutdown);
68+
process.on("SIGTERM", shutdown);
69+
}
70+
71+
/**
72+
* Starts an MCP server with stdio transport.
73+
*
74+
* @param createServer - Factory function that creates a new McpServer instance.
75+
*/
76+
export async function startStdioServer(
77+
createServer: () => McpServer,
78+
): Promise<void> {
79+
await createServer().connect(new StdioServerTransport());
80+
}
81+
82+
async function main() {
83+
if (process.argv.includes("--stdio")) {
84+
await startStdioServer(createServer);
85+
} else {
86+
await startStreamableHTTPServer(createServer);
87+
}
88+
}
89+
90+
main().catch((e) => {
91+
console.error(e);
92+
process.exit(1);
93+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
7+
<title>Get Time App</title>
8+
<link rel="stylesheet" href="/src/global.css">
9+
</head>
10+
<body>
11+
<app-root></app-root>
12+
<script type="module" src="/src/mcp-app.ts"></script>
13+
</body>
14+
</html>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"name": "@modelcontextprotocol/server-basic-angular",
3+
"version": "1.0.1",
4+
"type": "module",
5+
"description": "Basic MCP App Server example using Angular",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/modelcontextprotocol/ext-apps",
9+
"directory": "examples/basic-server-angular"
10+
},
11+
"license": "MIT",
12+
"main": "dist/server.js",
13+
"types": "dist/server.d.ts",
14+
"bin": {
15+
"mcp-server-basic-angular": "dist/index.js"
16+
},
17+
"files": [
18+
"dist"
19+
],
20+
"exports": {
21+
".": {
22+
"types": "./dist/server.d.ts",
23+
"default": "./dist/server.js"
24+
}
25+
},
26+
"scripts": {
27+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
28+
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
29+
"serve": "bun --watch main.ts",
30+
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
31+
"dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"",
32+
"prepublishOnly": "npm run build"
33+
},
34+
"dependencies": {
35+
"@angular/common": "^21.0.0",
36+
"@angular/core": "^21.0.0",
37+
"@angular/forms": "^21.0.0",
38+
"@angular/platform-browser": "^21.0.0",
39+
"@modelcontextprotocol/ext-apps": "^1.0.0",
40+
"@modelcontextprotocol/sdk": "^1.24.0",
41+
"cors": "^2.8.5",
42+
"express": "^5.1.0",
43+
"rxjs": "^7.8.1",
44+
"tslib": "^2.8.0",
45+
"zod": "^4.1.13"
46+
},
47+
"devDependencies": {
48+
"@analogjs/vite-plugin-angular": "^2.2.3",
49+
"@angular-devkit/build-angular": "^21.0.0",
50+
"@angular/compiler": "^21.0.0",
51+
"@angular/compiler-cli": "^21.0.0",
52+
"@types/cors": "^2.8.19",
53+
"@types/express": "^5.0.0",
54+
"@types/node": "22.10.0",
55+
"concurrently": "^9.2.1",
56+
"cross-env": "^10.1.0",
57+
"typescript": "^5.9.3",
58+
"vite": "^6.0.0",
59+
"vite-plugin-singlefile": "^2.3.0"
60+
}
61+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
2+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
4+
import fs from "node:fs/promises";
5+
import path from "node:path";
6+
7+
// Works both from source (server.ts) and compiled (dist/server.js)
8+
const DIST_DIR = import.meta.filename.endsWith(".ts")
9+
? path.join(import.meta.dirname, "dist")
10+
: import.meta.dirname;
11+
12+
/**
13+
* Creates a new MCP server instance with tools and resources registered.
14+
*/
15+
export function createServer(): McpServer {
16+
const server = new McpServer({
17+
name: "Basic MCP App Server (Angular)",
18+
version: "1.0.0",
19+
});
20+
21+
// Two-part registration: tool + resource, tied together by the resource URI.
22+
const resourceUri = "ui://get-time/mcp-app.html";
23+
24+
// Register a tool with UI metadata. When the host calls this tool, it reads
25+
// `_meta.ui.resourceUri` to know which resource to fetch and render as an
26+
// interactive UI.
27+
registerAppTool(server,
28+
"get-time",
29+
{
30+
title: "Get Time",
31+
description: "Returns the current server time as an ISO 8601 string.",
32+
inputSchema: {},
33+
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
34+
},
35+
async (): Promise<CallToolResult> => {
36+
const time = new Date().toISOString();
37+
return { content: [{ type: "text", text: time }] };
38+
},
39+
);
40+
41+
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
42+
registerAppResource(server,
43+
resourceUri,
44+
resourceUri,
45+
{ mimeType: RESOURCE_MIME_TYPE },
46+
async (): Promise<ReadResourceResult> => {
47+
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
48+
return {
49+
contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
50+
};
51+
},
52+
);
53+
54+
return server;
55+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
:root {
2+
color-scheme: light dark;
3+
4+
/*
5+
* Fallbacks for host style variables used by this app.
6+
* The host may provide these (and many more) via the host context.
7+
*/
8+
--color-text-primary: light-dark(#1f2937, #f3f4f6);
9+
--color-text-inverse: light-dark(#f3f4f6, #1f2937);
10+
--color-text-info: light-dark(#1d4ed8, #60a5fa);
11+
--color-background-primary: light-dark(#ffffff, #1a1a1a);
12+
--color-background-inverse: light-dark(#1a1a1a, #ffffff);
13+
--color-background-info: light-dark(#eff6ff, #1e3a5f);
14+
--color-ring-primary: light-dark(#3b82f6, #60a5fa);
15+
--border-radius-md: 6px;
16+
--border-width-regular: 1px;
17+
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
18+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
19+
--font-weight-normal: 400;
20+
--font-weight-bold: 700;
21+
--font-text-md-size: 1rem;
22+
--font-text-md-line-height: 1.5;
23+
--font-heading-3xl-size: 2.25rem;
24+
--font-heading-3xl-line-height: 1.1;
25+
--font-heading-2xl-size: 1.875rem;
26+
--font-heading-2xl-line-height: 1.2;
27+
--font-heading-xl-size: 1.5rem;
28+
--font-heading-xl-line-height: 1.25;
29+
--font-heading-lg-size: 1.25rem;
30+
--font-heading-lg-line-height: 1.3;
31+
--font-heading-md-size: 1rem;
32+
--font-heading-md-line-height: 1.4;
33+
--font-heading-sm-size: 0.875rem;
34+
--font-heading-sm-line-height: 1.4;
35+
36+
/* Spacing derived from host typography */
37+
--spacing-unit: var(--font-text-md-size);
38+
--spacing-xs: calc(var(--spacing-unit) * 0.25);
39+
--spacing-sm: calc(var(--spacing-unit) * 0.5);
40+
--spacing-md: var(--spacing-unit);
41+
--spacing-lg: calc(var(--spacing-unit) * 1.5);
42+
43+
/* App accent color (customize for your brand) */
44+
--color-accent: #2563eb;
45+
--color-text-on-accent: #ffffff;
46+
}
47+
48+
* {
49+
box-sizing: border-box;
50+
}
51+
52+
html, body {
53+
font-family: var(--font-sans);
54+
font-size: var(--font-text-md-size);
55+
font-weight: var(--font-weight-normal);
56+
line-height: var(--font-text-md-line-height);
57+
color: var(--color-text-primary);
58+
}
59+
60+
h1 {
61+
font-size: var(--font-heading-3xl-size);
62+
line-height: var(--font-heading-3xl-line-height);
63+
}
64+
h2 {
65+
font-size: var(--font-heading-2xl-size);
66+
line-height: var(--font-heading-2xl-line-height);
67+
}
68+
h3 {
69+
font-size: var(--font-heading-xl-size);
70+
line-height: var(--font-heading-xl-line-height);
71+
}
72+
h4 {
73+
font-size: var(--font-heading-lg-size);
74+
line-height: var(--font-heading-lg-line-height);
75+
}
76+
h5 {
77+
font-size: var(--font-heading-md-size);
78+
line-height: var(--font-heading-md-line-height);
79+
}
80+
h6 {
81+
font-size: var(--font-heading-sm-size);
82+
line-height: var(--font-heading-sm-line-height);
83+
}
84+
85+
code, pre, kbd {
86+
font-family: var(--font-mono);
87+
font-size: 1em;
88+
}
89+
90+
b, strong {
91+
font-weight: var(--font-weight-bold);
92+
}

0 commit comments

Comments
 (0)