Skip to content

Commit 2cf67ad

Browse files
committed
Enable MCP for all non-WebSocket API endpoints
Add mcp=True to 38 remaining endpoints across 10 modules. Only WebSocket endpoints (agent dock, event/activity tail, event ingest) are excluded as they use a different protocol incompatible with MCP. Newly enabled endpoints by module: - agents: create_agent, delete_agent, get_agent, get_scan_status - activity: list_activities, query_activities, count_activities - assets: query_assets, count_assets - emails: get_emails - events: insert_event, get_event, list_events, query_events, count_events, archive_old_events - findings: query_findings, count_findings, set_risk - presets: create_preset, update_preset, delete_preset - scans: get_scans, query_scans, count_scans, cancel_scan - targets: count_targets, set_default_target, create_target, update_target, copy_target, delete_target, is_blacklisted, query_targets, list_ids - technologies: list_technologies, query_technologies, count_technologies Total MCP coverage: 69/73 (94.5%)
1 parent 2d63c2e commit 2cf67ad

13 files changed

Lines changed: 56 additions & 52 deletions

File tree

bbot_server/modules/activity/activity_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async def handle_activity(self, activity: Activity, asset: Asset = None):
1616
await self.collection.insert_one(activity.model_dump())
1717

1818
@api_endpoint(
19-
"/list", methods=["GET"], type="http_stream", response_model=Activity, summary="Stream all activities"
19+
"/list", methods=["GET"], type="http_stream", response_model=Activity, summary="Stream all activities", mcp=True
2020
)
2121
async def list_activities(self, host: str = None, type: str = None):
2222
query = {}
@@ -27,15 +27,15 @@ async def list_activities(self, host: str = None, type: str = None):
2727
async for activity in self.collection.find(query, sort=[("timestamp", 1), ("created", 1)]):
2828
yield self.model(**activity)
2929

30-
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="List activities")
30+
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="List activities", mcp=True)
3131
async def query_activities(self, query: ActivityQuery):
3232
"""
3333
Advanced querying of activities. Choose your own filters and fields.
3434
"""
3535
async for activity in query.mongo_iter(self):
3636
yield activity
3737

38-
@api_endpoint("/count", methods=["POST"], summary="Count activities")
38+
@api_endpoint("/count", methods=["POST"], summary="Count activities", mcp=True)
3939
async def count_activities(self, query: ActivityQuery) -> int:
4040
"""
4141
Same as query_activities, except only returns the count

bbot_server/modules/agents/agents_api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ async def get_agents(self) -> list[Agent]:
4343
agents.append(agent)
4444
return agents
4545

46-
@api_endpoint("/", methods=["POST"], summary="Create an agent")
46+
@api_endpoint("/", methods=["POST"], summary="Create an agent", mcp=True)
4747
async def create_agent(self, name: str, description: str = "") -> Agent:
4848
agent = Agent(name=name, description=description)
4949
try:
@@ -52,12 +52,12 @@ async def create_agent(self, name: str, description: str = "") -> Agent:
5252
raise self.BBOTServerError(f"Error creating agent {name}: {e}") from e
5353
return agent
5454

55-
@api_endpoint("/", methods=["DELETE"], summary="Delete an agent")
55+
@api_endpoint("/", methods=["DELETE"], summary="Delete an agent", mcp=True)
5656
async def delete_agent(self, id: str):
5757
agent = await self.get_agent(id)
5858
await self.collection.delete_one({"id": str(agent.id)})
5959

60-
@api_endpoint("/", methods=["GET"], summary="Get an agent by its id")
60+
@api_endpoint("/", methods=["GET"], summary="Get an agent by its id", mcp=True)
6161
async def get_agent(self, id: str) -> Agent:
6262
try:
6363
query = {"id": str(UUID(str(id)))}
@@ -92,7 +92,7 @@ async def get_agent_status(
9292
agent_status = {"agent_status": "OFFLINE", "scan_status": "UNKNOWN"}
9393
return agent_status
9494

95-
@api_endpoint("/scan_status", methods=["GET"], summary="Get the status of an agent's scan")
95+
@api_endpoint("/scan_status", methods=["GET"], summary="Get the status of an agent's scan", mcp=True)
9696
async def get_scan_status(self, id: UUID, detailed: bool = False) -> dict[str, str]:
9797
command_response = await self.connection_manager.execute_command(
9898
str(id), "get_scan_status", timeout=10, detailed=detailed

bbot_server/modules/assets/assets_api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class AssetsApplet(BaseApplet):
1313

1414
model = Asset
1515

16-
@api_endpoint("/list", methods=["GET"], type="http_stream", response_model=Asset, summary="Stream all assets")
16+
@api_endpoint("/list", methods=["GET"], type="http_stream", response_model=Asset, summary="Stream all assets", mcp=True)
1717
async def list_assets(
1818
self,
1919
domain: Annotated[str, Query(description="Filter assets by domain or subdomain")] = None,
@@ -27,22 +27,22 @@ async def list_assets(
2727
async for asset in query.mongo_iter(self):
2828
yield self.model(**asset)
2929

30-
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query assets")
30+
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query assets", mcp=True)
3131
async def query_assets(self, query: AdvancedAssetQuery | None = None):
3232
"""
3333
Advanced querying of assets. Choose your own filters and fields.
3434
"""
3535
async for asset in query.mongo_iter(self):
3636
yield asset
3737

38-
@api_endpoint("/count", methods=["POST"], summary="Count assets")
38+
@api_endpoint("/count", methods=["POST"], summary="Count assets", mcp=True)
3939
async def count_assets(self, query: AdvancedAssetQuery | None = None) -> int:
4040
"""
4141
Same as query_assets, except only returns the count
4242
"""
4343
return await query.mongo_count(self)
4444

45-
@api_endpoint("/{host}/detail", methods=["GET"], summary="Get a single asset by its host")
45+
@api_endpoint("/{host}/detail", methods=["GET"], summary="Get a single asset by its host", mcp=True)
4646
async def get_asset(self, host: Annotated[str, Path(description="The host of the asset to get")]) -> Asset:
4747
asset = await self.collection.find_one({"host": host})
4848
if not asset:
@@ -63,7 +63,7 @@ async def get_asset_history(self, host: str) -> list[str]:
6363
history.append(activity["description"])
6464
return history
6565

66-
@api_endpoint("/hosts", methods=["GET"], summary="List hosts")
66+
@api_endpoint("/hosts", methods=["GET"], summary="List hosts", mcp=True)
6767
async def get_hosts(self, domain: str = None, target_id: str = None) -> list[str]:
6868
"""
6969
List all hosts.

bbot_server/modules/cloud/cloud_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ async def get_cloud_providers_for_asset(self, host: str) -> list[dict[str, str]]
4747
"/check/{host}",
4848
methods=["GET"],
4949
summary="Check a hostname or IP address against the cloud provider database",
50+
mcp=True,
5051
)
5152
async def cloudcheck(self, host: str) -> list[dict[str, str]]:
5253
# cloudcheck v9+ uses async API and handles caching internally

bbot_server/modules/emails/emails_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class EmailsApplet(BaseApplet):
1313
class AssetFields(BaseModel):
1414
emails: list[str] = Field(default_factory=list)
1515

16-
@api_endpoint("/emails/{domain}", methods=["GET"], summary="Get emails by domain")
16+
@api_endpoint("/emails/{domain}", methods=["GET"], summary="Get emails by domain", mcp=True)
1717
async def get_emails(self, domain: str) -> list[str]:
1818
matching_assets = await self.root.assets.list_assets(host=domain)
1919
emails = set()

bbot_server/modules/events/events_api.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async def handle_event(self, event: Event, asset):
2323
# write the event to the database
2424
await self.collection.insert_one(event.model_dump())
2525

26-
@api_endpoint("/", methods=["POST"], summary="Insert a BBOT event into the asset database")
26+
@api_endpoint("/", methods=["POST"], summary="Insert a BBOT event into the asset database", mcp=True)
2727
async def insert_event(self, event: Event):
2828
"""
2929
Insert a BBOT event into the asset database
@@ -32,14 +32,14 @@ async def insert_event(self, event: Event):
3232
# it will be picked up by the worker and ingested
3333
await self.root.message_queue.publish_event(event)
3434

35-
@api_endpoint("/get/{uuid}", methods=["GET"], summary="Get an event by its UUID")
35+
@api_endpoint("/get/{uuid}", methods=["GET"], summary="Get an event by its UUID", mcp=True)
3636
async def get_event(self, uuid: str) -> Event:
3737
event = await self.collection.find_one({"uuid": uuid})
3838
if event is None:
3939
raise self.BBOTServerNotFoundError(f"Event {uuid} not found")
4040
return Event(**event)
4141

42-
@api_endpoint("/list", methods=["GET"], type="http_stream", response_model=Event, summary="Stream all events")
42+
@api_endpoint("/list", methods=["GET"], type="http_stream", response_model=Event, summary="Stream all events", mcp=True)
4343
async def list_events(
4444
self,
4545
type: str = None,
@@ -64,15 +64,15 @@ async def list_events(
6464
async for event in query.mongo_iter(self):
6565
yield Event(**event)
6666

67-
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query events")
67+
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query events", mcp=True)
6868
async def query_events(self, query: EventsQuery | None = None):
6969
"""
7070
Advanced querying of events. Choose your own filters and fields.
7171
"""
7272
async for event in query.mongo_iter(self):
7373
yield event
7474

75-
@api_endpoint("/count", methods=["POST"], summary="Count events")
75+
@api_endpoint("/count", methods=["POST"], summary="Count events", mcp=True)
7676
async def count_events(self, query: EventsQuery | None = None) -> int:
7777
"""
7878
Same as query_events, except only returns the count
@@ -84,7 +84,7 @@ async def tail_events(self, n: int = 0):
8484
async for event in self.message_queue.tail_events(n=n):
8585
yield event
8686

87-
@api_endpoint("/archive", methods=["POST"], summary="Archive old events")
87+
@api_endpoint("/archive", methods=["POST"], summary="Archive old events", mcp=True)
8888
async def archive_old_events(
8989
self,
9090
older_than: Annotated[int, Query(description="Archive events older than this many days")],

bbot_server/modules/findings/findings_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ async def list_findings(
8484
async for finding in query.mongo_iter(self):
8585
yield Finding(**finding)
8686

87-
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query findings")
87+
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query findings", mcp=True)
8888
async def query_findings(self, query: FindingsQuery | None = None):
8989
"""
9090
Advanced querying of findings. Choose your own filters and fields.
9191
"""
9292
async for finding in query.mongo_iter(self):
9393
yield finding
9494

95-
@api_endpoint("/count", methods=["POST"], summary="Count findings")
95+
@api_endpoint("/count", methods=["POST"], summary="Count findings", mcp=True)
9696
async def count_findings(self, query: FindingsQuery | None = None) -> int:
9797
"""
9898
Same as query_findings, except only returns the count
@@ -152,7 +152,7 @@ async def severity_counts(
152152
findings = dict(sorted(findings.items(), key=lambda x: x[1], reverse=True))
153153
return findings
154154

155-
@api_endpoint("/set_risk", methods=["PATCH"], summary="Set or clear a manual risk score for an asset")
155+
@api_endpoint("/set_risk", methods=["PATCH"], summary="Set or clear a manual risk score for an asset", mcp=True)
156156
async def set_risk(
157157
self,
158158
host: Annotated[str, Query(description="The host of the asset to update")],

bbot_server/modules/open_ports/open_ports_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async def compute_stats(self, asset, statistics):
4747
open_ports_stats = dict(sorted(open_ports_stats.items(), key=lambda x: x[1], reverse=True))
4848
statistics["open_ports"] = open_ports_stats
4949

50-
@api_endpoint("/list", methods=["GET"], summary="Get all the open ports for all hosts")
50+
@api_endpoint("/list", methods=["GET"], summary="Get all the open ports for all hosts", mcp=True)
5151
async def get_open_ports(self, domain: str = None, target_id: str = None) -> dict[str, list[int]]:
5252
open_ports = {}
5353
query = AssetQuery(
@@ -60,7 +60,7 @@ async def get_open_ports(self, domain: str = None, target_id: str = None) -> dic
6060
open_ports[asset["host"]] = asset["open_ports"]
6161
return open_ports
6262

63-
@api_endpoint("/list/{host}", methods=["GET"], summary="Get all the open ports for a host")
63+
@api_endpoint("/list/{host}", methods=["GET"], summary="Get all the open ports for a host", mcp=True)
6464
async def get_open_ports_by_host(self, host: str) -> list[int]:
6565
asset = await self.collection.find_one({"host": str(host), "type": "Asset"}, {"open_ports": 1})
6666
if asset is None:

bbot_server/modules/presets/presets_api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class PresetsApplet(BaseApplet):
1212
model = Preset
1313
attach_to = "scans"
1414

15-
@api_endpoint("/get/{preset_id}", methods=["GET"], summary="Get a preset by its name or id")
15+
@api_endpoint("/get/{preset_id}", methods=["GET"], summary="Get a preset by its name or id", mcp=True)
1616
async def get_preset(self, preset_id: UUID | str) -> Preset:
1717
try:
1818
query = {"id": str(UUID(str(preset_id)))}
@@ -23,12 +23,12 @@ async def get_preset(self, preset_id: UUID | str) -> Preset:
2323
raise self.BBOTServerNotFoundError(f"Preset not found: {query}")
2424
return Preset(**preset)
2525

26-
@api_endpoint("/list", methods=["GET"], summary="List all presets")
26+
@api_endpoint("/list", methods=["GET"], summary="List all presets", mcp=True)
2727
async def get_presets(self) -> list[Preset]:
2828
presets = await self.collection.find().to_list(length=None)
2929
return [Preset(**preset) for preset in presets]
3030

31-
@api_endpoint("/create", methods=["POST"], summary="Create a new preset")
31+
@api_endpoint("/create", methods=["POST"], summary="Create a new preset", mcp=True)
3232
async def create_preset(self, preset: dict[str, Any]) -> Preset:
3333
preset = Preset(preset=preset)
3434
if not preset.name:
@@ -39,7 +39,7 @@ async def create_preset(self, preset: dict[str, Any]) -> Preset:
3939
raise self.BBOTServerValueError(f"Preset with name '{preset.name}' already exists")
4040
return preset
4141

42-
@api_endpoint("/update/{preset_id}", methods=["PATCH"], summary="Update a preset by its name or id")
42+
@api_endpoint("/update/{preset_id}", methods=["PATCH"], summary="Update a preset by its name or id", mcp=True)
4343
async def update_preset(self, preset_id: UUID | str, preset: dict[str, Any]) -> Preset:
4444
existing_preset = await self.get_preset(preset_id)
4545
# Create new preset with the updated dictionary
@@ -54,7 +54,7 @@ async def update_preset(self, preset_id: UUID | str, preset: dict[str, Any]) ->
5454
raise self.BBOTServerValueError(f"Preset with name '{new_preset.name}' already exists")
5555
return new_preset
5656

57-
@api_endpoint("/delete/{preset_id}", methods=["DELETE"], summary="Delete a preset by its name or id")
57+
@api_endpoint("/delete/{preset_id}", methods=["DELETE"], summary="Delete a preset by its name or id", mcp=True)
5858
async def delete_preset(self, preset_id: UUID | str) -> None:
5959
existing_preset = await self.get_preset(preset_id)
6060
await self.collection.delete_one({"id": str(existing_preset.id)})

bbot_server/modules/scans/scans_api.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ async def setup(self):
3737
self.scan_watch_task = self.create_task(self.start_scans_loop())
3838
return True, ""
3939

40-
@api_endpoint("/get/{id}", methods=["GET"], summary="Get a single scan by its name or ID")
40+
@api_endpoint("/get/{id}", methods=["GET"], summary="Get a single scan by its name or ID", mcp=True)
4141
async def get_scan(self, id: str) -> Scan:
4242
scan = await self.collection.find_one({"$or": [{"id": str(id)}, {"name": str(id)}]})
4343
if scan is None:
4444
raise self.BBOTServerNotFoundError("Scan not found")
4545
return Scan(**scan)
4646

47-
@api_endpoint("/start", methods=["POST"], summary="Create a new scan")
47+
@api_endpoint("/start", methods=["POST"], summary="Create a new scan", mcp=True)
4848
async def start_scan(
4949
self,
5050
target_id: str,
@@ -85,20 +85,20 @@ async def start_scan(
8585
)
8686
return scan
8787

88-
@api_endpoint("/list", methods=["GET"], type="http_stream", response_model=Scan, summary="Get all scans")
88+
@api_endpoint("/list", methods=["GET"], type="http_stream", response_model=Scan, summary="Get all scans", mcp=True)
8989
async def get_scans(self):
9090
async for scan in self.collection.find():
9191
yield Scan(**scan)
9292

93-
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query scans")
93+
@api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query scans", mcp=True)
9494
async def query_scans(self, query: ScanQuery | None = None):
9595
"""
9696
Advanced querying of scans. Choose your own filters and fields.
9797
"""
9898
async for scan in query.mongo_iter(self):
9999
yield scan
100100

101-
@api_endpoint("/count", methods=["POST"], summary="Count scans")
101+
@api_endpoint("/count", methods=["POST"], summary="Count scans", mcp=True)
102102
async def count_scans(self, query: ScanQuery | None = None) -> int:
103103
"""
104104
Same as query_scans, except only returns the count.
@@ -124,13 +124,13 @@ async def get_available_scan_name(self) -> str:
124124
valid_name = True
125125
return name
126126

127-
@api_endpoint("/queued", methods=["GET"], summary="List queued scans")
127+
@api_endpoint("/queued", methods=["GET"], summary="List queued scans", mcp=True)
128128
async def get_queued_scans(self) -> list[Scan]:
129129
# we sort by `created` ascending to get the oldest queued scans first
130130
cursor = self.collection.find({"status": "QUEUED"}, sort=[("created", ASCENDING)])
131131
return [Scan(**run) for run in await cursor.to_list(length=None)]
132132

133-
@api_endpoint("/cancel/{id}", methods=["POST"], summary="Cancel a scan by its name or ID")
133+
@api_endpoint("/cancel/{id}", methods=["POST"], summary="Cancel a scan by its name or ID", mcp=True)
134134
async def cancel_scan(self, id: str, force: bool = False):
135135
# get the scan
136136
scan = await self.get_scan(id)

0 commit comments

Comments
 (0)