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.
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/.claude-plugin/plugin.json +1 -1
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/.mcp.json +1 -1
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/PKG-INFO +2 -2
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/RELEASE_NOTES.md +25 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/pyproject.toml +2 -2
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/server.json +2 -2
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/_scores.json +29 -51
- vmware_debug-1.11.1/tests/eval/regression/test_every_tool_parameter_is_documented.py +132 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/uv.lock +5 -5
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/__init__.py +1 -1
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/mcp_server/server.py +204 -75
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/.gitignore +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/README-CN.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/README.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/SECURITY.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/SKILL.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/agent-guardrails.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/capabilities.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/cli-reference.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/event-envelope.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/routing.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/setup-guide.md +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/__init__.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/__init__.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/_family.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/_scoring.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/_skill.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/conftest.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/__init__.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_case_id_non_ascii.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_debug_regressions.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_evidence_payload_contract.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_host_lifecycle_classification.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_investigation_metrics.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_skill_name_aliases.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_spike_resolution.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_tool_annotations.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_conclusion.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_evidence.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_grading.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_hypotheses.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_ids.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_plan.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_readiness.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_store.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_timeline_close.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_case_tools.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_knowledge.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/test_timeline.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/cli.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/envelope.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/mcp/__init__.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/mcp/tools.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/mcp_server/__init__.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/__init__.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/__init__.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/api.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/conclusion.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/evidence.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/grading.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/hypotheses.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/ids.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/knowledge.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/model.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/payloads.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/plan.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/readiness.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/sources.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/store.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/cases/timeline.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/skill_names.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/ops/timeline.py +0 -0
- {vmware_debug-1.11.0 → vmware_debug-1.11.1}/vmware_debug/rules/evidence_sources.yaml +0 -0
- {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.
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vmware-debug
|
|
3
|
-
Version: 1.11.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
10
|
+
"version": "1.11.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-debug",
|
|
15
|
-
"version": "1.11.
|
|
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":
|
|
197
|
-
"case_plan":
|
|
198
|
-
"case_record_gap":
|
|
199
|
-
"case_submit_evidence":
|
|
200
|
-
"incident_timeline":
|
|
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":
|
|
203
|
-
"mean_tokens_per_tool":
|
|
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":
|
|
208
|
+
"pct": 70.6,
|
|
209
209
|
"unit": "tokens",
|
|
210
|
-
"value":
|
|
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":
|
|
217
|
+
"pct": 100.0,
|
|
231
218
|
"unit": "parameters",
|
|
232
|
-
"value":
|
|
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":
|
|
239
|
-
"case_plan":
|
|
240
|
-
"
|
|
241
|
-
"
|
|
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":
|
|
234
|
+
"pct": 57.1,
|
|
246
235
|
"unit": "tools",
|
|
247
|
-
"value":
|
|
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":
|
|
305
|
-
"gotcha":
|
|
293
|
+
"args": 100.0,
|
|
294
|
+
"gotcha": 100.0,
|
|
306
295
|
"marker": 100.0,
|
|
307
|
-
"next_hop":
|
|
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
|
-
|
|
315
|
-
],
|
|
316
|
-
"
|
|
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":
|
|
321
|
+
"pct": 95.2,
|
|
344
322
|
"unit": "points",
|
|
345
|
-
"value":
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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/
|
|
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
|
]
|
|
@@ -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
|
-
#
|
|
165
|
-
# pointing at `binning` and `classification
|
|
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
|
|
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 ~
|
|
312
|
-
#
|
|
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
|
-
|
|
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
|
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/docs/plans/2026-08-28-investigation-layer-design.md
RENAMED
|
File without changes
|
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/agent-guardrails.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/skills/vmware-debug/references/event-envelope.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_entity_reachability.py
RENAMED
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_error_actionability.py
RENAMED
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_tool_description_quality.py
RENAMED
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/capability/test_tool_manifest_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_evidence_payload_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_investigation_metrics.py
RENAMED
|
File without changes
|
|
File without changes
|
{vmware_debug-1.11.0 → vmware_debug-1.11.1}/tests/eval/regression/test_skill_name_aliases.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|