@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.
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "dependencies": {
3
+ "@modelcontextprotocol/sdk": "^1.17.0",
4
+ "zod": "3.25.76"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-framework-react-cra",
3
- "version": "0.16.10",
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.16.10"
26
+ "@tanstack/cta-engine": "0.17.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.13.4",