feat: add progress notifications and async tree walker#305
Merged
Conversation
Each request creates its own McpServer and transport with sessionIdGenerator: undefined. Removes the session registry, SSE transport, /messages endpoint, and progress notification interval. StreamableHTTP is served at both /mcp and /sse for backward compatibility. GET and DELETE return 405.
Express 4 does not catch rejected promises from async handlers. Without a try/catch, a failure in connect() or handleRequest() causes an unhandled rejection that crashes the process.
Aligns with the MCP SDK which already depends on Express 5. Express 5 natively catches async route handler rejections, so the manual try/catch added in the previous commit is no longer needed.
Close transport and McpServer when the response ends, matching the SDK's recommended pattern for stateless servers. Add Express error-handling middleware that returns a JSON-RPC error response instead of Express's default HTML 500.
Replace raw express() with the SDK's createMcpExpressApp(), which applies localhost DNS rebinding protection middleware automatically. This also handles express.json() globally, removing the need for per-route body parsing.
Thread the SDK's RequestHandlerExtra through registerTool wiring to tool handlers. Add sendProgress helper that sends notifications/progress when the client provides a progressToken. get_figma_data reports progress after fetching from the Figma API and after processing design data (2 phases). download_figma_images reports progress after resolving download items and after completing all downloads (2 phases).
Break the circular dependency where tools imported from their own registration module (mcp/index.ts -> tools -> mcp/index.ts).
Progress was only sent after the Figma API fetch completed, which defeats the purpose — the timeout fires during the fetch. Now sends progress at 0/3 before the slow operation starts, so clients with resetTimeoutOnProgress get an immediate signal.
Figma API calls can take up to ~55 seconds. A single progress notification before the call isn't enough if the client timeout is shorter. startProgressHeartbeat sends periodic notifications every 5 seconds during slow I/O, keeping clients with resetTimeoutOnProgress alive. The heartbeat stops as soon as the operation completes or errors.
The tree walker and YAML serializer are synchronous and block the event loop, preventing heartbeat intervals from firing. Send progress notifications between each phase so the client gets a fresh timeout window before each synchronous step: API fetch -> simplify -> serialize -> return.
The synchronous tree walk blocks the event loop for large Figma files (75MB+), preventing progress heartbeats from firing, SIGINT from being handled, and the HTTP server from shutting down cleanly. Make extractFromDesign and simplifyRawFigmaObject async. The tree walker yields the event loop every 500 nodes via setImmediate, allowing heartbeats and signal handlers to run during processing. Also fix progress total (3 -> 4), add diagnostic logging to sendProgress, and force-close keep-alive connections on shutdown.
The heartbeat was only active during the Figma API call but not during the tree walk simplification, which is the actual slow part for large files. Now that the tree walker yields the event loop, the heartbeat can fire during simplification.
Report how many nodes have been processed in each heartbeat message (e.g. "Simplifying design data (3500 nodes processed)"). The heartbeat message function is now evaluated dynamically on each tick.
Reduce yield interval from 500 to 100 nodes — later nodes in large files are deeper and more complex, so 500 nodes could take longer than the heartbeat interval. Reduce heartbeat from 5s to 3s for more margin against 10s client timeouts.
Track active per-request transport/server pairs. On shutdown, close each transport and server cleanly before terminating HTTP connections. This gives clients a proper stream close instead of an abrupt disconnect.
Test extractFromDesign and simplifyRawFigmaObject with a representative node tree covering visibility filtering, depth limits, child recursion, VECTOR->IMAGE-SVG type mapping, and global style variable accumulation. Verified against both the original sync and new async implementations to confirm the async conversion produces identical output.
Keep active connection tracking and graceful shutdown from the progress notifications branch. The squash merge of #304 into main didn't include these additions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
get_figma_dataanddownload_figma_imagessend progress notifications when the client provides aprogressToken, using the SDK'snotifications/progressmethod. Clients withresetTimeoutOnProgressno longer time out on large files.extractFromDesignandsimplifyRawFigmaObjectare now async. The tree walker yields the event loop every 100 nodes viasetImmediate, which:closeAllConnections()force-closes idle keep-alive sockets so the process exits promptly.sendProgresshelper insrc/mcp/progress.ts: No-ops when noprogressTokenis present. Avoids circular dependency by living outside the tool registration module.startProgressHeartbeathelper: Accepts a static string or dynamic() => stringmessage function. Returns a stop function for cleanup infinallyblocks.Breaking changes
simplifyRawFigmaObjectandextractFromDesignare now async (returnPromise). External consumers importing from the library mustawaitthem.Test plan
pnpm test— 46 pass, 1 skippedpnpm type-check— cleanpnpm lint— clean