Skip to content

Commit 2ce212d

Browse files
authored
Merge pull request #1 from AprovanLabs/agent/backend-dev/8e26222d
feat(mcp-app-server): scaffold MCP App Server package (APR-56)
2 parents c36f160 + 6bd2fe5 commit 2ce212d

7 files changed

Lines changed: 263 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@aprovan/mcp-app-server",
3+
"version": "0.1.0",
4+
"description": "MCP App Server — hosts MCP tools that render interactive UIs in Claude Desktop and Claude web.",
5+
"type": "module",
6+
"main": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
8+
"exports": {
9+
".": {
10+
"types": "./dist/index.d.ts",
11+
"import": "./dist/index.js"
12+
}
13+
},
14+
"scripts": {
15+
"build": "tsup",
16+
"check-types": "tsc --noEmit",
17+
"dev": "tsx watch src/server.ts",
18+
"start": "node dist/server.js",
19+
"typecheck": "tsc --noEmit"
20+
},
21+
"dependencies": {
22+
"@modelcontextprotocol/ext-apps": "^1.7.3",
23+
"@modelcontextprotocol/sdk": "^1.29.0",
24+
"cors": "^2.8.5",
25+
"express": "^5.1.0"
26+
},
27+
"devDependencies": {
28+
"@types/cors": "^2.8.17",
29+
"@types/express": "^5.0.2",
30+
"@types/node": "^22.10.5",
31+
"tsup": "^8.3.5",
32+
"tsx": "^4.19.2",
33+
"typescript": "^5.7.3"
34+
},
35+
"engines": {
36+
"node": ">=20.0.0"
37+
}
38+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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" />
6+
<title>Hello World — Patchwork MCP App</title>
7+
<style>
8+
*,
9+
*::before,
10+
*::after {
11+
box-sizing: border-box;
12+
}
13+
14+
body {
15+
margin: 0;
16+
font-family: system-ui, -apple-system, sans-serif;
17+
background: #f0f4ff;
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
min-height: 100vh;
22+
}
23+
24+
.card {
25+
background: #fff;
26+
border-radius: 1rem;
27+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
28+
padding: 2.5rem 3rem;
29+
text-align: center;
30+
max-width: 420px;
31+
width: 100%;
32+
}
33+
34+
.wave {
35+
font-size: 3rem;
36+
line-height: 1;
37+
margin-bottom: 1rem;
38+
}
39+
40+
h1 {
41+
margin: 0 0 0.75rem;
42+
font-size: 1.75rem;
43+
color: #111827;
44+
}
45+
46+
p {
47+
margin: 0;
48+
color: #6b7280;
49+
font-size: 0.95rem;
50+
line-height: 1.6;
51+
}
52+
53+
.badge {
54+
display: inline-block;
55+
margin-top: 1.5rem;
56+
background: #eff6ff;
57+
color: #3b82f6;
58+
font-size: 0.75rem;
59+
font-weight: 600;
60+
letter-spacing: 0.05em;
61+
text-transform: uppercase;
62+
padding: 0.25rem 0.75rem;
63+
border-radius: 999px;
64+
}
65+
</style>
66+
</head>
67+
<body>
68+
<div class="card">
69+
<div class="wave">👋</div>
70+
<h1>Hello, world!</h1>
71+
<p>
72+
This widget is served by the <strong>Patchwork MCP App Server</strong>.
73+
It renders inline inside Claude Desktop and Claude web via a
74+
<code>StreamableHTTPServerTransport</code>.
75+
</p>
76+
<span class="badge">MCP App</span>
77+
</div>
78+
</body>
79+
</html>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module '*.html' {
2+
const content: string;
3+
export default content;
4+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import {
3+
registerAppTool,
4+
registerAppResource,
5+
RESOURCE_MIME_TYPE,
6+
} from '@modelcontextprotocol/ext-apps/server';
7+
// tsup loader: '.html' -> 'text' inlines the file as a string at build time
8+
import HELLO_WORLD_HTML from './hello-world.html';
9+
10+
const HELLO_WORLD_RESOURCE_URI = 'ui://hello-world/view.html';
11+
12+
/**
13+
* Creates and configures a new McpServer with the hello-world MCP App tool
14+
* and its associated HTML resource.
15+
*
16+
* Each call returns a fresh McpServer instance ready to be connected to a
17+
* transport.
18+
*/
19+
export function createMcpAppServer(): McpServer {
20+
const server = new McpServer({
21+
name: 'patchwork-mcp-app-server',
22+
version: '0.1.0',
23+
});
24+
25+
registerAppTool(
26+
server,
27+
'hello_world',
28+
{
29+
description:
30+
'Display a hello-world widget inline in the conversation. ' +
31+
'Returns a static greeting card rendered as an MCP App.',
32+
_meta: { ui: { resourceUri: HELLO_WORLD_RESOURCE_URI } },
33+
},
34+
async () => ({
35+
content: [
36+
{
37+
type: 'text' as const,
38+
text: 'Hello, world! The widget is rendered inline above.',
39+
},
40+
],
41+
}),
42+
);
43+
44+
registerAppResource(
45+
server,
46+
'Hello World View',
47+
HELLO_WORLD_RESOURCE_URI,
48+
{ description: 'Hello-world HTML widget for the Patchwork MCP App Server demo.' },
49+
async () => ({
50+
contents: [
51+
{
52+
uri: HELLO_WORLD_RESOURCE_URI,
53+
mimeType: RESOURCE_MIME_TYPE,
54+
text: HELLO_WORLD_HTML,
55+
},
56+
],
57+
}),
58+
);
59+
60+
return server;
61+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import cors from 'cors';
2+
import { randomUUID } from 'node:crypto';
3+
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
4+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5+
import { createMcpAppServer } from './index.js';
6+
7+
const PORT = Number(process.env['PORT'] ?? 3000);
8+
const HOST = process.env['HOST'] ?? '0.0.0.0';
9+
10+
const app = createMcpExpressApp({ host: HOST });
11+
12+
// Allow cross-origin requests so Claude web (behind cloudflared) can reach the server
13+
app.use(cors());
14+
15+
/**
16+
* MCP endpoint — stateless mode.
17+
*
18+
* Each HTTP request gets its own McpServer + StreamableHTTPServerTransport pair.
19+
* This is the simplest correct approach for a single-tool hello-world server.
20+
* Swap to a session store if you need multi-turn stateful interactions.
21+
*/
22+
app.all('/mcp', async (req, res) => {
23+
const mcpServer = createMcpAppServer();
24+
const transport = new StreamableHTTPServerTransport({
25+
sessionIdGenerator: () => randomUUID(),
26+
});
27+
28+
// Clean up when the response finishes
29+
res.on('close', () => {
30+
void mcpServer.close();
31+
});
32+
33+
try {
34+
await mcpServer.connect(transport);
35+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
36+
await transport.handleRequest(req, res, req.body);
37+
} catch (err) {
38+
console.error('[mcp] request error', err);
39+
if (!res.headersSent) {
40+
res.status(500).json({ error: 'Internal server error' });
41+
}
42+
}
43+
});
44+
45+
/** Health-check endpoint — useful for cloudflared and load-balancer probes. */
46+
app.get('/health', (_req, res) => {
47+
res.json({ status: 'ok', service: 'patchwork-mcp-app-server' });
48+
});
49+
50+
app.listen(PORT, HOST, () => {
51+
console.log(`MCP App Server listening on http://${HOST}:${PORT}`);
52+
console.log(` POST /mcp — MCP Streamable HTTP endpoint`);
53+
console.log(` GET /health — health check`);
54+
console.log();
55+
console.log('To expose locally via cloudflared:');
56+
console.log(` cloudflared tunnel --url http://localhost:${PORT}`);
57+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "./dist",
5+
"rootDir": "./src",
6+
"moduleResolution": "bundler",
7+
"module": "ESNext"
8+
},
9+
"include": ["src/**/*"]
10+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig } from 'tsup';
2+
3+
export default defineConfig({
4+
entry: ['src/index.ts', 'src/server.ts'],
5+
format: ['esm'],
6+
target: 'node20',
7+
clean: true,
8+
dts: true,
9+
splitting: false,
10+
sourcemap: true,
11+
shims: true,
12+
skipNodeModulesBundle: true,
13+
loader: { '.html': 'text' },
14+
});

0 commit comments

Comments
 (0)