vmware-debug 1.11.0__tar.gz → 1.11.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 (82) hide show
  1. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/.claude-plugin/plugin.json +1 -1
  2. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/.mcp.json +1 -1
  3. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/PKG-INFO +2 -2
  4. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/RELEASE_NOTES.md +25 -0
  5. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/pyproject.toml +2 -2
  6. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/server.json +2 -2
  7. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/_scores.json +29 -51
  8. vmware_debug-1.11.1/tests/eval/regression/test_every_tool_parameter_is_documented.py +132 -0
  9. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/uv.lock +5 -5
  10. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/__init__.py +1 -1
  11. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/mcp_server/server.py +204 -75
  12. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/.gitignore +0 -0
  13. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/README-CN.md +0 -0
  14. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/README.md +0 -0
  15. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/SECURITY.md +0 -0
  16. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
  17. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/SKILL.md +0 -0
  18. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/agent-guardrails.md +0 -0
  19. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/capabilities.md +0 -0
  20. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/cli-reference.md +0 -0
  21. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/event-envelope.md +0 -0
  22. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/routing.md +0 -0
  23. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/setup-guide.md +0 -0
  24. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/__init__.py +0 -0
  25. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/__init__.py +0 -0
  26. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/_family.py +0 -0
  27. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/_scoring.py +0 -0
  28. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/_skill.py +0 -0
  29. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/conftest.py +0 -0
  30. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_entity_reachability.py +0 -0
  31. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_error_actionability.py +0 -0
  32. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_tool_description_quality.py +0 -0
  33. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
  34. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/__init__.py +0 -0
  35. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_capability_grader.py +0 -0
  36. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_case_id_non_ascii.py +0 -0
  37. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_debug_regressions.py +0 -0
  38. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_evidence_payload_contract.py +0 -0
  39. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_host_lifecycle_classification.py +0 -0
  40. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_investigation_metrics.py +0 -0
  41. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_result_envelope.py +0 -0
  42. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_skill_name_aliases.py +0 -0
  43. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_spike_resolution.py +0 -0
  44. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_tool_annotations.py +0 -0
  45. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_conclusion.py +0 -0
  46. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_evidence.py +0 -0
  47. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_grading.py +0 -0
  48. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_hypotheses.py +0 -0
  49. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_ids.py +0 -0
  50. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_plan.py +0 -0
  51. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_readiness.py +0 -0
  52. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_store.py +0 -0
  53. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_timeline_close.py +0 -0
  54. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_tools.py +0 -0
  55. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_knowledge.py +0 -0
  56. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_safe_error_passthrough.py +0 -0
  57. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_timeline.py +0 -0
  58. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/cli.py +0 -0
  59. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/envelope.py +0 -0
  60. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/mcp/__init__.py +0 -0
  61. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/mcp/tools.py +0 -0
  62. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/mcp_server/__init__.py +0 -0
  63. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/__init__.py +0 -0
  64. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/__init__.py +0 -0
  65. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/api.py +0 -0
  66. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/conclusion.py +0 -0
  67. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/evidence.py +0 -0
  68. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/grading.py +0 -0
  69. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/hypotheses.py +0 -0
  70. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/ids.py +0 -0
  71. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/knowledge.py +0 -0
  72. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/model.py +0 -0
  73. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/payloads.py +0 -0
  74. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/plan.py +0 -0
  75. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/readiness.py +0 -0
  76. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/sources.py +0 -0
  77. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/store.py +0 -0
  78. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/timeline.py +0 -0
  79. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/skill_names.py +0 -0
  80. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/timeline.py +0 -0
  81. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/rules/evidence_sources.yaml +0 -0
  82. {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/rules/grading_rules.yaml +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "vmware-debug",
4
4
  "displayName": "VMware Debug",
5
- "version": "1.11.0",
5
+ "version": "1.11.1",
6
6
  "description": "Read-only VMware incident correlation: timeline, spikes, root-cause routing. 2 MCP tools.",
7
7
  "author": {
8
8
  "name": "Wei Zhou",
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "vmware-debug==1.11.0",
8
+ "vmware-debug==1.11.1",
9
9
  "vmware-debug-mcp"
10
10
  ]
11
11
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vmware-debug
3
- Version: 1.11.0
3
+ Version: 1.11.1
4
4
  Summary: VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family
5
5
  Project-URL: Homepage, https://github.com/vmware-skills/VMware-Debug
6
6
  Project-URL: Repository, https://github.com/vmware-skills/VMware-Debug
@@ -18,7 +18,7 @@ Requires-Dist: mcp[cli]<2.0,>=1.10
18
18
  Requires-Dist: pyyaml<7.0,>=6.0
19
19
  Requires-Dist: rich<15.0,>=13.0
20
20
  Requires-Dist: typer<1.0,>=0.12
21
- Requires-Dist: vmware-policy<2.0,>=1.8.5
21
+ Requires-Dist: vmware-policy<2.0,>=1.10.0
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
  <!-- mcp-name: io.github.vmware-skills/vmware-debug -->
@@ -1,3 +1,28 @@
1
+ ## v1.11.1 — the schema an agent reads now carries the descriptions
2
+
3
+ Parameter descriptions reach the JSON schema for the first time. An MCP client
4
+ sees the schema, not the docstring, and this repo's coverage of `description`
5
+ and `additionalProperties` was 0% — while nearly every parameter was already
6
+ described in an `Args:` block no client ever receives.
7
+
8
+ Measured on a real VCF 9.1 estate, the gap produced a silent failure with no
9
+ error at any stage: a parameter name guessed wrong is discarded and the tool
10
+ returns the full unfiltered result; a value guessed wrong (`power_state=
11
+ "running"`) returns 0 rows where there were 11.
12
+
13
+ vmware-policy 1.10.0's `describe_tool_parameters` copies what is already
14
+ written, so the docstring is now load-bearing and the two cannot drift apart. It
15
+ removes the `Args:` block from the description once copied — both travel in
16
+ every `tools/list` response, so leaving it bills the same sentences twice
17
+ against the manifest's token budget. `additionalProperties` is closed: an open
18
+ schema is room for a model to invent arguments that are then silently
19
+ discarded, which is the other half of the same failure.
20
+
21
+ **The `vmware-policy` floor moves to >=1.10.0.** Older releases have no
22
+ `describe_tool_parameters`, and resolving one gives an ImportError at server
23
+ start rather than a missing feature.
24
+
25
+
1
26
  ## v1.11.0 — the correlation engine, on a real incident
2
27
 
3
28
  A tester fed the investigation layer a genuine failure: on 08-03 at 05:49, four
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vmware-debug"
7
- version = "1.11.0"
7
+ version = "1.11.1"
8
8
  description = "VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -21,7 +21,7 @@ dependencies = [
21
21
  "typer>=0.12,<1.0",
22
22
  "rich>=13.0,<15.0",
23
23
  "mcp[cli]>=1.10,<2.0",
24
- "vmware-policy>=1.8.5,<2.0",
24
+ "vmware-policy>=1.10.0,<2.0",
25
25
  # Imported directly by ops/cases/grading.py for the rules file. It has
26
26
  # always arrived transitively via vmware-policy; a package should
27
27
  # declare what it imports rather than rely on a dependency's dependency.
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/vmware-skills/VMware-Debug",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.11.0",
10
+ "version": "1.11.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-debug",
15
- "version": "1.11.0",
15
+ "version": "1.11.1",
16
16
  "runtimeHint": "uvx",
17
17
  "packageArguments": [
18
18
  {
@@ -193,58 +193,47 @@
193
193
  "manifest_context_headroom": {
194
194
  "detail": {
195
195
  "heaviest_tools": {
196
- "case_open": 354,
197
- "case_plan": 395,
198
- "case_record_gap": 337,
199
- "case_submit_evidence": 693,
200
- "incident_timeline": 357
196
+ "case_open": 478,
197
+ "case_plan": 444,
198
+ "case_record_gap": 380,
199
+ "case_submit_evidence": 902,
200
+ "incident_timeline": 454
201
201
  },
202
- "manifest_tokens": 3988,
203
- "mean_tokens_per_tool": 284.9,
202
+ "manifest_tokens": 4812,
203
+ "mean_tokens_per_tool": 343.7,
204
204
  "reference_context": 16384,
205
205
  "tool_count": 14
206
206
  },
207
207
  "maximum": 16384,
208
- "pct": 75.7,
208
+ "pct": 70.6,
209
209
  "unit": "tokens",
210
- "value": 12396
210
+ "value": 11572
211
211
  },
212
212
  "parameter_documentation_coverage": {
213
213
  "detail": {
214
- "undocumented_by_tool": {
215
- "case_grade": [
216
- "case_id"
217
- ],
218
- "case_submit_evidence": [
219
- "fetched_at"
220
- ],
221
- "case_timeline": [
222
- "bin_seconds",
223
- "case_id",
224
- "top_n",
225
- "z_threshold"
226
- ]
227
- }
214
+ "undocumented_by_tool": {}
228
215
  },
229
216
  "maximum": 45,
230
- "pct": 86.7,
217
+ "pct": 100.0,
231
218
  "unit": "parameters",
232
- "value": 39
219
+ "value": 45
233
220
  },
234
221
  "per_tool_token_discipline": {
235
222
  "detail": {
236
223
  "ceiling": 350,
237
224
  "over_ceiling": {
238
- "case_open": 354,
239
- "case_plan": 395,
240
- "case_submit_evidence": 693,
241
- "incident_timeline": 357
225
+ "case_open": 478,
226
+ "case_plan": 444,
227
+ "case_record_gap": 380,
228
+ "case_submit_evidence": 902,
229
+ "case_timeline": 374,
230
+ "incident_timeline": 454
242
231
  }
243
232
  },
244
233
  "maximum": 14,
245
- "pct": 71.4,
234
+ "pct": 57.1,
246
235
  "unit": "tools",
247
- "value": 10
236
+ "value": 8
248
237
  },
249
238
  "read_only_manifest_saving": {
250
239
  "detail": {
@@ -301,48 +290,37 @@
301
290
  "tool_description_quality": {
302
291
  "detail": {
303
292
  "per_dimension_pct": {
304
- "args": 78.6,
305
- "gotcha": 85.7,
293
+ "args": 100.0,
294
+ "gotcha": 100.0,
306
295
  "marker": 100.0,
307
- "next_hop": 57.1,
296
+ "next_hop": 78.6,
308
297
  "what": 100.0,
309
298
  "when": 92.9
310
299
  },
311
300
  "tools_graded": 14,
312
301
  "weakest_tools": {
313
- "case_close": [
314
- "next_hop"
315
- ],
316
- "case_get": [
317
- "gotcha"
318
- ],
319
- "case_grade": [
320
- "args",
321
- "next_hop"
322
- ],
302
+ "case_close": [],
303
+ "case_get": [],
304
+ "case_grade": [],
305
+ "case_hypotheses": [],
323
306
  "case_knowledge": [
324
307
  "next_hop"
325
308
  ],
326
309
  "case_list": [
327
- "gotcha",
328
310
  "next_hop"
329
311
  ],
330
312
  "case_readiness": [
331
313
  "next_hop"
332
314
  ],
333
315
  "case_record_gap": [
334
- "next_hop",
335
316
  "when"
336
- ],
337
- "case_submit_evidence": [
338
- "args"
339
317
  ]
340
318
  }
341
319
  },
342
320
  "maximum": 84,
343
- "pct": 85.7,
321
+ "pct": 95.2,
344
322
  "unit": "points",
345
- "value": 72
323
+ "value": 80
346
324
  },
347
325
  "tool_failure_payload_quality": {
348
326
  "detail": {
@@ -0,0 +1,132 @@
1
+ """Regression: every MCP tool parameter carries a Google-style ``Args:`` entry.
2
+
3
+ Why this exists
4
+ ---------------
5
+ The JSON schema is the only part of a tool an agent can actually read. Prose in
6
+ the docstring body explains the tool; it says nothing about a *parameter*. A
7
+ measurement across the family's 327 tools found ``description`` coverage on
8
+ parameter schemas at 0% — every word of guidance was in prose the schema never
9
+ carried — and every one of this skill's 45 parameters was undescribed — including
10
+ ``case_plan.category`` and ``case_submit_evidence.source_skill``, both of
11
+ which have a closed set of legal values that appeared nowhere in the schema.
12
+
13
+ The failure that produces is silent twice over. Guess a parameter name wrong
14
+ and the extra key is discarded, so a filtered query quietly returns the
15
+ unfiltered set. Guess a *value* wrong — ``aggregation="count()"`` where the
16
+ appliance wants ``COUNT`` — and you get an error or an empty series where there
17
+ was data. Neither looks like a mistake from the outside.
18
+
19
+ The family is landing a shared helper that copies each parameter's ``Args:``
20
+ entry into the schema at registration time. That helper can only copy text that
21
+ exists, so this test guards the input side: it fails the moment a tool grows a
22
+ parameter nobody described.
23
+
24
+ Not a style check
25
+ -----------------
26
+ It also refuses a description that only restates the name (``target: The
27
+ target.``). That is worse than an absent one: it fills the slot, so the gap
28
+ stops being visible while the agent still learns nothing.
29
+
30
+ 形态 #1 — the check must not pass vacuously. If the registry hands back no
31
+ tools, or no tool has parameters, that is this test being broken rather than
32
+ this skill being clean, and it fails saying so.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import inspect
38
+ import re
39
+
40
+ from vmware_debug.mcp_server.server import build_server
41
+
42
+ #: Words that carry no information about a parameter beyond its own name.
43
+ #: A description built only from these has filled the slot without saying
44
+ #: anything — the shape this test exists to reject.
45
+ _FILLER = {"a", "an", "the", "of", "for", "to", "this", "s", "id", "name", "optional"}
46
+
47
+
48
+ def parse_args_block(doc: str | None) -> dict[str, str]:
49
+ """Return {parameter: description} from a Google-style ``Args:`` block.
50
+
51
+ Deliberately the same parsing the family's schema-injection helper does:
52
+ a check that reads docstrings differently from the code that consumes them
53
+ would go green on text the helper cannot use.
54
+ """
55
+ if not doc:
56
+ return {}
57
+ block = re.search(
58
+ r"\n\s*Args:\s*\n(.*?)(?:\n\s*(?:Returns|Raises|Yields|Examples?|Notes?):|\Z)",
59
+ doc,
60
+ re.DOTALL,
61
+ )
62
+ if not block:
63
+ return {}
64
+ out: dict[str, str] = {}
65
+ current: str | None = None
66
+ for line in block.group(1).splitlines():
67
+ if not line.strip():
68
+ continue
69
+ entry = re.match(r"\s{4,}(\*{0,2}\w+)\s*(?:\([^)]*\))?\s*:\s*(.*)", line)
70
+ if entry:
71
+ current = entry.group(1).lstrip("*")
72
+ out[current] = entry.group(2).strip()
73
+ elif current:
74
+ out[current] += " " + line.strip()
75
+ return out
76
+
77
+
78
+ def _says_something(param: str, text: str) -> bool:
79
+ """True when the description adds anything beyond the parameter's own name.
80
+
81
+ Deliberately the weakest rule that still catches ``target: The target.`` —
82
+ one word that is neither part of the name nor a connective. A stricter
83
+ length or word-count rule would start failing short descriptions that are
84
+ complete ("Server IP address."), and a check that fires on correct text is
85
+ a check people learn to route around.
86
+ """
87
+ words = {w for w in re.findall(r"[a-z0-9]+", text.lower()) if w}
88
+ words -= _FILLER
89
+ words -= set(re.findall(r"[a-z0-9]+", param.lower()))
90
+ return bool(words)
91
+
92
+
93
+ def test_every_tool_parameter_has_an_args_entry():
94
+ """No registered tool parameter may be missing from its ``Args:`` block."""
95
+ tools = build_server()._tool_manager._tools
96
+ assert tools, "no MCP tools registered — this check would pass vacuously"
97
+
98
+ checked = 0
99
+ missing: list[str] = []
100
+ for name, tool in tools.items():
101
+ documented = parse_args_block(inspect.getdoc(tool.fn))
102
+ for param in (tool.parameters or {}).get("properties", {}):
103
+ checked += 1
104
+ if param not in documented:
105
+ missing.append(f"{name}.{param}")
106
+
107
+ assert checked, "no tool parameters found — this check would pass vacuously"
108
+ assert not missing, (
109
+ f"{len(missing)} of {checked} tool parameters have no Args: entry, so the "
110
+ f"schema an agent reads describes them not at all: {sorted(missing)}"
111
+ )
112
+
113
+
114
+ def test_no_parameter_description_merely_restates_its_name():
115
+ """A description that only echoes the name hides the gap instead of closing it."""
116
+ tools = build_server()._tool_manager._tools
117
+ assert tools, "no MCP tools registered — this check would pass vacuously"
118
+
119
+ checked = 0
120
+ empty: list[str] = []
121
+ for name, tool in tools.items():
122
+ documented = parse_args_block(inspect.getdoc(tool.fn))
123
+ for param in (tool.parameters or {}).get("properties", {}):
124
+ text = documented.get(param)
125
+ if text is None:
126
+ continue # the test above owns this failure
127
+ checked += 1
128
+ if not _says_something(param, text):
129
+ empty.append(f"{name}.{param}: {text!r}")
130
+
131
+ assert checked, "no documented parameters found — this check would pass vacuously"
132
+ assert not empty, "parameter descriptions that say nothing beyond the name: " + str(empty)
@@ -1293,7 +1293,7 @@ wheels = [
1293
1293
 
1294
1294
  [[package]]
1295
1295
  name = "vmware-debug"
1296
- version = "1.11.0"
1296
+ version = "1.11.1"
1297
1297
  source = { editable = "." }
1298
1298
  dependencies = [
1299
1299
  { name = "mcp", extra = ["cli"] },
@@ -1316,7 +1316,7 @@ requires-dist = [
1316
1316
  { name = "pyyaml", specifier = ">=6.0,<7.0" },
1317
1317
  { name = "rich", specifier = ">=13.0,<15.0" },
1318
1318
  { name = "typer", specifier = ">=0.12,<1.0" },
1319
- { name = "vmware-policy", specifier = ">=1.8.5,<2.0" },
1319
+ { name = "vmware-policy", specifier = ">=1.10.0,<2.0" },
1320
1320
  ]
1321
1321
 
1322
1322
  [package.metadata.requires-dev]
@@ -1328,14 +1328,14 @@ dev = [
1328
1328
 
1329
1329
  [[package]]
1330
1330
  name = "vmware-policy"
1331
- version = "1.9.0"
1331
+ version = "1.10.0"
1332
1332
  source = { registry = "https://pypi.org/simple" }
1333
1333
  dependencies = [
1334
1334
  { name = "pyyaml" },
1335
1335
  { name = "rich" },
1336
1336
  { name = "typer" },
1337
1337
  ]
1338
- sdist = { url = "https://files.pythonhosted.org/packages/fb/07/53c849427b78bd16fd4cecaeb62eda381c66274d5cda1325c6f7a8dd3a4b/vmware_policy-1.9.0.tar.gz", hash = "sha256:e7dd8507351837d6aa1b06ccef4675382db8167ff2005964243f5a6a5f379102", size = 126406, upload-time = "2026-08-28T05:10:13.361Z" }
1338
+ sdist = { url = "https://files.pythonhosted.org/packages/48/10/a1b83a1d61286cdc0e02fb73bea5e28b252e143842cbd190358224259932/vmware_policy-1.10.0.tar.gz", hash = "sha256:fb0125858634bfae256886d604065702e5cf16944b9a51fba48ff1baf33d55f9", size = 136492, upload-time = "2026-08-30T09:19:44.457Z" }
1339
1339
  wheels = [
1340
- { url = "https://files.pythonhosted.org/packages/28/9c/9cdc7305f7ae5c7e05d3b217dda9b69705f696081eae6dc56eec534b78fc/vmware_policy-1.9.0-py3-none-any.whl", hash = "sha256:8ea644eacbc34164c352f20c704c39c12d1884b9e61c68509e7ec276210c33b7", size = 47588, upload-time = "2026-08-28T05:10:11.641Z" },
1340
+ { url = "https://files.pythonhosted.org/packages/68/e0/5d10c715e464c0ae08f8f22fd0801cfa06249e3cd39d27f95463b959ded6/vmware_policy-1.10.0-py3-none-any.whl", hash = "sha256:7c7c8745e1764cac507df0c032fbe291217e9751f1947ceb3d4ac78ab2e2f349", size = 52110, upload-time = "2026-08-30T09:19:43.079Z" },
1341
1341
  ]
@@ -5,4 +5,4 @@ into a unified timeline, detect spikes, rank root-cause hypotheses, and route
5
5
  remediation to vmware-aiops / vmware-pilot. Never writes; never executes fixes.
6
6
  """
7
7
 
8
- __version__ = "1.11.0"
8
+ __version__ = "1.11.1"
@@ -13,7 +13,7 @@ import sys
13
13
  from typing import Optional, Union
14
14
 
15
15
  from mcp.server.fastmcp import FastMCP
16
- from vmware_policy import sanitize, set_environment_resolver
16
+ from vmware_policy import describe_tool_parameters, sanitize, set_environment_resolver
17
17
 
18
18
  from vmware_debug.mcp import tools as t
19
19
  from vmware_debug import __version__
@@ -161,12 +161,16 @@ def build_server() -> FastMCP:
161
161
  # than ours. Set it so a client can tell which release it is talking to.
162
162
  server._mcp_server.version = __version__
163
163
 
164
- # 357 tokens against a 350 ceiling, and the seven are the two sentences
165
- # pointing at `binning` and `classification`. Both were added because a real
164
+ # 454 tokens against a 350 ceiling. The first seven over were the two
165
+ # sentences pointing at `binning` and `classification`, added because a real
166
166
  # investigation went wrong for want of them: the answer had been computed at
167
167
  # 23-hour resolution and over a stream it could read 20% of, and said
168
168
  # neither. A result whose limits are not stated is read as one without any,
169
- # so trimming these two would keep the cost and remove the point.
169
+ # so trimming those two would keep the cost and remove the point. The rest
170
+ # is the `Args:` block, which is not decoration either: without a per-
171
+ # parameter description the schema an agent reads says only "number", and
172
+ # bin_seconds and z_threshold are exactly the two knobs a wrong guess makes
173
+ # silently useless.
170
174
  @server.tool(name="incident_timeline", annotations=_READ)
171
175
  def _incident_timeline_impl(
172
176
  events: list[dict],
@@ -183,12 +187,6 @@ def build_server() -> FastMCP:
183
187
  next. Not sure which events to pull? Run list_symptom_categories
184
188
  first. This tool does NOT fetch anything itself.
185
189
 
186
- INPUT: events = event envelopes, each {ts, source, severity, entity,
187
- text, fields} (ts may be ISO-8601, epoch seconds or millis; severity
188
- is normalised). Optional: bin_seconds (time-bin width; auto if
189
- omitted), z_threshold (spike sensitivity, default 2.0), top_n (max
190
- hypotheses, default 5).
191
-
192
190
  RETURNS: {event_count, window, binning, classification, spikes
193
191
  (strongest anomalous bins), spikes_total, hypotheses (ranked
194
192
  root-cause candidates, each with a suggested_check), next_checks (which
@@ -199,7 +197,24 @@ def build_server() -> FastMCP:
199
197
  GOTCHAS: read-only, stateless, no network — nothing is executed.
200
198
  Remediation routes to vmware-aiops (single fix) or vmware-pilot
201
199
  (multi-step). A malformed event returns {error, hint} naming the
202
- offending index."""
200
+ offending index.
201
+
202
+ Args:
203
+ events: Event envelopes, each {ts, source, severity, entity, text,
204
+ fields}. ts may be ISO-8601, epoch seconds or epoch millis and
205
+ is required; severity is normalised onto critical/error/warning/
206
+ info/unknown, so vendor spellings (fatal, red, warn, yellow,
207
+ notice, green) are accepted. An entry that cannot be normalised
208
+ is refused with its index, not skipped.
209
+ bin_seconds: Time-bin width in seconds. Omit and it is chosen from
210
+ event density off the ladder 1/10/60/300/900/3600/21600/86400,
211
+ taking the finest width still averaging 4 events per bin.
212
+ z_threshold: Standard deviations above the mean bin count that mark
213
+ a spike (default 2.0). Under 3 bins, or a flat series, yields
214
+ none at any threshold — empty spikes is not "calm".
215
+ top_n: How many ranked hypotheses come back (default 5). Spikes are
216
+ capped separately at 20, true count in 'spikes_total'.
217
+ """
203
218
  try:
204
219
  return t.incident_timeline(events, bin_seconds, z_threshold, top_n)
205
220
  except Exception as exc:
@@ -251,18 +266,32 @@ def build_server() -> FastMCP:
251
266
  WHEN: at the start of an incident you expect to reason about rather than
252
267
  glance at. For a one-off lookup use incident_timeline alone.
253
268
 
254
- INPUT: summary (what is wrong, one line); determined_by (HOW the scope
255
- was decided — "user report", "vCenter alarm 42" — required, because a
256
- scope from a phone call and one from an alarm id support different
257
- conclusions and nobody remembers which it was later); optional objects,
258
- window_start/window_end (ISO-8601), product_versions (used to check
259
- whether a knowledge-base entry actually applies).
260
-
261
269
  RETURNS: {case_id, path, state, grade, ceiling, ceiling_reasons, next}.
262
270
  Read `ceiling` now — it is the best grade this install can reach at all.
263
271
 
264
272
  GOTCHAS: writes only under $OPS_HOME, never to a VMware system, and
265
- never over an existing case."""
273
+ never over an existing case.
274
+
275
+ Args:
276
+ summary: What is wrong, one line. case_plan reads it to infer the
277
+ symptom category, so name the symptom in the estate's own words
278
+ ("datastore latency on cluster-a"), not a diagnosis.
279
+ determined_by: HOW the scope was decided — "user report", "vCenter
280
+ alarm 42". Required: a scope from a phone call and one from an
281
+ alarm id support different conclusions, and nobody remembers
282
+ which it was later.
283
+ objects: Entities in scope, names or MoIDs ("vm-web01", "host-12").
284
+ Omit for a case not yet pinned to specific objects.
285
+ window_start: When the incident is believed to have started,
286
+ ISO-8601. Omit if unknown — evidence gets fetched against it,
287
+ so an invented window is worse than none.
288
+ window_end: Believed end, ISO-8601. Omit while still in progress.
289
+ product_versions: Versions keyed by product — {"esxi": "8.0.3"} —
290
+ plus "driver.<name>" / "firmware.<name>" entries. Knowledge-base
291
+ entries are version-checked against this: one constraining a
292
+ product absent here can support a hypothesis but never make the
293
+ case Confirmed, since an unanswerable constraint is not a pass.
294
+ """
266
295
  try:
267
296
  return t.case_open(
268
297
  summary=summary,
@@ -283,7 +312,13 @@ def build_server() -> FastMCP:
283
312
  Returns the family list envelope {items, returned, limit, total,
284
313
  truncated, hint}; each item is {case_id, summary, state, grade,
285
314
  opened_at}. A case whose folder is damaged appears with
286
- state="unreadable" rather than vanishing from the list."""
315
+ state="unreadable" rather than vanishing from the list.
316
+
317
+ Args:
318
+ limit: Maximum cases in the page, newest first (default 50). There
319
+ is no offset — 'total' reports every case, so when truncated is
320
+ true the only way to reach older ones is a larger limit.
321
+ """
287
322
  try:
288
323
  return t.case_list(limit=limit)
289
324
  except Exception as exc:
@@ -301,15 +336,21 @@ def build_server() -> FastMCP:
301
336
  RETURNS: {case_id, path, state, grade, opened_at, scope, evidence_count,
302
337
  sources, gap_count, blocking_gaps, grade_history}. `sources` is the
303
338
  distinct skills evidence came from, which is what corroboration is
304
- counted in."""
339
+ counted in.
340
+
341
+ Args:
342
+ case_id: The case id returned by case_open, or listed by case_list.
343
+ An unknown id is an error, never an empty case.
344
+ """
305
345
  try:
306
346
  return t.case_get(case_id=case_id)
307
347
  except Exception as exc:
308
348
  return _case_error(exc, "case_get")
309
349
 
310
350
  # The widest signature here, and deliberately so. `per_tool_token_discipline`
311
- # flags it, but ~322 of its ~570 manifest tokens are the JSON schema for
312
- # twelve parameters, not prose — and those twelve ARE the evidence contract:
351
+ # flags it, but ~322 of its ~900 manifest tokens are the JSON schema for
352
+ # thirteen parameters, not prose — and those thirteen ARE the evidence
353
+ # contract:
313
354
  # drop one and a conclusion stops being traceable to what produced it.
314
355
  # Folding the four time fields into a nested `time_basis` object would shrink
315
356
  # the schema and make it likelier to be filled wrong; flat optional fields
@@ -339,21 +380,6 @@ def build_server() -> FastMCP:
339
380
 
340
381
  WHEN: after every read-tool call you intend to reason from.
341
382
 
342
- INPUT: source_skill + source_tool (which produced it) and query (the
343
- exact parameters), so it can be re-run. window_start/window_end are the
344
- period the DATA COVERS, not when it was fetched — list_events(hours=24)
345
- run at 10:00 and at 18:00 answer different questions, and correlation
346
- depends on which. time_source ("vcenter"/"host"/"client") and
347
- clock_skew_s feed skew detection; pass null when unknown, never a guess.
348
- falsifies: hypothesis ids this RULES OUT — the only thing that can
349
- exclude one. knowledge_entry_id: REQUIRED when source_skill is
350
- knowledge-kb or knowledge-sr — which mounted entry this is. Without it
351
- the entry's applies_to cannot be checked against the case, so it is not
352
- decisive; run case_knowledge to see what is mounted. payload: the read
353
- tool's RAW result, not a summary of it — for its events to reach
354
- case_timeline it must be a list of event dicts, or carry them under
355
- `items` (the family list envelope), `events` or `rows`.
356
-
357
383
  RETURNS: {case_id, evidence_id, payload_events, payload_note, grade,
358
384
  reasons} — the resulting grade, so you need no second call to see
359
385
  whether this changed anything, and what the payload was read as, so a
@@ -361,7 +387,50 @@ def build_server() -> FastMCP:
361
387
  zero from case_timeline later.
362
388
 
363
389
  GOTCHAS: a fetch that failed or came back empty goes to case_record_gap,
364
- not here."""
390
+ not here.
391
+
392
+ Args:
393
+ case_id: The case this fact belongs to (from case_open/case_list).
394
+ Required.
395
+ source_skill: The skill that produced it, as the family spells it —
396
+ "vmware-monitor", "vmware-aria", "vmware-log-insight". Required,
397
+ non-blank. Two items from the SAME skill count as ONE source
398
+ when corroboration is counted, so this string decides whether
399
+ the case can reach Probable. Two reserved values name the
400
+ knowledge layer instead: "knowledge-kb" and "knowledge-sr", the
401
+ only sources that can be decisive.
402
+ source_tool: The tool within that skill, e.g. "get_events".
403
+ Required, non-blank — "monitor said so" is not reproducible.
404
+ summary: What this item shows, one line.
405
+ query: The exact parameters the tool was called with, so it can be
406
+ re-run. Omit only if there genuinely were none.
407
+ fetched_at: When the fetch happened, ISO-8601. Omit to stamp now —
408
+ wrong for an item transcribed from earlier.
409
+ window_start: Start of the period the DATA COVERS, ISO-8601 — not
410
+ when it was fetched. get_events(hours=24) run at 10:00 and at
411
+ 18:00 answer different questions.
412
+ window_end: End of the period the data covers, ISO-8601.
413
+ time_source: Whose clock stamped it — "vcenter", "host" or "client".
414
+ Recorded so a reader can judge whether two sources' timestamps
415
+ compare; nothing in this release corrects for it. Null when
416
+ unknown, never a guess.
417
+ clock_skew_s: Known offset of that clock from UTC, in seconds.
418
+ Recorded, likewise not applied. Null when unknown.
419
+ falsifies: Hypothesis ids (H1, H2, …) this observation RULES OUT —
420
+ the only route to Excluded; "we looked and found nothing" is a
421
+ gap, not an exclusion. Ids must already exist via
422
+ case_hypotheses; an unregistered id is refused and nothing is
423
+ written.
424
+ knowledge_entry_id: Which mounted knowledge entry this item IS.
425
+ REQUIRED when source_skill is knowledge-kb or knowledge-sr —
426
+ without it the entry's applies_to cannot be checked, so it
427
+ counts as ordinary support and can never make the case
428
+ Confirmed. case_knowledge lists what is mounted.
429
+ payload: The read tool's RAW result, not a summary. For its events
430
+ to reach case_timeline it must be a list of event dicts, or
431
+ carry them under 'items', 'events' or 'rows'. What was found
432
+ comes back as payload_events/payload_note.
433
+ """
365
434
  try:
366
435
  return t.case_submit_evidence(
367
436
  case_id=case_id,
@@ -397,20 +466,28 @@ def build_server() -> FastMCP:
397
466
  keeps a case honest: an unrecorded gap makes it look better supported
398
467
  than it is.
399
468
 
400
- INPUT: what (the missing observation); why (why it could not be had);
401
- how_to_close (the next action, even if it is outside this system — a
402
- gap with no stated next action reads like a to-do and gets skipped);
403
- blocks (hypothesis ids it holds up); could_falsify — would obtaining
404
- this be able to prove the hypothesis WRONG? Most gaps are missing
405
- corroboration (false: caps the case below Confirmed). A gap that could
406
- overturn the hypothesis is different in kind (true: holds it at
407
- Candidate, because claiming otherwise claims a check nobody ran).
408
-
409
469
  RETURNS: {case_id, gap_id, grade, reasons}.
410
470
 
411
471
  GOTCHAS: recording a gap does not punish the case for the evidence it
412
472
  does have — a missing confirmation caps the grade, it does not demote
413
- it. Writing gaps down is meant to be free."""
473
+ it. Writing gaps down is meant to be free.
474
+
475
+ Args:
476
+ case_id: The case this gap belongs to (from case_open/case_list).
477
+ what: The observation that could not be obtained — the thing you
478
+ wanted, not the error you got.
479
+ why: Why it could not be had — refused, unreachable, collected by
480
+ nothing in this family, absent from this environment.
481
+ how_to_close: The next action that would close it, even outside this
482
+ system ("open a vendor SR"). A gap with no stated next action
483
+ reads like a to-do and gets skipped.
484
+ blocks: Hypothesis ids (H1, H2, …) this gap holds up; they must
485
+ already exist via case_hypotheses. Empty is fine.
486
+ could_falsify: Would OBTAINING this be able to prove the hypothesis
487
+ WRONG? false (default) is the ordinary case — missing
488
+ corroboration, capping the case below Confirmed. true means it
489
+ could overturn the hypothesis, holding it at Candidate.
490
+ """
414
491
  try:
415
492
  return t.case_record_gap(
416
493
  case_id=case_id,
@@ -451,7 +528,12 @@ def build_server() -> FastMCP:
451
528
  needs a decisive source and there is neither a hardware-diagnostic
452
529
  channel nor a knowledge library mounted yet. That is a real limit, not
453
530
  a caution. Every grading is appended to conclusion.md and none is ever
454
- rewritten."""
531
+ rewritten.
532
+
533
+ Args:
534
+ case_id: The case to grade (from case_open/case_list). This is the
535
+ only parameter — see above for why there is no grade parameter.
536
+ """
455
537
  try:
456
538
  return t.case_grade(case_id=case_id)
457
539
  except Exception as exc:
@@ -465,11 +547,6 @@ def build_server() -> FastMCP:
465
547
  higher and you want to know whether that is fixable. Answering this
466
548
  first is worth far more than discovering it halfway through.
467
549
 
468
- INPUT: available_skills — the skills actually installed and configured,
469
- in either of the family's spellings ("monitor" and "vmware-monitor" name
470
- the same thing). Omit to assume all of them, which reports the ceiling
471
- imposed by the family itself rather than by this install.
472
-
473
550
  RETURNS: {classes, categories, unrecognised_skills, note}. A name that
474
551
  matched no skill in the catalogue comes back in `unrecognised_skills`
475
552
  rather than being absorbed into "not installed" — otherwise a typo reads
@@ -486,7 +563,16 @@ def build_server() -> FastMCP:
486
563
  two available classes do not always mean Probable. The hardware class is
487
564
  unavailable no matter what is installed — nothing in this family reaches
488
565
  below ESXi — and the knowledge class becomes available only when entries
489
- are mounted under $OPS_HOME/knowledge/."""
566
+ are mounted under $OPS_HOME/knowledge/.
567
+
568
+ Args:
569
+ available_skills: The skills actually installed and configured, in
570
+ either of the family's spellings — "monitor" and "vmware-monitor"
571
+ name the same thing. Omit to assume all of them, which reports
572
+ the ceiling imposed by the family itself rather than by this
573
+ install. A name matching no catalogued skill comes back in
574
+ `unrecognised_skills` rather than being read as "not installed".
575
+ """
490
576
  try:
491
577
  return t.case_readiness(available_skills=available_skills)
492
578
  except Exception as exc:
@@ -505,14 +591,6 @@ def build_server() -> FastMCP:
505
591
  is not a checklist: submit something and the next plan is shorter, lose
506
592
  a source and it routes around it.
507
593
 
508
- INPUT: case_id. Optional category to force the symptom class (storage,
509
- network, compute, ha_drs, configuration, accelerator, kubernetes,
510
- hardware, host_lifecycle, power_lifecycle, auth, platform) — omit to
511
- infer it from the scope. Optional available_skills to narrow to what is
512
- installed, in either spelling (monitor or vmware-monitor).
513
- max_steps caps how many steps come back (default 6) — a storage case has
514
- fourteen reachable tools, and a plan that long stops being a next step.
515
-
516
594
  RETURNS: {category, category_signals, steps, already_covered,
517
595
  held_back, unavailable,
518
596
  ceiling, note}. Steps are interleaved across evidence classes, so you
@@ -529,7 +607,23 @@ def build_server() -> FastMCP:
529
607
  reachable is already in, or whether nothing here can be reached.
530
608
  `category_signals` names the words that chose the category, and any
531
609
  category that also matched — check it first, since the rest runs off
532
- that one word."""
610
+ that one word.
611
+
612
+ Args:
613
+ case_id: The case to plan for (from case_open/case_list).
614
+ category: Force the symptom class instead of inferring it. Exactly
615
+ one of: storage, network, compute, ha_drs, configuration,
616
+ accelerator, kubernetes, hardware, host_lifecycle,
617
+ power_lifecycle, auth, platform. Omit to infer, then read
618
+ `category_signals` for the word that decided it.
619
+ available_skills: Narrow to the skills actually installed, in either
620
+ spelling ("monitor" or "vmware-monitor"). Omit to assume all of
621
+ them, which can produce steps this install cannot run.
622
+ max_steps: HAS NO EFFECT in this release — accepted but never
623
+ forwarded to the planner, which always caps at 6. The result's
624
+ note may still advise raising it; doing so changes nothing.
625
+ Read `held_back` for how many steps were cut.
626
+ """
533
627
  try:
534
628
  return t.case_plan(
535
629
  case_id=case_id,
@@ -560,7 +654,17 @@ def build_server() -> FastMCP:
560
654
  supported, the same way a case does not get to state its own grade.
561
655
 
562
656
  GOTCHAS: refuted outranks blocked. Once an observation settles the
563
- question, a missing measurement no longer matters."""
657
+ question, a missing measurement no longer matters.
658
+
659
+ Args:
660
+ case_id: The case whose hypothesis ledger this is (from
661
+ case_open/case_list).
662
+ statement: The candidate explanation, in one line. Pass it to
663
+ register a new hypothesis, which is assigned the next id (H1,
664
+ H2, …); omit it to read the ledger without changing it. There is
665
+ no parameter for a hypothesis's status — status is computed from
666
+ the evidence and gaps that point at it.
667
+ """
564
668
  try:
565
669
  return t.case_hypotheses(case_id=case_id, statement=statement)
566
670
  except Exception as exc:
@@ -591,7 +695,21 @@ def build_server() -> FastMCP:
591
695
  evidence item it came from; dropping those silently would shrink the
592
696
  picture the conclusion rests on. Submit a read tool's raw result as
593
697
  `payload` for its events to reach here — a summary of the result carries
594
- no rows, and case_submit_evidence says so at the time."""
698
+ no rows, and case_submit_evidence says so at the time.
699
+
700
+ Args:
701
+ case_id: The case whose submitted payloads are correlated (from
702
+ case_open/case_list). Unlike incident_timeline this takes no
703
+ events — everything comes from the case folder.
704
+ bin_seconds: Time-bin width in seconds. Omit and it is chosen from
705
+ event density (ladder 1..86400, finest width still averaging 4
706
+ events per bin); `binning` reports which was used.
707
+ z_threshold: Standard deviations above the mean bin count that mark
708
+ a spike (default 2.0). Under 3 bins, or a flat series, yields
709
+ none at any threshold.
710
+ top_n: How many ranked hypotheses come back (default 5). Spikes are
711
+ capped separately at 20, true count in 'spikes_total'.
712
+ """
595
713
  try:
596
714
  return t.case_timeline(
597
715
  case_id=case_id,
@@ -616,7 +734,13 @@ def build_server() -> FastMCP:
616
734
 
617
735
  GOTCHAS: a closed case is not closed again and its record is never
618
736
  rewritten. To reopen the question, open a new case that cites this one,
619
- so the original conclusion and whatever changed it both stay readable."""
737
+ so the original conclusion and whatever changed it both stay readable.
738
+
739
+ Args:
740
+ case_id: The case to close (from case_open/case_list). There is no
741
+ grade parameter — closing recomputes the grade from the ledger.
742
+ An already-closed case is refused rather than closed again.
743
+ """
620
744
  try:
621
745
  return t.case_close(case_id=case_id)
622
746
  except Exception as exc:
@@ -631,10 +755,6 @@ def build_server() -> FastMCP:
631
755
  they can act on. This is the answer to "which knowledge formats do you
632
756
  take" — the first question anyone mounting a library asks.
633
757
 
634
- INPUT: optional case_id. With it, each mounted entry is also checked
635
- against that case's recorded versions, so you can say which entries
636
- could make THAT case Confirmed and which cannot, with the reason.
637
-
638
758
  RETURNS: {root, sections, entries, with_applies_to, by_source,
639
759
  unreadable, unsupported, formats, needs_conversion, note} — plus
640
760
  {applicable, decisive_here} when a case_id is given.
@@ -650,15 +770,24 @@ def build_server() -> FastMCP:
650
770
  wrong build reads exactly like the right one. An entry with no
651
771
  `applies_to` can support a hypothesis but can never make a case
652
772
  Confirmed. A constraint the case scope cannot answer is not a match
653
- either: silence is not a pass."""
773
+ either: silence is not a pass.
774
+
775
+ Args:
776
+ case_id: Omit to describe the knowledge layer itself — what formats
777
+ are read and what is mounted. Pass a case id and every mounted
778
+ entry is additionally version-checked against THAT case's
779
+ product_versions, adding `applicable` and `decisive_here` with
780
+ the reason each entry did or did not qualify.
781
+ """
654
782
  try:
655
783
  return t.case_knowledge(case_id=case_id)
656
784
  except Exception as exc:
657
785
  return _case_error(exc, "case_knowledge")
658
-
659
- return server
786
+ # The docstrings above are the schema — see describe_tool_parameters.
787
+ describe_tool_parameters(server._tool_manager._tools)
660
788
 
661
789
 
790
+ return server
662
791
  def main() -> None:
663
792
  """Entry point for `vmware-debug-mcp` (stdio transport)."""
664
793
  # Floor is 3.10, matching `requires-python` and the other eleven skills.
File without changes
File without changes
File without changes