Skip to content

Commit 91a0857

Browse files
committed
Add CLI introspection flags to je_auto_control_mcp
--list-tools / --list-resources / --list-prompts emit the default catalogue as JSON and exit, so CI checks and manual debugging can inspect the server's surface without launching a real MCP client. --read-only filters --list-tools to the read-only subset (matches JE_AUTOCONTROL_MCP_READONLY runtime). With no flags the entry point still launches the stdio server.
1 parent 3468741 commit 91a0857

2 files changed

Lines changed: 131 additions & 4 deletions

File tree

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,76 @@
1-
"""``python -m je_auto_control.utils.mcp_server`` entry point."""
1+
"""``python -m je_auto_control.utils.mcp_server`` entry point.
2+
3+
Without flags this starts the stdio MCP server. With one of the
4+
``--list-*`` flags it prints the requested catalogue to stdout and
5+
exits — useful for inspection in CI or manual debugging.
6+
"""
7+
import argparse
8+
import json
9+
import sys
10+
11+
from je_auto_control.utils.mcp_server.prompts import default_prompt_provider
12+
from je_auto_control.utils.mcp_server.resources import (
13+
default_resource_provider,
14+
)
215
from je_auto_control.utils.mcp_server.server import start_mcp_stdio_server
16+
from je_auto_control.utils.mcp_server.tools import (
17+
build_default_tool_registry,
18+
)
19+
320

21+
def _build_parser() -> argparse.ArgumentParser:
22+
parser = argparse.ArgumentParser(
23+
prog="je_auto_control_mcp",
24+
description="Run AutoControl's MCP server (stdio) or list its catalogue.",
25+
)
26+
parser.add_argument(
27+
"--list-tools", action="store_true",
28+
help="Print all tool descriptors as JSON and exit.",
29+
)
30+
parser.add_argument(
31+
"--list-resources", action="store_true",
32+
help="Print all resource descriptors as JSON and exit.",
33+
)
34+
parser.add_argument(
35+
"--list-prompts", action="store_true",
36+
help="Print all prompt descriptors as JSON and exit.",
37+
)
38+
parser.add_argument(
39+
"--read-only", action="store_true",
40+
help="Restrict tools to those marked readOnlyHint=true.",
41+
)
42+
return parser
443

5-
def main() -> None:
6-
"""Launch the stdio MCP server; blocks until stdin closes."""
44+
45+
def main(argv: list = None) -> int:
46+
"""CLI entry point. Returns the process exit code."""
47+
parser = _build_parser()
48+
args = parser.parse_args(argv)
49+
listing_modes = (args.list_tools, args.list_resources, args.list_prompts)
50+
if any(listing_modes):
51+
_print_listings(args)
52+
return 0
753
start_mcp_stdio_server()
54+
return 0
55+
56+
57+
def _print_listings(args: argparse.Namespace) -> None:
58+
if args.list_tools:
59+
registry = build_default_tool_registry(read_only=args.read_only)
60+
json.dump([tool.to_descriptor() for tool in registry],
61+
sys.stdout, ensure_ascii=False, indent=2)
62+
sys.stdout.write("\n")
63+
if args.list_resources:
64+
provider = default_resource_provider()
65+
json.dump([resource.to_descriptor() for resource in provider.list()],
66+
sys.stdout, ensure_ascii=False, indent=2)
67+
sys.stdout.write("\n")
68+
if args.list_prompts:
69+
provider = default_prompt_provider()
70+
json.dump([prompt.to_descriptor() for prompt in provider.list()],
71+
sys.stdout, ensure_ascii=False, indent=2)
72+
sys.stdout.write("\n")
873

974

1075
if __name__ == "__main__":
11-
main()
76+
sys.exit(main())
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Tests for the je_auto_control_mcp CLI introspection flags."""
2+
import io
3+
import json
4+
import sys
5+
6+
import pytest
7+
8+
from je_auto_control.utils.mcp_server.__main__ import main
9+
10+
11+
def _capture(monkeypatch, argv):
12+
buffer = io.StringIO()
13+
monkeypatch.setattr(sys, "stdout", buffer)
14+
rc = main(argv)
15+
return rc, buffer.getvalue()
16+
17+
18+
def test_list_tools_emits_json_and_exits(monkeypatch):
19+
rc, output = _capture(monkeypatch, ["--list-tools"])
20+
assert rc == 0
21+
descriptors = json.loads(output)
22+
assert isinstance(descriptors, list)
23+
assert descriptors
24+
assert all("name" in d and "inputSchema" in d for d in descriptors)
25+
26+
27+
def test_list_tools_with_read_only_drops_destructive(monkeypatch):
28+
_, output = _capture(monkeypatch, ["--list-tools", "--read-only"])
29+
descriptors = json.loads(output)
30+
names = {d["name"] for d in descriptors}
31+
assert "ac_click_mouse" not in names
32+
assert "ac_get_mouse_position" in names
33+
34+
35+
def test_list_resources_emits_json(monkeypatch):
36+
rc, output = _capture(monkeypatch, ["--list-resources"])
37+
assert rc == 0
38+
descriptors = json.loads(output)
39+
assert isinstance(descriptors, list)
40+
uris = {d["uri"] for d in descriptors}
41+
assert "autocontrol://history" in uris
42+
assert "autocontrol://commands" in uris
43+
44+
45+
def test_list_prompts_emits_json(monkeypatch):
46+
rc, output = _capture(monkeypatch, ["--list-prompts"])
47+
assert rc == 0
48+
descriptors = json.loads(output)
49+
assert isinstance(descriptors, list)
50+
names = {d["name"] for d in descriptors}
51+
assert {"automate_ui_task", "find_widget"}.issubset(names)
52+
53+
54+
def test_no_flags_starts_stdio_server(monkeypatch):
55+
"""With no flags, main() should dispatch to start_mcp_stdio_server."""
56+
started = []
57+
import je_auto_control.utils.mcp_server.__main__ as cli_mod
58+
monkeypatch.setattr(cli_mod, "start_mcp_stdio_server",
59+
lambda: started.append(True))
60+
rc = main([])
61+
assert rc == 0
62+
assert started == [True]

0 commit comments

Comments
 (0)