Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion SPECS/ARCHIVE/INDEX.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# mcpbridge-wrapper Tasks Archive

**Last Updated:** 2026-02-15 (P11-T4)
**Last Updated:** 2026-02-15 (P12-T1)

## Archived Tasks

Expand Down Expand Up @@ -94,6 +94,7 @@
| P11-T2 | [P11-T2_Add_Session_Timeline_View/](P11-T2_Add_Session_Timeline_View/) | 2026-02-15 | PASS |
| P11-T3 | [P11-T3_Add_Dashboard_Theme_Toggle/](P11-T3_Add_Dashboard_Theme_Toggle/) | 2026-02-15 | PASS |
| P11-T4 | [P11-T4_Add_Keyboard_Shortcuts_Command_Palette/](P11-T4_Add_Keyboard_Shortcuts_Command_Palette/) | 2026-02-15 | PASS |
| P12-T1 | [P12-T1_Add_MCP_Client_Identification/](P12-T1_Add_MCP_Client_Identification/) | 2026-02-15 | PASS |

## Historical Artifacts

Expand Down Expand Up @@ -152,6 +153,7 @@
| [REVIEW_P11-T2_session_timeline.md](P11-T2_Add_Session_Timeline_View/REVIEW_P11-T2_session_timeline.md) | Review report for P11-T2 |
| [REVIEW_P11-T3_dashboard_theme_toggle.md](P11-T3_Add_Dashboard_Theme_Toggle/REVIEW_P11-T3_dashboard_theme_toggle.md) | Review report for P11-T3 |
| [REVIEW_P11-T4_keyboard_shortcuts.md](P11-T4_Add_Keyboard_Shortcuts_Command_Palette/REVIEW_P11-T4_keyboard_shortcuts.md) | Review report for P11-T4 |
| [REVIEW_P12-T1_mcp_client_identification.md](P12-T1_Add_MCP_Client_Identification/REVIEW_P12-T1_mcp_client_identification.md) | Review report for P12-T1 |

## Archive Log

Expand Down Expand Up @@ -257,3 +259,5 @@
| 2026-02-15 | P11-T3 | Archived REVIEW_P11-T3_dashboard_theme_toggle report |
| 2026-02-15 | P11-T4 | Archived Add_Keyboard_Shortcuts_Command_Palette (PASS) |
| 2026-02-15 | P11-T4 | Archived REVIEW_P11-T4_keyboard_shortcuts report |
| 2026-02-15 | P12-T1 | Archived Add_MCP_Client_Identification (PASS) |
| 2026-02-15 | P12-T1 | Archived REVIEW_P12-T1_mcp_client_identification report |
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# PRD: P12-T1 — Add MCP Client Identification

**Status:** In Progress
**Priority:** P0
**Branch:** feature/P12-T1-mcp-client-identification
**Date:** 2026-02-15

---

## Overview

Detect the calling MCP client from the `initialize` handshake. The `clientInfo` field in the initialize request contains `{name, version}`. Capture this and tag all subsequent metrics with the client identity.

---

## Deliverables

### 1. `src/mcpbridge_wrapper/schemas.py`
- Add `MCPClientInfo` model: `name: str`, `version: str`
- Add `MCPInitializeParams` model: `clientInfo: Optional[MCPClientInfo]`
- Add `get_client_info()` method to `MCPRequest` that extracts clientInfo when `method == "initialize"`

### 2. `src/mcpbridge_wrapper/__main__.py`
- In `on_request()`, detect `method == "initialize"`, extract `clientInfo.name` and `clientInfo.version`
- Call `metrics.set_client_info(name, version)` if metrics is not None
- Default to `"unknown"` if `clientInfo` is absent

### 3. `src/mcpbridge_wrapper/webui/metrics.py` (MetricsCollector)
- Add `_client_name: str` and `_client_version: str` fields (default `"unknown"`)
- Add `set_client_info(name: str, version: str)` method (thread-safe)
- Include `client_name` and `client_version` in `get_summary()` output
- Clear client info in `reset()`

### 4. `src/mcpbridge_wrapper/webui/shared_metrics.py` (SharedMetricsStore)
- Add `client_info` table: `id`, `client_name TEXT`, `client_version TEXT`, `updated_at REAL`
- Add `set_client_info(name: str, version: str)` method (upsert row)
- Include `client_name` and `client_version` in `get_summary()` output (latest row)
- Clear client info in `reset()`
- Migration: backward-compatible (nullable columns, table created if not exists)

### 5. `src/mcpbridge_wrapper/webui/server.py`
- No changes needed: `/api/metrics` already returns `metrics.get_summary()` which will include the new fields automatically

### 6. `src/mcpbridge_wrapper/webui/static/index.html`
- Add "Active Client" KPI card with id `kpi-client` (displays `name version`)

### 7. `src/mcpbridge_wrapper/webui/static/dashboard.js`
- Update `updateKPIs()` to render `summary.client_name` and `summary.client_version` in `kpi-client`
- Format: `"{name} {version}"` or `"unknown"` if not set

### 8. Tests
- `tests/unit/webui/test_metrics.py` — test `set_client_info`, summary includes fields, reset clears them
- `tests/unit/webui/test_shared_metrics.py` — test `set_client_info`, summary includes fields, reset clears them
- `tests/unit/test_main.py` — test initialize parsing calls `set_client_info` with correct values, missing clientInfo defaults to "unknown"

---

## Acceptance Criteria

- [x] `initialize` request `clientInfo.name` and `clientInfo.version` are captured
- [x] Metrics summary includes `client_name` and `client_version` fields
- [x] Dashboard displays "Active Client" KPI card (e.g. "Cursor 1.2.3")
- [x] Metrics reset clears client info
- [x] If `initialize` has no `clientInfo`, fields default to "unknown"
- [x] SQLite schema migration is backward-compatible (nullable column)
- [x] Tests cover initialize parsing, missing clientInfo, and metric tagging

---

## Implementation Notes

- `MCPRequest.params` is typed as `Optional[MCPParams]` which only has `name` and `arguments`
- Add `get_client_info()` on `MCPRequest` that raw-parses `params` as `MCPInitializeParams` when `method == "initialize"`
- Use `model_validate` with `strict=False` so unknown fields in the initialize params are ignored
- The `SharedMetricsStore.set_client_info` uses a simple single-row table (always upsert row id=1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Validation Report: P12-T1 — Add MCP Client Identification

**Date:** 2026-02-15
**Status:** PASS

---

## Quality Gates

| Gate | Result | Details |
|------|--------|---------|
| `pytest` | ✅ PASS | 413 passed, 5 skipped |
| `ruff check src/` | ✅ PASS | All checks passed |
| `mypy src/` | ✅ PASS | No issues found in 13 source files |
| `pytest --cov` | ✅ PASS | 96.04% coverage (≥ 90% required) |

---

## Tests Added

**10 new tests across 3 files:**

- `tests/unit/test_main.py` (+2 tests):
- `test_main_captures_client_info_from_initialize` — verifies clientInfo.name/version extracted and passed to set_client_info
- `test_main_defaults_unknown_when_no_client_info` — verifies missing clientInfo defaults to "unknown"

- `tests/unit/webui/test_metrics.py` (+4 tests):
- `test_initial_client_info_unknown` — client_name/client_version default to "unknown"
- `test_set_client_info` — set_client_info stored in summary
- `test_set_client_info_overwrite` — repeated calls overwrite previous values
- `test_reset_clears_client_info` — reset() restores "unknown"

- `tests/unit/webui/test_shared_metrics.py` (+4 tests):
- `test_initial_client_info_unknown` — no row returns "unknown"
- `test_set_client_info` — set_client_info persisted in SQLite
- `test_set_client_info_upsert` — ON CONFLICT upsert works correctly
- `test_reset_clears_client_info` — reset() deletes client_info row

---

## Changes Summary

### `src/mcpbridge_wrapper/schemas.py`
- Added `MCPClientInfo` model (`name`, `version`, `extra: allow`)
- Added `MCPInitializeParams` model (`clientInfo`)
- Updated `MCPParams` with `clientInfo: Optional[MCPClientInfo]` and `extra: allow`
- Added `MCPRequest.get_client_info()` method

### `src/mcpbridge_wrapper/__main__.py`
- In `on_request()`: detect `method == "initialize"`, call `metrics.set_client_info()`
- Default to `("unknown", "unknown")` when `clientInfo` absent

### `src/mcpbridge_wrapper/webui/metrics.py`
- Added `_client_name` / `_client_version` fields (default `"unknown"`)
- Added `set_client_info(name, version)` method (thread-safe)
- `get_summary()` now includes `client_name` and `client_version`
- `reset()` clears client info to `"unknown"`

### `src/mcpbridge_wrapper/webui/shared_metrics.py`
- Added `client_info` table (`id`, `client_name`, `client_version`, `updated_at`)
- Added `set_client_info(name, version)` upsert method
- `get_summary()` includes `client_name` and `client_version`
- `reset()` deletes from `client_info` table

### `src/mcpbridge_wrapper/webui/static/index.html`
- Added "Active Client" KPI card (`id="kpi-client"`)

### `src/mcpbridge_wrapper/webui/static/dashboard.js`
- `updateKPIs()` renders `client_name` + `client_version` in `kpi-client`

---

## Acceptance Criteria

- [x] `initialize` request `clientInfo.name` and `clientInfo.version` are captured
- [x] Metrics summary includes `client_name` and `client_version` fields
- [x] Dashboard displays "Active Client" KPI card (e.g. "Cursor 1.2.3")
- [x] Metrics reset clears client info
- [x] If `initialize` has no `clientInfo`, fields default to "unknown"
- [x] SQLite schema migration is backward-compatible (nullable column, CREATE IF NOT EXISTS)
- [x] Tests cover initialize parsing, missing clientInfo, and metric tagging
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## REVIEW REPORT — P12-T1: MCP Client Identification

**Scope:** origin/main..HEAD
**Files:** 8 source/test files changed

---

### Summary Verdict
- [ ] Approve
- [x] Approve with comments
- [ ] Request changes
- [ ] Block

**Overall:** The implementation is correct, well-tested, and follows the existing patterns. Minor observations noted below — none are blockers.

---

### Critical Issues

None.

---

### Secondary Issues

**[Low] `MCPInitializeParams` is unused**

`MCPInitializeParams` was added to `schemas.py` as a planned export but is not used anywhere in the codebase. `MCPParams.clientInfo` now covers the same purpose directly. It's harmless but adds confusion about intent.

*Suggested fix:* Either remove it (simplest) or add a usage in tests or a helper function to justify its existence.

**[Low] `initialize_client_info` extracted only on stdin path, not on stdout path**

If a reflective/proxied initialize arrives on stdout (not stdin), it won't be captured. This is by design since `on_request` only handles stdin, but the comment "extract from initialize handshake" might imply both directions.

*Suggested fix:* Add a code comment in `on_request` clarifying this only captures client→bridge direction.

**[Nit] dashboard.js: line length**

Line adding `clientName + " " + clientVersion` is slightly over the implicit 100-char limit used elsewhere in JS.

*Suggested fix:* No action required — JS doesn't have an enforced linter rule here.

---

### Architectural Notes

- `MCPParams.model_config = {"extra": "allow"}` now silently ignores unknown fields in all request params, not just initialize. This is safe but is a broader change than strictly needed.
- The `client_info` table uses a fixed `id=1` sentinel row. This is correct for single-client-per-session semantics but would need rethinking if multiple simultaneous clients were supported.
- `SharedMetricsStore.reset()` now clears client info. This is semantically reasonable but means client identity is lost on reset, requiring a new `initialize` to restore it.

---

### Tests

- 10 new tests added across 3 files.
- Coverage: 96.04% (above 90% threshold).
- New tests cover: clientInfo extraction, missing clientInfo defaulting to "unknown", set/reset in both MetricsCollector and SharedMetricsStore.
- No gaps identified.

---

### Next Steps

- FU-P12-T1-1: Remove or document `MCPInitializeParams` (Low priority)
- FU-P12-T1-2: Add comment to `on_request` clarifying stdin-only capture direction
5 changes: 3 additions & 2 deletions SPECS/INPROGRESS/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ The previously selected task has been archived.

## Recently Archived

- 2026-02-15 — P12-T1: Add MCP Client Identification (PASS)
- 2026-02-15 — P11-T4: Add Keyboard Shortcuts & Command Palette (PASS)
- 2026-02-15 — P11-T3: Add Dashboard Theme Toggle (Dark/Light) (PASS)
- 2026-02-15 — P11-T2: Add Session Timeline View (PASS)
- 2026-02-15 — BUG-T8: Audit log cross-process visibility (PASS)
- 2026-02-15 — P11-T1: Add Tool Call Detail Inspector (Request/Response Viewer) (PASS)

## Suggested Next Tasks

- P12-T1: Add MCP Client Identification (P0, depends on P10-T1 ✅)
- P12-T2: Add Tool Parameter Frequency Analysis (P3, depends on P12-T1 ✅)
- P12-T3: Add Error Classification & Categorization (P1, depends on P10-T1 ✅)
- FU-BUG-T7-1: Cap `pending_methods` map to guard against unbounded growth (P3)
- FU-P11-T1-1: Refactor `_FakeWebUIConfig` test stub to `MagicMock(spec=WebUIConfig)` (P3)
28 changes: 27 additions & 1 deletion SPECS/Workplan.md
Original file line number Diff line number Diff line change
Expand Up @@ -1671,7 +1671,7 @@ Phase 9 Follow-up Backlog

**Intent:** Enrich collected telemetry with client identity, parameter patterns, and structured error classification for deeper operational insight.

#### P12-T1: Add MCP Client Identification
#### P12-T1: Add MCP Client Identification
- **Description:** Detect the calling MCP client from the `initialize` handshake. The `clientInfo` field in the initialize request contains `{name, version}`. Capture this and tag all subsequent metrics with the client identity. Add `client` column to shared metrics SQLite schema. Dashboard: new KPI card "Active Client" showing the connected client name and version. Charts: optional client-based breakdown in tool usage.
- **Priority:** P0
- **Dependencies:** P10-T1
Expand Down Expand Up @@ -1919,6 +1919,32 @@ Phase 9 Follow-up Backlog

---

#### FU-P12-T1-1: Remove or document `MCPInitializeParams` in schemas
- **Description:** `MCPInitializeParams` was added to `schemas.py` during P12-T1 but is not used anywhere in the codebase — `MCPParams.clientInfo` covers the same purpose. Either remove it to reduce confusion, or add a usage (e.g., a helper or test) that justifies its existence as a public export.
- **Priority:** P3
- **Dependencies:** P12-T1
- **Parallelizable:** yes
- **Outputs/Artifacts:**
- Updated `src/mcpbridge_wrapper/schemas.py` — `MCPInitializeParams` removed or documented with usage
- **Acceptance Criteria:**
- [ ] `MCPInitializeParams` is either removed or has a clear, tested usage
- [ ] `pytest` suite remains green

---

#### FU-P12-T1-2: Add code comment clarifying stdin-only client capture in `on_request`
- **Description:** In `__main__.py`'s `on_request()`, the `initialize` client info capture only fires for requests arriving on stdin (client→bridge). Add a brief comment clarifying this intentional scope to prevent future confusion about whether stdout initialize messages are also handled.
- **Priority:** P3
- **Dependencies:** P12-T1
- **Parallelizable:** yes
- **Outputs/Artifacts:**
- Updated `src/mcpbridge_wrapper/__main__.py` — comment added near client info capture block
- **Acceptance Criteria:**
- [ ] Comment clearly states stdin-only capture direction
- [ ] No functional changes

---

#### FU-P11-T1-1: Refactor `_FakeWebUIConfig` test stub to use `MagicMock(spec=WebUIConfig)`
- **Description:** The hand-rolled `_FakeWebUIConfig` class in `tests/unit/test_main.py` must be manually updated every time a new property is added to `WebUIConfig`. Refactor it to use `MagicMock(spec=WebUIConfig)` with only the properties needed by the test wired up, so the spec auto-enforces the real interface and future property additions do not break the test.
- **Priority:** P3
Expand Down
8 changes: 8 additions & 0 deletions src/mcpbridge_wrapper/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ def on_request(line: str) -> None:
if request_id is not None and method is not None:
pending_methods[request_id] = method

# Extract MCP client identity from initialize handshake
if method == "initialize" and metrics is not None:
client_info = req.get_client_info()
if client_info is not None:
metrics.set_client_info(client_info.name, client_info.version)
else:
metrics.set_client_info("unknown", "unknown")

if metrics is None:
return

Expand Down
45 changes: 45 additions & 0 deletions src/mcpbridge_wrapper/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,48 @@
raise


class MCPClientInfo(BaseModel):
"""MCP client identification from initialize handshake.

Attributes:
name: Client name (e.g., "Cursor", "Claude")
version: Client version (e.g., "1.2.3")
"""

model_config = {"extra": "allow"}

name: str = Field(default="unknown", description="Client name")
version: str = Field(default="unknown", description="Client version")


class MCPInitializeParams(BaseModel):
"""MCP initialize request parameters.

Attributes:
clientInfo: Optional client identification
"""

model_config = {"extra": "allow"}

clientInfo: Optional[MCPClientInfo] = Field(default=None, description="Client info") # noqa: N815


class MCPParams(BaseModel):
"""MCP tool call parameters.

Attributes:
name: The tool name (e.g., "BuildProject", "XcodeRead")
arguments: Optional tool arguments
clientInfo: Optional client identification (present in initialize requests)
"""

model_config = {"extra": "allow"}

name: Optional[str] = Field(default=None, description="Tool name")
arguments: Optional[Dict[str, Any]] = Field(default=None, description="Tool arguments")
clientInfo: Optional[MCPClientInfo] = Field( # noqa: N815
default=None, description="Client info (initialize)"
)


class MCPRequest(BaseModel):
Expand Down Expand Up @@ -64,6 +96,19 @@ def get_tool_name(self) -> Optional[str]:

return None

def get_client_info(self) -> Optional["MCPClientInfo"]:
"""Extract client info from an initialize request.

Returns:
MCPClientInfo if method is "initialize" and clientInfo is present,
None otherwise.
"""
if self.method != "initialize":
return None
if self.params is not None and self.params.clientInfo is not None:
return self.params.clientInfo
return None


class MCPResponseResult(BaseModel):
"""MCP response result container.
Expand Down
Loading