Repository navigation
v3.9.0
Correctness and reliability sweep. Started from dashboard/report bugs hit against a
live portal (root causes confirmed against the LM v3 API shapes -- widget placement
lives on the dashboard's widgetsConfig keyed by widget id; the report schedule is a
flat cron string, not a nested object) and extended to silent failures that corrupt
health verdicts.
Added
- Server instructions that steer the model to call
search_toolsfirst and point at
the composite workflow tools, so the large tool surface is navigated by search rather
than enumeration (the established pattern for large MCP servers). - A tool-contract snapshot test (
tests/test_tool_contract.pyagainst
tests/fixtures/tool_list.json) that fails on any accidental change to a tool name,
parameter, type, required field, description, or schema across all 280 tools.
Intentional changes regenerate the fixture with
uv run python tests/test_tool_contract.py. - A
WORKFLOW_TOOLSmembership guard (every tool implemented intools/workflows.py
must be in the curated set), asearch_toolscoverage test (configured AWX/Terraform
tools are searchable), and a duplicate-tool-name guard.
Fixed
create_dashboard(template=...)silently dropped every widget. The handler
copied the exported definition into the create body and POSTed it once; the dashboard
create endpoint does not accept embedded widgets, so cloning an exported dashboard
produced an empty one (HTTP 200,success: true, zero widgets) while only
widgetTokenssurvived.create_dashboardnow creates the shell, recreates each
widget from the template'swidgets_fullon the new dashboard (old id stripped, new
dashboardIdset), and re-applies the exportedwidgetsConfigplacement remapped to
the new widget ids. A single failed widget is logged with stack trace and surfaced as
awidget_warningsentry rather than aborting the clone or vanishing. This makes
export_dashboard->create_dashboardround-trip widgets, not just the shell.add_widgetsilently discarded widget placement.columnIdx/rowSpan/colSpan
were sent on the/dashboard/widgetsbody, which the API accepts and discards, so
every widget landed at the default cell and the position parameters were dead.
Placement lives on the parent dashboard'swidgetsConfig(keyed by widget id);
add_widgetnow writes it there via a PATCH that preserves sibling positions, and
only when a position is requested (otherwise the portal auto-places). Added arow
parameter for explicit vertical placement.get_dashboard_widgetsreturned nullcolumn/row_span/col_spanfor every
widget. It read those off the widget objects, which never carry them; it now sources
column/row/col_span/row_spanfrom the parent dashboard'swidgetsConfig.get_dashboardsreportedwidget_count: 0for widget-bearing dashboards.
_count_widgetsonly countedwidgetsConfigas a list or a literalcountkey;
real portals return a dict keyed by widget id. It now counts the keyed dict, and the
same helper backsdelete_dashboard's count (previouslylen(..., [])on the wrong
default type).update_widgetcould 400 on echoed read-only fields and could not move a widget.
Content updates now strip server-managed read-only fields (lastUpdatedOn,
lastUpdatedBy,userPermission) before the PUT; placement updates route to the
dashboardwidgetsConfigPATCH.update_dashboardlikewise strips dashboard
read-only fields (fullName,userPermission,groupName,groupFullPath,
owner) before its PUT.run_reportposted to a nonexistent/functionsendpoint and 404'd on every v3
portal. It now triggers generation viaPOST /report/reports/{id}/executionsand
returns the executiontaskIdandresulturlfor polling.- The report
schedulewas modeled as a nested dict, but the LM v3 field is a flat
string (a cron expression; empty means unscheduled) with a separate
scheduleTimezonestring. The dict model broke every path:update_report_schedule
raisedTypeError('str' object does not support item assignment) on a real
scheduled report,get_scheduled_reportsraisedAttributeErrorcalling.get()on
a string, andcreate_reportsent a wrong-typed body.create_report,
update_report_schedule,get_scheduled_reports, andget_reportnow use the flat
schedule/scheduleTimezonestrings, andupdate_report_schedulestrips report
read-only fields before its PUT. - Three health-verdict signals were silently zeroed on API failure, none logged:
get_collector_healthreturnedactive_collector_down_alerts: 0(and
is_down: false) whenever the CollectorDown alert query failed, so a real outage
could readcollectors_down: 0. The probe now returns "unknown" instead of 0 on
failure, sets a per-collectordown_signal_unavailableflag plus a top-level
collectors_down_signal_unavailablecount and warning, and logs the exception.analyze_blast_radiustreated a failed neighbor lookup as "no neighbors" and a
failed per-device alert lookup ashas_critical: false, understating the blast
radius and reporting alerting devices as healthy. Failures are now logged, the
device is markedalert_status_unavailablewithhas_critical: null, and the
response carries adegradedflag.get_remediation_historyturned a failed audit-log read (commonly a 403) into the
benign "no remediation execution records found" note, conflating "could not read"
with "nothing ran." It now logs the failure and returns a distinct
audit_read_failednote stating history is unavailable, not confirmed empty.
- Added module loggers to
collectors,topology_analysis, andremediationsources
so these degrade-on-failure paths log a stack trace before falling back (per the
no-silent-except rule). - MED/LOW silent-failure hardening across degrade-on-failure paths, each now logged
before the fallback:ingest_postreturned blanket success on an HTTP 202 even when the body reported
per-record rejections (silent ingestion data loss). It now inspects the 202 body
and raises on an error envelope orsuccess: false.correlate_changesturned a failed audit/change-log read into "0 changes"; it now
setsaudit_read_failed+ a warning so "no changes" is not confused with "could
not read."- the watsonx TTM forecast silently fell back to linear regression; it now logs the
failure and tags the result withttm_fallback_reason(watsonx_not_configured
vswatsonx_error: ...). - the
terraformenv-var build and theget_collector_healthadvisory fallbacks
(downstream device count, collector-down history) now log before degrading.
- Added module loggers to
event_correlation,forecasting, andterraform. - Composite workflows no longer swallow sub-step failures silently. In
triage,
health_check/diagnose, andcapacity_plan, eight bare
except Exception: pass / continue / = Noneblocks now log via the audit logger and
surface the failure: the higher-signal ones (blast radius, health score, datasource
and instance fetches) append to the responsewarnings, and the per-metric
capacity_plananalyses (forecast, trend, seasonality, change points) attach a
<field>_erroralongside theNoneso a failure is distinguishable from "no data." - The empty/null-id import silent-failure guard now covers all eight
import_*
tools. Previously onlyimport_datasourcedetected a 200 response with no id and no
error (the wrong-definition-format case); the other seven reportedimported_id: null
as a success. All eight now route through a shared_import_result_responsehelper
that returnsIMPORT_SILENT_FAILUREfor that shape. - Registration / discoverability drift.
LM_MCP_CATEGORIES=workflow(the documented Cursor 40-tool-cap workaround) silently
droppeddetect_site_outageandaudit_network_monitoring_coverage: the curated
WORKFLOW_TOOLSset was never updated when those v3.8.0 composites landed. Both are
now included.search_toolssearched only the coreTOOLSlist, so the 29 AWX/watsonx/Terraform
tools were unsearchable even when configured (andsearch_tools(category="ansible" |"terraform"|"watsonx")always returned empty). It now builds its corpus the same
way the server advertises tools.recover_deviceandcollect_device_configmutate the portal but lacked a write
prefix, so they executed with no audit-log entry;recover_/collect_were added to
WRITE_TOOL_PREFIXES.- Removed the dead, unreachable
ops.get_audit_logsduplicate (the registry routes
get_audit_logsto the more specificaudit.get_audit_logs). - Corrected stale tool counts in the README (272/225/220 -> 280/240/280).
import_*LogicModule tools now redirect REST/exported definitions tocreate_*.
Feeding a REST API definition (fromexport_<type>or the API) to animport_*tool
made the importjson endpoint reject it with a generic "does not match the expected
module type" 400, and you had to already know to pivot tocreate_*. Theimport_*
tools now detect that rejection and return an actionableIMPORT_FORMAT_MISMATCHerror
naming the matchingcreate_*tool. (import_*accepts only LM Exchange JSON;
export_*yields REST format thatcreate_*consumes -- the names mislead, so the
error now says so.)
Changed
add_widgetandupdate_widgetexpose arowparameter and treatcolumn_index,
row_span, andcol_spanas 1-indexed grid coordinates clamped to the 12-column
grid. Omitting all placement onadd_widgetlets the portal auto-place the widget
below existing ones.create_reportandupdate_report_scheduletake a cron schedule string plus
schedule_timezone(replacing the prior nested-dict /schedule_type/
schedule_enabledparameters);get_scheduled_reportsreturns theschedulestring
andschedule_timezone.create_dashboardaccepts atemplate_pathto load the dashboard definition (or an
export_dashboardenvelope) from a local file by reference, keeping a large export out
of the model context. There is intentionally noimport_dashboardtool: dashboards
have no LM Exchange format, so recreating an export is a create, not an import.
Documentation
- Slimmed the README from roughly 1380 to roughly 640 lines: the full tool catalog moved
todocumentation/tools.md, the per-client configuration matrix to
documentation/client-setup.md, and the example-prompt list and What's New section were
trimmed. Full version history remains in this changelog. documentation/tools.mdis now generated from the tool registry and the
lm://guide/tool-categoriesindex bytests/test_tools_doc.py. Drift guards fail the
suite when a tool is added without a category or when the committed doc is stale, so the
tool reference can no longer fall out of sync with the code.
Verified
- Live-portal read validation of the
widgetsConfigshape (dict keyed by widget id ->
{col, row, sizex, sizey}) on dashboards 3 and 15, and of the report
schedule/scheduleTimezoneflat-string shape on reports 1, 2, and 6. - Live-portal write validation: a widget placed via
add_widgetpersisted its position
and anexport_dashboard->create_dashboardclone round-tripped its widget;
run_reportreached the executions endpoint and a cron written via
update_report_schedulestoredschedule="0 8 * * 1"+scheduleTimezone(report 6,
restored afterward). - New regression tests assert the degraded/unavailable surfacing for the three
silent-failure fixes (collector down-signal, blast-radius alert lookup, remediation
audit read). - Full test suite green; ruff check and format clean. Dashboard and report mock fixtures
corrected from the prior list /{"count": N}/ nested-dict-schedule shapes to the
real API shapes.