Skip to content

Add server inspection diff engine and change detection UI#1731

Open
chelojimenez wants to merge 2 commits intomainfrom
claude/add-mcp-diffs-ui-KE61i
Open

Add server inspection diff engine and change detection UI#1731
chelojimenez wants to merge 2 commits intomainfrom
claude/add-mcp-diffs-ui-KE61i

Conversation

@chelojimenez
Copy link
Copy Markdown
Contributor

@chelojimenez chelojimenez commented Apr 8, 2026

Summary

Implements a complete server inspection system that captures MCP server snapshots on connection and detects meaningful changes between connections. Includes a diff engine, state management, coordinator hook, and UI panel to display changes to users.

Key Changes

  • Diff Engine (diff-engine.ts): Pure, deterministic functions for normalizing server snapshots, computing diffs, and detecting meaningful changes. Uses stable JSON serialization to handle object key ordering consistently.

  • Inspection Store (inspection-store.ts): Zustand store persisted to localStorage that maintains inspection records keyed by {workspaceId}::{serverName}, storing both the latest snapshot and diff.

  • Inspection Coordinator Hook (use-inspection-coordinator.ts): App-level effect hook that monitors all workspace servers for connection transitions, automatically runs the inspection pipeline (snapshot + diff) after successful connects, and shows toast notifications with "View changes" CTA when meaningful changes are detected.

  • Server Changes Panel (ServerChangesPanel.tsx): New UI component that displays a summary of changes since last connect, with collapsible sections for initialization changes and tool changes (added/removed/changed), including side-by-side JSON diffs for complex fields.

  • Supporting Utilities:

    • diff-summary.ts: Formats diffs into human-readable one-line summaries
    • inspection-detail-request.ts: localStorage-based request pattern for opening the detail modal from toast CTAs with 5-minute TTL
    • types.ts: TypeScript definitions for snapshots, diffs, and related structures
  • Comprehensive Test Coverage: Added 1000+ lines of tests covering the diff engine, coordinator hook, UI component, store, and utilities with realistic scenarios (pagination, errors, stale requests, etc.)

  • Integration Points:

    • App.tsx: Mounted useInspectionCoordinator hook via zero-UI bridge component
    • ServersTab.tsx: Consumes inspection detail requests to open detail modal on Overview tab
    • ServerInfoContent.tsx: Integrated ServerChangesPanel into the Overview tab

Implementation Details

  • Deterministic Snapshots: All snapshots are normalized and stabilized with sorted keys to ensure identical diffs regardless of object construction order
  • Safe Async Handling: Captures workspaceId and serverName before async work, verifies they still match after completion to handle rapid workspace/server changes
  • Incomplete Pagination: If tool list pagination fails mid-stream, the inspection is skipped and the previous baseline is preserved (no partial diffs)
  • Error Resilience: Failed inspections never overwrite previous records; only successful complete inspections update state
  • Metadata Merging: Tool metadata is merged from both tool._meta (preferred) and external toolsMetadata record, matching existing UI behavior
  • Client-Only: Entirely client-side inspection system with no backend/Convex involvement; uses localStorage for persistence

https://claude.ai/code/session_01KBjUEbjapGUfujDhE4gYqA


Note

Medium Risk
Adds new persisted client-side state plus async inspection/diffing on server connect events; mistakes could lead to noisy toasts, stale localStorage, or incorrect server modal navigation, but it does not touch auth/payment/back-end data paths.

Overview
Adds a full client-side inspection system that snapshots MCP server initialization info + tool catalogs on successful connect, computes deterministic diffs on subsequent connects, and persists the latest snapshot/diff per {workspaceId}::{serverName} in a new Zustand inspection-store (localStorage-backed).

When meaningful changes are detected, the app now shows a toast with a “View changes” CTA; clicking it writes a short-lived inspection-detail-request (TTL) and navigates to ServersTab, which consumes the request to open the server detail modal on the Overview tab. The Overview UI is extended with a new ServerChangesPanel that summarizes added/removed/changed tools and init-field changes with expandable JSON before/after views, plus extensive new unit tests covering diffing, summary formatting, coordinator behavior, and request/store lifecycle.

Reviewed by Cursor Bugbot for commit 6dbfa85. Bugbot is set up for automated code reviews on this repo. Configure here.

Ship a local-first "Changes since last successful connect" feature for
the MCPJam Inspector. Shows semantic diffs for tools/list and initialize
data (protocolVersion, transport, serverVersion, instructions,
serverCapabilities) between consecutive successful connections.

Key components:
- Pure diff engine with deterministic normalization (sorted keys, merged
  tool metadata via _meta ?? toolsMetadata pattern)
- Zustand store persisted to localStorage (atomic records per server)
- App-level inspection coordinator that watches all workspace servers
  for connection transitions, with workspace scoping guards
- Rich toast with "View changes" action using TTL-based localStorage
  handoff to open the server detail modal Overview tab
- ServerChangesPanel UI with color-coded badges and collapsible
  before/after sections for init and tool changes

Safety: incomplete pagination skips diff, inspection errors never
overwrite previous baselines, workspace switches cancel in-flight work.

81 new tests across 6 test files (diff engine, summary, store,
coordinator, UI panel, detail request helper).

https://claude.ai/code/session_01KBjUEbjapGUfujDhE4gYqA
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Apr 8, 2026
@railway-app
Copy link
Copy Markdown

railway-app Bot commented Apr 8, 2026

🚅 Deployed to the inspector-pr-1731 environment in triumphant-alignment

Service Status Web Updated (UTC)
mcp-inspector ◻️ Removed (View Logs) Web Apr 16, 2026 at 2:01 am

@railway-app railway-app Bot temporarily deployed to triumphant-alignment / inspector-pr-1731 April 8, 2026 21:42 Destroyed
@chelojimenez
Copy link
Copy Markdown
Contributor Author

chelojimenez commented Apr 8, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@railway-app railway-app Bot temporarily deployed to triumphant-alignment / inspector-pr-1731 April 8, 2026 21:43 Destroyed
@dosubot dosubot Bot added the enhancement New feature or request label Apr 8, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

Walkthrough

This pull request introduces a server inspection system that tracks MCP server initialization state and tool catalogs across reconnections. It adds snapshot capture, diffing logic to detect changes, persistent storage via localStorage, and UI components to display server changes. The implementation includes the useInspectionCoordinator hook for managing inspection workflows, ServerChangesPanel component for rendering diffs, and supporting modules for type definitions, diff computation, summary formatting, and localStorage-backed detail request handling.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
mcpjam-inspector/client/src/stores/inspection-store.ts (1)

32-37: Consider potential for unbounded growth.

The records object accumulates entries indefinitely. As users connect to many servers across workspaces, localStorage usage grows without cleanup. Consider adding a clearForWorkspace(workspaceId) method or periodic pruning of stale entries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/stores/inspection-store.ts` around lines 32 - 37,
The records map in inspection-store.ts (used with inspectionStoreKey) can grow
unbounded because entries are never removed; add a
clearForWorkspace(workspaceId: string) method that scans stored keys (matching
`${workspaceId}::`) and removes them from the in-memory records object and
localStorage, and call it where workspace teardown/disconnect logic occurs;
alternatively implement periodic pruning by adding a timestamp field to each
record and a pruneStaleRecords(maxAgeMs) helper that deletes entries older than
the threshold (update load/save routines such as loadRecords/saveRecords to
persist the timestamp and run pruneStaleRecords on init).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcpjam-inspector/client/src/App.tsx`:
- Around line 913-920: The toast callback only sends serverName, losing the
originating workspace identity; update handleInspectionViewChanges to include
the current workspace id when calling writeInspectionDetailRequest (e.g., pass {
workspaceId, serverName }) and keep applyNavigation as-is, and then update the
consumer in ServersTab (or wherever writeInspectionDetailRequest is read) to
validate that the incoming request's workspaceId matches the active workspace
before resolving/opening the diff; if it doesn't match, ignore or surface a
user-facing message.

In `@mcpjam-inspector/client/src/components/connection/ServerInfoContent.tsx`:
- Around line 29-31: The selector is using the live activeWorkspaceId
(useSharedAppState) which can change under you; instead use the workspace id
captured when the modal opened (the modal's snapshot/prop) rather than
activeWorkspaceId so the inspectionStoreKey stays stable. Replace usage of
activeWorkspaceId in the construction of storeKey
(inspectionStoreKey(activeWorkspaceId, server.name)) with the captured workspace
id provided by the modal (e.g., detailModalState.workspaceId or
serverSnapshot.workspaceId) so useInspectionStore reads the record for that
captured workspace; alternatively ensure the modal is closed on workspace
switch. Ensure you update any references in ServerInfoContent (and the call
sites that pass serverSnapshot) so the captured id is available to build the
stable storeKey.

---

Nitpick comments:
In `@mcpjam-inspector/client/src/stores/inspection-store.ts`:
- Around line 32-37: The records map in inspection-store.ts (used with
inspectionStoreKey) can grow unbounded because entries are never removed; add a
clearForWorkspace(workspaceId: string) method that scans stored keys (matching
`${workspaceId}::`) and removes them from the in-memory records object and
localStorage, and call it where workspace teardown/disconnect logic occurs;
alternatively implement periodic pruning by adding a timestamp field to each
record and a pruneStaleRecords(maxAgeMs) helper that deletes entries older than
the threshold (update load/save routines such as loadRecords/saveRecords to
persist the timestamp and run pruneStaleRecords on init).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f1650ef-af82-4e71-a83a-6fcd761a7f48

📥 Commits

Reviewing files that changed from the base of the PR and between 677fd09 and a2261fd.

📒 Files selected for processing (16)
  • mcpjam-inspector/client/src/App.tsx
  • mcpjam-inspector/client/src/components/ServersTab.tsx
  • mcpjam-inspector/client/src/components/connection/ServerChangesPanel.tsx
  • mcpjam-inspector/client/src/components/connection/ServerInfoContent.tsx
  • mcpjam-inspector/client/src/components/connection/__tests__/ServerChangesPanel.test.tsx
  • mcpjam-inspector/client/src/hooks/__tests__/use-inspection-coordinator.test.ts
  • mcpjam-inspector/client/src/hooks/use-inspection-coordinator.ts
  • mcpjam-inspector/client/src/lib/inspection-detail-request.ts
  • mcpjam-inspector/client/src/lib/inspection/__tests__/diff-engine.test.ts
  • mcpjam-inspector/client/src/lib/inspection/__tests__/diff-summary.test.ts
  • mcpjam-inspector/client/src/lib/inspection/__tests__/inspection-detail-request.test.ts
  • mcpjam-inspector/client/src/lib/inspection/diff-engine.ts
  • mcpjam-inspector/client/src/lib/inspection/diff-summary.ts
  • mcpjam-inspector/client/src/lib/inspection/types.ts
  • mcpjam-inspector/client/src/stores/__tests__/inspection-store.test.ts
  • mcpjam-inspector/client/src/stores/inspection-store.ts

Comment on lines +913 to +920
// Callback for inspection toast "View changes" action
const handleInspectionViewChanges = useCallback(
(serverName: string) => {
writeInspectionDetailRequest(serverName);
applyNavigation("servers", { updateHash: true });
},
[applyNavigation],
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Carry workspace identity through the toast callback.

Inspection records are keyed by workspaceId::serverName, but this handoff persists only serverName. If a toast created in workspace A is clicked after the user has moved to workspace B, ServersTab will resolve the request against B’s servers and can open the wrong diff—or drop the request entirely. Persist the originating workspace id alongside the server name and validate it when consuming the request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/App.tsx` around lines 913 - 920, The toast
callback only sends serverName, losing the originating workspace identity;
update handleInspectionViewChanges to include the current workspace id when
calling writeInspectionDetailRequest (e.g., pass { workspaceId, serverName })
and keep applyNavigation as-is, and then update the consumer in ServersTab (or
wherever writeInspectionDetailRequest is read) to validate that the incoming
request's workspaceId matches the active workspace before resolving/opening the
diff; if it doesn't match, ignore or surface a user-facing message.

Comment on lines +29 to +31
const { activeWorkspaceId } = useSharedAppState();
const storeKey = inspectionStoreKey(activeWorkspaceId, server.name);
const inspectionRecord = useInspectionStore((s) => s.records[storeKey]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t key the diff panel off live workspace state.

ServersTab can keep showing detailModalState.serverSnapshot after the live server entry disappears, so this selector may outlive the workspace it came from. Once the user switches workspaces, Line 30 starts reading the new workspace’s inspection record for the same server name, which can make “changes since last connect” disappear or show the wrong diff. Pass the workspace id captured when the modal opened, or close the modal on workspace switch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/components/connection/ServerInfoContent.tsx`
around lines 29 - 31, The selector is using the live activeWorkspaceId
(useSharedAppState) which can change under you; instead use the workspace id
captured when the modal opened (the modal's snapshot/prop) rather than
activeWorkspaceId so the inspectionStoreKey stays stable. Replace usage of
activeWorkspaceId in the construction of storeKey
(inspectionStoreKey(activeWorkspaceId, server.name)) with the captured workspace
id provided by the modal (e.g., detailModalState.workspaceId or
serverSnapshot.workspaceId) so useInspectionStore reads the record for that
captured workspace; alternatively ensure the modal is closed on workspace
switch. Ensure you update any references in ServerInfoContent (and the call
sites that pass serverSnapshot) so the captured id is available to build the
stable storeKey.

Comment on lines +571 to +590
// Consume inspection detail request (from toast "View changes" CTA)
useEffect(() => {
if (detailModalState.isOpen) return;

const request = readInspectionDetailRequest();
if (!request) return;

clearInspectionDetailRequest();

const server = workspaceServers[request.serverName];
if (!server) return;

setDetailModalState((prev) => ({
isOpen: true,
serverName: server.name,
defaultTab: "overview",
sessionKey: prev.sessionKey + 1,
serverSnapshot: server,
}));
}, [detailModalState.isOpen, workspaceServers]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This effect can both miss and prematurely delete inspection requests.

The new consumer only reruns when detailModalState.isOpen or workspaceServers changes, so a localStorage write from the toast does nothing when the user is already on the Servers tab. It also clears the request at Line 578 before Line 580 proves the server exists, which permanently drops the CTA if workspaceServers has not hydrated yet. Drive this off an explicit signal/custom event and clear the request only after the modal is actually opened.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants