Skip to content

Commit 06da7a6

Browse files
docs(modules): correct hosted MCP notes and guard stale note keys
AgentWorks is available on the hosted Falcon MCP, so drop its self-hosted-only note and remove it from the overview summary. Reword the discovery paragraph. The previous text said this server needs no discovery step, which holds only by default: dynamic mode uses the same pattern through falcon_search_tools, falcon_execute_tool, and falcon_list_enabled_tools. Link that page and call out the falcon_ prefix so the two tool sets are not conflated. Say "not available" rather than "not yet available" for the cloud insights tools, matching their per-tool notes, link the module names in the summary, and resolve module URLs through a helper so a slug change cannot break them. Add validate_hosted_mcp_notes. Both note dicts are keyed by name, so a rename silently dropped the note and the docs freshness check could not catch it, since regenerating drops the note from the committed page as well. The generator now fails instead, with tests covering a stale module key, a stale tool key, and a note missing from the summary.
1 parent ae936e8 commit 06da7a6

4 files changed

Lines changed: 148 additions & 22 deletions

File tree

docs/modules/agentworks.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66

77
Calling, listing, and observing CrowdStrike AgentWorks (agentic-studio) Charlotte AI agents and their execution traces
88

9-
> [!NOTE]
10-
> This module is not available on CrowdStrike's hosted Falcon MCP; it is only available when self-hosting this server. See [module overview](/falcon-mcp/modules/overview/#crowdstrike-hosted-mcp-differences).
11-
129
## API Scopes
1310

1411
- `Charlotte AI Agent Definition:read`

docs/modules/overview.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ The Falcon MCP Server provides the following modules. Each module requires speci
3939
## CrowdStrike-hosted MCP differences
4040

4141
> [!NOTE]
42-
> This section compares this self-hosted server against CrowdStrike's hosted Falcon MCP. It does not apply if you're only running this server yourself.
42+
> This section compares this self-hosted server against CrowdStrike's hosted Falcon MCP. Skip it unless you also use the hosted MCP, or are moving between the two.
4343
44-
The hosted Falcon MCP does not register each `falcon_*` tool directly. Instead it exposes two tools, `search_tools` and `execute_tool`: a client calls `search_tools` to find the right Falcon tool by name or keyword, then `execute_tool` to invoke it by name with arguments. This server registers every `falcon_*` tool (and its `falcon://` resources) directly, so no discovery step is needed.
44+
The two servers differ in how a client reaches a tool. The hosted Falcon MCP works through discovery: a client calls `search_tools` to find a Falcon tool by name or keyword, then `execute_tool` to run it with arguments. This server registers each `falcon_*` tool up front instead, so a client calls one by name with no discovery round-trip.
45+
46+
If you self-host and want the same discovery pattern, enable [dynamic mode](/falcon-mcp/usage/dynamic-mode/): it swaps the full tool surface for `falcon_search_tools`, `falcon_execute_tool`, and an always-on `falcon_list_enabled_tools` inventory. Mind the `falcon_` prefix — those three are this server's tools, not the hosted MCP's.
4547

4648
Module and tool coverage also differs:
4749

48-
- **AgentWorks**, **Fusion SOAR**, and **Zero Trust Assessment** are available only on this self-hosted server; the hosted MCP has no equivalent modules.
49-
- [Cloud Security](/falcon-mcp/modules/cloud/): `falcon_search_cloud_insights`, `falcon_list_cloud_insight_definitions`, and `falcon_get_cloud_asset_insights` are not yet available on the hosted MCP.
50+
- [Fusion SOAR](/falcon-mcp/modules/fusion/) and [Zero Trust Assessment](/falcon-mcp/modules/zero-trust-assessment/) are available only on this self-hosted server; the hosted MCP has no equivalent modules.
51+
- [Cloud Security](/falcon-mcp/modules/cloud/): `falcon_search_cloud_insights`, `falcon_list_cloud_insight_definitions`, and `falcon_get_cloud_asset_insights` are not available on the hosted MCP.
5052
- [Discover](/falcon-mcp/modules/discover/): `falcon_search_managed_assets` is not available on the hosted MCP.
5153
- [Policies](/falcon-mcp/modules/policies/): the hosted MCP does not use the unified `policy_type`-discriminated tools. It instead exposes six policy-type-specific variants of each tool (for example `falcon_search_policies_firewall`, `falcon_create_policy_prevention`).

scripts/generate_module_docs.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,17 @@
7777

7878
_OVERVIEW_LINK = f"{SITE_BASE_PATH}/modules/overview/#crowdstrike-hosted-mcp-differences"
7979

80+
81+
def _module_link(module_key: str) -> str:
82+
"""Site URL for a module page, honoring any slug override in MODULE_METADATA."""
83+
slug = MODULE_METADATA.get(module_key, {}).get("slug", module_key)
84+
return f"{SITE_BASE_PATH}/modules/{slug}/"
85+
86+
8087
# Notes on differences from CrowdStrike's hosted Falcon MCP, rendered as an
8188
# admonition under the module description. Keyed by module_key. See
8289
# generate_overview_page for the summary of these differences.
8390
HOSTED_MCP_MODULE_NOTES: dict[str, str] = {
84-
"agentworks": (
85-
"This module is not available on CrowdStrike's hosted Falcon MCP; it is only "
86-
f"available when self-hosting this server. See [module overview]({_OVERVIEW_LINK})."
87-
),
8891
"fusion": (
8992
"This module is not available on CrowdStrike's hosted Falcon MCP; it is only "
9093
f"available when self-hosting this server. See [module overview]({_OVERVIEW_LINK})."
@@ -1432,34 +1435,43 @@ def generate_overview_page(modules: dict[str, dict[str, Any]]) -> str:
14321435
lines.append("> [!NOTE]")
14331436
lines.append(
14341437
"> This section compares this self-hosted server against CrowdStrike's hosted "
1435-
"Falcon MCP. It does not apply if you're only running this server yourself."
1438+
"Falcon MCP. Skip it unless you also use the hosted MCP, or are moving between the two."
14361439
)
14371440
lines.append("")
14381441
lines.append(
1439-
"The hosted Falcon MCP does not register each `falcon_*` tool directly. Instead it "
1440-
"exposes two tools, `search_tools` and `execute_tool`: a client calls `search_tools` "
1441-
"to find the right Falcon tool by name or keyword, then `execute_tool` to invoke it by "
1442-
"name with arguments. This server registers every `falcon_*` tool (and its `falcon://` "
1443-
"resources) directly, so no discovery step is needed."
1442+
"The two servers differ in how a client reaches a tool. The hosted Falcon MCP works "
1443+
"through discovery: a client calls `search_tools` to find a Falcon tool by name or "
1444+
"keyword, then `execute_tool` to run it with arguments. This server registers each "
1445+
"`falcon_*` tool up front instead, so a client calls one by name with no discovery "
1446+
"round-trip."
1447+
)
1448+
lines.append("")
1449+
lines.append(
1450+
"If you self-host and want the same discovery pattern, enable "
1451+
f"[dynamic mode]({SITE_BASE_PATH}/usage/dynamic-mode/): it swaps the full tool surface "
1452+
"for `falcon_search_tools`, `falcon_execute_tool`, and an always-on "
1453+
"`falcon_list_enabled_tools` inventory. Mind the `falcon_` prefix — those three are "
1454+
"this server's tools, not the hosted MCP's."
14441455
)
14451456
lines.append("")
14461457
lines.append("Module and tool coverage also differs:")
14471458
lines.append("")
14481459
lines.append(
1449-
"- **AgentWorks**, **Fusion SOAR**, and **Zero Trust Assessment** are available only "
1460+
f"- [Fusion SOAR]({_module_link('fusion')}) and "
1461+
f"[Zero Trust Assessment]({_module_link('zerotrustassessment')}) are available only "
14501462
"on this self-hosted server; the hosted MCP has no equivalent modules."
14511463
)
14521464
lines.append(
1453-
f"- [Cloud Security]({SITE_BASE_PATH}/modules/cloud/): `falcon_search_cloud_insights`, "
1465+
f"- [Cloud Security]({_module_link('cloud')}): `falcon_search_cloud_insights`, "
14541466
"`falcon_list_cloud_insight_definitions`, and `falcon_get_cloud_asset_insights` are not "
1455-
"yet available on the hosted MCP."
1467+
"available on the hosted MCP."
14561468
)
14571469
lines.append(
1458-
f"- [Discover]({SITE_BASE_PATH}/modules/discover/): `falcon_search_managed_assets` is "
1470+
f"- [Discover]({_module_link('discover')}): `falcon_search_managed_assets` is "
14591471
"not available on the hosted MCP."
14601472
)
14611473
lines.append(
1462-
f"- [Policies]({SITE_BASE_PATH}/modules/policies/): the hosted MCP does not use the "
1474+
f"- [Policies]({_module_link('policies')}): the hosted MCP does not use the "
14631475
"unified `policy_type`-discriminated tools. It instead exposes six policy-type-specific "
14641476
"variants of each tool (for example `falcon_search_policies_firewall`, "
14651477
"`falcon_create_policy_prevention`)."
@@ -1468,13 +1480,47 @@ def generate_overview_page(modules: dict[str, dict[str, Any]]) -> str:
14681480
return "\n".join(lines)
14691481

14701482

1483+
def validate_hosted_mcp_notes(modules: dict[str, dict[str, Any]]) -> None:
1484+
"""Fail loudly when a hosted-MCP note key matches no module or no registered tool.
1485+
1486+
Both note dicts are keyed by name, so a module or tool rename silently drops the
1487+
note: the page regenerates without it, the committed docs match, and the docs
1488+
freshness check passes. Raise here instead so a rename is caught at generation time.
1489+
"""
1490+
stale_modules = sorted(set(HOSTED_MCP_MODULE_NOTES) - set(modules))
1491+
1492+
known_tools = {
1493+
f"falcon_{registered}"
1494+
for mod_info in modules.values()
1495+
for registered in extract_registered_tool_names(mod_info["cls"]).values()
1496+
}
1497+
stale_tools = sorted(set(HOSTED_MCP_TOOL_NOTES) - known_tools)
1498+
1499+
problems = []
1500+
if stale_modules:
1501+
problems.append(
1502+
f"HOSTED_MCP_MODULE_NOTES keys match no discovered module: {', '.join(stale_modules)}"
1503+
)
1504+
if stale_tools:
1505+
problems.append(
1506+
f"HOSTED_MCP_TOOL_NOTES keys match no registered tool: {', '.join(stale_tools)}"
1507+
)
1508+
if problems:
1509+
raise ValueError(
1510+
"Stale hosted-MCP note keys in scripts/generate_module_docs.py. "
1511+
"Update or remove them after a rename:\n " + "\n ".join(problems)
1512+
)
1513+
1514+
14711515
def main() -> None:
14721516
"""Generate all module documentation pages."""
14731517
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
14741518

14751519
modules = discover_module_classes()
14761520
print(f"Discovered {len(modules)} modules: {', '.join(sorted(modules.keys()))}")
14771521

1522+
validate_hosted_mcp_notes(modules)
1523+
14781524
# Generate overview page
14791525
overview = generate_overview_page(modules)
14801526
(OUTPUT_DIR / "overview.md").write_text(overview)

tests/test_generate_module_docs.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
generate_module_page,
3030
generate_overview_page,
3131
main,
32+
validate_hosted_mcp_notes,
3233
)
3334

3435
# ---------------------------------------------------------------------------
@@ -1083,5 +1084,85 @@ def test_main_removes_stale_files(self):
10831084
self.assertFalse(stale.exists(), "Stale file should have been removed by main()")
10841085

10851086

1087+
# ---------------------------------------------------------------------------
1088+
# TestHostedMcpNotes — note keys must track real module and tool names
1089+
# ---------------------------------------------------------------------------
1090+
1091+
class TestHostedMcpNotes(unittest.TestCase):
1092+
"""Both hosted-MCP note dicts are keyed by name, so a rename silently drops the note.
1093+
1094+
The docs freshness check cannot catch that: regenerating after a rename removes the
1095+
note from the committed page too, so committed and generated agree. These tests are
1096+
the guard instead, mirroring the bidirectional coverage test for filter hints.
1097+
"""
1098+
1099+
@classmethod
1100+
def setUpClass(cls):
1101+
cls.modules = discover_module_classes()
1102+
cls.tool_names = {
1103+
f"falcon_{registered}"
1104+
for mod_info in cls.modules.values()
1105+
for registered in extract_registered_tool_names(mod_info["cls"]).values()
1106+
}
1107+
1108+
def test_module_note_keys_are_real_modules(self):
1109+
from scripts.generate_module_docs import HOSTED_MCP_MODULE_NOTES
1110+
1111+
for key in HOSTED_MCP_MODULE_NOTES:
1112+
self.assertIn(key, self.modules, f"HOSTED_MCP_MODULE_NOTES key {key!r} is not a module")
1113+
1114+
def test_tool_note_keys_are_registered_tools(self):
1115+
from scripts.generate_module_docs import HOSTED_MCP_TOOL_NOTES
1116+
1117+
for name in HOSTED_MCP_TOOL_NOTES:
1118+
self.assertIn(
1119+
name, self.tool_names, f"HOSTED_MCP_TOOL_NOTES key {name!r} is not a registered tool"
1120+
)
1121+
1122+
def test_live_registry_passes_validation(self):
1123+
validate_hosted_mcp_notes(self.modules) # must not raise
1124+
1125+
def test_stale_module_key_raises(self):
1126+
import scripts.generate_module_docs as _gmd
1127+
1128+
with patch.dict(_gmd.HOSTED_MCP_MODULE_NOTES, {"zero_trust_assessment": "x"}, clear=True):
1129+
with self.assertRaises(ValueError) as ctx:
1130+
validate_hosted_mcp_notes(self.modules)
1131+
self.assertIn("zero_trust_assessment", str(ctx.exception))
1132+
1133+
def test_stale_tool_key_raises(self):
1134+
import scripts.generate_module_docs as _gmd
1135+
1136+
with patch.dict(_gmd.HOSTED_MCP_TOOL_NOTES, {"falcon_search_cloud_insight": "x"}, clear=True):
1137+
with self.assertRaises(ValueError) as ctx:
1138+
validate_hosted_mcp_notes(self.modules)
1139+
self.assertIn("falcon_search_cloud_insight", str(ctx.exception))
1140+
1141+
def test_overview_mentions_every_noted_module_and_tool(self):
1142+
"""The overview summary is hand-written prose; keep it in step with the note dicts."""
1143+
from scripts.generate_module_docs import (
1144+
HOSTED_MCP_MODULE_NOTES,
1145+
HOSTED_MCP_TOOL_NOTES,
1146+
MODULE_METADATA,
1147+
)
1148+
1149+
page = generate_overview_page(self.modules)
1150+
section = page.split("## CrowdStrike-hosted MCP differences", 1)[1]
1151+
1152+
for key in HOSTED_MCP_MODULE_NOTES:
1153+
slug = MODULE_METADATA.get(key, {}).get("slug", key)
1154+
self.assertIn(
1155+
f"/modules/{slug}/",
1156+
section,
1157+
f"Module {key!r} has a hosted-MCP note but is not linked in the overview summary",
1158+
)
1159+
for name in HOSTED_MCP_TOOL_NOTES:
1160+
self.assertIn(
1161+
name,
1162+
section,
1163+
f"Tool {name!r} has a hosted-MCP note but is not named in the overview summary",
1164+
)
1165+
1166+
10861167
if __name__ == "__main__":
10871168
unittest.main()

0 commit comments

Comments
 (0)