From 3eab4898d0f5a553282f3ccb134bae6ad00a615a Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 16 Feb 2026 10:42:13 +0300 Subject: [PATCH 1/8] Select task P12-T2: Add Tool Parameter Frequency Analysis --- SPECS/INPROGRESS/next.md | 43 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/SPECS/INPROGRESS/next.md b/SPECS/INPROGRESS/next.md index c4dba9a0..3e3d4139 100644 --- a/SPECS/INPROGRESS/next.md +++ b/SPECS/INPROGRESS/next.md @@ -1,6 +1,36 @@ -# No Active Task +# Active Task: P12-T2 -The previously selected task has been archived. +## Task Metadata + +- **ID:** P12-T2 +- **Name:** Add Tool Parameter Frequency Analysis +- **Priority:** P3 +- **Dependencies:** P12-T1 ✅ +- **Branch:** feature/P12-T2-tool-parameter-frequency-analysis +- **Selected:** 2026-02-16 + +## Description + +Optionally capture and aggregate tool call parameter keys (not values by default) for pattern analysis. Config flag `capture_params: bool` (default off). On request capture, extract `params.arguments` key names. Store parameter key signatures per tool (e.g. `XcodeGrep(pattern, path, tabIdentifier)`). New API: `GET /api/analytics/param-patterns?tool=` returns top-N parameter combinations. Dashboard: expandable section in latency table showing common param combos. + +## Outputs/Artifacts + +- Updated `src/mcpbridge_wrapper/__main__.py` - extract argument keys from tool call params +- New module or section in `src/mcpbridge_wrapper/webui/metrics.py` - param pattern aggregation +- Updated `src/mcpbridge_wrapper/webui/shared_metrics.py` - `param_keys` column in requests table +- New API endpoint `GET /api/analytics/param-patterns` in `src/mcpbridge_wrapper/webui/server.py` +- Updated `src/mcpbridge_wrapper/webui/config.py` - `capture_params` flag +- Updated `src/mcpbridge_wrapper/webui/static/dashboard.js` - param pattern display +- Tests in `tests/unit/webui/test_metrics.py` + +## Acceptance Criteria + +- [ ] `capture_params: true` enables parameter key capture +- [ ] Only argument key names are stored (not values) by default +- [ ] `GET /api/analytics/param-patterns?tool=XcodeGrep` returns ranked param combos +- [ ] Dashboard shows expandable param pattern info per tool +- [ ] Default behavior (flag off) is unchanged — no extra capture overhead +- [ ] Tests cover param extraction, aggregation, and API response format ## Recently Archived @@ -9,12 +39,3 @@ The previously selected task has been 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) - -## Suggested Next Tasks - -- P12-T2: Add Tool Parameter Frequency Analysis (P3, depends on P12-T1 ✅) -- P11-T1: Add Tool Call Detail Inspector (P2, depends on P10-T1 ✅) -- FU-P12-T1-1: Remove or document `MCPInitializeParams` in schemas (P3, depends on P12-T1 ✅) -- FU-P12-T1-2: Add code comment clarifying stdin-only client capture in `on_request` (P3, depends on P12-T1 ✅) -- FU-P12-T3-1: Document unused `error_message` parameter in `MetricsCollector.record_response` (P3, depends on P12-T3 ✅) -- FU-P12-T3-2: Add `error_code` column to audit CSV export (P3, depends on P12-T3 ✅) From 9e6c84e0b4730cee26607f684e7b4f3ffd99f924 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 16 Feb 2026 10:43:12 +0300 Subject: [PATCH 2/8] Plan task P12-T2: Add Tool Parameter Frequency Analysis --- ...2_Add_Tool_Parameter_Frequency_Analysis.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 SPECS/INPROGRESS/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md diff --git a/SPECS/INPROGRESS/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md b/SPECS/INPROGRESS/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md new file mode 100644 index 00000000..69e70cf5 --- /dev/null +++ b/SPECS/INPROGRESS/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md @@ -0,0 +1,132 @@ +# PRD: P12-T2 — Add Tool Parameter Frequency Analysis + +**Status:** In Progress +**Branch:** feature/P12-T2-tool-parameter-frequency-analysis +**Date:** 2026-02-16 + +--- + +## 1. Overview + +Add optional capture and aggregation of tool call parameter **key names** (not values) to support pattern analysis. This enables operators to see which parameter combinations are most commonly used per tool without exposing sensitive argument values. + +--- + +## 2. Deliverables + +| File | Change | +|------|--------| +| `src/mcpbridge_wrapper/webui/config.py` | Add `capture_params: bool` flag (default `False`) under the `"metrics"` key | +| `src/mcpbridge_wrapper/webui/metrics.py` | Add `record_param_keys(tool_name, param_keys)` method and `get_param_patterns(tool_name)` aggregation | +| `src/mcpbridge_wrapper/webui/shared_metrics.py` | Add `param_patterns` table; `record_param_keys()` and `get_param_patterns()` methods | +| `src/mcpbridge_wrapper/webui/server.py` | Add `GET /api/analytics/param-patterns?tool=` endpoint | +| `src/mcpbridge_wrapper/__main__.py` | Extract `params.arguments` key names when `capture_params=True` and call `metrics.record_param_keys()` | +| `src/mcpbridge_wrapper/webui/static/dashboard.js` | Add expandable param pattern row in tool latency table | +| `tests/unit/webui/test_metrics.py` | New test class covering param pattern recording and retrieval | +| `tests/unit/webui/test_shared_metrics.py` | New tests for SQLite param pattern storage and `get_param_patterns` | + +--- + +## 3. Acceptance Criteria + +- [ ] `capture_params: true` in config enables parameter key capture +- [ ] Only argument key names are stored (not values) by default +- [ ] `GET /api/analytics/param-patterns?tool=XcodeGrep` returns ranked param combos +- [ ] Dashboard shows expandable param pattern info per tool in the latency table +- [ ] Default behavior (`capture_params: false`) is unchanged — no extra capture overhead +- [ ] Tests cover param extraction, aggregation, and API response format + +--- + +## 4. Design Details + +### 4.1 Config Change + +Add `"capture_params": False` under the `"metrics"` section in `_DEFAULTS`. Add `capture_params` property to `WebUIConfig`. + +### 4.2 Param Key Extraction + +In `__main__.py` `on_request` callback: after extracting `tool_name`, if `config.capture_params` is enabled, extract `sorted(req.params.arguments.keys())` from the `MCPRequest`. The sorted tuple of key names forms a "param signature" (e.g. `("path", "pattern", "tabIdentifier")`). + +### 4.3 In-Memory Metrics (MetricsCollector) + +Add: +- `_param_patterns: Dict[str, Dict[Tuple[str, ...], int]]` — maps `tool_name → {signature → count}` +- `record_param_keys(tool_name: str, param_keys: List[str]) -> None` — increments counter for sorted key tuple +- `get_param_patterns(tool_name: str, top_n: int = 10) -> List[Dict[str, Any]]` — returns sorted list of `{keys: [...], count: N}` dicts + +`reset()` must also clear `_param_patterns`. + +### 4.4 SQLite Storage (SharedMetricsStore) + +New table `param_patterns`: +```sql +CREATE TABLE IF NOT EXISTS param_patterns ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tool_name TEXT NOT NULL, + param_signature TEXT NOT NULL, -- JSON array of sorted key names + count INTEGER NOT NULL DEFAULT 1, + UNIQUE(tool_name, param_signature) +); +``` + +- `record_param_keys(tool_name, param_keys)`: upsert (INSERT OR REPLACE) incrementing `count`. +- `get_param_patterns(tool_name, top_n=10)`: SELECT ordered by `count DESC LIMIT top_n`. + +### 4.5 API Endpoint + +``` +GET /api/analytics/param-patterns?tool=&top_n=10 +``` + +Response: +```json +{ + "tool": "XcodeGrep", + "patterns": [ + {"keys": ["path", "pattern"], "count": 42}, + {"keys": ["path", "pattern", "tabIdentifier"], "count": 17} + ] +} +``` + +### 4.6 Dashboard JS + +In the tool latency table, add an expandable chevron button per row. On click, fetch `/api/analytics/param-patterns?tool=` and render a sub-row listing the top param combos (formatted as `pattern, path × 42`). + +--- + +## 5. Dependencies + +- P12-T1 (MCP client identification) — ✅ completed +- `MCPRequest.params.arguments` already available in `schemas.py` +- Existing `on_request` callback in `__main__.py` + +--- + +## 6. Non-Goals + +- Do not store argument values (only key names) +- Do not add persistent storage for param patterns in the audit CSV +- Do not add a config UI for the flag; JSON config file is sufficient + +--- + +## 7. Test Plan + +### MetricsCollector +- `test_record_param_keys_basic`: record two calls with same keys → count = 2 +- `test_record_param_keys_different_signatures`: two different key combos tracked separately +- `test_get_param_patterns_sorted_by_count`: returns highest-count first +- `test_get_param_patterns_top_n`: respects limit +- `test_get_param_patterns_unknown_tool`: returns empty list +- `test_reset_clears_param_patterns` + +### SharedMetricsStore +- `test_record_param_keys_upserts_count` +- `test_get_param_patterns_returns_ranked_list` +- `test_get_param_patterns_unknown_tool_empty` + +### Server +- `test_param_patterns_endpoint_returns_tool_patterns` +- `test_param_patterns_endpoint_unknown_tool_empty` From 40b59ac32395733dd0c2d6ac98e2659b05c28f3b Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 16 Feb 2026 10:47:58 +0300 Subject: [PATCH 3/8] Implement P12-T2: add tool parameter frequency analysis --- SPECS/INPROGRESS/P12-T2_Validation_Report.md | 56 ++++++++++++++ src/mcpbridge_wrapper/__main__.py | 11 +++ src/mcpbridge_wrapper/webui/config.py | 6 ++ src/mcpbridge_wrapper/webui/metrics.py | 42 +++++++++++ src/mcpbridge_wrapper/webui/server.py | 13 ++++ src/mcpbridge_wrapper/webui/shared_metrics.py | 62 ++++++++++++++++ .../webui/static/dashboard.css | 51 +++++++++++++ .../webui/static/dashboard.js | 71 ++++++++++++++++-- tests/unit/webui/test_config.py | 16 ++++ tests/unit/webui/test_metrics.py | 74 +++++++++++++++++++ tests/unit/webui/test_server.py | 59 +++++++++++++++ tests/unit/webui/test_shared_metrics.py | 44 +++++++++++ 12 files changed, 499 insertions(+), 6 deletions(-) create mode 100644 SPECS/INPROGRESS/P12-T2_Validation_Report.md diff --git a/SPECS/INPROGRESS/P12-T2_Validation_Report.md b/SPECS/INPROGRESS/P12-T2_Validation_Report.md new file mode 100644 index 00000000..70f4b9d7 --- /dev/null +++ b/SPECS/INPROGRESS/P12-T2_Validation_Report.md @@ -0,0 +1,56 @@ +# Validation Report: P12-T2 — Add Tool Parameter Frequency Analysis + +**Date:** 2026-02-16 +**Branch:** feature/P12-T2-tool-parameter-frequency-analysis +**Verdict:** PASS + +--- + +## Quality Gates + +| Gate | Result | Details | +|------|--------|---------| +| `pytest` | ✅ PASS | 457 passed, 5 skipped | +| `ruff check src/` | ✅ PASS | No issues | +| `mypy src/` | ✅ PASS | No issues in 13 source files | +| `pytest --cov` | ✅ PASS | Coverage: 95.95% (≥ 90%) | + +--- + +## Acceptance Criteria + +| Criterion | Status | +|-----------|--------| +| `capture_params: true` enables parameter key capture | ✅ Implemented — `WebUIConfig.capture_params` property, `on_request` reads flag | +| Only argument key names are stored (not values) | ✅ Only `req.params.arguments.keys()` captured, values never touched | +| `GET /api/analytics/param-patterns?tool=` returns ranked combos | ✅ Endpoint added to `server.py` | +| Dashboard shows expandable param pattern info per tool | ✅ Chevron buttons + fetch in `dashboard.js`, CSS in `dashboard.css` | +| Default behavior unchanged (flag off) | ✅ `capture_params` defaults to `False`; no capture overhead when off | +| Tests cover param extraction, aggregation, API response | ✅ 8 tests in `test_metrics.py`, 7 in `test_shared_metrics.py`, 4 in `test_server.py`, 2 in `test_config.py` | + +--- + +## Files Changed + +| File | Change | +|------|--------| +| `src/mcpbridge_wrapper/webui/config.py` | Added `capture_params` default and property | +| `src/mcpbridge_wrapper/webui/metrics.py` | Added `_param_patterns` dict, `record_param_keys()`, `get_param_patterns()`, updated `reset()` | +| `src/mcpbridge_wrapper/webui/shared_metrics.py` | Added `import json`, `param_patterns` table, `record_param_keys()`, `get_param_patterns()`, updated `reset()` | +| `src/mcpbridge_wrapper/webui/server.py` | Added `GET /api/analytics/param-patterns` endpoint | +| `src/mcpbridge_wrapper/__main__.py` | Added `config = None` initializer; param key extraction in `on_request` | +| `src/mcpbridge_wrapper/webui/static/dashboard.js` | Rewrote `updateLatencyTable` with expandable param pattern rows + `fetchParamPatterns()` | +| `src/mcpbridge_wrapper/webui/static/dashboard.css` | Added styles for `.param-toggle-btn`, `.param-detail-row`, `.param-patterns-table` | +| `tests/unit/webui/test_metrics.py` | Added `TestParamPatterns` class (8 tests) | +| `tests/unit/webui/test_shared_metrics.py` | Added 6 param pattern tests | +| `tests/unit/webui/test_server.py` | Added `TestParamPatternsEndpoint` class (4 tests) | +| `tests/unit/webui/test_config.py` | Added 2 tests for `capture_params` config flag | + +--- + +## Notes + +- `SharedMetricsStore.record_param_keys` uses SQLite UPSERT (`ON CONFLICT DO UPDATE`) for atomic count increment across processes. +- `MetricsCollector.record_param_keys` uses a nested dict for in-memory single-process use. +- The `updateLatencyTable` function now builds DOM elements directly instead of innerHTML string concatenation for the expand-row listener pattern. +- When `capture_params` is disabled (default), the `on_request` path reaches the `if` check but immediately short-circuits — no argument key extraction occurs. diff --git a/src/mcpbridge_wrapper/__main__.py b/src/mcpbridge_wrapper/__main__.py index 0ab2fc79..3405ec53 100644 --- a/src/mcpbridge_wrapper/__main__.py +++ b/src/mcpbridge_wrapper/__main__.py @@ -210,6 +210,7 @@ def main() -> int: return 2 # Initialize web UI components if enabled + config = None metrics = None audit = None @@ -334,6 +335,16 @@ def on_request(line: str) -> None: metrics.record_request(tool_name, request_id=request_id) pending_requests[request_id] = (tool_name, start_time) + # Capture parameter key names when feature flag is enabled + if ( + config is not None + and config.capture_params + and req.params is not None + and req.params.arguments is not None + ): + param_keys = list(req.params.arguments.keys()) + metrics.record_param_keys(tool_name, param_keys) + except Exception: pass diff --git a/src/mcpbridge_wrapper/webui/config.py b/src/mcpbridge_wrapper/webui/config.py index bcbbccff..44731edd 100644 --- a/src/mcpbridge_wrapper/webui/config.py +++ b/src/mcpbridge_wrapper/webui/config.py @@ -19,6 +19,7 @@ "metrics": { "window_seconds": 3600, "max_datapoints": 3600, + "capture_params": False, }, "audit": { "enabled": True, @@ -130,6 +131,11 @@ def metrics_max_datapoints(self) -> int: """Maximum metrics data points per time-series.""" return int(self._data["metrics"]["max_datapoints"]) + @property + def capture_params(self) -> bool: + """Whether to capture tool call parameter key names for pattern analysis.""" + return bool(self._data["metrics"].get("capture_params", False)) + @property def audit_enabled(self) -> bool: """Whether audit logging is enabled.""" diff --git a/src/mcpbridge_wrapper/webui/metrics.py b/src/mcpbridge_wrapper/webui/metrics.py index e8799129..67f36a77 100644 --- a/src/mcpbridge_wrapper/webui/metrics.py +++ b/src/mcpbridge_wrapper/webui/metrics.py @@ -84,6 +84,9 @@ def __init__(self, window_seconds: int = 3600, max_datapoints: int = 3600) -> No # Error breakdown by code self._error_counts_by_code: Dict[int, int] = {} + # Param pattern tracking: tool_name -> {sorted_key_tuple -> count} + self._param_patterns: Dict[str, Dict[Tuple[str, ...], int]] = {} + def set_client_info(self, name: str, version: str) -> None: """Record the connected MCP client identity. @@ -262,6 +265,44 @@ def get_timeseries(self, seconds: int = 300) -> Dict[str, Any]: "latencies": [{"t": round(t - now, 2), "v": round(v, 2)} for t, v in latencies], } + def record_param_keys(self, tool_name: str, param_keys: List[str]) -> None: + """Record a parameter key signature for a tool call. + + Only key names are stored — argument values are never captured. + + Args: + tool_name: Name of the MCP tool. + param_keys: List of argument key names from the tool call. + """ + signature: Tuple[str, ...] = tuple(sorted(param_keys)) + with self._lock: + if tool_name not in self._param_patterns: + self._param_patterns[tool_name] = {} + self._param_patterns[tool_name][signature] = ( + self._param_patterns[tool_name].get(signature, 0) + 1 + ) + + def get_param_patterns( + self, tool_name: str, top_n: int = 10 + ) -> List[Dict[str, Any]]: + """Return the most common parameter key combinations for a tool. + + Args: + tool_name: Name of the MCP tool to query. + top_n: Maximum number of patterns to return. + + Returns: + List of dicts with ``keys`` (sorted list) and ``count``, ordered + by descending count. + """ + with self._lock: + patterns = self._param_patterns.get(tool_name, {}) + sorted_patterns = sorted(patterns.items(), key=lambda kv: kv[1], reverse=True) + return [ + {"keys": list(sig), "count": cnt} + for sig, cnt in sorted_patterns[:top_n] + ] + def reset(self) -> None: """Reset all metrics to initial state.""" with self._lock: @@ -278,3 +319,4 @@ def reset(self) -> None: self._client_name = "unknown" self._client_version = "unknown" self._error_counts_by_code.clear() + self._param_patterns.clear() diff --git a/src/mcpbridge_wrapper/webui/server.py b/src/mcpbridge_wrapper/webui/server.py index 2acd5486..a16d8b6b 100644 --- a/src/mcpbridge_wrapper/webui/server.py +++ b/src/mcpbridge_wrapper/webui/server.py @@ -300,6 +300,19 @@ async def get_sessions( sessions = detect_sessions(entries, gap_seconds=float(effective_gap)) return {"sessions": sessions, "total": len(sessions)} + # --- API: Analytics --- + + @app.get("/api/analytics/param-patterns") + async def get_param_patterns( + request: Request, + tool: str = Query(..., description="Tool name to query param patterns for"), + top_n: int = Query(default=10, ge=1, le=100), + ) -> dict[str, Any]: + """Get the most common parameter key combinations for a tool.""" + _check_auth(request, config) + patterns = metrics.get_param_patterns(tool, top_n=top_n) + return {"tool": tool, "patterns": patterns} + # --- API: Configuration --- @app.get("/api/config") diff --git a/src/mcpbridge_wrapper/webui/shared_metrics.py b/src/mcpbridge_wrapper/webui/shared_metrics.py index f001f151..a08c199f 100644 --- a/src/mcpbridge_wrapper/webui/shared_metrics.py +++ b/src/mcpbridge_wrapper/webui/shared_metrics.py @@ -6,6 +6,7 @@ """ import contextlib +import json import sqlite3 import threading import time @@ -80,6 +81,20 @@ def _ensure_db(self) -> None: updated_at REAL ) """) + # Param patterns table: stores frequency of argument key combinations per tool + conn.execute(""" + CREATE TABLE IF NOT EXISTS param_patterns ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tool_name TEXT NOT NULL, + param_signature TEXT NOT NULL, + count INTEGER NOT NULL DEFAULT 1, + UNIQUE(tool_name, param_signature) + ) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_param_patterns_tool + ON param_patterns(tool_name) + """) @contextmanager def _transaction(self) -> Generator[sqlite3.Connection, None, None]: @@ -348,11 +363,58 @@ def get_timeseries(self, seconds: int = 300) -> Dict[str, List[Dict[str, Any]]]: "latencies": latencies_data, } + def record_param_keys(self, tool_name: str, param_keys: List[str]) -> None: + """Record a parameter key signature for a tool call. + + Only key names are stored — argument values are never captured. + + Args: + tool_name: Name of the MCP tool. + param_keys: List of argument key names from the tool call. + """ + signature = json.dumps(sorted(param_keys)) + with self._transaction() as conn: + conn.execute( + """INSERT INTO param_patterns (tool_name, param_signature, count) + VALUES (?, ?, 1) + ON CONFLICT(tool_name, param_signature) + DO UPDATE SET count = count + 1""", + (tool_name, signature), + ) + + def get_param_patterns( + self, tool_name: str, top_n: int = 10 + ) -> List[Dict[str, Any]]: + """Return the most common parameter key combinations for a tool. + + Args: + tool_name: Name of the MCP tool to query. + top_n: Maximum number of patterns to return. + + Returns: + List of dicts with ``keys`` (sorted list) and ``count``, ordered + by descending count. + """ + with self._transaction() as conn: + cursor = conn.execute( + """SELECT param_signature, count + FROM param_patterns + WHERE tool_name = ? + ORDER BY count DESC + LIMIT ?""", + (tool_name, top_n), + ) + return [ + {"keys": json.loads(row["param_signature"]), "count": row["count"]} + for row in cursor + ] + def reset(self) -> None: """Clear all metrics data.""" with self._transaction() as conn: conn.execute("DELETE FROM requests") conn.execute("DELETE FROM client_info") + conn.execute("DELETE FROM param_patterns") def close(self) -> None: """Close database connection.""" diff --git a/src/mcpbridge_wrapper/webui/static/dashboard.css b/src/mcpbridge_wrapper/webui/static/dashboard.css index 15590dc0..293cb3b9 100644 --- a/src/mcpbridge_wrapper/webui/static/dashboard.css +++ b/src/mcpbridge_wrapper/webui/static/dashboard.css @@ -593,3 +593,54 @@ kbd { color: var(--text-primary); box-shadow: 0 1px 0 var(--border-color); } + +/* Param pattern toggle button in latency table */ +.param-toggle-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary); + font-size: 0.65rem; + padding: 0 4px 0 0; + vertical-align: middle; + transition: color 0.15s; +} + +.param-toggle-btn:hover { + color: var(--accent-blue); +} + +/* Expandable detail row for param patterns */ +.param-detail-row td { + background: var(--bg-secondary); + padding: 8px 16px 8px 32px; +} + +.param-patterns-container { + font-size: 0.85rem; +} + +.param-patterns-table { + width: auto; + min-width: 300px; + border-collapse: collapse; +} + +.param-patterns-table th { + text-align: left; + padding: 4px 12px 4px 0; + color: var(--text-secondary); + font-weight: 600; + font-size: 0.78rem; + border-bottom: 1px solid var(--border-color); +} + +.param-patterns-table td { + padding: 3px 12px 3px 0; + color: var(--text-primary); + border-bottom: none; +} + +.param-patterns-table tr:last-child td { + border-bottom: none; +} diff --git a/src/mcpbridge_wrapper/webui/static/dashboard.js b/src/mcpbridge_wrapper/webui/static/dashboard.js index b89291ea..e5768b42 100644 --- a/src/mcpbridge_wrapper/webui/static/dashboard.js +++ b/src/mcpbridge_wrapper/webui/static/dashboard.js @@ -296,22 +296,81 @@ function updateLatencyTable(toolLatency) { var tbody = el("latency-table").querySelector("tbody"); - var rows = ""; + tbody.innerHTML = ""; var tools = Object.keys(toolLatency).sort(); + if (tools.length === 0) { + tbody.innerHTML = "No latency data"; + return; + } tools.forEach(function (tool) { var s = toolLatency[tool]; - rows += "" - + "" + tool + "" + var rowId = "param-row-" + tool.replace(/[^a-zA-Z0-9]/g, "_"); + var tr = document.createElement("tr"); + tr.innerHTML = "" + + " " + tool + + "" + "" + s.count + "" + "" + s.avg_ms.toFixed(1) + "" + "" + s.p50_ms.toFixed(1) + "" + "" + s.p95_ms.toFixed(1) + "" + "" + s.p99_ms.toFixed(1) + "" + "" + s.min_ms.toFixed(1) + "" - + "" + s.max_ms.toFixed(1) + "" - + ""; + + "" + s.max_ms.toFixed(1) + ""; + tbody.appendChild(tr); + + var detailTr = document.createElement("tr"); + detailTr.id = rowId; + detailTr.className = "param-detail-row"; + detailTr.style.display = "none"; + detailTr.innerHTML = "
" + + "Loading\u2026
"; + tbody.appendChild(detailTr); + }); + + tbody.addEventListener("click", function (e) { + var btn = e.target.closest(".param-toggle-btn"); + if (!btn) return; + var targetId = btn.getAttribute("data-target"); + var toolName = btn.getAttribute("data-tool"); + var detailRow = document.getElementById(targetId); + if (!detailRow) return; + var isOpen = detailRow.style.display !== "none"; + if (isOpen) { + detailRow.style.display = "none"; + btn.innerHTML = "▶"; + btn.setAttribute("aria-expanded", "false"); + } else { + detailRow.style.display = ""; + btn.innerHTML = "▼"; + btn.setAttribute("aria-expanded", "true"); + fetchParamPatterns(toolName, "patterns-" + targetId); + } }); - tbody.innerHTML = rows || "No latency data"; + } + + function fetchParamPatterns(toolName, containerId) { + fetch("/api/analytics/param-patterns?tool=" + encodeURIComponent(toolName) + "&top_n=10") + .then(function (r) { return r.json(); }) + .then(function (data) { + var container = document.getElementById(containerId); + if (!container) return; + if (!data.patterns || data.patterns.length === 0) { + container.innerHTML = "No parameter patterns captured. Enable capture_params in config."; + return; + } + var html = ""; + data.patterns.forEach(function (p) { + html += ""; + }); + html += "
Parameter KeysCount
" + p.keys.join(", ") + "" + p.count + "
"; + container.innerHTML = html; + }) + .catch(function () { + var container = document.getElementById(containerId); + if (container) container.innerHTML = "Failed to load patterns."; + }); } function handleMetricsUpdate(data) { diff --git a/tests/unit/webui/test_config.py b/tests/unit/webui/test_config.py index 84d98dc7..edc13a76 100644 --- a/tests/unit/webui/test_config.py +++ b/tests/unit/webui/test_config.py @@ -117,3 +117,19 @@ def test_merge_does_not_affect_original_defaults(self): assert _DEFAULTS["port"] == original_defaults["port"] finally: os.unlink(temp_path) + + def test_capture_params_default_false(self): + """capture_params defaults to False.""" + config = WebUIConfig() + assert config.capture_params is False + + def test_capture_params_enabled_via_config_file(self): + """capture_params can be enabled via JSON config file.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump({"metrics": {"capture_params": True}}, f) + temp_path = f.name + try: + config = WebUIConfig(config_path=temp_path) + assert config.capture_params is True + finally: + os.unlink(temp_path) diff --git a/tests/unit/webui/test_metrics.py b/tests/unit/webui/test_metrics.py index 0dec2cff..9a96d260 100644 --- a/tests/unit/webui/test_metrics.py +++ b/tests/unit/webui/test_metrics.py @@ -288,6 +288,80 @@ def test_reset_clears_error_counts(self): assert summary["error_counts_by_code"] == {} +class TestParamPatterns: + """Tests for param pattern recording and retrieval.""" + + def test_record_param_keys_basic(self): + """Recording the same key set twice increments count to 2.""" + metrics = MetricsCollector() + metrics.record_param_keys("XcodeGrep", ["pattern", "path"]) + metrics.record_param_keys("XcodeGrep", ["path", "pattern"]) # same keys, different order + patterns = metrics.get_param_patterns("XcodeGrep") + assert len(patterns) == 1 + assert patterns[0]["count"] == 2 + assert sorted(patterns[0]["keys"]) == ["path", "pattern"] + + def test_record_param_keys_different_signatures(self): + """Different key combos are tracked separately.""" + metrics = MetricsCollector() + metrics.record_param_keys("XcodeGrep", ["pattern", "path"]) + metrics.record_param_keys("XcodeGrep", ["pattern", "path", "tabIdentifier"]) + patterns = metrics.get_param_patterns("XcodeGrep") + assert len(patterns) == 2 + + def test_get_param_patterns_sorted_by_count(self): + """Patterns are returned in descending count order.""" + metrics = MetricsCollector() + metrics.record_param_keys("Tool", ["a"]) + metrics.record_param_keys("Tool", ["b"]) + metrics.record_param_keys("Tool", ["b"]) + metrics.record_param_keys("Tool", ["b"]) + patterns = metrics.get_param_patterns("Tool") + assert patterns[0]["keys"] == ["b"] + assert patterns[0]["count"] == 3 + assert patterns[1]["keys"] == ["a"] + assert patterns[1]["count"] == 1 + + def test_get_param_patterns_top_n(self): + """top_n parameter limits result count.""" + metrics = MetricsCollector() + for i in range(5): + metrics.record_param_keys("Tool", [f"key{i}"]) + patterns = metrics.get_param_patterns("Tool", top_n=3) + assert len(patterns) == 3 + + def test_get_param_patterns_unknown_tool(self): + """Returns empty list for unknown tool.""" + metrics = MetricsCollector() + patterns = metrics.get_param_patterns("NoSuchTool") + assert patterns == [] + + def test_reset_clears_param_patterns(self): + """reset() removes all param pattern data.""" + metrics = MetricsCollector() + metrics.record_param_keys("XcodeGrep", ["pattern"]) + metrics.reset() + patterns = metrics.get_param_patterns("XcodeGrep") + assert patterns == [] + + def test_record_param_keys_empty_list(self): + """Empty param key list is stored as empty signature.""" + metrics = MetricsCollector() + metrics.record_param_keys("Tool", []) + patterns = metrics.get_param_patterns("Tool") + assert len(patterns) == 1 + assert patterns[0]["keys"] == [] + assert patterns[0]["count"] == 1 + + def test_record_param_keys_multiple_tools(self): + """Patterns are isolated per tool.""" + metrics = MetricsCollector() + metrics.record_param_keys("ToolA", ["x"]) + metrics.record_param_keys("ToolB", ["y"]) + assert metrics.get_param_patterns("ToolA")[0]["keys"] == ["x"] + assert metrics.get_param_patterns("ToolB")[0]["keys"] == ["y"] + + class TestCategorizeError: """Tests for the categorize_error helper function.""" diff --git a/tests/unit/webui/test_server.py b/tests/unit/webui/test_server.py index e11d4b5b..7ec0e34b 100644 --- a/tests/unit/webui/test_server.py +++ b/tests/unit/webui/test_server.py @@ -297,3 +297,62 @@ def test_detail_none_payloads(self, config, metrics, audit_with_capture): data = response.json() assert data["request"] is None assert data["response"] is None + + +class TestParamPatternsEndpoint: + """Tests for GET /api/analytics/param-patterns endpoint.""" + + @pytest.fixture + def config(self): + return WebUIConfig() + + @pytest.fixture + def audit(self): + with tempfile.TemporaryDirectory() as tmpdir: + yield AuditLogger(log_dir=tmpdir) + + def test_param_patterns_endpoint_returns_tool_patterns(self, config, audit): + """Endpoint returns recorded param patterns for a tool.""" + metrics = MetricsCollector() + metrics.record_param_keys("XcodeGrep", ["pattern", "path"]) + metrics.record_param_keys("XcodeGrep", ["path", "pattern"]) + app = create_app(config, metrics, audit) + client = TestClient(app) + response = client.get("/api/analytics/param-patterns?tool=XcodeGrep") + assert response.status_code == 200 + data = response.json() + assert data["tool"] == "XcodeGrep" + assert len(data["patterns"]) == 1 + assert data["patterns"][0]["count"] == 2 + assert sorted(data["patterns"][0]["keys"]) == ["path", "pattern"] + + def test_param_patterns_endpoint_unknown_tool_empty(self, config, audit): + """Endpoint returns empty patterns list for unknown tool.""" + metrics = MetricsCollector() + app = create_app(config, metrics, audit) + client = TestClient(app) + response = client.get("/api/analytics/param-patterns?tool=NoSuchTool") + assert response.status_code == 200 + data = response.json() + assert data["tool"] == "NoSuchTool" + assert data["patterns"] == [] + + def test_param_patterns_endpoint_requires_tool_param(self, config, audit): + """Endpoint returns 422 when tool query param is missing.""" + metrics = MetricsCollector() + app = create_app(config, metrics, audit) + client = TestClient(app) + response = client.get("/api/analytics/param-patterns") + assert response.status_code == 422 + + def test_param_patterns_endpoint_top_n(self, config, audit): + """top_n query param limits returned results.""" + metrics = MetricsCollector() + for i in range(5): + metrics.record_param_keys("Tool", [f"k{i}"]) + app = create_app(config, metrics, audit) + client = TestClient(app) + response = client.get("/api/analytics/param-patterns?tool=Tool&top_n=2") + assert response.status_code == 200 + data = response.json() + assert len(data["patterns"]) == 2 diff --git a/tests/unit/webui/test_shared_metrics.py b/tests/unit/webui/test_shared_metrics.py index 39d3d6e4..b66e743d 100644 --- a/tests/unit/webui/test_shared_metrics.py +++ b/tests/unit/webui/test_shared_metrics.py @@ -262,3 +262,47 @@ def test_record_response_error_code_no_request_id(self, store): store.record_response("BuildProject", error=True, latency_ms=50.0, error_code=-32001) summary = store.get_summary() assert summary["error_counts_by_code"] == {-32001: 1} + + def test_record_param_keys_upserts_count(self, store): + """record_param_keys increments count on repeated same signature.""" + store.record_param_keys("XcodeGrep", ["pattern", "path"]) + store.record_param_keys("XcodeGrep", ["path", "pattern"]) # sorted same + patterns = store.get_param_patterns("XcodeGrep") + assert len(patterns) == 1 + assert patterns[0]["count"] == 2 + assert sorted(patterns[0]["keys"]) == ["path", "pattern"] + + def test_record_param_keys_different_signatures(self, store): + """Different key combos stored separately.""" + store.record_param_keys("XcodeGrep", ["pattern"]) + store.record_param_keys("XcodeGrep", ["pattern", "path"]) + patterns = store.get_param_patterns("XcodeGrep") + assert len(patterns) == 2 + + def test_get_param_patterns_returns_ranked_list(self, store): + """get_param_patterns returns patterns sorted by count descending.""" + store.record_param_keys("Tool", ["a"]) + store.record_param_keys("Tool", ["b"]) + store.record_param_keys("Tool", ["b"]) + patterns = store.get_param_patterns("Tool") + assert patterns[0]["keys"] == ["b"] + assert patterns[0]["count"] == 2 + + def test_get_param_patterns_unknown_tool_empty(self, store): + """Returns empty list for tool with no recorded patterns.""" + patterns = store.get_param_patterns("NoSuchTool") + assert patterns == [] + + def test_get_param_patterns_top_n(self, store): + """top_n limits the number of returned patterns.""" + for i in range(5): + store.record_param_keys("Tool", [f"key{i}"]) + patterns = store.get_param_patterns("Tool", top_n=2) + assert len(patterns) == 2 + + def test_reset_clears_param_patterns(self, store): + """reset() removes all param_patterns rows.""" + store.record_param_keys("XcodeGrep", ["pattern"]) + store.reset() + patterns = store.get_param_patterns("XcodeGrep") + assert patterns == [] From b7aeebf01cc3a2300d99af99b489909a69337ac7 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 16 Feb 2026 10:49:06 +0300 Subject: [PATCH 4/8] Archive task P12-T2: Add Tool Parameter Frequency Analysis (PASS) --- SPECS/ARCHIVE/INDEX.md | 4 +- ...2_Add_Tool_Parameter_Frequency_Analysis.md | 0 .../P12-T2_Validation_Report.md | 0 SPECS/INPROGRESS/next.md | 45 +++++-------------- SPECS/Workplan.md | 2 +- 5 files changed, 16 insertions(+), 35 deletions(-) rename SPECS/{INPROGRESS => ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis}/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md (100%) rename SPECS/{INPROGRESS => ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis}/P12-T2_Validation_Report.md (100%) diff --git a/SPECS/ARCHIVE/INDEX.md b/SPECS/ARCHIVE/INDEX.md index 3c0bcf59..3c9b9c33 100644 --- a/SPECS/ARCHIVE/INDEX.md +++ b/SPECS/ARCHIVE/INDEX.md @@ -1,6 +1,6 @@ # mcpbridge-wrapper Tasks Archive -**Last Updated:** 2026-02-15 (P12-T4) +**Last Updated:** 2026-02-16 (P12-T2) ## Archived Tasks @@ -97,6 +97,7 @@ | P12-T1 | [P12-T1_Add_MCP_Client_Identification/](P12-T1_Add_MCP_Client_Identification/) | 2026-02-15 | PASS | | P12-T3 | [P12-T3_Add_Error_Classification_and_Categorization/](P12-T3_Add_Error_Classification_and_Categorization/) | 2026-02-15 | PASS | | P12-T4 | [P12-T4_Add_documentation_about_data_storage/](P12-T4_Add_documentation_about_data_storage/) | 2026-02-15 | PASS | +| P12-T2 | [P12-T2_Add_Tool_Parameter_Frequency_Analysis/](P12-T2_Add_Tool_Parameter_Frequency_Analysis/) | 2026-02-16 | PASS | ## Historical Artifacts @@ -269,3 +270,4 @@ | 2026-02-15 | P12-T3 | Archived REVIEW_P12-T3_error_classification_categorization report | | 2026-02-15 | P12-T4 | Archived Add_documentation_about_data_storage (PASS) | | 2026-02-15 | P12-T4 | Archived REVIEW_P12-T4_data_storage_documentation report | +| 2026-02-16 | P12-T2 | Archived Add_Tool_Parameter_Frequency_Analysis (PASS) | diff --git a/SPECS/INPROGRESS/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md b/SPECS/ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md similarity index 100% rename from SPECS/INPROGRESS/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md rename to SPECS/ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis/P12-T2_Add_Tool_Parameter_Frequency_Analysis.md diff --git a/SPECS/INPROGRESS/P12-T2_Validation_Report.md b/SPECS/ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis/P12-T2_Validation_Report.md similarity index 100% rename from SPECS/INPROGRESS/P12-T2_Validation_Report.md rename to SPECS/ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis/P12-T2_Validation_Report.md diff --git a/SPECS/INPROGRESS/next.md b/SPECS/INPROGRESS/next.md index 3e3d4139..507263f1 100644 --- a/SPECS/INPROGRESS/next.md +++ b/SPECS/INPROGRESS/next.md @@ -1,41 +1,20 @@ -# Active Task: P12-T2 +# No Active Task -## Task Metadata - -- **ID:** P12-T2 -- **Name:** Add Tool Parameter Frequency Analysis -- **Priority:** P3 -- **Dependencies:** P12-T1 ✅ -- **Branch:** feature/P12-T2-tool-parameter-frequency-analysis -- **Selected:** 2026-02-16 - -## Description - -Optionally capture and aggregate tool call parameter keys (not values by default) for pattern analysis. Config flag `capture_params: bool` (default off). On request capture, extract `params.arguments` key names. Store parameter key signatures per tool (e.g. `XcodeGrep(pattern, path, tabIdentifier)`). New API: `GET /api/analytics/param-patterns?tool=` returns top-N parameter combinations. Dashboard: expandable section in latency table showing common param combos. - -## Outputs/Artifacts - -- Updated `src/mcpbridge_wrapper/__main__.py` - extract argument keys from tool call params -- New module or section in `src/mcpbridge_wrapper/webui/metrics.py` - param pattern aggregation -- Updated `src/mcpbridge_wrapper/webui/shared_metrics.py` - `param_keys` column in requests table -- New API endpoint `GET /api/analytics/param-patterns` in `src/mcpbridge_wrapper/webui/server.py` -- Updated `src/mcpbridge_wrapper/webui/config.py` - `capture_params` flag -- Updated `src/mcpbridge_wrapper/webui/static/dashboard.js` - param pattern display -- Tests in `tests/unit/webui/test_metrics.py` - -## Acceptance Criteria - -- [ ] `capture_params: true` enables parameter key capture -- [ ] Only argument key names are stored (not values) by default -- [ ] `GET /api/analytics/param-patterns?tool=XcodeGrep` returns ranked param combos -- [ ] Dashboard shows expandable param pattern info per tool -- [ ] Default behavior (flag off) is unchanged — no extra capture overhead -- [ ] Tests cover param extraction, aggregation, and API response format +The previously selected task has been archived. ## Recently Archived +- 2026-02-16 — P12-T2: Add Tool Parameter Frequency Analysis (PASS) - 2026-02-15 — P12-T4: Add documentation about data storage (PASS) - 2026-02-15 — P12-T3: Add Error Classification & Categorization (PASS) - 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) + +## Suggested Next Tasks + +- P11-T1: Add Tool Call Detail Inspector (P2, depends on P10-T1 ✅) +- FU-P12-T1-1: Remove or document `MCPInitializeParams` in schemas (P3, depends on P12-T1 ✅) +- FU-P12-T1-2: Add code comment clarifying stdin-only client capture in `on_request` (P3, depends on P12-T1 ✅) +- FU-P12-T3-1: Document unused `error_message` parameter in `MetricsCollector.record_response` (P3, depends on P12-T3 ✅) +- FU-P12-T3-2: Add `error_code` column to audit CSV export (P3, depends on P12-T3 ✅) +- P11-T2: Add Session Timeline View (P3, depends on P11-T1) diff --git a/SPECS/Workplan.md b/SPECS/Workplan.md index 9a0d0603..3d1f002e 100644 --- a/SPECS/Workplan.md +++ b/SPECS/Workplan.md @@ -1696,7 +1696,7 @@ Phase 9 Follow-up Backlog --- -#### P12-T2: Add Tool Parameter Frequency Analysis +#### ✅ P12-T2: Add Tool Parameter Frequency Analysis - **Description:** Optionally capture and aggregate tool call parameter keys (not values by default) for pattern analysis. Config flag `capture_params: bool` (default off). On request capture, extract `params.arguments` key names. Store parameter key signatures per tool (e.g. `XcodeGrep(pattern, path, tabIdentifier)`). New API: `GET /api/analytics/param-patterns?tool=` returns top-N parameter combinations. Dashboard: expandable section in latency table showing common param combos. - **Priority:** P3 - **Dependencies:** P12-T1 From 044399241612f512c36a8ca0235bc89cad6c54f8 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 16 Feb 2026 10:49:59 +0300 Subject: [PATCH 5/8] Review P12-T2: param frequency analysis --- .../REVIEW_P12-T2_param_frequency_analysis.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 SPECS/INPROGRESS/REVIEW_P12-T2_param_frequency_analysis.md diff --git a/SPECS/INPROGRESS/REVIEW_P12-T2_param_frequency_analysis.md b/SPECS/INPROGRESS/REVIEW_P12-T2_param_frequency_analysis.md new file mode 100644 index 00000000..042dfa14 --- /dev/null +++ b/SPECS/INPROGRESS/REVIEW_P12-T2_param_frequency_analysis.md @@ -0,0 +1,63 @@ +## REVIEW REPORT — P12-T2: Tool Parameter Frequency Analysis + +**Scope:** origin/main..HEAD (4 commits) +**Files:** 12 modified, 2 created (archive artifacts) +**Date:** 2026-02-16 + +--- + +### Summary Verdict + +- [x] Approve + +--- + +### Critical Issues + +None. + +--- + +### Secondary Issues + +**[Low] `on_request` catch-all `except Exception: pass` swallows param capture errors silently** + +In `__main__.py`, the entire `on_request` body is wrapped in a bare `except Exception: pass`. This is pre-existing, not introduced by this task, and is intentional to prevent metrics logic from crashing the bridge. However the new `record_param_keys` call inherits this behavior — any bug in param key extraction would be silently suppressed. This is acceptable given the non-critical nature of the feature, but worth noting. + +**[Low] `SharedMetricsStore.record_param_keys` opens a new transaction per call** + +For high-frequency tool calls, each `record_param_keys` invocation commits a separate SQLite transaction. This is consistent with the existing `record_request`/`record_response` pattern and does not change behavior, but at very high call rates (>1000 req/s) SQLite write contention could increase. Acceptable for the use case. + +**[Nit] Dashboard event listener is re-registered on every `updateLatencyTable` call** + +The `tbody.addEventListener("click", ...)` call inside `updateLatencyTable` adds a new click listener each time metrics update (every 1 second). This means multiple handlers accumulate on the `tbody` element. Since the tbody is replaced via `innerHTML = ""` / `appendChild` on each refresh, old listeners are detached but the outer `tbody` element persists — so multiple listeners stack. Functionally harmless (duplicate fetches for the same tool would simply overwrite the container) but slightly wasteful. + +**Fix suggestion:** Move the event listener attachment to a one-time setup function or use event delegation on the parent table/section. Alternatively, track whether the listener is already attached via a `data-listener-attached` attribute. + +--- + +### Architectural Notes + +- The design correctly separates in-memory (`MetricsCollector`) and process-safe (`SharedMetricsStore`) implementations, consistent with the existing pattern. +- The `capture_params` flag is opt-in and defaults to `False` — safe for production use where argument values might be sensitive. +- The API endpoint (`/api/analytics/param-patterns`) is read-only and requires the same auth as other endpoints. Good. +- Param signatures are sorted before storage so `(pattern, path)` and `(path, pattern)` produce the same key — correct deduplication behavior. +- The SQLite UPSERT (`ON CONFLICT DO UPDATE SET count = count + 1`) is atomic and correct for multi-process use. + +--- + +### Tests + +- 8 new tests in `tests/unit/webui/test_metrics.py` (class `TestParamPatterns`) +- 6 new tests in `tests/unit/webui/test_shared_metrics.py` +- 4 new tests in `tests/unit/webui/test_server.py` (class `TestParamPatternsEndpoint`) +- 2 new tests in `tests/unit/webui/test_config.py` +- All 457 tests pass; coverage 95.95% (≥90%) +- Note: No test covers the `on_request` param key extraction path in `__main__.py` (pre-existing limitation of `__main__` test coverage). This is acceptable. + +--- + +### Next Steps + +- **FU-P12-T2-1 (Low):** Fix stacking event listeners in `updateLatencyTable` dashboard JS — move click handler attachment to a one-time setup. +- No blockers. Feature is complete and ready to merge. From 35287b09d04c6d99ad8385ccc863f2f3be374cfe Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 16 Feb 2026 10:50:38 +0300 Subject: [PATCH 6/8] Follow-up P12-T2: stacking event listener in updateLatencyTable --- SPECS/INPROGRESS/next.md | 1 + SPECS/Workplan.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/SPECS/INPROGRESS/next.md b/SPECS/INPROGRESS/next.md index 507263f1..6cab6905 100644 --- a/SPECS/INPROGRESS/next.md +++ b/SPECS/INPROGRESS/next.md @@ -13,6 +13,7 @@ The previously selected task has been archived. ## Suggested Next Tasks - P11-T1: Add Tool Call Detail Inspector (P2, depends on P10-T1 ✅) +- FU-P12-T2-1: Fix stacking click event listeners in `updateLatencyTable` (P3, depends on P12-T2 ✅) - FU-P12-T1-1: Remove or document `MCPInitializeParams` in schemas (P3, depends on P12-T1 ✅) - FU-P12-T1-2: Add code comment clarifying stdin-only client capture in `on_request` (P3, depends on P12-T1 ✅) - FU-P12-T3-1: Document unused `error_message` parameter in `MetricsCollector.record_response` (P3, depends on P12-T3 ✅) diff --git a/SPECS/Workplan.md b/SPECS/Workplan.md index 3d1f002e..2eb9a843 100644 --- a/SPECS/Workplan.md +++ b/SPECS/Workplan.md @@ -1993,6 +1993,20 @@ Phase 9 Follow-up Backlog --- +#### FU-P12-T2-1: Fix stacking click event listeners in `updateLatencyTable` +- **Description:** The `tbody.addEventListener("click", ...)` call inside `updateLatencyTable()` in `dashboard.js` is re-registered on every metrics refresh (every 1 second), causing listeners to accumulate. Move the click handler to a one-time setup function called once during dashboard initialization, or guard with a `data-listener-attached` sentinel attribute on the tbody element. +- **Priority:** P3 +- **Dependencies:** P12-T2 +- **Parallelizable:** yes +- **Outputs/Artifacts:** + - Updated `src/mcpbridge_wrapper/webui/static/dashboard.js` — click handler moved out of `updateLatencyTable` +- **Acceptance Criteria:** + - [ ] Click handler on latency tbody is registered exactly once per page load + - [ ] Param pattern expand/collapse still works correctly after fix + - [ ] No regression in existing dashboard behavior + +--- + #### 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 From 48874d4a5fd16b78f643957a1e70c3176e424c08 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 16 Feb 2026 10:51:11 +0300 Subject: [PATCH 7/8] Archive REVIEW_P12-T2_param_frequency_analysis report --- SPECS/ARCHIVE/INDEX.md | 2 ++ .../REVIEW_P12-T2_param_frequency_analysis.md | 0 2 files changed, 2 insertions(+) rename SPECS/{INPROGRESS => ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis}/REVIEW_P12-T2_param_frequency_analysis.md (100%) diff --git a/SPECS/ARCHIVE/INDEX.md b/SPECS/ARCHIVE/INDEX.md index 3c9b9c33..9bc9e90b 100644 --- a/SPECS/ARCHIVE/INDEX.md +++ b/SPECS/ARCHIVE/INDEX.md @@ -159,6 +159,7 @@ | [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 | | [REVIEW_P12-T3_error_classification_categorization.md](P12-T3_Add_Error_Classification_and_Categorization/REVIEW_P12-T3_error_classification_categorization.md) | Review report for P12-T3 | | [REVIEW_P12-T4_data_storage_documentation.md](P12-T4_Add_documentation_about_data_storage/REVIEW_P12-T4_data_storage_documentation.md) | Review report for P12-T4 | +| [REVIEW_P12-T2_param_frequency_analysis.md](P12-T2_Add_Tool_Parameter_Frequency_Analysis/REVIEW_P12-T2_param_frequency_analysis.md) | Review report for P12-T2 | ## Archive Log @@ -271,3 +272,4 @@ | 2026-02-15 | P12-T4 | Archived Add_documentation_about_data_storage (PASS) | | 2026-02-15 | P12-T4 | Archived REVIEW_P12-T4_data_storage_documentation report | | 2026-02-16 | P12-T2 | Archived Add_Tool_Parameter_Frequency_Analysis (PASS) | +| 2026-02-16 | P12-T2 | Archived REVIEW_P12-T2_param_frequency_analysis report | diff --git a/SPECS/INPROGRESS/REVIEW_P12-T2_param_frequency_analysis.md b/SPECS/ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis/REVIEW_P12-T2_param_frequency_analysis.md similarity index 100% rename from SPECS/INPROGRESS/REVIEW_P12-T2_param_frequency_analysis.md rename to SPECS/ARCHIVE/P12-T2_Add_Tool_Parameter_Frequency_Analysis/REVIEW_P12-T2_param_frequency_analysis.md From 14f2a421516369a22d1d2c5c0299ce7bf616b7cb Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 16 Feb 2026 10:53:17 +0300 Subject: [PATCH 8/8] Fix ruff format violations in metrics.py and shared_metrics.py --- src/mcpbridge_wrapper/webui/metrics.py | 9 ++------- src/mcpbridge_wrapper/webui/shared_metrics.py | 4 +--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/mcpbridge_wrapper/webui/metrics.py b/src/mcpbridge_wrapper/webui/metrics.py index 67f36a77..b125ec17 100644 --- a/src/mcpbridge_wrapper/webui/metrics.py +++ b/src/mcpbridge_wrapper/webui/metrics.py @@ -282,9 +282,7 @@ def record_param_keys(self, tool_name: str, param_keys: List[str]) -> None: self._param_patterns[tool_name].get(signature, 0) + 1 ) - def get_param_patterns( - self, tool_name: str, top_n: int = 10 - ) -> List[Dict[str, Any]]: + def get_param_patterns(self, tool_name: str, top_n: int = 10) -> List[Dict[str, Any]]: """Return the most common parameter key combinations for a tool. Args: @@ -298,10 +296,7 @@ def get_param_patterns( with self._lock: patterns = self._param_patterns.get(tool_name, {}) sorted_patterns = sorted(patterns.items(), key=lambda kv: kv[1], reverse=True) - return [ - {"keys": list(sig), "count": cnt} - for sig, cnt in sorted_patterns[:top_n] - ] + return [{"keys": list(sig), "count": cnt} for sig, cnt in sorted_patterns[:top_n]] def reset(self) -> None: """Reset all metrics to initial state.""" diff --git a/src/mcpbridge_wrapper/webui/shared_metrics.py b/src/mcpbridge_wrapper/webui/shared_metrics.py index a08c199f..6567f405 100644 --- a/src/mcpbridge_wrapper/webui/shared_metrics.py +++ b/src/mcpbridge_wrapper/webui/shared_metrics.py @@ -382,9 +382,7 @@ def record_param_keys(self, tool_name: str, param_keys: List[str]) -> None: (tool_name, signature), ) - def get_param_patterns( - self, tool_name: str, top_n: int = 10 - ) -> List[Dict[str, Any]]: + def get_param_patterns(self, tool_name: str, top_n: int = 10) -> List[Dict[str, Any]]: """Return the most common parameter key combinations for a tool. Args: