upfynai-code 2.9.0 → 2.9.2

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.
Files changed (229) hide show
  1. package/README.md +91 -66
  2. package/client/dist/api-docs.html +838 -0
  3. package/client/dist/assets/AppContent-BXZDeSIC.js +545 -0
  4. package/client/dist/assets/CanvasFullScreen-mnpCnLZ9.js +1 -0
  5. package/client/dist/assets/CanvasWorkspace-4CqmjAVQ.js +163 -0
  6. package/client/dist/assets/DashboardPanel-zFIFlw56.js +1 -0
  7. package/client/dist/assets/FileTree-B0c_GaB3.js +1 -0
  8. package/client/dist/assets/GitPanel-DUP4zVU4.js +2 -0
  9. package/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  10. package/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  11. package/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  12. package/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  13. package/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  14. package/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  15. package/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  16. package/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  17. package/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  18. package/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  19. package/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  20. package/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  21. package/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  22. package/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  23. package/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  24. package/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  25. package/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  26. package/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  27. package/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  28. package/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  29. package/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  30. package/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  31. package/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  32. package/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  33. package/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  34. package/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  35. package/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  36. package/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  37. package/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  38. package/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  39. package/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  40. package/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  41. package/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  42. package/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  43. package/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  44. package/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  45. package/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  46. package/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  47. package/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  48. package/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  49. package/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  50. package/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  51. package/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  52. package/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  53. package/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  54. package/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  55. package/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  56. package/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  57. package/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  58. package/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  59. package/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  60. package/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  61. package/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  62. package/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  63. package/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  64. package/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  65. package/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  66. package/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  67. package/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  68. package/client/dist/assets/LoginModal-BRycfsyD.js +13 -0
  69. package/client/dist/assets/MarkdownPreview-DHmk3qzu.js +1 -0
  70. package/client/dist/assets/MermaidBlock-BuBc_G-F.js +2 -0
  71. package/client/dist/assets/Onboarding-BcnaZZ0o.js +1 -0
  72. package/client/dist/assets/PreviewPanel-CqCa92Tf.js +32 -0
  73. package/client/dist/assets/SetupForm-S0g6u5yT.js +1 -0
  74. package/client/dist/assets/WorkflowsPanel-CouH9JDO.js +1 -0
  75. package/client/dist/assets/index-BFuqS0tY.css +1 -0
  76. package/client/dist/assets/index-CNDcVl2g.js +68 -0
  77. package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
  78. package/client/dist/assets/vendor-canvas-BZV40eAE.css +1 -0
  79. package/client/dist/assets/vendor-canvas-D39yWul6.js +49 -0
  80. package/client/dist/assets/vendor-codemirror-CbtmxxaB.js +35 -0
  81. package/client/dist/assets/vendor-diff-DNQpbhrT.js +69 -0
  82. package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
  83. package/client/dist/assets/vendor-icons-BaD0x9SL.js +711 -0
  84. package/client/dist/assets/vendor-markdown-CimbIo6Y.js +296 -0
  85. package/client/dist/assets/vendor-mermaid-CH7SGc99.js +2556 -0
  86. package/client/dist/assets/vendor-react-96lCPsRK.js +67 -0
  87. package/client/dist/assets/vendor-syntax-DuHI9Ok6.js +16 -0
  88. package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
  89. package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
  90. package/client/dist/clear-cache.html +85 -0
  91. package/client/dist/convert-icons.md +53 -0
  92. package/client/dist/favicon.png +0 -0
  93. package/client/dist/favicon.svg +5 -0
  94. package/client/dist/generate-icons.js +49 -0
  95. package/client/dist/icons/claude-ai-icon.svg +1 -0
  96. package/client/dist/icons/codex-white.svg +3 -0
  97. package/client/dist/icons/codex.svg +3 -0
  98. package/client/dist/icons/cursor-white.svg +12 -0
  99. package/client/dist/icons/cursor.svg +1 -0
  100. package/client/dist/icons/icon-128x128.png +0 -0
  101. package/client/dist/icons/icon-128x128.svg +5 -0
  102. package/client/dist/icons/icon-144x144.png +0 -0
  103. package/client/dist/icons/icon-144x144.svg +5 -0
  104. package/client/dist/icons/icon-152x152.png +0 -0
  105. package/client/dist/icons/icon-152x152.svg +5 -0
  106. package/client/dist/icons/icon-192x192.png +0 -0
  107. package/client/dist/icons/icon-192x192.svg +5 -0
  108. package/client/dist/icons/icon-384x384.png +0 -0
  109. package/client/dist/icons/icon-384x384.svg +5 -0
  110. package/client/dist/icons/icon-512x512.png +0 -0
  111. package/client/dist/icons/icon-512x512.svg +5 -0
  112. package/client/dist/icons/icon-72x72.png +0 -0
  113. package/client/dist/icons/icon-72x72.svg +5 -0
  114. package/client/dist/icons/icon-96x96.png +0 -0
  115. package/client/dist/icons/icon-96x96.svg +5 -0
  116. package/client/dist/icons/icon-template.svg +5 -0
  117. package/client/dist/index.html +119 -0
  118. package/client/dist/logo-128.png +0 -0
  119. package/client/dist/logo-256.png +0 -0
  120. package/client/dist/logo-32.png +0 -0
  121. package/client/dist/logo-512.png +0 -0
  122. package/client/dist/logo-64.png +0 -0
  123. package/client/dist/logo.svg +14 -0
  124. package/client/dist/manifest.json +61 -0
  125. package/client/dist/mcp-docs.html +108 -0
  126. package/client/dist/offline.html +84 -0
  127. package/client/dist/screenshots/cli-selection.png +0 -0
  128. package/client/dist/screenshots/desktop-main.png +0 -0
  129. package/client/dist/screenshots/mobile-chat.png +0 -0
  130. package/client/dist/screenshots/tools-modal.png +0 -0
  131. package/client/dist/sw.js +82 -0
  132. package/commands/upfynai-connect.md +59 -0
  133. package/commands/upfynai-disconnect.md +31 -0
  134. package/commands/upfynai-doctor.md +99 -0
  135. package/commands/upfynai-export.md +49 -0
  136. package/commands/upfynai-local.md +82 -0
  137. package/commands/upfynai-status.md +75 -0
  138. package/commands/upfynai-stop.md +49 -0
  139. package/commands/upfynai-uninstall.md +58 -0
  140. package/commands/upfynai.md +69 -0
  141. package/package.json +143 -82
  142. package/scripts/build-client.js +17 -0
  143. package/scripts/fix-node-pty.js +67 -0
  144. package/scripts/install-commands.js +78 -0
  145. package/server/agent-loop.js +242 -0
  146. package/server/auto-compact.js +99 -0
  147. package/server/claude-sdk.js +797 -0
  148. package/server/cli-ui.js +785 -0
  149. package/server/cli.js +596 -0
  150. package/server/constants/config.js +31 -0
  151. package/server/cursor-cli.js +270 -0
  152. package/server/database/auth.db +0 -0
  153. package/server/database/db.js +1391 -0
  154. package/server/database/init.sql +70 -0
  155. package/server/index.js +3799 -0
  156. package/server/load-env.js +26 -0
  157. package/server/mcp-server.js +621 -0
  158. package/server/middleware/auth.js +176 -0
  159. package/server/middleware/relayHelpers.js +44 -0
  160. package/server/middleware/sandboxRouter.js +174 -0
  161. package/server/openai-codex.js +403 -0
  162. package/server/openrouter.js +137 -0
  163. package/server/projects.js +1807 -0
  164. package/server/provider-factory.js +174 -0
  165. package/server/relay-client.js +379 -0
  166. package/server/routes/agent.js +1226 -0
  167. package/server/routes/auth.js +554 -0
  168. package/server/routes/canvas.js +53 -0
  169. package/server/routes/cli-auth.js +263 -0
  170. package/server/routes/codex.js +396 -0
  171. package/server/routes/commands.js +707 -0
  172. package/server/routes/composio.js +176 -0
  173. package/server/routes/cursor.js +770 -0
  174. package/server/routes/dashboard.js +295 -0
  175. package/server/routes/git.js +1208 -0
  176. package/server/routes/keys.js +34 -0
  177. package/server/routes/mcp-utils.js +48 -0
  178. package/server/routes/mcp.js +661 -0
  179. package/server/routes/payments.js +227 -0
  180. package/server/routes/projects.js +655 -0
  181. package/server/routes/sessions.js +146 -0
  182. package/server/routes/settings.js +261 -0
  183. package/server/routes/taskmaster.js +1928 -0
  184. package/server/routes/user.js +106 -0
  185. package/server/routes/vapi-chat.js +624 -0
  186. package/server/routes/voice.js +235 -0
  187. package/server/routes/webhooks.js +166 -0
  188. package/server/routes/workflows.js +312 -0
  189. package/server/sandbox.js +120 -0
  190. package/server/services/composio.js +204 -0
  191. package/server/services/sessionRegistry.js +139 -0
  192. package/server/services/whisperService.js +84 -0
  193. package/server/services/workflowScheduler.js +206 -0
  194. package/server/tests/relay-flow.test.js +570 -0
  195. package/server/tests/sessions.test.js +259 -0
  196. package/server/utils/commandParser.js +303 -0
  197. package/server/utils/email.js +61 -0
  198. package/server/utils/gitConfig.js +24 -0
  199. package/server/utils/mcp-detector.js +198 -0
  200. package/server/utils/taskmaster-websocket.js +129 -0
  201. package/shared/integrationCatalog.d.ts +12 -0
  202. package/shared/integrationCatalog.js +172 -0
  203. package/shared/modelConstants.js +96 -0
  204. package/bin/cli.js +0 -97
  205. package/dist/agents/claude.js +0 -229
  206. package/dist/agents/codex.js +0 -48
  207. package/dist/agents/cursor.js +0 -48
  208. package/dist/agents/detect.js +0 -51
  209. package/dist/agents/exec.js +0 -31
  210. package/dist/agents/files.js +0 -105
  211. package/dist/agents/git.js +0 -18
  212. package/dist/agents/gitagent.js +0 -67
  213. package/dist/agents/index.js +0 -88
  214. package/dist/agents/shell.js +0 -38
  215. package/dist/agents/utils.js +0 -136
  216. package/scripts/postinstall.js +0 -9
  217. package/scripts/prepublish.js +0 -58
  218. package/src/animation.js +0 -228
  219. package/src/auth.js +0 -122
  220. package/src/config.js +0 -40
  221. package/src/connect.js +0 -416
  222. package/src/launch.js +0 -78
  223. package/src/mcp.js +0 -57
  224. package/src/permissions.js +0 -140
  225. package/src/persistent-shell.js +0 -261
  226. package/src/server.js +0 -54
  227. /package/{dist → shared}/gitagent/index.js +0 -0
  228. /package/{dist → shared}/gitagent/parser.js +0 -0
  229. /package/{dist → shared}/gitagent/prompt-builder.js +0 -0
@@ -0,0 +1,26 @@
1
+ // Load environment variables from .env before other imports execute.
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ try {
11
+ const envPath = path.join(__dirname, '../.env');
12
+ const envFile = fs.readFileSync(envPath, 'utf8');
13
+ envFile.split('\n').forEach(line => {
14
+ const trimmedLine = line.trim();
15
+ if (trimmedLine && !trimmedLine.startsWith('#')) {
16
+ const [key, ...valueParts] = trimmedLine.split('=');
17
+ if (key && valueParts.length > 0 && !process.env[key]) {
18
+ process.env[key] = valueParts.join('=').trim();
19
+ }
20
+ }
21
+ });
22
+ } catch (e) {
23
+ if (!process.env.VERCEL) {
24
+ console.log('No .env file found or error reading it:', e.message);
25
+ }
26
+ }
@@ -0,0 +1,621 @@
1
+ /**
2
+ * Upfyn-Code MCP Server
3
+ *
4
+ * Exposes the app's capabilities as MCP tools and resources so any MCP-compatible
5
+ * client (ChatGPT, Claude Desktop, Cursor, etc.) can control the app.
6
+ *
7
+ * Transport: Streamable HTTP mounted at /mcp on the Express server
8
+ *
9
+ * Tools:
10
+ * - send-prompt: Send a message to Claude and get a response
11
+ * - list-projects: List all available projects
12
+ * - list-sessions: List sessions for a project
13
+ * - get-session-messages: Get messages from a session
14
+ * - get-canvas-state: Get current canvas nodes
15
+ * - add-canvas-node: Add a node to the canvas
16
+ * - clear-canvas: Clear all canvas nodes
17
+ * - create-session: Start a new Claude session
18
+ * - abort-session: Stop an active session
19
+ * - read-file: Read a file from a project
20
+ * - list-files: List files in a project directory
21
+ *
22
+ * Resources:
23
+ * - upfynai://canvas/state: Current canvas state
24
+ * - upfynai://sessions/active: Active sessions list
25
+ */
26
+
27
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
28
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
29
+ import { z } from 'zod';
30
+ import crypto from 'crypto';
31
+ import jwt from 'jsonwebtoken';
32
+ import { userDb, apiKeysDb, relayTokensDb } from './database/db.js';
33
+
34
+ import { IS_PLATFORM } from './constants/config.js';
35
+ const JWT_SECRET = process.env.JWT_SECRET?.trim() || (IS_PLATFORM ? crypto.randomBytes(32).toString('hex') : (() => { throw new Error('JWT_SECRET required'); })());
36
+
37
+ // In-memory canvas state (Excalidraw elements, synced via WebSocket with browser clients)
38
+ let canvasElements = [];
39
+ const canvasListeners = new Set();
40
+
41
+ export function getCanvasElements() {
42
+ return canvasElements;
43
+ }
44
+
45
+ export function setCanvasElements(elements) {
46
+ canvasElements = elements;
47
+ notifyCanvasListeners();
48
+ }
49
+
50
+ export function addCanvasElement(element) {
51
+ canvasElements.push(element);
52
+ notifyCanvasListeners();
53
+ }
54
+
55
+ export function clearCanvas() {
56
+ canvasElements = [];
57
+ notifyCanvasListeners();
58
+ }
59
+
60
+ export function onCanvasChange(listener) {
61
+ canvasListeners.add(listener);
62
+ return () => canvasListeners.delete(listener);
63
+ }
64
+
65
+ function notifyCanvasListeners() {
66
+ for (const listener of canvasListeners) {
67
+ try { listener(canvasElements); } catch (e) { /* ignore */ }
68
+ }
69
+ }
70
+
71
+ // Helper: create an Excalidraw rectangle + text element pair
72
+ function createExcalidrawNote(text, { x = 100, y = 100, width = 300, height = 100, label = '' } = {}) {
73
+ const id = `el-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
74
+ const textId = `${id}-text`;
75
+ const fullText = label ? `${label}\n\n${text}` : text;
76
+
77
+ const rect = {
78
+ id,
79
+ type: 'rectangle',
80
+ x,
81
+ y,
82
+ width,
83
+ height,
84
+ strokeColor: '#a855f7',
85
+ backgroundColor: '#1a1a2e',
86
+ fillStyle: 'solid',
87
+ strokeWidth: 2,
88
+ roughness: 0,
89
+ opacity: 100,
90
+ angle: 0,
91
+ groupIds: [],
92
+ roundness: { type: 3 },
93
+ boundElements: [{ id: textId, type: 'text' }],
94
+ isDeleted: false,
95
+ version: 1,
96
+ versionNonce: Math.floor(Math.random() * 1e9),
97
+ };
98
+
99
+ const textEl = {
100
+ id: textId,
101
+ type: 'text',
102
+ x: x + 10,
103
+ y: y + 10,
104
+ width: width - 20,
105
+ height: height - 20,
106
+ text: fullText,
107
+ fontSize: 16,
108
+ fontFamily: 1,
109
+ textAlign: 'left',
110
+ verticalAlign: 'top',
111
+ containerId: id,
112
+ originalText: fullText,
113
+ isDeleted: false,
114
+ version: 1,
115
+ versionNonce: Math.floor(Math.random() * 1e9),
116
+ };
117
+
118
+ return [rect, textEl];
119
+ }
120
+
121
+ /**
122
+ * Create and configure the MCP server with all tools and resources
123
+ */
124
+ export function createMcpServer({ getProjects, getSessions, getSessionMessages, queryClaudeSDK, abortClaudeSDKSession, getActiveClaudeSDKSessions, connectedClients }) {
125
+ const server = new McpServer({
126
+ name: 'upfynai-code',
127
+ version: '2.0.0',
128
+ });
129
+
130
+ // ═══════════════════════════════════════════
131
+ // TOOLS
132
+ // ═══════════════════════════════════════════
133
+
134
+ // Send a prompt to Claude
135
+ server.tool(
136
+ 'send-prompt',
137
+ 'Send a message to Claude and get a streaming response. The response will appear on the canvas and in chat.',
138
+ {
139
+ prompt: z.string().describe('The message to send to Claude'),
140
+ projectPath: z.string().optional().describe('Project directory path for context'),
141
+ sessionId: z.string().optional().describe('Session ID to resume, or omit for new session'),
142
+ model: z.string().optional().describe('Model to use (e.g. claude-sonnet-4-5-20250929)'),
143
+ },
144
+ async ({ prompt, projectPath, sessionId, model }) => {
145
+ return new Promise((resolve) => {
146
+ const responseChunks = [];
147
+ let resolved = false;
148
+
149
+ // Create a mock WebSocket writer that collects the response
150
+ const mockWs = {
151
+ readyState: 1, // WebSocket.OPEN
152
+ send: (data) => {
153
+ try {
154
+ const msg = typeof data === 'string' ? JSON.parse(data) : data;
155
+ if (msg.type === 'claude-response' && msg.data?.text) {
156
+ responseChunks.push(msg.data.text);
157
+ }
158
+ if (msg.type === 'claude-complete' && !resolved) {
159
+ resolved = true;
160
+ const fullText = responseChunks.join('');
161
+ // Add to canvas as Excalidraw elements
162
+ const yOffset = canvasElements.length * 60;
163
+ const els = createExcalidrawNote(fullText, { y: 100 + yOffset, label: 'Claude' });
164
+ els.forEach(el => addCanvasElement(el));
165
+ // Broadcast canvas update to browser clients
166
+ broadcastToClients(connectedClients, {
167
+ type: 'canvas-update',
168
+ elements: canvasElements,
169
+ });
170
+ resolve({
171
+ content: [{ type: 'text', text: fullText || 'No response received.' }],
172
+ });
173
+ }
174
+ if (msg.type === 'error' && !resolved) {
175
+ resolved = true;
176
+ resolve({
177
+ content: [{ type: 'text', text: `Error: ${msg.error || 'Unknown error'}` }],
178
+ isError: true,
179
+ });
180
+ }
181
+ } catch (e) { /* ignore parse errors */ }
182
+ },
183
+ };
184
+
185
+ // Add user prompt to canvas as Excalidraw elements
186
+ const userYOffset = canvasElements.length * 60;
187
+ const userEls = createExcalidrawNote(prompt, { y: 100 + userYOffset, label: 'You (MCP)' });
188
+ userEls.forEach(el => addCanvasElement(el));
189
+
190
+ // Broadcast the user elements
191
+ broadcastToClients(connectedClients, {
192
+ type: 'canvas-update',
193
+ elements: canvasElements,
194
+ });
195
+
196
+ const options = {
197
+ projectPath: projectPath || process.cwd(),
198
+ cwd: projectPath || process.cwd(),
199
+ sessionId: sessionId || undefined,
200
+ resume: Boolean(sessionId),
201
+ model: model || undefined,
202
+ };
203
+
204
+ queryClaudeSDK(prompt, options, mockWs).catch((err) => {
205
+ if (!resolved) {
206
+ resolved = true;
207
+ resolve({
208
+ content: [{ type: 'text', text: `SDK Error: ${err.message}` }],
209
+ isError: true,
210
+ });
211
+ }
212
+ });
213
+
214
+ // Safety timeout
215
+ setTimeout(() => {
216
+ if (!resolved) {
217
+ resolved = true;
218
+ const partial = responseChunks.join('');
219
+ resolve({
220
+ content: [{ type: 'text', text: partial || 'Response timed out after 5 minutes.' }],
221
+ });
222
+ }
223
+ }, 300000);
224
+ });
225
+ }
226
+ );
227
+
228
+ // List projects
229
+ server.tool(
230
+ 'list-projects',
231
+ 'List all available projects that Claude can work on',
232
+ {},
233
+ async () => {
234
+ try {
235
+ const projects = await getProjects();
236
+ const summary = projects.map((p) => ({
237
+ name: p.name,
238
+ displayName: p.displayName || p.name,
239
+ path: p.fullPath || p.path,
240
+ sessions: (p.sessions || []).length,
241
+ }));
242
+ return {
243
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
244
+ };
245
+ } catch (err) {
246
+ return {
247
+ content: [{ type: 'text', text: `Error listing projects: ${err.message}` }],
248
+ isError: true,
249
+ };
250
+ }
251
+ }
252
+ );
253
+
254
+ // List sessions
255
+ server.tool(
256
+ 'list-sessions',
257
+ 'List sessions for a project',
258
+ {
259
+ projectName: z.string().describe('Project name to list sessions for'),
260
+ limit: z.number().optional().describe('Max sessions to return (default 10)'),
261
+ },
262
+ async ({ projectName, limit }) => {
263
+ try {
264
+ const sessions = await getSessions(projectName, limit || 10, 0);
265
+ return {
266
+ content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }],
267
+ };
268
+ } catch (err) {
269
+ return {
270
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
271
+ isError: true,
272
+ };
273
+ }
274
+ }
275
+ );
276
+
277
+ // Get session messages
278
+ server.tool(
279
+ 'get-session-messages',
280
+ 'Get message history from a specific session',
281
+ {
282
+ projectName: z.string().describe('Project name'),
283
+ sessionId: z.string().describe('Session ID'),
284
+ limit: z.number().optional().describe('Max messages (default 50)'),
285
+ },
286
+ async ({ projectName, sessionId, limit }) => {
287
+ try {
288
+ const messages = await getSessionMessages(projectName, sessionId, limit || 50, 0);
289
+ return {
290
+ content: [{ type: 'text', text: JSON.stringify(messages, null, 2) }],
291
+ };
292
+ } catch (err) {
293
+ return {
294
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
295
+ isError: true,
296
+ };
297
+ }
298
+ }
299
+ );
300
+
301
+ // Get canvas state
302
+ server.tool(
303
+ 'get-canvas-state',
304
+ 'Get the current Upfyn-Canvas state including all elements',
305
+ {},
306
+ async () => {
307
+ return {
308
+ content: [{
309
+ type: 'text',
310
+ text: JSON.stringify({
311
+ elementCount: canvasElements.length,
312
+ elements: canvasElements,
313
+ }, null, 2),
314
+ }],
315
+ };
316
+ }
317
+ );
318
+
319
+ // Add canvas node (creates Upfyn-Canvas rectangle + text)
320
+ server.tool(
321
+ 'add-canvas-node',
322
+ 'Add a visual note to the Upfyn-Canvas whiteboard (rectangle with text)',
323
+ {
324
+ text: z.string().describe('Text content for the note'),
325
+ label: z.string().optional().describe('Label for the note (default: "MCP Note")'),
326
+ x: z.number().optional().describe('X position (default: 100)'),
327
+ y: z.number().optional().describe('Y position (default: auto)'),
328
+ },
329
+ async ({ text, label, x, y }) => {
330
+ const yPos = y ?? (100 + canvasElements.length * 60);
331
+ const els = createExcalidrawNote(text, { x: x ?? 100, y: yPos, label: label || 'MCP Note' });
332
+ els.forEach(el => addCanvasElement(el));
333
+ broadcastToClients(connectedClients, {
334
+ type: 'canvas-update',
335
+ elements: canvasElements,
336
+ });
337
+ return {
338
+ content: [{ type: 'text', text: `Note added to canvas: ${els[0].id}` }],
339
+ };
340
+ }
341
+ );
342
+
343
+ // Clear canvas
344
+ server.tool(
345
+ 'clear-canvas',
346
+ 'Clear all elements from the Upfyn-Canvas whiteboard',
347
+ {},
348
+ async () => {
349
+ clearCanvas();
350
+ broadcastToClients(connectedClients, {
351
+ type: 'canvas-update',
352
+ elements: [],
353
+ });
354
+ return {
355
+ content: [{ type: 'text', text: 'Canvas cleared.' }],
356
+ };
357
+ }
358
+ );
359
+
360
+ // Update full canvas scene
361
+ server.tool(
362
+ 'update-canvas-scene',
363
+ 'Replace the entire Upfyn-Canvas with new elements',
364
+ {
365
+ elements: z.array(z.object({}).passthrough()).describe('Array of canvas elements'),
366
+ },
367
+ async ({ elements }) => {
368
+ setCanvasElements(elements);
369
+ broadcastToClients(connectedClients, {
370
+ type: 'canvas-update',
371
+ elements: canvasElements,
372
+ });
373
+ return {
374
+ content: [{ type: 'text', text: `Canvas updated with ${elements.length} elements.` }],
375
+ };
376
+ }
377
+ );
378
+
379
+ // Get active sessions
380
+ server.tool(
381
+ 'get-active-sessions',
382
+ 'Get list of currently active Claude sessions',
383
+ {},
384
+ async () => {
385
+ try {
386
+ const sessions = getActiveClaudeSDKSessions();
387
+ return {
388
+ content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }],
389
+ };
390
+ } catch (err) {
391
+ return {
392
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
393
+ isError: true,
394
+ };
395
+ }
396
+ }
397
+ );
398
+
399
+ // Abort session
400
+ server.tool(
401
+ 'abort-session',
402
+ 'Stop an active Claude session',
403
+ {
404
+ sessionId: z.string().describe('Session ID to abort'),
405
+ },
406
+ async ({ sessionId }) => {
407
+ try {
408
+ const result = await abortClaudeSDKSession(sessionId);
409
+ return {
410
+ content: [{ type: 'text', text: result ? `Session ${sessionId} aborted.` : `Session ${sessionId} not found or already stopped.` }],
411
+ };
412
+ } catch (err) {
413
+ return {
414
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
415
+ isError: true,
416
+ };
417
+ }
418
+ }
419
+ );
420
+
421
+ // ═══════════════════════════════════════════
422
+ // RESOURCES
423
+ // ═══════════════════════════════════════════
424
+
425
+ server.resource(
426
+ 'canvas-state',
427
+ 'upfynai://canvas/state',
428
+ {
429
+ description: 'Current Upfyn-Canvas state with all elements',
430
+ mimeType: 'application/json',
431
+ },
432
+ async (uri) => ({
433
+ contents: [{
434
+ uri: uri.href,
435
+ text: JSON.stringify({ elementCount: canvasElements.length, elements: canvasElements }, null, 2),
436
+ }],
437
+ })
438
+ );
439
+
440
+ server.resource(
441
+ 'active-sessions',
442
+ 'upfynai://sessions/active',
443
+ {
444
+ description: 'Currently active Claude sessions',
445
+ mimeType: 'application/json',
446
+ },
447
+ async (uri) => ({
448
+ contents: [{
449
+ uri: uri.href,
450
+ text: JSON.stringify(getActiveClaudeSDKSessions(), null, 2),
451
+ }],
452
+ })
453
+ );
454
+
455
+ return server;
456
+ }
457
+
458
+ /**
459
+ * Mount the MCP server on an Express app at /mcp
460
+ */
461
+ export async function mountMcpServer(app, mcpServer, mcpServerFactory) {
462
+ // On Vercel serverless, in-memory session state is lost between invocations.
463
+ // Use per-request stateless MCP servers on Vercel; session-based on local.
464
+ const isServerless = !!process.env.VERCEL;
465
+ const transports = new Map();
466
+
467
+ // MCP authentication middleware — cookie → Bearer → API key → query param
468
+ const authenticateMcp = async (req, res, next) => {
469
+ // 1. Try httpOnly cookie (browser sessions)
470
+ if (req.cookies?.session) {
471
+ try {
472
+ const decoded = jwt.verify(req.cookies.session, JWT_SECRET);
473
+ const user = await userDb.getUserById(decoded.userId);
474
+ if (user) { req.user = user; return next(); }
475
+ } catch (e) { /* fall through */ }
476
+ }
477
+
478
+ // 2. Try Bearer token — supports JWT, relay token (upfyn_/rt_), or API key (up-cli-)
479
+ const authHeader = req.headers['authorization'];
480
+ const token = authHeader && authHeader.split(' ')[1];
481
+ if (token) {
482
+ // 2a. Relay token (upfyn_xxx or legacy rt_xxx) — same token used for CLI connect
483
+ if (token.startsWith('upfyn_') || token.startsWith('rt_')) {
484
+ try {
485
+ const tokenData = await relayTokensDb.validateToken(token);
486
+ if (tokenData) {
487
+ const user = await userDb.getUserById(tokenData.user_id);
488
+ if (user) { req.user = user; return next(); }
489
+ }
490
+ } catch (e) { /* fall through */ }
491
+ }
492
+ // 2b. API key (up-cli-xxx)
493
+ if (token.startsWith('up-cli-')) {
494
+ try {
495
+ const user = await apiKeysDb.validateApiKey(token);
496
+ if (user) { req.user = user; return next(); }
497
+ } catch (e) { /* fall through */ }
498
+ }
499
+ // 2c. JWT token
500
+ try {
501
+ const decoded = jwt.verify(token, JWT_SECRET);
502
+ const user = await userDb.getUserById(decoded.userId);
503
+ if (user) { req.user = user; return next(); }
504
+ } catch (e) { /* fall through */ }
505
+ }
506
+
507
+ // 3. Try API key header (MCP clients: ChatGPT, Claude Desktop, Cursor, etc.)
508
+ const apiKey = req.headers['x-api-key'];
509
+ if (apiKey) {
510
+ try {
511
+ const user = await apiKeysDb.validateApiKey(apiKey);
512
+ if (user) { req.user = user; return next(); }
513
+ } catch (e) { /* ignore */ }
514
+ }
515
+
516
+ // 4. Try query param token (SSE EventSource fallback)
517
+ if (req.query.token) {
518
+ try {
519
+ const decoded = jwt.verify(req.query.token, JWT_SECRET);
520
+ const user = await userDb.getUserById(decoded.userId);
521
+ if (user) { req.user = user; return next(); }
522
+ } catch (e) { /* ignore */ }
523
+ }
524
+
525
+ res.status(401).json({ error: 'Authentication required. Use Authorization: Bearer <jwt>, x-api-key header, or up-cli- API key.' });
526
+ };
527
+
528
+ // Handle MCP requests at /mcp endpoint
529
+ app.post('/mcp', authenticateMcp, async (req, res) => {
530
+ try {
531
+ const sessionId = req.headers['mcp-session-id'];
532
+ let transport;
533
+
534
+ if (!isServerless && sessionId && transports.has(sessionId)) {
535
+ // Local: reuse existing session transport
536
+ transport = transports.get(sessionId);
537
+ } else {
538
+ // Create a fresh MCP server + transport per request on serverless,
539
+ // or a new session on local
540
+ const server = isServerless && mcpServerFactory ? mcpServerFactory() : mcpServer;
541
+
542
+ // On serverless: no sessionIdGenerator → disables session validation
543
+ // entirely, so requests don't need the initialize handshake.
544
+ // On local: sessionIdGenerator enables session tracking.
545
+ transport = new StreamableHTTPServerTransport({
546
+ sessionIdGenerator: isServerless ? undefined : (() => crypto.randomUUID()),
547
+ stateless: isServerless,
548
+ });
549
+
550
+ transport.onclose = () => {
551
+ const sid = transport.sessionId;
552
+ if (sid) transports.delete(sid);
553
+ };
554
+
555
+ await server.connect(transport);
556
+
557
+ if (!isServerless && transport.sessionId) {
558
+ transports.set(transport.sessionId, transport);
559
+ }
560
+ }
561
+
562
+ // Express already consumed the raw body stream, so we must pass the
563
+ // parsed body as the 3rd arg to avoid "Parse error: Invalid JSON"
564
+ await transport.handleRequest(req, res, req.body);
565
+ } catch (err) {
566
+ console.error('[MCP] Error handling POST:', err.message);
567
+ if (!res.headersSent) {
568
+ res.status(500).json({ error: 'MCP server error', details: err.message });
569
+ }
570
+ }
571
+ });
572
+
573
+ // SSE stream for server-initiated messages (local only — serverless is stateless)
574
+ app.get('/mcp', authenticateMcp, async (req, res) => {
575
+ if (isServerless) {
576
+ return res.status(405).json({ error: 'SSE not supported on serverless. Use stateless POST requests.' });
577
+ }
578
+ const sessionId = req.headers['mcp-session-id'];
579
+ if (!sessionId || !transports.has(sessionId)) {
580
+ res.status(400).json({ error: 'Invalid or missing session ID' });
581
+ return;
582
+ }
583
+ const transport = transports.get(sessionId);
584
+ await transport.handleRequest(req, res);
585
+ });
586
+
587
+ // Session termination
588
+ app.delete('/mcp', authenticateMcp, async (req, res) => {
589
+ if (isServerless) {
590
+ return res.json({ ok: true });
591
+ }
592
+ const sessionId = req.headers['mcp-session-id'];
593
+ if (sessionId && transports.has(sessionId)) {
594
+ const transport = transports.get(sessionId);
595
+ await transport.handleRequest(req, res);
596
+ transports.delete(sessionId);
597
+ } else {
598
+ res.status(404).json({ error: 'Session not found' });
599
+ }
600
+ });
601
+
602
+ console.log(`${c_info('[MCP]')} MCP server mounted at /mcp (${isServerless ? 'stateless/serverless' : 'session-based'})`);
603
+ console.log(`${c_info('[MCP]')} Tools: send-prompt, list-projects, list-sessions, get-session-messages, get-canvas-state, add-canvas-node, clear-canvas, update-canvas-scene, get-active-sessions, abort-session`);
604
+ }
605
+
606
+ // Simple color helper (avoids importing from index.js)
607
+ function c_info(text) {
608
+ return `\x1b[36m${text}\x1b[0m`;
609
+ }
610
+
611
+ function broadcastToClients(clients, message) {
612
+ if (!clients || clients.size === 0) return;
613
+ const data = JSON.stringify(message);
614
+ for (const client of clients) {
615
+ try {
616
+ if (client.readyState === 1) {
617
+ client.send(data);
618
+ }
619
+ } catch (e) { /* ignore */ }
620
+ }
621
+ }