Skip to content

Commit 1a594bf

Browse files
committed
feat: adding mcp support
1 parent ef823db commit 1a594bf

File tree

9 files changed

+290
-1
lines changed

9 files changed

+290
-1
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mcp-todos.json
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import fs from 'node:fs'
2+
3+
const todosPath = './mcp-todos.json'
4+
5+
// In-memory todos storage
6+
const todos = fs.existsSync(todosPath)
7+
? JSON.parse(fs.readFileSync(todosPath, 'utf8'))
8+
: [
9+
{
10+
id: 1,
11+
title: 'Buy groceries',
12+
},
13+
]
14+
15+
// Subscription callbacks per userID
16+
let subscribers: ((todos: Todo[]) => void)[] = []
17+
18+
export type Todo = {
19+
id: number
20+
title: string
21+
}
22+
23+
// Get the todos for a user
24+
export function getTodos(): Todo[] {
25+
return todos
26+
}
27+
28+
// Add an item to the todos
29+
export function addTodo(title: string) {
30+
todos.push({ id: todos.length + 1, title })
31+
fs.writeFileSync(todosPath, JSON.stringify(todos, null, 2))
32+
notifySubscribers()
33+
}
34+
35+
// Subscribe to cart changes for a user
36+
export function subscribeToTodos(callback: (todos: Todo[]) => void) {
37+
subscribers.push(callback)
38+
callback(todos)
39+
return () => {
40+
subscribers = subscribers.filter((cb) => cb !== callback)
41+
}
42+
}
43+
44+
// Notify all subscribers of a user's cart
45+
function notifySubscribers() {
46+
subscribers.forEach((cb) => cb(todos))
47+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createServerFileRoute } from "@tanstack/react-start/server";
2+
3+
import { addTodo, getTodos, subscribeToTodos } from "@/mcp-todos";
4+
5+
export const ServerRoute = createServerFileRoute("/api/mcp-todos").methods({
6+
GET: () => {
7+
const stream = new ReadableStream({
8+
start(controller) {
9+
setInterval(() => {
10+
controller.enqueue(`event: ping\n\n`);
11+
}, 1000);
12+
const unsubscribe = subscribeToTodos((todos) => {
13+
controller.enqueue(`data: ${JSON.stringify(todos)}\n\n`);
14+
});
15+
const todos = getTodos();
16+
controller.enqueue(`data: ${JSON.stringify(todos)}\n\n`);
17+
return () => unsubscribe();
18+
},
19+
});
20+
return new Response(stream, {
21+
headers: { "Content-Type": "text/event-stream" },
22+
});
23+
},
24+
POST: async ({ request }) => {
25+
const { title } = await request.json();
26+
addTodo(title);
27+
return Response.json(getTodos());
28+
},
29+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useCallback, useState, useEffect } from 'react'
2+
import { createFileRoute } from '@tanstack/react-router'
3+
4+
type Todo = {
5+
id: number
6+
title: string
7+
}
8+
9+
export const Route = createFileRoute('/demo/mcp-todos')({
10+
component: ORPCTodos,
11+
})
12+
13+
function ORPCTodos() {
14+
const [todos, setTodos] = useState<Todo[]>([])
15+
16+
useEffect(() => {
17+
const eventSource = new EventSource('/api/mcp-todos')
18+
eventSource.onmessage = (event) => {
19+
setTodos(JSON.parse(event.data))
20+
}
21+
return () => eventSource.close()
22+
}, [])
23+
24+
const [todo, setTodo] = useState('')
25+
26+
const submitTodo = useCallback(async () => {
27+
await fetch('/api/mcp-todos', {
28+
method: 'POST',
29+
body: JSON.stringify({ title: todo }),
30+
})
31+
setTodo('')
32+
}, [todo])
33+
34+
return (
35+
<div
36+
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-teal-200 to-emerald-900 p-4 text-white"
37+
style={{
38+
backgroundImage:
39+
'radial-gradient(70% 70% at 20% 20%, #07A798 0%, #045C4B 60%, #01251F 100%)',
40+
}}
41+
>
42+
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
43+
<h1 className="text-2xl mb-4">MCP Todos list</h1>
44+
<ul className="mb-4 space-y-2">
45+
{todos?.map((t) => (
46+
<li
47+
key={t.id}
48+
className="bg-white/10 border border-white/20 rounded-lg p-3 backdrop-blur-sm shadow-md"
49+
>
50+
<span className="text-lg text-white">{t.title}</span>
51+
</li>
52+
))}
53+
</ul>
54+
<div className="flex flex-col gap-2">
55+
<input
56+
type="text"
57+
value={todo}
58+
onChange={(e) => setTodo(e.target.value)}
59+
onKeyDown={(e) => {
60+
if (e.key === 'Enter') {
61+
submitTodo()
62+
}
63+
}}
64+
placeholder="Enter a new todo..."
65+
className="w-full px-4 py-3 rounded-lg border border-white/20 bg-white/10 backdrop-blur-sm text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent"
66+
/>
67+
<button
68+
disabled={todo.trim().length === 0}
69+
onClick={submitTodo}
70+
className="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-500/50 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition-colors"
71+
>
72+
Add todo
73+
</button>
74+
</div>
75+
</div>
76+
</div>
77+
)
78+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { createServerFileRoute } from "@tanstack/react-start/server";
3+
import z from "zod";
4+
5+
import { handleMcpRequest } from "@/utils/mcp-handler";
6+
7+
import { addTodo } from "@/mcp-todos";
8+
9+
const server = new McpServer({
10+
name: "start-server",
11+
version: "1.0.0",
12+
});
13+
14+
server.registerTool(
15+
"addTodo",
16+
{
17+
title: "Tool to add a todo to a list of todos",
18+
description: "Add a todo to a list of todos",
19+
inputSchema: {
20+
title: z.string().describe("The title of the todo"),
21+
},
22+
},
23+
({ title }) => ({
24+
content: [{ type: "text", text: String(addTodo(title)) }],
25+
})
26+
);
27+
28+
// server.registerResource(
29+
// "counter-value",
30+
// "count://",
31+
// {
32+
// title: "Counter Resource",
33+
// description: "Returns the current value of the counter",
34+
// },
35+
// async (uri) => {
36+
// return {
37+
// contents: [
38+
// {
39+
// uri: uri.href,
40+
// text: `The counter is at 20!`,
41+
// },
42+
// ],
43+
// };
44+
// }
45+
// );
46+
47+
export const ServerRoute = createServerFileRoute("/mcp").methods({
48+
POST: async ({ request }) => handleMcpRequest(request, server),
49+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2+
import { getEvent } from "@tanstack/react-start/server";
3+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4+
5+
export async function handleMcpRequest(request: Request, server: McpServer) {
6+
const body = await request.json();
7+
const event = getEvent();
8+
const res = event.node.res;
9+
const req = event.node.req;
10+
11+
return new Promise<Response>((resolve, reject) => {
12+
const transport = new StreamableHTTPServerTransport({
13+
sessionIdGenerator: undefined,
14+
});
15+
16+
const cleanup = () => {
17+
transport.close();
18+
server.close();
19+
};
20+
21+
let settled = false;
22+
const safeResolve = (response: Response) => {
23+
if (!settled) {
24+
settled = true;
25+
cleanup();
26+
resolve(response);
27+
}
28+
};
29+
30+
const safeReject = (error: any) => {
31+
if (!settled) {
32+
settled = true;
33+
cleanup();
34+
reject(error);
35+
}
36+
};
37+
38+
res.on("finish", () => safeResolve(new Response(null, { status: 200 })));
39+
res.on("close", () => safeResolve(new Response(null, { status: 200 })));
40+
res.on("error", safeReject);
41+
42+
server
43+
.connect(transport)
44+
.then(() => transport.handleRequest(req, res, body))
45+
.catch((error) => {
46+
console.error("Transport error:", error);
47+
cleanup();
48+
if (!res.headersSent) {
49+
res.writeHead(500, { "Content-Type": "application/json" });
50+
res.end(
51+
JSON.stringify({
52+
jsonrpc: "2.0",
53+
error: { code: -32603, message: "Internal server error" },
54+
id: null,
55+
})
56+
);
57+
}
58+
safeReject(error);
59+
});
60+
});
61+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "MCP",
3+
"phase": "setup",
4+
"description": "Add Model Context Protocol (MCP) support.",
5+
"link": "https://mcp.dev",
6+
"modes": ["file-router"],
7+
"type": "add-on",
8+
"warning": "MCP is still in development and may change significantly or not be compatible with other add-ons.\nThe MCP implementation does not support authentication.",
9+
"routes": [
10+
{
11+
"url": "/demo/mcp-todos",
12+
"name": "MCP",
13+
"path": "src/routes/demo.mcp-todos.tsx",
14+
"jsName": "MCPTodosDemo"
15+
}
16+
],
17+
"dependsOn": ["start"]
18+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"dependencies": {
3+
"@modelcontextprotocol/sdk": "^1.17.0",
4+
"zod": "3.25.76"
5+
}
6+
}

packages/cta-engine/src/create-app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ Use the following commands to start your app:
234234
getPackageManagerScriptCommand(options.packageManager, ['dev']),
235235
)}
236236
237-
Please check the README.md for information on testing, styling, adding routes, etc.${errorStatement}`,
237+
Please read the README.md for information on testing, styling, adding routes, etc.${errorStatement}`,
238238
)
239239
}
240240

0 commit comments

Comments
 (0)