Skip to content

Commit 0ca3eba

Browse files
committed
feat: Upgrade dependencies and add OAuth Middleware
1 parent 7bbe1bb commit 0ca3eba

File tree

20 files changed

+880
-417
lines changed

20 files changed

+880
-417
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ The following environment variables are **required** for configuring the Sysdig
172172
173173
You can also set the following variables to override the default configuration:
174174
175-
- `SYSDIG_MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`.
176-
- `SYSDIG_MCP_MOUNT_PATH`: The URL prefix for the Streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server`
175+
- `MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`.
176+
- `MCP_MOUNT_PATH`: The URL prefix for the Streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server`
177177
- `LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO`
178178
- `SYSDIG_MCP_LISTENING_PORT`: The port for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `8080`
179179
- `SYSDIG_MCP_LISTENING_HOST`: The host for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `localhost`

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
[project]
22
name = "sysdig-mcp-server"
3-
version = "0.1.5"
3+
version = "0.2.0"
44
description = "Sysdig MCP Server"
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
8-
"mcp[cli]==1.10.0",
8+
"mcp[cli]==1.12.4",
99
"python-dotenv>=1.1.0",
1010
"pyyaml==6.0.2",
1111
"sqlalchemy==2.0.36",
1212
"sqlmodel==0.0.22",
13-
"sysdig-sdk @ git+https://github.com/sysdiglabs/sysdig-sdk-python@e9b0d336c2f617f3bbd752416860f84eed160c41",
13+
"sysdig-sdk-python @ git+https://github.com/sysdiglabs/sysdig-sdk-python@597285143188019cd0e86fde43f94b1139f5441d",
1414
"dask==2025.4.1",
1515
"oauthlib==3.2.2",
1616
"fastapi==0.116.1",
17-
"fastmcp==2.5.1",
17+
"fastmcp==2.11.3",
1818
"requests",
1919
]
2020

tests/events_feed_test.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
from utils.app_config import AppConfig
88
from .conftest import util_load_json
99
from unittest.mock import MagicMock, AsyncMock, create_autospec
10+
from sysdig_client.api import SecureEventsApi
1011
import os
12+
from fastmcp.server.context import Context
13+
from fastmcp.server import FastMCP
1114

1215
# Get the absolute path of the current module file
1316
module_path = os.path.abspath(__file__)
@@ -40,12 +43,27 @@ def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds
4043

4144
tools_client = EventsFeedTools(app_config=mock_app_config())
4245

46+
ctx = Context(FastMCP())
47+
48+
# Seed FastMCP Context state with mocked API instances expected by the tools
49+
secure_events_api = MagicMock(spec=SecureEventsApi)
50+
# The tool returns whatever the SDK method returns; make it be our mocked HTTP response
51+
secure_events_api.get_event_v1_without_preload_content.return_value = mock_success_response.return_value
52+
53+
api_instances = {
54+
"secure_events": secure_events_api,
55+
# Not used by this test, but present in real runtime; keep as empty mock to avoid KeyErrors elsewhere
56+
"legacy_sysdig_api": MagicMock(),
57+
}
58+
ctx.set_state("api_instances", api_instances)
59+
4360
# Pass the mocked Context object
44-
result: dict = tools_client.tool_get_event_info("12345")
61+
result: dict = tools_client.tool_get_event_info(ctx=ctx, event_id="12345")
4562
results: dict = result["results"]
4663

4764
assert result.get("status_code") == HTTPStatus.OK
4865
assert results.get("results").get("name") == "Sysdig Runtime Threat Intelligence"
4966
assert results.get("results").get("content", {}).get("ruleName") == "Fileless execution via memfd_create"
5067
assert results.get("results").get("id") == "123456789012"
5168
assert results.get("results").get("content", {}).get("type") == "workloadRuntimeDetection"
69+
print("Event info retrieved successfully.")

tools/events_feed/tool.py

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,17 @@
88
import logging
99
import os
1010
import time
11-
from datetime import datetime
12-
from typing import Optional, Annotated, Any, Dict
11+
import datetime
12+
from typing import Optional, Annotated
1313
from pydantic import Field
14-
from sysdig_client import ApiException
1514
from fastmcp.prompts.prompt import PromptMessage, TextContent
1615
from fastmcp.exceptions import ToolError
17-
from starlette.requests import Request
16+
from fastmcp.server.context import Context
17+
from sysdig_client import ApiException
1818
from sysdig_client.api import SecureEventsApi
19-
from utils.sysdig.old_sysdig_api import OldSysdigApi
20-
from fastmcp.server.dependencies import get_http_request
19+
from utils.sysdig.legacy_sysdig_api import LegacySysdigApi
2120
from utils.query_helpers import create_standard_response
22-
from utils.sysdig.client_config import get_configuration
2321
from utils.app_config import AppConfig
24-
from utils.sysdig.api import initialize_api_client
2522

2623

2724
class EventsFeedTools:
@@ -32,55 +29,23 @@ class EventsFeedTools:
3229

3330
def __init__(self, app_config: AppConfig):
3431
self.app_config = app_config
35-
logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level())
3632
self.log = logging.getLogger(__name__)
3733

38-
def init_client(self, old_api: bool = False) -> SecureEventsApi | OldSysdigApi:
39-
"""
40-
Initializes the SecureEventsApi client from the request state.
41-
If the request does not have the API client initialized, it will create a new instance
42-
using the Sysdig Secure token and host from the environment variables.
43-
Args:
44-
old_api (bool): If True, initializes the OldSysdigApi client instead of SecureEventsApi.
45-
Returns:
46-
SecureEventsApi | OldSysdigApi: An instance of the SecureEventsApi or OldSysdigApi client.
47-
"""
48-
secure_events_api: SecureEventsApi = None
49-
old_sysdig_api: OldSysdigApi = None
50-
transport = self.app_config.transport()
51-
if transport in ["streamable-http", "sse"]:
52-
# Try to get the HTTP request
53-
self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.")
54-
request: Request = get_http_request()
55-
secure_events_api = request.state.api_instances["secure_events"]
56-
old_sysdig_api = request.state.api_instances["old_sysdig_api"]
57-
else:
58-
# If running in STDIO mode, we need to initialize the API client from environment variables
59-
self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.")
60-
cfg = get_configuration()
61-
api_client = initialize_api_client(cfg)
62-
secure_events_api = SecureEventsApi(api_client)
63-
# Initialize the old Sysdig API client for process tree requests
64-
old_cfg = get_configuration(old_api=True)
65-
old_sysdig_api = initialize_api_client(old_cfg)
66-
old_sysdig_api = OldSysdigApi(old_sysdig_api)
67-
68-
if old_api:
69-
return old_sysdig_api
70-
return secure_events_api
71-
72-
def tool_get_event_info(self, event_id: str) -> dict:
34+
def tool_get_event_info(self, ctx: Context, event_id: str) -> dict:
7335
"""
7436
Retrieves detailed information for a specific security event.
7537
7638
Args:
39+
ctx (Context): Context to use.
7740
event_id (str): The unique identifier of the security event.
7841
7942
Returns:
8043
Event: The Event object containing detailed information about the specified event.
8144
"""
8245
# Init of the sysdig client
83-
secure_events_api = self.init_client()
46+
api_instances: dict = ctx.get_state("api_instances")
47+
secure_events_api: SecureEventsApi = api_instances.get("secure_events")
48+
8449
try:
8550
# Get the HTTP request
8651
start_time = time.time()
@@ -98,6 +63,7 @@ def tool_get_event_info(self, event_id: str) -> dict:
9863

9964
def tool_list_runtime_events(
10065
self,
66+
ctx: Context,
10167
cursor: Optional[str] = None,
10268
scope_hours: int = 1,
10369
limit: int = 50,
@@ -136,6 +102,7 @@ def tool_list_runtime_events(
136102
cluster name, or an optional filter expression.
137103
138104
Args:
105+
ctx (Context): Context to use.
139106
cursor (Optional[str]): Cursor for pagination.
140107
scope_hours (int): Number of hours back from now to include events. Defaults to 1.
141108
limit (int): Maximum number of events to return. Defaults to 50.
@@ -144,7 +111,9 @@ def tool_list_runtime_events(
144111
Returns:
145112
dict: A dictionary containing the results of the runtime events query, including pagination information.
146113
"""
147-
secure_events_api = self.init_client()
114+
api_instances: dict = ctx.get_state("api_instances")
115+
secure_events_api: SecureEventsApi = api_instances.get("secure_events")
116+
148117
start_time = time.time()
149118
# Compute time window
150119
now_ns = time.time_ns()
@@ -176,7 +145,7 @@ def tool_list_runtime_events(
176145

177146
# A tool to retrieve all the process-tree information for a specific event.Add commentMore actions
178147

179-
def tool_get_event_process_tree(self, event_id: str) -> dict:
148+
def tool_get_event_process_tree(self, ctx: Context, event_id: str) -> dict:
180149
"""
181150
Retrieves the process tree for a specific security event.
182151
Not every event has a process tree, so this may return an empty tree.
@@ -191,12 +160,14 @@ def tool_get_event_process_tree(self, event_id: str) -> dict:
191160
ToolError: If there is an error constructing or processing the response.
192161
"""
193162
try:
163+
api_instances: dict = ctx.get_state("api_instances")
164+
legacy_api_client: LegacySysdigApi = api_instances.get("legacy_sysdig_api")
165+
194166
start_time = time.time()
195167
# Get process tree branches
196-
old_api_client = self.init_client(old_api=True)
197-
branches = old_api_client.request_process_tree_branches(event_id)
168+
branches = legacy_api_client.request_process_tree_branches(event_id)
198169
# Get process tree
199-
tree = old_api_client.request_process_tree_trees(event_id)
170+
tree = legacy_api_client.request_process_tree_trees(event_id)
200171

201172
# Parse the response (tolerates empty bodies)
202173
branches_std = create_standard_response(results=branches, execution_time_ms=(time.time() - start_time) * 1000)
@@ -209,7 +180,7 @@ def tool_get_event_process_tree(self, event_id: str) -> dict:
209180
"tree": tree_std.get("results", {}),
210181
"metadata": {
211182
"execution_time_ms": execution_time,
212-
"timestamp": datetime.utcnow().isoformat() + "Z",
183+
"timestamp": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z"),
213184
},
214185
}
215186

@@ -222,7 +193,7 @@ def tool_get_event_process_tree(self, event_id: str) -> dict:
222193
"tree": {},
223194
"metadata": {
224195
"execution_time_ms": (time.time() - start_time) * 1000,
225-
"timestamp": datetime.utcnow().isoformat() + "Z",
196+
"timestamp": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z"),
226197
"note": "Process tree not available for this event"
227198
},
228199
}

tools/inventory/tool.py

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
import logging
66
import time
77
from typing import Annotated
8+
9+
from fastmcp import Context
810
from pydantic import Field
9-
from fastmcp.server.dependencies import get_http_request
1011
from fastmcp.exceptions import ToolError
11-
from starlette.requests import Request
1212
from sysdig_client.api import InventoryApi
13-
from utils.sysdig.client_config import get_configuration
1413
from utils.app_config import AppConfig
15-
from utils.sysdig.api import initialize_api_client
1614
from utils.query_helpers import create_standard_response
1715

1816

@@ -24,34 +22,11 @@ class InventoryTools:
2422
def __init__(self, app_config: AppConfig):
2523
self.app_config = app_config
2624
# Configure logging
27-
logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=self.app_config.log_level())
2825
self.log = logging.getLogger(__name__)
2926

30-
def init_client(self) -> InventoryApi:
31-
"""
32-
Initializes the InventoryApi client from the request state.
33-
If the request does not have the API client initialized, it will create a new instance
34-
using the Sysdig Secure token and host from the environment variables.
35-
Returns:
36-
InventoryApi: An instance of the InventoryApi client.
37-
"""
38-
inventory_api: InventoryApi = None
39-
transport = self.app_config.transport()
40-
if transport in ["streamable-http", "sse"]:
41-
# Try to get the HTTP request
42-
self.log.debug("Attempting to get the HTTP request to initialize the Sysdig API client.")
43-
request: Request = get_http_request()
44-
inventory_api = request.state.api_instances["inventory"]
45-
else:
46-
# If running in STDIO mode, we need to initialize the API client from environment variables
47-
self.log.debug("Running in STDIO mode, initializing the Sysdig API client from environment variables.")
48-
cfg = get_configuration()
49-
api_client = initialize_api_client(cfg)
50-
inventory_api = InventoryApi(api_client)
51-
return inventory_api
52-
5327
def tool_list_resources(
5428
self,
29+
ctx: Context,
5530
filter_exp: Annotated[
5631
str,
5732
Field(
@@ -141,6 +116,7 @@ def tool_list_resources(
141116
List inventory items based on a filter expression, with optional pagination.
142117
143118
Args:
119+
ctx (Context): A context object containing configuration information.
144120
filter_exp (str): Sysdig query filter expression to filter inventory resources.
145121
Use the resource://filter-query-language to get the expected filter expression format.
146122
Supports operators: =, !=, in, exists, contains, startsWith.
@@ -162,7 +138,9 @@ def tool_list_resources(
162138
Or a dict containing an error message if the call fails.
163139
"""
164140
try:
165-
inventory_api = self.init_client()
141+
api_instances: dict = ctx.get_state("api_instances")
142+
inventory_api: InventoryApi = api_instances.get("inventory")
143+
166144
start_time = time.time()
167145

168146
api_response = inventory_api.get_resources_without_preload_content(
@@ -180,19 +158,23 @@ def tool_list_resources(
180158

181159
def tool_get_resource(
182160
self,
161+
ctx: Context,
183162
resource_hash: Annotated[str, Field(description="The unique hash of the inventory resource to retrieve.")],
184163
) -> dict:
185164
"""
186165
Fetch a specific inventory resource by hash.
187166
188167
Args:
168+
ctx (Context): A context object containing configuration information.
189169
resource_hash (str): The hash identifier of the resource.
190170
191171
Returns:
192172
dict: A dictionary containing the details of the requested inventory resource.
193173
"""
194174
try:
195-
inventory_api = self.init_client()
175+
api_instances: dict = ctx.get_state("api_instances")
176+
inventory_api: InventoryApi = api_instances.get("inventory")
177+
196178
start_time = time.time()
197179

198180
api_response = inventory_api.get_resource_without_preload_content(hash=resource_hash)

0 commit comments

Comments
 (0)