@tanstack/cta-framework-react-cra 0.16.10 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/add-ons/mcp/assets/_dot_gitignore.append +1 -0
- package/add-ons/mcp/assets/src/mcp-todos.ts +47 -0
- package/add-ons/mcp/assets/src/routes/api.mcp-todos.ts +29 -0
- package/add-ons/mcp/assets/src/routes/demo.mcp-todos.tsx +78 -0
- package/add-ons/mcp/assets/src/routes/mcp.ts +49 -0
- package/add-ons/mcp/assets/src/utils/mcp-handler.ts +61 -0
- package/add-ons/mcp/info.json +18 -0
- package/add-ons/mcp/package.json +6 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp-todos.json
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/cta-framework-react-cra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "CTA Framework for React (Create React App)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "Jack Herrington <jherr@pobox.com>",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@tanstack/cta-engine": "0.
|
|
26
|
+
"@tanstack/cta-engine": "0.17.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.13.4",
|