Skip to content

Commit 02f6d9f

Browse files
JooHyung Parkcursoragent
andcommitted
[ai-assisted] feat(sse): detect legacy SSE clients and show migration dialog
Add SseDetectionServer that listens on port 3056 for 60 seconds on app launch to detect clients still using the deprecated SSE transport. When detected, a migration dialog guides users to reconfigure their MCP client to use the new stdio-based setup. Includes a preview button in the Settings page for manual testing. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7aac617 commit 02f6d9f

8 files changed

Lines changed: 254 additions & 3 deletions

File tree

src/App.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AppSidebar, PageId } from '@/components/app-sidebar'
44
import { Separator } from '@/components/ui/separator'
55
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'
66
import { TutorialDialog } from '@/components/TutorialDialog'
7+
import { SseMigrationDialog } from '@/components/SseMigrationDialog'
78
import { ThemeProvider } from '@/components/ThemeProvider'
89
import { AnimatedThemeToggler } from '@/components/ui/animated-theme-toggler'
910
import { Button } from '@/components/ui/button'
@@ -49,6 +50,7 @@ function App() {
4950
const [figmaUser, setFigmaUser] = useState<{ name: string; email: string; avatar?: string } | null>(null)
5051
const [logs, setLogs] = useState<string[]>([])
5152
const [tutorialOpen, setTutorialOpen] = useState(false)
53+
const [migrationDialogOpen, setMigrationDialogOpen] = useState(false)
5254
const [copied, setCopied] = useState(false)
5355

5456
// Track page view changes
@@ -87,6 +89,14 @@ Ready to bridge Figma and AI tools via MCP
8789
checkFirstLaunch()
8890
}, [])
8991

92+
// Listen for legacy SSE client detection
93+
useEffect(() => {
94+
const unsubscribe = window.electron?.sse?.onClientDetected(() => {
95+
setMigrationDialogOpen(true)
96+
})
97+
return () => unsubscribe?.()
98+
}, [])
99+
90100
const handleTutorialComplete = async () => {
91101
try {
92102
await window.electron.settings.set('hasSeenTutorial', true)
@@ -264,7 +274,7 @@ Ready to bridge Figma and AI tools via MCP
264274
case 'terminal':
265275
return <TerminalPage logs={logs} />
266276
case 'settings':
267-
return <SettingsPage />
277+
return <SettingsPage onNavigateToSettings={() => setCurrentPage('settings')} />
268278
case 'help':
269279
return <HelpPage />
270280
default:
@@ -279,6 +289,14 @@ Ready to bridge Figma and AI tools via MCP
279289
onOpenChange={setTutorialOpen}
280290
onComplete={handleTutorialComplete}
281291
/>
292+
<SseMigrationDialog
293+
open={migrationDialogOpen}
294+
onClose={() => setMigrationDialogOpen(false)}
295+
onGoToSettings={() => {
296+
setMigrationDialogOpen(false)
297+
setCurrentPage('settings')
298+
}}
299+
/>
282300
<SidebarProvider>
283301
<AppSidebar
284302
currentPage={currentPage}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* SSE Migration Dialog
3+
*
4+
* Shown when a legacy MCP client attempts to connect via the deprecated SSE transport
5+
* (GET http://127.0.0.1:3056/sse). Guides the user to update their configuration
6+
* to the new stdio-based transport.
7+
*/
8+
9+
import {
10+
Dialog,
11+
DialogContent,
12+
DialogDescription,
13+
DialogFooter,
14+
DialogHeader,
15+
DialogTitle,
16+
} from '@/components/ui/dialog'
17+
import { Button } from '@/components/ui/button'
18+
import { AlertTriangle, ArrowRight, Settings } from 'lucide-react'
19+
20+
interface SseMigrationDialogProps {
21+
open: boolean
22+
onClose: () => void
23+
onGoToSettings?: () => void
24+
}
25+
26+
export function SseMigrationDialog({ open, onClose, onGoToSettings }: SseMigrationDialogProps) {
27+
const handleGoToSettings = () => {
28+
onClose()
29+
onGoToSettings?.()
30+
}
31+
32+
return (
33+
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onClose() }}>
34+
<DialogContent className="max-w-md">
35+
<DialogHeader>
36+
<DialogTitle className="flex items-center gap-2">
37+
<AlertTriangle className="size-5 text-amber-500 shrink-0" />
38+
Legacy Configuration Detected
39+
</DialogTitle>
40+
<DialogDescription>
41+
An MCP client is trying to connect using the old SSE method, which is no longer supported.
42+
</DialogDescription>
43+
</DialogHeader>
44+
45+
<div className="space-y-4 py-2">
46+
{/* What happened */}
47+
<div className="rounded-lg bg-amber-500/10 border border-amber-500/20 p-3 text-sm">
48+
<p className="font-medium text-amber-600 dark:text-amber-400 mb-1">What happened?</p>
49+
<p className="text-muted-foreground">
50+
A connection was attempted to{' '}
51+
<code className="bg-muted px-1 py-0.5 rounded text-xs">
52+
http://127.0.0.1:3056/sse
53+
</code>
54+
. This SSE transport has been replaced by stdio.
55+
</p>
56+
</div>
57+
58+
{/* Steps */}
59+
<div className="space-y-2">
60+
<p className="text-sm font-medium">How to fix</p>
61+
<div className="space-y-2 text-sm text-muted-foreground">
62+
<div className="flex items-start gap-2">
63+
<ArrowRight className="size-4 mt-0.5 shrink-0 text-primary" />
64+
<span>
65+
Open <strong>Settings</strong> and find the MCP Client Configuration section
66+
</span>
67+
</div>
68+
<div className="flex items-start gap-2">
69+
<ArrowRight className="size-4 mt-0.5 shrink-0 text-primary" />
70+
<span>
71+
Remove any existing SSE-based configuration from your MCP client
72+
</span>
73+
</div>
74+
<div className="flex items-start gap-2">
75+
<ArrowRight className="size-4 mt-0.5 shrink-0 text-primary" />
76+
<span>
77+
Follow the new stdio setup instructions for your client (Cursor, Claude Code, etc.)
78+
</span>
79+
</div>
80+
</div>
81+
</div>
82+
</div>
83+
84+
<DialogFooter className="gap-2">
85+
<Button variant="ghost" onClick={onClose}>
86+
Dismiss
87+
</Button>
88+
<Button onClick={handleGoToSettings} className="gap-2">
89+
<Settings className="size-4" />
90+
Go to Settings
91+
</Button>
92+
</DialogFooter>
93+
</DialogContent>
94+
</Dialog>
95+
)
96+
}

src/main.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { getStore, saveFigmaUser, getFigmaUser } from './main/utils/store';
2121
import { installStdioServer } from './main/utils/stdio-installer';
2222
import { initializeUpdater } from './main/utils/updater';
2323
import { createMenu } from './main/menu';
24+
import { SseDetectionServer } from './main/server/SseDetectionServer';
2425

2526
// Declare Vite plugin globals
2627
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
@@ -419,6 +420,17 @@ app.on('ready', async () => {
419420
createTray();
420421
}
421422

423+
// Start SSE detection server — listens on port 3056 for 60s to detect legacy clients
424+
if (mainWindow) {
425+
const sseDetection = new SseDetectionServer(() => {
426+
emitToRenderer(mainWindow!, IPC_CHANNELS.SSE_CLIENT_DETECTED, {});
427+
sseDetection.stop();
428+
});
429+
sseDetection.start().then(() => {
430+
setTimeout(() => sseDetection.stop(), 60_000);
431+
});
432+
}
433+
422434
// Auto-start servers in development
423435
if (process.env.NODE_ENV === 'development') {
424436
try {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2026 Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved.
3+
*
4+
* Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
5+
*/
6+
7+
/**
8+
* SSE Detection Server
9+
*
10+
* Listens on port 3056 for a short window after app start to detect legacy MCP clients
11+
* that are still configured to use the deprecated SSE transport (GET /sse).
12+
* When detected, fires a callback so the app can show a migration guide dialog.
13+
*/
14+
15+
import * as http from 'http';
16+
import { createLogger } from '../utils/logger';
17+
import { PORTS } from '@/shared/constants';
18+
19+
const logger = createLogger('SseDetection');
20+
21+
const MIGRATION_RESPONSE = JSON.stringify({
22+
error: 'SSE transport is no longer supported',
23+
message:
24+
'This MCP server has been upgraded to use stdio transport. ' +
25+
'Please update your MCP client configuration to use the stdio server instead. ' +
26+
'Open TalkToFigma Desktop and go to Settings to get the new configuration.',
27+
migration: {
28+
from: `http://127.0.0.1:${PORTS.MCP_SSE}/sse`,
29+
to: 'stdio (see TalkToFigma Desktop → Settings)',
30+
},
31+
});
32+
33+
export class SseDetectionServer {
34+
private httpServer: http.Server | null = null;
35+
private onDetected: () => void;
36+
37+
constructor(onDetected: () => void) {
38+
this.onDetected = onDetected;
39+
}
40+
41+
start(): Promise<void> {
42+
return new Promise((resolve) => {
43+
this.httpServer = http.createServer((req, res) => {
44+
res.setHeader('Content-Type', 'application/json');
45+
res.setHeader('Access-Control-Allow-Origin', '*');
46+
47+
if (req.url === '/sse' && req.method === 'GET') {
48+
logger.info('Legacy SSE client detected on port 3056 — showing migration dialog');
49+
this.onDetected();
50+
51+
res.writeHead(426, { 'Upgrade': 'stdio' });
52+
res.end(MIGRATION_RESPONSE);
53+
} else {
54+
res.writeHead(400);
55+
res.end(JSON.stringify({ error: 'SSE transport is no longer supported' }));
56+
}
57+
});
58+
59+
this.httpServer.on('error', (error: NodeJS.ErrnoException) => {
60+
if (error.code === 'EADDRINUSE') {
61+
logger.warn(`Port ${PORTS.MCP_SSE} already in use — SSE detection disabled`);
62+
resolve(); // silently skip, don't block app start
63+
} else {
64+
logger.error('SSE detection server error:', error);
65+
resolve(); // non-fatal
66+
}
67+
});
68+
69+
this.httpServer.listen(PORTS.MCP_SSE, '127.0.0.1', () => {
70+
logger.info(`SSE detection server listening on port ${PORTS.MCP_SSE} (60s window)`);
71+
resolve();
72+
});
73+
});
74+
}
75+
76+
stop(): Promise<void> {
77+
return new Promise((resolve) => {
78+
if (!this.httpServer) {
79+
resolve();
80+
return;
81+
}
82+
83+
this.httpServer.close(() => {
84+
logger.info('SSE detection server stopped');
85+
this.httpServer = null;
86+
resolve();
87+
});
88+
});
89+
}
90+
}

src/pages/Settings.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
2-
import { Link2, Copy } from 'lucide-react'
2+
import { Link2, Copy, AlertTriangle } from 'lucide-react'
33
import { Figma, MCP } from '@lobehub/icons'
44
import { McpMultiClientConfig } from '@/components/mcp'
55
import { Button } from '@/components/ui/button'
66
import { useToast } from '@/hooks/use-toast'
77
import { useEffect, useState } from 'react'
8+
import { SseMigrationDialog } from '@/components/SseMigrationDialog'
89

9-
export function SettingsPage() {
10+
interface SettingsPageProps {
11+
onNavigateToSettings?: () => void
12+
}
13+
14+
export function SettingsPage({ onNavigateToSettings }: SettingsPageProps) {
1015
const { toast } = useToast()
1116
const [stdioPath, setStdioPath] = useState<string>('Loading...')
17+
const [showMigrationDialog, setShowMigrationDialog] = useState(false)
1218

1319
useEffect(() => {
1420
// Load stdio server path
@@ -91,8 +97,26 @@ export function SettingsPage() {
9197
</div>
9298
<code className="bg-background px-2 py-1 rounded text-xs">ws://localhost:3055</code>
9399
</div>
100+
<div className="flex justify-between items-center p-3 bg-muted rounded-lg">
101+
<div className="flex items-center gap-2">
102+
<AlertTriangle size={16} className="shrink-0 text-muted-foreground" />
103+
<div>
104+
<p className="font-medium">SSE Migration</p>
105+
<p className="text-muted-foreground text-xs">Legacy SSE connection guide</p>
106+
</div>
107+
</div>
108+
<Button variant="outline" size="sm" onClick={() => setShowMigrationDialog(true)}>
109+
Preview
110+
</Button>
111+
</div>
94112
</CardContent>
95113
</Card>
114+
115+
<SseMigrationDialog
116+
open={showMigrationDialog}
117+
onClose={() => setShowMigrationDialog(false)}
118+
onGoToSettings={onNavigateToSettings}
119+
/>
96120
</div>
97121
)
98122
}

src/preload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ const electronAPI: ElectronAPI = {
8989
update: {
9090
check: () => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_CHECK),
9191
},
92+
93+
sse: {
94+
onClientDetected: createEventListener<Record<string, never>>(IPC_CHANNELS.SSE_CLIENT_DETECTED),
95+
},
9296
};
9397

9498
// Expose API to renderer

src/shared/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export const IPC_CHANNELS = {
7979

8080
// Updates
8181
UPDATE_CHECK: 'update:check',
82+
83+
// SSE migration detection
84+
SSE_CLIENT_DETECTED: 'sse:client-detected',
8285
} as const;
8386

8487
// Store keys for electron-store

src/shared/types/ipc.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ export interface ElectronAPI {
123123
update: {
124124
check: () => Promise<void>;
125125
};
126+
127+
sse: {
128+
onClientDetected: (callback: () => void) => () => void;
129+
};
126130
}
127131

128132
// MCP Configuration types

0 commit comments

Comments
 (0)