Skip to content

Commit ae236e9

Browse files
committed
Bring MCP server up to date: 9 -> 19 tools + dict-shape lint fix
The MCP server was last touched when only nine helpers existed; ~30 new pure-function modules have landed since (action_formatter, md_authoring, sel_to_pw, pom_codegen, pii_scanner, failure_cluster, a11y_diff, locator_strength's batch scorer). This wires the most useful 10 of them into ``build_default_tools()`` so MCP-aware clients can call them directly. New tools: - webrunner_format_actions - webrunner_parse_markdown - webrunner_translate_actions_to_playwright - webrunner_translate_python_to_playwright - webrunner_pom_from_html - webrunner_scan_pii - webrunner_redact_pii - webrunner_cluster_failures - webrunner_a11y_diff - webrunner_score_action_locators Bug: _tool_lint_action used dataclass-style attribute access (f.level, f.message) but ``lint_action`` actually returns ``List[Dict[str, Any]]`` keyed on rule / severity / message / location. Pass the list through verbatim so MCP clients see the same shape the Python API exposes. Test: TestNewTools covers each new tool end-to-end through the MCP server. TestDefaultTools.test_full_default_tool_surface freezes the 19-tool list so accidental removals fail loudly in review. README + EN/ZH Sphinx docs grouped the now-larger tool list by purpose (authoring / codegen / quality / security / reporting / infra) so operators can scan the surface at a glance. Tests: 1230 -> 1241.
1 parent a04fa5b commit ae236e9

5 files changed

Lines changed: 434 additions & 17 deletions

File tree

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -687,11 +687,26 @@ WebRunner ships a [Model Context Protocol](https://modelcontextprotocol.io/) ser
687687
python -m je_web_runner.mcp_server
688688
```
689689

690-
The default tool list exposes:
690+
The default tool list (19 tools) exposes:
691691

692-
- `webrunner_lint_action`, `webrunner_locator_strength`
693-
- `webrunner_render_template`, `webrunner_compute_trend`
694-
- `webrunner_validate_response`, `webrunner_summary_markdown`
692+
Action JSON authoring & linting:
693+
- `webrunner_lint_action`, `webrunner_score_action_locators`, `webrunner_locator_strength`
694+
- `webrunner_format_actions`, `webrunner_parse_markdown`, `webrunner_render_template`
695+
- `webrunner_translate_actions_to_playwright`, `webrunner_translate_python_to_playwright`
696+
697+
Code generation:
698+
- `webrunner_pom_from_html`
699+
700+
Quality / triage:
701+
- `webrunner_a11y_diff`, `webrunner_cluster_failures`, `webrunner_compute_trend`
702+
703+
Security & privacy:
704+
- `webrunner_scan_pii`, `webrunner_redact_pii`
705+
706+
Reporting & contract:
707+
- `webrunner_summary_markdown`, `webrunner_validate_response`
708+
709+
Sharding & infra:
695710
- `webrunner_diff_shard`, `webrunner_render_k8s`, `webrunner_partition_shard`
696711

697712
```python

docs/source/Eng/doc/extended_features/extended_features_doc.rst

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,22 @@ drive it over JSON-RPC stdio:
380380
381381
python -m je_web_runner.mcp_server
382382
383-
Default tools registered: ``webrunner_lint_action``,
384-
``webrunner_locator_strength``, ``webrunner_render_template``,
385-
``webrunner_compute_trend``, ``webrunner_validate_response``,
386-
``webrunner_summary_markdown``, ``webrunner_diff_shard``,
387-
``webrunner_render_k8s``, ``webrunner_partition_shard``.
383+
Default tools registered (19 in total):
384+
385+
* Action authoring & lint: ``webrunner_lint_action``,
386+
``webrunner_score_action_locators``, ``webrunner_locator_strength``,
387+
``webrunner_format_actions``, ``webrunner_parse_markdown``,
388+
``webrunner_render_template``,
389+
``webrunner_translate_actions_to_playwright``,
390+
``webrunner_translate_python_to_playwright``
391+
* Code generation: ``webrunner_pom_from_html``
392+
* Quality & triage: ``webrunner_a11y_diff``,
393+
``webrunner_cluster_failures``, ``webrunner_compute_trend``
394+
* Security: ``webrunner_scan_pii``, ``webrunner_redact_pii``
395+
* Reporting & contract: ``webrunner_summary_markdown``,
396+
``webrunner_validate_response``
397+
* Sharding / infra: ``webrunner_diff_shard``,
398+
``webrunner_render_k8s``, ``webrunner_partition_shard``
388399

389400
Custom tools register via ``McpServer.register(Tool(...))``; the server
390401
implements MCP ``2024-11-05`` (``initialize`` / ``tools/list`` /

docs/source/Zh/doc/extended_features/extended_features_doc.rst

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,25 @@ MCP server
264264
265265
python -m je_web_runner.mcp_server
266266
267-
預設工具:``webrunner_lint_action`` / ``webrunner_locator_strength`` /
268-
``webrunner_render_template`` / ``webrunner_compute_trend`` /
269-
``webrunner_validate_response`` / ``webrunner_summary_markdown`` /
270-
``webrunner_diff_shard`` / ``webrunner_render_k8s`` /
271-
``webrunner_partition_shard``。可透過 ``McpServer.register(Tool(...))``
272-
擴充自訂工具,協定版本 ``2024-11-05``。
267+
預設工具共 19 個,依用途分組:
268+
269+
* Action 撰寫 / lint:``webrunner_lint_action`` /
270+
``webrunner_score_action_locators`` / ``webrunner_locator_strength`` /
271+
``webrunner_format_actions`` / ``webrunner_parse_markdown`` /
272+
``webrunner_render_template`` /
273+
``webrunner_translate_actions_to_playwright`` /
274+
``webrunner_translate_python_to_playwright``
275+
* 程式碼生成:``webrunner_pom_from_html``
276+
* 品質 / triage:``webrunner_a11y_diff`` / ``webrunner_cluster_failures``
277+
/ ``webrunner_compute_trend``
278+
* 安全 / 隱私:``webrunner_scan_pii`` / ``webrunner_redact_pii``
279+
* 報告 / contract:``webrunner_summary_markdown`` /
280+
``webrunner_validate_response``
281+
* Sharding / infra:``webrunner_diff_shard`` / ``webrunner_render_k8s``
282+
/ ``webrunner_partition_shard``
283+
284+
可透過 ``McpServer.register(Tool(...))`` 自行擴充工具,協定版本
285+
``2024-11-05``。
273286

274287
Action JSON LSP
275288
===============

je_web_runner/mcp_server/server.py

Lines changed: 265 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,10 @@ def _tool_lint_action(arguments: Dict[str, Any]) -> Any:
145145
actions = arguments.get("actions")
146146
if not isinstance(actions, list):
147147
raise McpServerError("'actions' must be a list")
148-
return [{"index": f.index, "level": f.level, "message": f.message,
149-
"rule": f.rule} for f in lint_action(actions)]
148+
# ``lint_action`` returns ``List[Dict[str, Any]]`` with ``rule`` /
149+
# ``severity`` / ``message`` / ``location`` keys; pass through verbatim
150+
# so MCP clients see the same shape the Python API exposes.
151+
return list(lint_action(actions))
150152

151153

152154
def _tool_locator_strength(arguments: Dict[str, Any]) -> Any:
@@ -232,6 +234,127 @@ def _tool_partition(arguments: Dict[str, Any]) -> Any:
232234
)
233235

234236

237+
def _tool_format_actions(arguments: Dict[str, Any]) -> Any:
238+
from je_web_runner.utils.action_formatter.formatter import format_actions
239+
actions = arguments.get("actions")
240+
if not isinstance(actions, list):
241+
raise McpServerError("'actions' must be a list")
242+
return format_actions(actions, indent=int(arguments.get("indent", 2)))
243+
244+
245+
def _tool_parse_markdown(arguments: Dict[str, Any]) -> Any:
246+
from je_web_runner.utils.md_authoring.markdown_to_actions import parse_markdown
247+
text = arguments.get("text")
248+
if not isinstance(text, str):
249+
raise McpServerError("'text' must be a string")
250+
return parse_markdown(text)
251+
252+
253+
def _tool_translate_actions_to_playwright(arguments: Dict[str, Any]) -> Any:
254+
from je_web_runner.utils.sel_to_pw.translator import translate_action_list
255+
actions = arguments.get("actions")
256+
if not isinstance(actions, list):
257+
raise McpServerError("'actions' must be a list")
258+
return translate_action_list(actions)
259+
260+
261+
def _tool_translate_python_to_playwright(arguments: Dict[str, Any]) -> Any:
262+
from je_web_runner.utils.sel_to_pw.translator import translate_python_source
263+
source = arguments.get("source")
264+
if not isinstance(source, str):
265+
raise McpServerError("'source' must be a string")
266+
translations = translate_python_source(source)
267+
return [
268+
{"line": t.line, "original": t.original,
269+
"translated": t.translated, "note": t.note}
270+
for t in translations
271+
]
272+
273+
274+
def _tool_pom_from_html(arguments: Dict[str, Any]) -> Any:
275+
from je_web_runner.utils.pom_codegen.codegen import (
276+
discover_elements_from_html,
277+
render_pom_module,
278+
)
279+
html = arguments.get("html")
280+
if not isinstance(html, str):
281+
raise McpServerError("'html' must be a string")
282+
elements = discover_elements_from_html(html)
283+
class_name = str(arguments.get("class_name", "WebRunnerPage"))
284+
return {
285+
"module": render_pom_module(elements, class_name=class_name),
286+
"elements": [
287+
{"name": e.name, "strategy": e.strategy,
288+
"value": e.value, "tag": e.tag, "source": e.source}
289+
for e in elements
290+
],
291+
}
292+
293+
294+
def _tool_scan_pii(arguments: Dict[str, Any]) -> Any:
295+
from je_web_runner.utils.pii_scanner.scanner import scan_text
296+
text = arguments.get("text")
297+
if not isinstance(text, str):
298+
raise McpServerError("'text' must be a string")
299+
categories = arguments.get("categories")
300+
findings = scan_text(text, categories=categories)
301+
return [
302+
{"category": f.category, "start": f.start,
303+
"end": f.end, "redacted": f.redacted}
304+
for f in findings
305+
]
306+
307+
308+
def _tool_redact_pii(arguments: Dict[str, Any]) -> Any:
309+
from je_web_runner.utils.pii_scanner.scanner import redact_text
310+
text = arguments.get("text")
311+
if not isinstance(text, str):
312+
raise McpServerError("'text' must be a string")
313+
return redact_text(
314+
text,
315+
replacement=str(arguments.get("replacement", "[REDACTED]")),
316+
categories=arguments.get("categories"),
317+
)
318+
319+
320+
def _tool_cluster_failures(arguments: Dict[str, Any]) -> Any:
321+
from je_web_runner.utils.failure_cluster.clustering import (
322+
cluster_failures,
323+
cluster_summary,
324+
)
325+
failures = arguments.get("failures")
326+
if not isinstance(failures, list):
327+
raise McpServerError("'failures' must be a list")
328+
top_n = arguments.get("top_n")
329+
if top_n is not None:
330+
top_n = int(top_n)
331+
clusters = cluster_failures(failures, top_n=top_n)
332+
return cluster_summary(clusters)
333+
334+
335+
def _tool_a11y_diff(arguments: Dict[str, Any]) -> Any:
336+
from je_web_runner.utils.accessibility.a11y_diff import diff_violations
337+
baseline = arguments.get("baseline")
338+
current = arguments.get("current")
339+
if not isinstance(baseline, list) or not isinstance(current, list):
340+
raise McpServerError("'baseline' and 'current' must be lists")
341+
diff = diff_violations(baseline, current)
342+
return {
343+
"added": diff.added,
344+
"resolved": diff.resolved,
345+
"persisting": diff.persisting,
346+
"regressed": diff.regressed,
347+
}
348+
349+
350+
def _tool_score_action_locators(arguments: Dict[str, Any]) -> Any:
351+
from je_web_runner.utils.linter.locator_strength import score_action_locators
352+
actions = arguments.get("actions")
353+
if not isinstance(actions, list):
354+
raise McpServerError("'actions' must be a list")
355+
return list(score_action_locators(actions))
356+
357+
235358
def build_default_tools() -> List[Tool]:
236359
"""Construct the default tool list shipped with the server."""
237360
return [
@@ -351,6 +474,146 @@ def build_default_tools() -> List[Tool]:
351474
},
352475
handler=_tool_partition,
353476
),
477+
Tool(
478+
name="webrunner_format_actions",
479+
description="Format an action JSON list with canonical kwarg order.",
480+
input_schema={
481+
"type": "object",
482+
"properties": {
483+
"actions": {"type": "array"},
484+
"indent": {"type": "integer"},
485+
},
486+
"required": ["actions"],
487+
},
488+
handler=_tool_format_actions,
489+
),
490+
Tool(
491+
name="webrunner_parse_markdown",
492+
description="Transpile a Markdown bullet list into a WR_* action list.",
493+
input_schema={
494+
"type": "object",
495+
"properties": {"text": {"type": "string"}},
496+
"required": ["text"],
497+
},
498+
handler=_tool_parse_markdown,
499+
),
500+
Tool(
501+
name="webrunner_translate_actions_to_playwright",
502+
description=(
503+
"Rewrite a WR_* action list to its WR_pw_* Playwright"
504+
" equivalent (drops WR_implicitly_wait)."
505+
),
506+
input_schema={
507+
"type": "object",
508+
"properties": {"actions": {"type": "array"}},
509+
"required": ["actions"],
510+
},
511+
handler=_tool_translate_actions_to_playwright,
512+
),
513+
Tool(
514+
name="webrunner_translate_python_to_playwright",
515+
description=(
516+
"Static translator: rewrites Selenium-style Python source"
517+
" into Playwright equivalents; returns per-line diffs."
518+
),
519+
input_schema={
520+
"type": "object",
521+
"properties": {"source": {"type": "string"}},
522+
"required": ["source"],
523+
},
524+
handler=_tool_translate_python_to_playwright,
525+
),
526+
Tool(
527+
name="webrunner_pom_from_html",
528+
description=(
529+
"Discover [data-testid] / id / form fields in HTML and"
530+
" render a Python Page Object module."
531+
),
532+
input_schema={
533+
"type": "object",
534+
"properties": {
535+
"html": {"type": "string"},
536+
"class_name": {"type": "string"},
537+
},
538+
"required": ["html"],
539+
},
540+
handler=_tool_pom_from_html,
541+
),
542+
Tool(
543+
name="webrunner_scan_pii",
544+
description=(
545+
"Scan text for PII (email / phone / Luhn-card / SSN /"
546+
" ROC ID / IPv4); returns category + redacted preview."
547+
),
548+
input_schema={
549+
"type": "object",
550+
"properties": {
551+
"text": {"type": "string"},
552+
"categories": {"type": "array", "items": {"type": "string"}},
553+
},
554+
"required": ["text"],
555+
},
556+
handler=_tool_scan_pii,
557+
),
558+
Tool(
559+
name="webrunner_redact_pii",
560+
description="Replace each detected PII match with a sentinel string.",
561+
input_schema={
562+
"type": "object",
563+
"properties": {
564+
"text": {"type": "string"},
565+
"replacement": {"type": "string"},
566+
"categories": {"type": "array", "items": {"type": "string"}},
567+
},
568+
"required": ["text"],
569+
},
570+
handler=_tool_redact_pii,
571+
),
572+
Tool(
573+
name="webrunner_cluster_failures",
574+
description=(
575+
"Group failures by normalised error signature; returns"
576+
" top buckets sorted by count."
577+
),
578+
input_schema={
579+
"type": "object",
580+
"properties": {
581+
"failures": {"type": "array"},
582+
"top_n": {"type": "integer"},
583+
},
584+
"required": ["failures"],
585+
},
586+
handler=_tool_cluster_failures,
587+
),
588+
Tool(
589+
name="webrunner_a11y_diff",
590+
description=(
591+
"Diff two axe-core ``violations`` arrays and bucket the"
592+
" findings into added / resolved / persisting."
593+
),
594+
input_schema={
595+
"type": "object",
596+
"properties": {
597+
"baseline": {"type": "array"},
598+
"current": {"type": "array"},
599+
},
600+
"required": ["baseline", "current"],
601+
},
602+
handler=_tool_a11y_diff,
603+
),
604+
Tool(
605+
name="webrunner_score_action_locators",
606+
description=(
607+
"Score every locator referenced by an action JSON list on"
608+
" a 0–100 scale; lower = more fragile."
609+
),
610+
input_schema={
611+
"type": "object",
612+
"properties": {"actions": {"type": "array"}},
613+
"required": ["actions"],
614+
},
615+
handler=_tool_score_action_locators,
616+
),
354617
]
355618

356619

0 commit comments

Comments
 (0)