-
Notifications
You must be signed in to change notification settings - Fork 10
Enable MCP for all API endpoints #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
2cf67ad
e6c8b80
c3d856d
ff3c03f
ca927c1
690e265
beaf38a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -218,7 +218,6 @@ def wrapped_function(self): | |
| """ | ||
|
|
||
| # Define a new async function that wraps the original function | ||
| @functools.wraps(self.orig_function) | ||
| async def wrapper(*args, **kwargs): | ||
| generator = self.orig_function(*args, **kwargs) | ||
|
|
||
|
|
@@ -237,7 +236,12 @@ async def async_generator(): | |
|
|
||
| return StreamingResponse(async_generator()) | ||
|
|
||
| # Set the wrapper's signature to match the original function | ||
| # Preserve the original function's name and signature for FastAPI routing, | ||
| # but do NOT use @functools.wraps as it preserves async generator markers | ||
| # that cause FastAPI to mishandle the StreamingResponse | ||
| wrapper.__name__ = self.orig_function.__name__ | ||
| wrapper.__qualname__ = self.orig_function.__qualname__ | ||
| wrapper.__signature__ = inspect.signature(self.orig_function) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking. Your diagnosis is right and I reproduced it: under fastapi 0.136.1 a But this copies One line fixes it, verified still 200: wrapper.__doc__ = self.orig_function.__doc__ |
||
| return wrapper | ||
|
|
||
| def add_to_router(self, router, **fastapi_kwargs): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,7 +43,7 @@ async def get_agents(self) -> list[Agent]: | |
| agents.append(agent) | ||
| return agents | ||
|
|
||
| @api_endpoint("/", methods=["POST"], summary="Create an agent") | ||
| @api_endpoint("/", methods=["POST"], summary="Create an agent", mcp=True) | ||
| async def create_agent(self, name: str, description: str = "") -> Agent: | ||
| agent = Agent(name=name, description=description) | ||
| try: | ||
|
|
@@ -52,12 +52,12 @@ async def create_agent(self, name: str, description: str = "") -> Agent: | |
| raise self.BBOTServerError(f"Error creating agent {name}: {e}") from e | ||
| return agent | ||
|
|
||
| @api_endpoint("/", methods=["DELETE"], summary="Delete an agent") | ||
| @api_endpoint("/", methods=["DELETE"], summary="Delete an agent", mcp=True) | ||
| async def delete_agent(self, id: str): | ||
| agent = await self.get_agent(id) | ||
| await self.collection.delete_one({"id": str(agent.id)}) | ||
|
|
||
| @api_endpoint("/", methods=["GET"], summary="Get an agent by its id") | ||
| @api_endpoint("/", methods=["GET"], summary="Get an agent by its id", mcp=True) | ||
| async def get_agent(self, id: str) -> Agent: | ||
| try: | ||
| query = {"id": str(UUID(str(id)))} | ||
|
|
@@ -92,7 +92,7 @@ async def get_agent_status( | |
| agent_status = {"agent_status": "OFFLINE", "scan_status": "UNKNOWN"} | ||
| return agent_status | ||
|
|
||
| @api_endpoint("/scan_status", methods=["GET"], summary="Get the status of an agent's scan") | ||
| @api_endpoint("/scan_status", methods=["GET"], summary="Get the status of an agent's scan", mcp=True) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking. While you are here, switch |
||
| async def get_scan_status(self, id: UUID, detailed: bool = False) -> dict[str, str]: | ||
| command_response = await self.connection_manager.execute_command( | ||
| str(id), "get_scan_status", timeout=10, detailed=detailed | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ class AssetsApplet(BaseApplet): | |
|
|
||
| model = Asset | ||
|
|
||
| @api_endpoint("/list", methods=["GET"], type="http_stream", response_model=Asset, summary="Stream all assets") | ||
| @api_endpoint("/list", methods=["GET"], type="http_stream", response_model=Asset, summary="Stream all assets", mcp=True) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking. |
||
| async def list_assets( | ||
| self, | ||
| domain: Annotated[str, Query(description="Filter assets by domain or subdomain")] = None, | ||
|
|
@@ -27,22 +27,26 @@ async def list_assets( | |
| async for asset in query.mongo_iter(self): | ||
| yield self.model(**asset) | ||
|
|
||
| @api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query assets") | ||
| @api_endpoint("/query", methods=["POST"], type="http_stream", response_model=dict, summary="Query assets", mcp=True) | ||
| async def query_assets(self, query: AdvancedAssetQuery | None = None): | ||
| """ | ||
| Advanced querying of assets. Choose your own filters and fields. | ||
| """ | ||
| if query is None: | ||
| query = AdvancedAssetQuery() | ||
| async for asset in query.mongo_iter(self): | ||
| yield asset | ||
|
|
||
| @api_endpoint("/count", methods=["POST"], summary="Count assets") | ||
| @api_endpoint("/count", methods=["POST"], summary="Count assets", mcp=True) | ||
| async def count_assets(self, query: AdvancedAssetQuery | None = None) -> int: | ||
| """ | ||
| Same as query_assets, except only returns the count | ||
| """ | ||
| if query is None: | ||
| query = AdvancedAssetQuery() | ||
| return await query.mongo_count(self) | ||
|
|
||
| @api_endpoint("/{host}/detail", methods=["GET"], summary="Get a single asset by its host") | ||
| @api_endpoint("/{host}/detail", methods=["GET"], summary="Get a single asset by its host", mcp=True) | ||
| async def get_asset(self, host: Annotated[str, Path(description="The host of the asset to get")]) -> Asset: | ||
| asset = await self.collection.find_one({"host": host}) | ||
| if not asset: | ||
|
|
@@ -63,7 +67,7 @@ async def get_asset_history(self, host: str) -> list[str]: | |
| history.append(activity["description"]) | ||
| return history | ||
|
|
||
| @api_endpoint("/hosts", methods=["GET"], summary="List hosts") | ||
| @api_endpoint("/hosts", methods=["GET"], summary="List hosts", mcp=True) | ||
| async def get_hosts(self, domain: str = None, target_id: str = None) -> list[str]: | ||
| """ | ||
| List all hosts. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,10 @@ | ||
| # from bbot_server.workers.emails import EmailWorker | ||
| from bbot_server.applets.base import BaseApplet, api_endpoint, BaseModel, Field | ||
| from bbot_server.assets import CustomAssetFields | ||
| from bbot_server.applets.base import BaseApplet, api_endpoint, Field | ||
|
|
||
|
|
||
| class EmailsFields(CustomAssetFields): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking. This is a correct fix. The nested That is a change to the stored asset schema. It is not mentioned in the PR body and no test covers it. Call it out and add one. |
||
| emails: list[str] = Field(default_factory=list) | ||
|
|
||
|
|
||
| class EmailsApplet(BaseApplet): | ||
|
|
@@ -10,15 +15,11 @@ class EmailsApplet(BaseApplet): | |
| # workers = [EmailWorker] | ||
| attach_to = "assets" | ||
|
|
||
| class AssetFields(BaseModel): | ||
| emails: list[str] = Field(default_factory=list) | ||
|
|
||
| @api_endpoint("/emails/{domain}", methods=["GET"], summary="Get emails by domain") | ||
| @api_endpoint("/emails/{domain}", methods=["GET"], summary="Get emails by domain", mcp=True) | ||
| async def get_emails(self, domain: str) -> list[str]: | ||
| matching_assets = await self.root.assets.list_assets(host=domain) | ||
| emails = set() | ||
| for asset in matching_assets: | ||
| emails.update(asset.fields.get("emails", [])) | ||
| async for asset in self.root.assets.list_assets(domain=domain): | ||
| emails.update(getattr(asset, "emails", [])) | ||
| return sorted(emails) | ||
|
|
||
| # async def handle_event(self, asset: Asset, event: Event) -> list[Activity]: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Blocking.
config.py:77already owns this asauth_header. Read it from config instead of pasting the literal.Also note this replaces the library default of
["authorization"]rather than extending it. Confirm nothing depends on Authorization passthrough.