prototype: FastMCPApp NGSIEM query pilot (Prefab UI + text fallback)#19
Draft
willwebster5 wants to merge 40 commits into
Draft
prototype: FastMCPApp NGSIEM query pilot (Prefab UI + text fallback)#19willwebster5 wants to merge 40 commits into
willwebster5 wants to merge 40 commits into
Conversation
Adds opt-in [prefab-pilot] extra bringing in PrefectHQ fastmcp[apps]. Three pure-logic modules with 20 passing unit tests: - mock_data.generate_process_events: deterministic synthetic NGSIEM events so the pilot runs without Falcon creds - summary.summarize_events: reduces event rows to a compact QuerySummary (row count, top host, top event_simpleName, time range, hourly buckets) — this is the model-facing payload that keeps the full result out of the context window - fallback.summary_to_text: self-contained text rendering for Claude Code and any host that doesn't render MCP Apps No Prefab UI assembly yet — that lands in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the runnable pilot:
- layout.build_ngsiem_query_layout: Prefab Column with Heading, summary Card
with Badges, BarChart of hourly event counts, and searchable/paginated
DataTable of events
- server.py: FastMCPApp('crowdstrike-prefab-pilot') with
- @app.ui('ngsiem_query_demo') returning ToolResult so Claude Desktop
gets the interactive layout (structured_content) and Claude Code /
non-rendering hosts get a self-contained text summary (content)
- @app.tool('ngsiem_query_drilldown') that returns full JSON for a
single event, ready to wire to DataTable onRowClick
- crowdstrike-prefab-pilot console script entry point
- README.md with install, Claude Desktop config, manual verification
checklist, and exact handoff instructions for swapping mock data for
live FalconPy calls
- .gitignore: exclude .venv-*/
Tests: 16 new (9 layout smoke + 7 server wiring), all passing alongside
the existing 396. Ruff clean. Server boots without error over stdio.
Not tested: actual host rendering. Listed in README as the manual step
for the next agent with Claude Desktop configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire ngsiem_query_demo to NGSIEMModule._execute_query behind a FALCON_CLIENT_ID env gate. Expose start_time and max_results params. Live-path exceptions and success:false responses degrade to mock with an explicit error note in the text fallback so silent failures and "synthetic fallback didn't engage" confusion are no longer possible. Fix the "fromisoformat: argument must be str" crash: live NGSIEM returns @timestamp as epoch milliseconds (int), which the old _hourly_buckets blindly fed into datetime.fromisoformat. summary now coerces int/float (epoch s or ms, auto-detected), numeric string, or ISO-8601, and skips None/missing/garbage rather than raising — a populated result with one bad row still produces a useful summary. Field extraction for ComputerName/event_simpleName also hardened. Tests cover both: 7 new summary cases for live-shape tolerance and 6 new server cases for the live/mock gate, live failure surfacing, and int-epoch regression. Suite: 49 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior commit gated the live path on FALCON_CLIENT_ID, but that
couples "is live wired up?" to "is that one specific env var set?" —
Claude Desktop users often resolve creds through the credential-file
fallback, where FALCON_CLIENT_ID is intentionally unset.
Use CROWDSTRIKE_PREFAB_LIVE={1,true,yes,on} as an explicit live
toggle. FalconClient continues to resolve creds through its normal
chain (env vars or ~/.config/falcon/credentials.json) independently.
This matches the gate name the local claude_desktop_config.json was
already set up for, so the pilot picks up on the next Claude Desktop
restart without any config changes.
Tests updated to use the new env var name. Suite: 49 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r it The @app.ui() structured_content was being produced via plain layout.model_dump(serialize_as_any=True), which emits the Python field names (css_class, on_mount, data_key, x_axis, ...). Prefab's React renderer expects the camelCase aliases those fields carry (cssClass, onMount, dataKey, xAxis, ...), so every prop came through as an unrecognized key and the renderer hung on "waiting for content" even when the layout was otherwise valid. Switch to layout.to_json() — Prefab's shipped serializer — which does model_dump(by_alias=True, exclude_none=True) and runs _sanitize_floats for NaN/Infinity. This is the same call Prefab's own CLI and standalone renderer use. Lock it in with a test that asserts the serialized structured_content contains no snake_case field names. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9276a72 to
c30c3b6
Compare
The renderer was still hanging on "waiting for content" after the
camelCase alias fix because we were serializing the bare Column tree
ourselves via layout.to_json() and passing the resulting dict as
structured_content. That skipped FastMCP's ToolResult special-case
for Prefab types, which wraps a Component in PrefabApp(view=...) and
emits the {"$prefab": {...}, "view": {...}} envelope the client
actually hydrates against.
Hand the Component instance directly to ToolResult.structured_content
and let FastMCP do the wrapping (tools/base.py:97-102). The resolver
threaded through by _prefab_to_json also rewrites peer-tool references
to the app's hashed form, which the drilldown tool relies on.
Tests updated: structured_content now has the $prefab/view envelope
instead of being a bare Column, which is the whole point of the fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactors detection into a WidgetType enum on QuerySummary so each
result type owns its own layout branch (no scattered is_X booleans).
Adds Metric for count() / single-value, donut PieChart for small-N
label+count aggregates, stacked AreaChart for timechart(by=...), and
ScatterChart for aggregates with one label and >=2 numeric fields.
Fixes a real correctness bug: LogScale ships event field values as
strings ("315841", not 315841). isinstance(v, (int, float)) was
rejecting them, every detection branch fell through to AGGREGATE_TABLE,
and we rendered tables for everything. _is_numeric and _to_number now
accept numeric strings, and the per-widget summarizers coerce values up
front so charts get real numbers (Recharts treats string slice values
as categorical and gives each slice an equal angle).
Wires DataTable.onRowClick to ngsiem_query_drilldown via CallTool with
the clicked row's full dict as $event. Drilldown rewritten to take the
row directly (was replaying a mock seed). The action's tool name is
pre-hashed via hashed_backend_name(app, tool) because FastMCP's auto
resolver only fires when handlers return a bare PrefabApp/Component;
we keep the ToolResult path so the text fallback survives, so we
resolve the wire name ourselves.
77 tests cover widget detection, summarize coercion, layout
composition, the LogScale-string regression shape, and the drilldown
wire name. Requires PREFAB_BUNDLED_RENDERER=1 in the server env so
Metric/PieChart's lazy chunks ship in the self-contained renderer
instead of being blocked by the iframe CSP on jsDelivr fetches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the six decisions from brainstorming (agent-driven call site, summary+ref_id agent payload, separate render tool with shared engine, inline-row drilldown carrier, FastMCPApp adoption, sibling render module) plus risks/mitigations including the FastMCPApp drop-in spike. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19 bite-sized tasks: spike, NGSIEMModule public-API rename, package migration (summary/fallback/layout/mock_data), NGSIEMRenderModule scaffolding + tool implementations, FastMCPApp swap, mock-mode env flag, test migration, scaffolding deletion, pyproject/README updates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 1 spike rejected the direct FastMCPApp swap (it's a Provider, not a server) and verified fastmcp.FastMCP v2 as the correct migration target. Spec D5 and R1, plus plan tasks 1, 8, 11, are revised: - Main server class moves from mcp.server.fastmcp.FastMCP to fastmcp.FastMCP v2 (different package, has add_provider). - NGSIEMRenderModule constructs its own FastMCPApp internally and mounts it via server.add_provider(app) in register_tools. - HTTP transport surface changes: sse_app()/streamable_http_app() → http_app(transport=...). Single call site update in server.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment claimed the method was "internal" and "also called by AlertsModule" — post-rename neither is true. AlertsModule has its own _execute_ngsiem_query and the method is now public for cross-module reuse. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 11 swapped the runtime FastMCP class but left 16 modules' type-hint imports pointing at the old stdlib mcp.server.fastmcp.FastMCP. These silently typed the wrong class for static checkers — same name, different package. Mechanical 1-line-per-file rewrite. Tests still 481 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation is complete
The prefab-pilot extra now installs the integrated NGSIEMRenderModule's runtime dependency (fastmcp[apps]); rename to prefab-render to match the new module path (modules/ngsiem_render). Also drop the crowdstrike-prefab-pilot console script — its target module was deleted in the previous commit so the entry was dangling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the new ngsiem_query_render / ngsiem_query_drilldown UI tools, the [prefab-render] install extra, and the CROWDSTRIKE_RENDER_MOCK dev-mode env flag. Pilot references were already absent from the README (the pilot's standalone server was undocumented at the top level), so no removals are needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…efactor TODO Reviewer flagged D1 (agent picks correctly between ngsiem_query and ngsiem_query_render) hinged on the descriptions being unambiguous — docstring fallbacks weren't pulling enough weight. Explicit description= strings now spell out: "use this when the user wants to see results, NOT when you need the data for reasoning." Also adds a TODO on the internal NGSIEMModule instance so the next contributor sees the shared-engine refactor path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gram 15 modules · 79 tools after the ngsiem_render integration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ngsiem_query_render builds its text fallback inline (text_lines = [...]) rather than calling summary_to_text() from fallback.py. The module had zero callers and zero tests after the integration. Deleting rather than wiring into _module.py — the inline build is idiomatic for this single call site and avoids carrying a parallel formatter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per spec D5: zero callers in the codebase, the modules that register resources use the server.resource() decorator directly. Also incompatible with fastmcp v2's add_resource() signature, so removing keeps BaseModule honest about what it actually supports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rom pilot
The deleted prefab_pilot/test_server.py held two assertions worth keeping
that aren't otherwise covered:
1. structured_content must use camelCase aliases (cssClass, dataKey, ...)
not snake_case Python field names — Prefab's React renderer silently
drops snake_case keys, surfacing as "waiting for content" in CD.
2. Live NGSIEM ships @timestamp as int epoch ms; the layout/summary
pipeline must not raise "fromisoformat: argument must be str".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lity
CI runs the base install without the optional [prefab-render] extra
(which pulls fastmcp[apps] → prefab-ui transitively). The integration
work assumed both were always present. Three fixes:
1. tests/modules/ngsiem_render/test_layout.py and test_module.py now use
pytest.importorskip("prefab_ui") at module load. Tests that exercise
layout/module code skip cleanly on the base install instead of
exploding at collection on `from prefab_ui.components import (...)`.
2. tests/test_smoke_tools_list.py drops ngsiem_query_render and
ngsiem_query_drilldown from the unconditional EXPECTED_READ_TOOLS set
and re-adds them only when prefab_ui is importable. Without this,
readonly/write-mode smoke tests asserted the render tools were
registered while RENDER_AVAILABLE=False kept the module out.
3. layout.py: removed unused `x_label` variable (F841 lint). Ruff also
auto-fixed unused SendMessage import (F401) and import ordering
(I001) across layout.py, test_module.py, test_smoke_tools_list.py.
Local: 457 passed with prefab_ui installed; lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run ruff format across the new package and migrated test files. CI workflow runs `ruff format --check` separately from `ruff check`; the formatter rewrote 8 files (no behavior change — whitespace, line breaks, dict/list trailing commas). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accidentally swept into the previous format commit via git add -A. Local Claude Code permission/settings file — per-developer, not project state. Gitignore so it stops following any contributor's checkout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/dev_render.py exposes NGSIEMRenderModule's FastMCPApp as `app` so `fastmcp dev apps scripts/dev_render.py` can spin up the browser preview at localhost:8080 with a tool picker, hot reload, and DevTools access. Forces CROWDSTRIKE_RENDER_MOCK=1 so no NG-SIEM creds are needed; deterministic mock events drive the renders. Replaces the Claude Desktop restart-and-test loop for iterating on layouts and (more importantly) for diagnosing the open row-click bug — DevTools network tab will show whether the CallTool action fires and what wire format it sends. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fastmcp's loader auto-discovers top-level `mcp`/`server`/`app` of type FastMCP via isinstance check (filesystem.py:157). A bare FastMCPApp doesn't match — discovery fails with "No server object found" and the dev preview can't start. Wrap FastMCPApp in a FastMCP server via add_provider — exactly what production does. Local smoke confirms `server` is a FastMCP instance named crowdstrike-render-dev with the render module's FastMCPApp mounted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original drilldown (CallTool that echoed the row back) was a no-op
visually — Claude Desktop ran the call but had nowhere to display the
result. Replace with ExpandableRow: each row's chevron toggle reveals a
detail panel containing:
- Card showing every row field (key/value pairs)
- Custom-prompt Form (Textarea + Send) — analyst types a free-form
question; the row JSON is appended as context and the agent decides
which tools to run
- Action buttons:
• Ask Claude about this event → SendMessage with row JSON
• Correlate ▾ Popover → User / Endpoint / SaaS / Network / IOC
sub-prompts (each conditional on relevant fields being present)
• Pivot ▾ Popover → Same host / Same user / Same event_simpleName /
Time window ±5min (each conditional)
• Open in Falcon → OpenLink to host page (URL stubbed; TODO
annotation for the customer's tenant pattern)
All actions are framed as natural-language SendMessages — no
predefined CQL. The agent receives the row context and chooses which
tools to invoke.
ngsiem_query_drilldown removed entirely (tool method, registration,
layout's _DRILLDOWN_BACKEND_NAME constant, hashed_backend_name import).
The drilldown wire-format hash machinery is no longer relevant.
Tests:
- test_module.py: drop drilldown tests, rename "two tools" assertion
to "render tool" (drilldown is no longer registered)
- test_layout.py: drop drilldown wire-format test, add ExpandableRow
shape tests + detail-panel content tests + conditional-popover test
- test_smoke_tools_list.py: drop ngsiem_query_drilldown from the
expected read set
- tests adjusted for ExpandableRow .data accessor (rows are no
longer plain dicts)
Local: 457 passed, lint + format clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft. Prototype only. Do not merge without manual host-render verification.
Implements the NGSIEM query pilot from the feasibility study in #18
(spec).
What this is
An additive experimental surface — separate package, separate optional
dep, separate entry point. Nothing in the main
server.pyor any existingmodule is touched. Deleting
src/crowdstrike_mcp/prefab_pilot/would leaveproduction unchanged.
What it does
Exposes a minimal FastMCPApp (PrefectHQ
fastmcp[apps]3.2+) server withtwo tools:
ngsiem_query_demo—@app.ui()entry point. Runs a synthetic NGSIEMquery and returns a Prefab
Columncontaining a heading, summaryCardwith badges (row count, top host, top event_simpleName, time range),
BarChartof hourly event counts, and a searchable/paginatedDataTableof events. Wrapped in a
ToolResultso Claude Code (no MCP Apps render)gets a self-contained text fallback instead of
[Rendered Prefab UI].ngsiem_query_drilldown—@app.tool()backend tool for the UI to callwhen the user clicks a row. Returns full JSON for a single event.
Uses synthetic data from
mock_data.generate_process_events— runs withno Falcon credentials. The README lists exactly where to swap the mock for
live FalconPy calls (single line in
server.py).Context-window arbitrage demonstration
The point of FastMCPApp isn't pretty charts — it's that the bulk event data
goes to
structured_content(the UI) while the model only sees the compactQuerySummaryin the text fallback. For a 100-event result, the model sees~5 lines of text instead of ~100 rows. For analyst sessions that's
materially less context pressure. See feasibility doc §7 for the full
argument.
Files
src/crowdstrike_mcp/prefab_pilot/mock_data.pysrc/crowdstrike_mcp/prefab_pilot/summary.pyQuerySummaryreducer — the model-facing payloadsrc/crowdstrike_mcp/prefab_pilot/fallback.pysrc/crowdstrike_mcp/prefab_pilot/layout.pysrc/crowdstrike_mcp/prefab_pilot/server.pyFastMCPAppwith@app.ui+@app.toolsrc/crowdstrike_mcp/prefab_pilot/README.mdtests/prefab_pilot/Tests
36 new unit tests pass alongside the existing 396 (432 total, 0 failing).
Ruff clean.
test_mock_data.py— 7 tests verifying deterministic seeding, shape, field coveragetest_summary.py— 7 tests for the reducer (edge cases: empty, top-K, time range, hourly buckets, immutability)test_fallback.py— 6 tests ensuring the text summary is self-contained (no "see the chart above" phrasing) and contains row count / top host / time rangetest_layout.py— 9 smoke tests verifying the Prefab component tree structure and polymorphic serializationtest_server.py— 7 tests exercising registered tools viaapp.list_tools()+tool.run()(no stdio transport needed)Not tested — explicitly flagged in the README: actual host-side
rendering. Cannot be verified without Claude Desktop + a live config.
Manual verification checklist (for the follow-up agent)
See
src/crowdstrike_mcp/prefab_pilot/README.mdfor the full list. Thecritical checks:
crowdstrike-prefab-pilot(verified locally — boots clean)ngsiem_query_demorenders a visible layout with heading, summary card, BarChart, and DataTable[Rendered Prefab UI]ngsiem_query_drilldownreturns a JSON row cleanlyrow_indexreturns a clean error, not a tracebackHow to install and run locally
Claude Desktop config snippet is in the README.
Known limitations
middleware is not ported; that remains the open question from
feasibility doc §5.6.
DataTablecolumns are static — adapting them to query projections isfuture work.
DataTable.onRowClickis not yet wired tongsiem_query_drilldown—the backend tool is registered but the UI action binding is a one-liner
follow-up once
prefab_ui.actionssemantics are confirmed.fastmcp[apps]is ~10 MB — acceptable for an opt-in extra.Relationship to PR #18
PR #18 is the research/design doc (merged-mergeable on its own).
This PR is the executable prototype that validates the migration path.
Both live on separate branches to keep reviewer load small.
🤖 Generated with Claude Code