Skip to content

feat: add progress notifications and async tree walker#305

Merged
GLips merged 19 commits intomainfrom
feat/tool-progress-notifications
Mar 24, 2026
Merged

feat: add progress notifications and async tree walker#305
GLips merged 19 commits intomainfrom
feat/tool-progress-notifications

Conversation

@GLips
Copy link
Owner

@GLips GLips commented Mar 24, 2026

Summary

  • Progress notifications: Both get_figma_data and download_figma_images send progress notifications when the client provides a progressToken, using the SDK's notifications/progress method. Clients with resetTimeoutOnProgress no longer time out on large files.
  • Progress heartbeat: A periodic heartbeat (every 3s) fires during long-running async operations (Figma API calls, design simplification). Messages include live node counts during simplification (e.g. "Simplifying design data (3500 nodes processed)").
  • Async tree walker: extractFromDesign and simplifyRawFigmaObject are now async. The tree walker yields the event loop every 100 nodes via setImmediate, which:
    • Allows heartbeat intervals to fire during processing
    • Allows SIGINT to interrupt long-running simplification
    • Prevents the server from becoming unresponsive during large file processing
  • Graceful shutdown: Active MCP transport/server pairs are tracked and closed cleanly before the HTTP server terminates. closeAllConnections() force-closes idle keep-alive sockets so the process exits promptly.
  • sendProgress helper in src/mcp/progress.ts: No-ops when no progressToken is present. Avoids circular dependency by living outside the tool registration module.
  • startProgressHeartbeat helper: Accepts a static string or dynamic () => string message function. Returns a stop function for cleanup in finally blocks.

Breaking changes

  • simplifyRawFigmaObject and extractFromDesign are now async (return Promise). External consumers importing from the library must await them.

Test plan

  • pnpm test — 46 pass, 1 skipped
  • pnpm type-check — clean
  • pnpm lint — clean
  • Manually verified in MCP Inspector: progress notifications flow during large file processing, heartbeat keeps connection alive, server shuts down cleanly on Ctrl+C, no inspector errors on server close
  • Tested with 75MB Figma file — no timeout, progress updates every ~3s during simplification

GLips added 18 commits March 24, 2026 08:15
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.
@GLips GLips changed the base branch from refactor/stateless-http-transport to main March 24, 2026 17:46
Keep active connection tracking and graceful shutdown from the
progress notifications branch. The squash merge of #304 into
main didn't include these additions.
@GLips GLips merged commit b5724ad into main Mar 24, 2026
1 check passed
@GLips GLips deleted the feat/tool-progress-notifications branch March 24, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant