ventilatepro-cli 0.2.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/PKG-INFO +54 -5
  2. ventilatepro_cli-0.3.1/README.md +102 -0
  3. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/pyproject.toml +3 -1
  4. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/tests/test_cli.py +183 -4
  5. ventilatepro_cli-0.3.1/tests/test_mcp_server.py +73 -0
  6. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/__init__.py +1 -1
  7. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/api_client.py +201 -0
  8. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/auth.py +2 -4
  9. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/cli.py +69 -2
  10. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/config.py +1 -0
  11. ventilatepro_cli-0.3.1/ventilatepro/entities.py +538 -0
  12. ventilatepro_cli-0.3.1/ventilatepro/mcp_cli.py +192 -0
  13. ventilatepro_cli-0.3.1/ventilatepro/mcp_server.py +558 -0
  14. ventilatepro_cli-0.3.1/ventilatepro/payloads.py +43 -0
  15. ventilatepro_cli-0.3.1/ventilatepro/projects.py +120 -0
  16. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/queue.py +8 -1
  17. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro_cli.egg-info/PKG-INFO +54 -5
  18. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro_cli.egg-info/SOURCES.txt +4 -0
  19. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro_cli.egg-info/entry_points.txt +1 -0
  20. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro_cli.egg-info/requires.txt +1 -0
  21. ventilatepro_cli-0.2.0/README.md +0 -54
  22. ventilatepro_cli-0.2.0/ventilatepro/entities.py +0 -210
  23. ventilatepro_cli-0.2.0/ventilatepro/projects.py +0 -55
  24. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/setup.cfg +0 -0
  25. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/tests/test_config.py +0 -0
  26. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/__main__.py +0 -0
  27. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/calc.py +0 -0
  28. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/design_day.py +0 -0
  29. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/notes.py +0 -0
  30. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/output.py +0 -0
  31. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro/systems.py +0 -0
  32. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro_cli.egg-info/dependency_links.txt +0 -0
  33. {ventilatepro_cli-0.2.0 → ventilatepro_cli-0.3.1}/ventilatepro_cli.egg-info/top_level.txt +0 -0
@@ -1,16 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ventilatepro-cli
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: VentilatePro CLI
5
5
  Project-URL: Homepage, https://ventilatepro.com/exam/cli/
6
6
  Project-URL: Documentation, https://ventilatepro.com/exam/cli/
7
7
  Project-URL: Support, https://ventilatepro.com/exam/support/
8
- Requires-Python: >=3.9
8
+ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: typer>=0.16.0
11
11
  Requires-Dist: requests>=2.32.0
12
12
  Requires-Dist: keyring>=25.6.0
13
13
  Requires-Dist: platformdirs>=4.3.0
14
+ Requires-Dist: mcp>=1.23.0
14
15
 
15
16
  # VentilatePro CLI
16
17
 
@@ -40,7 +41,7 @@ pip install -e .
40
41
  ## Commands
41
42
 
42
43
  ```bash
43
- ventilatepro auth login --url http://localhost:8000
44
+ ventilatepro auth login
44
45
  ventilatepro projects list
45
46
  ventilatepro projects show 1
46
47
  ventilatepro rooms list --project 1
@@ -51,18 +52,66 @@ ventilatepro calc ahu-properties --ahu 1
51
52
  ventilatepro design-day context --project 1
52
53
  ventilatepro notes create --project 1 --body "Captured from terminal"
53
54
  ventilatepro notes sync
55
+ ventilatepro mcp doctor
56
+ ventilatepro-mcp
54
57
  ```
55
58
 
59
+ `ventilatepro-mcp` starts the local stdio MCP server for Codex, Claude Desktop,
60
+ and other MCP clients. It uses the same saved base URL and CLI token created by
61
+ `ventilatepro auth login`.
62
+
63
+ ## MCP setup
64
+
65
+ 1. In the VentilatePro web app, open Account Settings and create a CLI token with
66
+ the `Agent Editor` preset.
67
+ 2. Log in locally:
68
+
69
+ ```bash
70
+ ventilatepro auth login --token vpcli_...
71
+ ```
72
+
73
+ The production URL defaults to `https://ventilatepro.com`. Use `--url` only for
74
+ local development, staging, or self-hosted VentilatePro servers.
75
+
76
+ 3. Confirm the local setup:
77
+
78
+ ```bash
79
+ ventilatepro mcp doctor
80
+ ```
81
+
82
+ 4. Add the stdio server to Codex:
83
+
84
+ ```bash
85
+ codex mcp add ventilatepro -- ventilatepro-mcp
86
+ ```
87
+
88
+ For JSON-based MCP clients, use:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "ventilatepro": {
94
+ "command": "ventilatepro-mcp",
95
+ "args": []
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ If the client cannot find `ventilatepro-mcp`, run `where ventilatepro-mcp` on
102
+ Windows or `which ventilatepro-mcp` on macOS/Linux and use the full path as the
103
+ command.
104
+
56
105
  If the hidden token prompt is awkward in your terminal, use either:
57
106
 
58
107
  ```bash
59
- ventilatepro auth login --url https://ventilatepro.com --token-prompt-visible
108
+ ventilatepro auth login --token-prompt-visible
60
109
  ```
61
110
 
62
111
  or pipe the token through stdin:
63
112
 
64
113
  ```bash
65
- printf '%s' "$VPCLI_TOKEN" | ventilatepro auth login --url https://ventilatepro.com --token-stdin
114
+ printf '%s' "$VPCLI_TOKEN" | ventilatepro auth login --token-stdin
66
115
  ```
67
116
 
68
117
  See the hosted install guide at `https://ventilatepro.com/exam/cli/`, plus [docs/commands.md](./docs/commands.md), [docs/auth.md](./docs/auth.md), [docs/api.md](./docs/api.md), and [docs/releasing.md](./docs/releasing.md) in this repo.
@@ -0,0 +1,102 @@
1
+ # VentilatePro CLI
2
+
3
+ VentilatePro CLI is a local command-line client for authenticating to VentilatePro and creating or syncing notes through the scoped CLI API.
4
+
5
+ ## Install
6
+
7
+ Published releases install from PyPI:
8
+
9
+ ```bash
10
+ pipx install ventilatepro-cli
11
+ ```
12
+
13
+ Upgrade an existing install:
14
+
15
+ ```bash
16
+ pipx upgrade ventilatepro-cli
17
+ ```
18
+
19
+ Local development install:
20
+
21
+ ```bash
22
+ cd ventilatepro-cli
23
+ pip install -e .
24
+ ```
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ ventilatepro auth login
30
+ ventilatepro projects list
31
+ ventilatepro projects show 1
32
+ ventilatepro rooms list --project 1
33
+ ventilatepro ahus list --project 1
34
+ ventilatepro systems hierarchy --project 1
35
+ ventilatepro calc status --project 1
36
+ ventilatepro calc ahu-properties --ahu 1
37
+ ventilatepro design-day context --project 1
38
+ ventilatepro notes create --project 1 --body "Captured from terminal"
39
+ ventilatepro notes sync
40
+ ventilatepro mcp doctor
41
+ ventilatepro-mcp
42
+ ```
43
+
44
+ `ventilatepro-mcp` starts the local stdio MCP server for Codex, Claude Desktop,
45
+ and other MCP clients. It uses the same saved base URL and CLI token created by
46
+ `ventilatepro auth login`.
47
+
48
+ ## MCP setup
49
+
50
+ 1. In the VentilatePro web app, open Account Settings and create a CLI token with
51
+ the `Agent Editor` preset.
52
+ 2. Log in locally:
53
+
54
+ ```bash
55
+ ventilatepro auth login --token vpcli_...
56
+ ```
57
+
58
+ The production URL defaults to `https://ventilatepro.com`. Use `--url` only for
59
+ local development, staging, or self-hosted VentilatePro servers.
60
+
61
+ 3. Confirm the local setup:
62
+
63
+ ```bash
64
+ ventilatepro mcp doctor
65
+ ```
66
+
67
+ 4. Add the stdio server to Codex:
68
+
69
+ ```bash
70
+ codex mcp add ventilatepro -- ventilatepro-mcp
71
+ ```
72
+
73
+ For JSON-based MCP clients, use:
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "ventilatepro": {
79
+ "command": "ventilatepro-mcp",
80
+ "args": []
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ If the client cannot find `ventilatepro-mcp`, run `where ventilatepro-mcp` on
87
+ Windows or `which ventilatepro-mcp` on macOS/Linux and use the full path as the
88
+ command.
89
+
90
+ If the hidden token prompt is awkward in your terminal, use either:
91
+
92
+ ```bash
93
+ ventilatepro auth login --token-prompt-visible
94
+ ```
95
+
96
+ or pipe the token through stdin:
97
+
98
+ ```bash
99
+ printf '%s' "$VPCLI_TOKEN" | ventilatepro auth login --token-stdin
100
+ ```
101
+
102
+ See the hosted install guide at `https://ventilatepro.com/exam/cli/`, plus [docs/commands.md](./docs/commands.md), [docs/auth.md](./docs/auth.md), [docs/api.md](./docs/api.md), and [docs/releasing.md](./docs/releasing.md) in this repo.
@@ -6,13 +6,14 @@ build-backend = "setuptools.build_meta"
6
6
  name = "ventilatepro-cli"
7
7
  dynamic = ["version"]
8
8
  description = "VentilatePro CLI"
9
- requires-python = ">=3.9"
9
+ requires-python = ">=3.10"
10
10
  readme = "README.md"
11
11
  dependencies = [
12
12
  "typer>=0.16.0",
13
13
  "requests>=2.32.0",
14
14
  "keyring>=25.6.0",
15
15
  "platformdirs>=4.3.0",
16
+ "mcp>=1.23.0",
16
17
  ]
17
18
 
18
19
  [project.urls]
@@ -22,6 +23,7 @@ Support = "https://ventilatepro.com/exam/support/"
22
23
 
23
24
  [project.scripts]
24
25
  ventilatepro = "ventilatepro.cli:main"
26
+ ventilatepro-mcp = "ventilatepro.mcp_server:main"
25
27
 
26
28
  [tool.setuptools.packages.find]
27
29
  where = ["."]
@@ -44,6 +44,31 @@ class CliTests(unittest.TestCase):
44
44
  os.environ.pop(config.CONFIG_ENV_VAR, None)
45
45
  os.environ.pop(config.DATA_ENV_VAR, None)
46
46
 
47
+ def test_auth_login_defaults_to_production_url(self):
48
+ with tempfile.TemporaryDirectory() as tmpdir:
49
+ os.environ[config.CONFIG_ENV_VAR] = tmpdir
50
+ os.environ[config.DATA_ENV_VAR] = tmpdir
51
+ try:
52
+ fake_client = mock.Mock()
53
+ fake_client.auth_me.return_value = {
54
+ "username": "demo-user",
55
+ "email": "demo@example.com",
56
+ }
57
+
58
+ with mock.patch.object(auth, "ApiClient", return_value=fake_client) as api_client:
59
+ with mock.patch.object(config, "_try_get_keyring", return_value=None):
60
+ result = self.runner.invoke(
61
+ app,
62
+ ["auth", "login", "--token", "vpcli_demo_secret", "--json"],
63
+ )
64
+
65
+ self.assertEqual(result.exit_code, 0)
66
+ api_client.assert_called_once_with(config.DEFAULT_BASE_URL, "vpcli_demo_secret")
67
+ self.assertEqual(config.get_base_url(), config.DEFAULT_BASE_URL)
68
+ finally:
69
+ os.environ.pop(config.CONFIG_ENV_VAR, None)
70
+ os.environ.pop(config.DATA_ENV_VAR, None)
71
+
47
72
  def test_auth_login_accepts_visible_prompt_input(self):
48
73
  with tempfile.TemporaryDirectory() as tmpdir:
49
74
  os.environ[config.CONFIG_ENV_VAR] = tmpdir
@@ -101,6 +126,41 @@ class CliTests(unittest.TestCase):
101
126
  os.environ.pop(config.CONFIG_ENV_VAR, None)
102
127
  os.environ.pop(config.DATA_ENV_VAR, None)
103
128
 
129
+ def test_mcp_doctor_json_reports_ok_status(self):
130
+ doctor_payload = {
131
+ "status": "ok",
132
+ "checks": [
133
+ {"name": "saved_login", "ok": True, "required": True, "message": "Saved login found."},
134
+ {"name": "mcp_catalog", "ok": True, "required": True, "message": "MCP catalog loaded."},
135
+ ],
136
+ "summary": {"mcp_catalog": {"tools": 17}},
137
+ "next_steps": [],
138
+ }
139
+
140
+ with mock.patch("ventilatepro.mcp_cli.run_doctor", return_value=doctor_payload):
141
+ result = self.runner.invoke(app, ["mcp", "doctor", "--json"])
142
+
143
+ self.assertEqual(result.exit_code, 0)
144
+ payload = json.loads(result.stdout)
145
+ self.assertEqual(payload["status"], "ok")
146
+ self.assertEqual(payload["summary"]["mcp_catalog"]["tools"], 17)
147
+
148
+ def test_mcp_doctor_exits_nonzero_on_required_failure(self):
149
+ doctor_payload = {
150
+ "status": "failed",
151
+ "checks": [
152
+ {"name": "saved_login", "ok": False, "required": True, "message": "Run login first."},
153
+ ],
154
+ "summary": {},
155
+ "next_steps": ["Run login first."],
156
+ }
157
+
158
+ with mock.patch("ventilatepro.mcp_cli.run_doctor", return_value=doctor_payload):
159
+ result = self.runner.invoke(app, ["mcp", "doctor"])
160
+
161
+ self.assertEqual(result.exit_code, 1)
162
+ self.assertIn("FAIL saved_login", result.output)
163
+
104
164
  def test_notes_create_queues_on_network_error(self):
105
165
  with tempfile.TemporaryDirectory() as tmpdir:
106
166
  os.environ[config.CONFIG_ENV_VAR] = tmpdir
@@ -326,6 +386,65 @@ class CliTests(unittest.TestCase):
326
386
  self.assertIn("Hospital South Wing", result.stdout)
327
387
  fake_client.project_show.assert_called_once_with(7)
328
388
 
389
+ def test_projects_create_reads_json_payload_from_inline_data(self):
390
+ fake_client = mock.Mock()
391
+ fake_client.project_create.return_value = {
392
+ "operation": "created",
393
+ "project": {"id": 9, "name": "CLI Write Project"},
394
+ }
395
+
396
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
397
+ result = self.runner.invoke(
398
+ app,
399
+ ["projects", "create", "--data", '{"name":"CLI Write Project","time_zone":"America/Los_Angeles"}', "--json"],
400
+ )
401
+
402
+ self.assertEqual(result.exit_code, 0)
403
+ fake_client.project_create.assert_called_once_with(
404
+ {"name": "CLI Write Project", "time_zone": "America/Los_Angeles"}
405
+ )
406
+
407
+ def test_notes_update_passes_patch_payload_to_api_client(self):
408
+ fake_client = mock.Mock()
409
+ fake_client.update_note.return_value = {
410
+ "operation": "updated",
411
+ "note": {"note_ref": "general-info:1"},
412
+ }
413
+
414
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
415
+ result = self.runner.invoke(
416
+ app,
417
+ [
418
+ "notes",
419
+ "update",
420
+ "general-info:1",
421
+ "--project",
422
+ "1",
423
+ "--title",
424
+ "Updated note",
425
+ "--tags",
426
+ "field,urgent",
427
+ "--json",
428
+ ],
429
+ )
430
+
431
+ self.assertEqual(result.exit_code, 0)
432
+ fake_client.update_note.assert_called_once_with(
433
+ "general-info:1",
434
+ project=1,
435
+ payload={"title": "Updated note", "tags": ["field", "urgent"]},
436
+ )
437
+
438
+ def test_rooms_delete_requires_yes_confirmation(self):
439
+ fake_client = mock.Mock()
440
+
441
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
442
+ result = self.runner.invoke(app, ["rooms", "delete", "12"])
443
+
444
+ self.assertNotEqual(result.exit_code, 0)
445
+ self.assertIn("requires --yes", result.output)
446
+ fake_client.delete_room.assert_not_called()
447
+
329
448
  def test_rooms_list_passes_filters_to_api_client(self):
330
449
  fake_client = mock.Mock()
331
450
  fake_client.list_rooms.return_value = {
@@ -357,6 +476,65 @@ class CliTests(unittest.TestCase):
357
476
  self.assertEqual(result.exit_code, 0)
358
477
  fake_client.list_rooms.assert_called_once_with(1, limit=5, offset=10, query="operating")
359
478
 
479
+ def test_terminal_units_create_includes_zone_in_payload(self):
480
+ fake_client = mock.Mock()
481
+ fake_client.create_terminal_unit.return_value = {
482
+ "operation": "created",
483
+ "terminal_unit": {"id": 3, "tag": "TU-CLI"},
484
+ }
485
+
486
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
487
+ result = self.runner.invoke(
488
+ app,
489
+ [
490
+ "terminal-units",
491
+ "create",
492
+ "--zone",
493
+ "7",
494
+ "--data",
495
+ '{"tag":"TU-CLI","unit_type":"FCU"}',
496
+ "--json",
497
+ ],
498
+ )
499
+
500
+ self.assertEqual(result.exit_code, 0)
501
+ fake_client.create_terminal_unit.assert_called_once_with(
502
+ {"zone": 7, "tag": "TU-CLI", "unit_type": "FCU"}
503
+ )
504
+
505
+ def test_zones_bulk_assign_ahu_accepts_assignment_list(self):
506
+ fake_client = mock.Mock()
507
+ fake_client.assign_zones_to_ahu.return_value = {
508
+ "operation": "assigned",
509
+ "updated": 2,
510
+ "errors": 0,
511
+ }
512
+
513
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
514
+ result = self.runner.invoke(
515
+ app,
516
+ [
517
+ "zones",
518
+ "bulk-assign-ahu",
519
+ "--project",
520
+ "1",
521
+ "--data",
522
+ '[{"zone_id":1,"ahu_id":2},{"zone_id":3,"ahu_id":2}]',
523
+ "--json",
524
+ ],
525
+ )
526
+
527
+ self.assertEqual(result.exit_code, 0)
528
+ fake_client.assign_zones_to_ahu.assert_called_once_with(
529
+ {
530
+ "project": 1,
531
+ "assignments": [
532
+ {"zone_id": 1, "ahu_id": 2},
533
+ {"zone_id": 3, "ahu_id": 2},
534
+ ],
535
+ }
536
+ )
537
+
360
538
  def test_calc_recalculate_stale_passes_mode_scope_and_force(self):
361
539
  fake_client = mock.Mock()
362
540
  fake_client.calc_recalculate_stale.return_value = {
@@ -390,14 +568,15 @@ class CliTests(unittest.TestCase):
390
568
  "state": "QUEUED",
391
569
  }
392
570
 
393
- with tempfile.NamedTemporaryFile("w+", encoding="utf-8") as handle:
394
- json.dump({"zones": [{"zone_id": 1}]}, handle)
395
- handle.flush()
571
+ with tempfile.TemporaryDirectory() as tmpdir:
572
+ payload_path = os.path.join(tmpdir, "design-day-request.json")
573
+ with open(payload_path, "w", encoding="utf-8") as handle:
574
+ json.dump({"zones": [{"zone_id": 1}]}, handle)
396
575
 
397
576
  with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
398
577
  result = self.runner.invoke(
399
578
  app,
400
- ["design-day", "run", "--project", "1", "--input", handle.name, "--json"],
579
+ ["design-day", "run", "--project", "1", "--input", payload_path, "--json"],
401
580
  )
402
581
 
403
582
  self.assertEqual(result.exit_code, 0)
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ import unittest
3
+ from unittest import mock
4
+
5
+ from ventilatepro.mcp_server import _requires_confirmation, build_server
6
+
7
+
8
+ class FakeClient:
9
+ def __init__(self):
10
+ self.calls = []
11
+
12
+ def schemas(self):
13
+ return {"schemas": {"api_request": {"description": "ok"}}}
14
+
15
+ def schema(self, schema_key):
16
+ return {"key": schema_key}
17
+
18
+ def auth_me(self):
19
+ return {"username": "agent", "scopes": ["projects:read"]}
20
+
21
+ def projects_list(self, *, limit=20, offset=0, query=None):
22
+ return {"count": 0, "limit": limit, "offset": offset, "query": query, "results": []}
23
+
24
+ def cli_proxy(self, method, api_path, *, json_payload=None, params=None):
25
+ self.calls.append((method, api_path, json_payload, params))
26
+ return {"method": method, "path": api_path, "payload": json_payload, "params": params}
27
+
28
+
29
+ class McpServerTests(unittest.TestCase):
30
+ def test_tool_catalog_contains_core_tools(self):
31
+ async def run():
32
+ server = build_server()
33
+ tools = await server.list_tools()
34
+ names = {tool.name for tool in tools}
35
+ self.assertIn("vp_whoami", names)
36
+ self.assertIn("vp_api_request", names)
37
+ self.assertIn("vp_list_entities", names)
38
+ self.assertIn("vp_run_workflow", names)
39
+
40
+ asyncio.run(run())
41
+
42
+ def test_api_request_tool_uses_cli_proxy(self):
43
+ async def run():
44
+ server = build_server()
45
+ fake_client = FakeClient()
46
+ with mock.patch(
47
+ "ventilatepro.mcp_server.auth.get_authenticated_client",
48
+ return_value=(fake_client, "http://localhost:8000"),
49
+ ):
50
+ _content, structured = await server.call_tool(
51
+ "vp_api_request",
52
+ {
53
+ "method": "GET",
54
+ "path": "projects/1/",
55
+ "params": {"include": "summary"},
56
+ },
57
+ )
58
+
59
+ self.assertEqual(structured["result"]["path"], "projects/1/")
60
+ self.assertEqual(fake_client.calls[0], ("GET", "projects/1/", None, {"include": "summary"}))
61
+
62
+ asyncio.run(run())
63
+
64
+ def test_guarded_operations_require_confirmation(self):
65
+ self.assertTrue(_requires_confirmation("DELETE", "rooms/1/", None))
66
+ self.assertTrue(_requires_confirmation("POST", "projects/1/recalculate-stale/", {}))
67
+ self.assertTrue(_requires_confirmation("POST", "projects/1/design-day/run/", {}))
68
+ self.assertTrue(_requires_confirmation("PATCH", "projects/1/", {"force": True}))
69
+ self.assertFalse(_requires_confirmation("GET", "projects/1/", None))
70
+
71
+
72
+ if __name__ == "__main__":
73
+ unittest.main()
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.3.1"