perf(install): parallelize tool validation and stream live progress#113
Merged
Conversation
`capa install` for a 65-tool / 6-server project dropped from ~62s to ~4.8s (~13x) and now shows a live "N/M validated · last-server ✓" counter instead of a static spinner. - `validateTools` groups MCP tools by server and fans out one `listTools` per server with `Promise.all`, then matches expected names against the returned set in memory. Accepts an optional `onProgress` callback and emits `validation_init` / `server_done` events. - `handleProjectConfigure` extracted into `runProjectConfigure(...)` so the existing logic can be reused with progress hooks. OAuth2 detection now runs in parallel across servers. - The endpoint emits NDJSON progress events when the client sends `Accept: application/x-ndjson`; otherwise it returns the same single JSON body as before — fully backward compatible. - The CLI's configure-tools task consumes the NDJSON stream and updates `task.output` per event, with a graceful fallback to `response.json()` for older servers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR speeds up capa install project configuration for projects with many MCP tools by parallelizing server-side validation work and adding an opt-in NDJSON progress stream so the CLI can render live progress (per OAuth2 probe and per MCP server validation) while keeping a JSON fallback for older clients.
Changes:
- Server: parallelize tool validation per MCP server (one
listTools()per server,Promise.all) and export typed validation progress events. - Server: parallelize OAuth2 requirement detection across servers and add an NDJSON streaming response mode for
/api/projects/:id/configure. - CLI: request NDJSON streaming, consume progress events to update task spinner live, and fall back to single-JSON parsing when streaming isn’t available.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/server/mcp-handler.ts | Reworks validateTools() to validate per-server in parallel and emits progress events for streaming clients. |
| src/server/index.ts | Extracts runProjectConfigure(), parallelizes OAuth2 probing, and adds NDJSON streaming support for live configure progress. |
| src/cli/commands/install-tasks/configure-tools.ts | Requests NDJSON, consumes progress stream to update spinner output, and preserves JSON fallback. |
| src/cli/ui/index.ts | Re-exports TaskWrapper type for typing streamed progress consumption. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two issues raised by review on the streaming branch of handleProjectConfigure: 1. After `cancel()`, `controllerRef` was nulled and subsequent `emit` calls fell through to `pending.push(...)` — but `start()` will never run again, so the buffer grew unbounded for the rest of the (possibly long-running) runProjectConfigure work. 2. The `work.then(...)` completion handler called `controller.enqueue` and `controller.close` without try/catch. If the client disconnected before completion, both throw and surface as an unhandled rejection. Fix: collapse the two paths through a single `writeLine` that respects a `streamClosed` flag, set both on client cancel and on terminal write, and drop the pending buffer on cancel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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
capa installagainst a project with many MCP tools used to take roughly a second per tool while the CLI sat on a single static "Configuring tools — validating…" spinner. On a reference project with 65 tools across 6 MCP servers, the configure step took ~62s end-to-end. After this change it takes ~4.8s (~13× faster) and shows live per-server progress.The win comes from two changes on the server plus a streaming channel that lets the CLI surface the progress:
validateToolsgroups MCP tools byserverId, fires onelistToolsper server in parallel viaPromise.all, then matches expected tool names against the returned set in memory. Wall time goes fromΣ Tᵢ(sum of all servers, sequential) tomax(Tᵢ)(slowest server, parallel).for (const server of capabilities.servers) { await detectOAuth2Requirement(...) }loop inhandleProjectConfigureis nowPromise.all, with order-preserved result collection.Accept: application/x-ndjson, the/api/projects/:id/configureendpoint returns a chunked stream of progress events terminated by a{type: "result", ...}line. The CLI consumes these and updates the spinner per event. Backward compatible: any client that doesn't set the header (or any older CLI hitting a newer server) still gets the original single-JSON response.What the user sees now
Files
src/server/mcp-handler.ts— rewritevalidateTools, addValidationProgressEventexport.src/server/index.ts— extractrunProjectConfigure(...)fromhandleProjectConfigure, parallelize OAuth2 detection, add NDJSON streaming branch.src/cli/commands/install-tasks/configure-tools.ts— stream-consume NDJSON, updatetask.outputper event, fall back toresponse.json()ifContent-Typeisn't NDJSON.src/cli/ui/index.ts— exportTaskWrappertype so the stream consumer can be typed.Test plan
bunx tsc --noEmitpassesnpm run buildsucceedsAccept: application/x-ndjsonshould still get the same single-JSON body it always did (verify by hitting the endpoint withcurl -H "Accept: application/json"against a configured project)capabilities.yaml, run install, confirm the failed-tools error message still surfaces correctly through the streamedresultevent🤖 Generated with Claude Code