Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 0 additions & 54 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,57 +45,3 @@ jobs:

- name: Run Unit Tests
run: make test

check_version:
name: Check Version
runs-on: ubuntu-latest
needs: test
permissions:
contents: write # required for creating a tag
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ github.sha }} # required for better experience using pre-releases
fetch-depth: '0' # Required due to the way Git works, without it this action won't be able to find any or the correct tags

- name: Extract current version
id: pyproject_version
run: |
TAG=v$(grep 'version =' pyproject.toml | sed -e 's/version = "\(.*\)"/\1/')
echo "TAG=$TAG" >> "$GITHUB_OUTPUT"

- name: Get branch ref name
id: branch_ref
run: |
BRANCH_NAME=${{ github.base_ref || github.ref_name }}
echo "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_OUTPUT"

- name: Get tag version
id: semantic_release
uses: anothrNick/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEFAULT_BUMP: "patch"
TAG_CONTEXT: 'repo'
WITH_V: true
DRY_RUN: true

- name: Compare versions
run: |
echo "Current version: ${{ steps.pyproject_version.outputs.TAG }}"
echo "New version: ${{ steps.semantic_release.outputs.tag }}"
if [ "${{ steps.pyproject_version.outputs.TAG }}" != "${{ steps.semantic_release.outputs.tag }}" ]; then
echo "### Version mismatch detected! :warning:
Current pyproject version: ${{ steps.pyproject_version.outputs.TAG }}
New Tag version: **${{ steps.semantic_release.outputs.tag }}**
Current Tag: ${{ steps.semantic_release.outputs.old_tag }}
Please update the version in pyproject.toml." >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "### Version match confirmed! :rocket:
Current pyproject version: ${{ steps.pyproject_version.outputs.TAG }}
New Tag version: **${{ steps.semantic_release.outputs.tag }}**
The version is up-to-date." >> $GITHUB_STEP_SUMMARY
fi
57 changes: 13 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
- [Requirements](#requirements)
- [UV Setup](#uv-setup)
- [Configuration](#configuration)
- [`app_config.yaml`](#app_configyaml)
- [Environment Variables](#environment-variables)
- [Running the Server](#running-the-server)
- [Docker](#docker)
Expand Down Expand Up @@ -85,21 +84,6 @@ Get up and running with the Sysdig MCP Server quickly using our pre-built Docker

## Available Tools

You can select what group of tools to add when running the server by adding/removing them from the `mcp.allowed_tools` list in the app_config.yaml file

```yaml
...
mcp:
transport: stdio
...
allowed_tools:
- "events-feed"
- "inventory"
- "vulnerability-management"
- "sysdig-sage"
- "sysdig-cli-scanner" # Only available in stdio local transport mode
```

<details>
<summary><strong>Events Feed</strong></summary>

Expand Down Expand Up @@ -168,13 +152,9 @@ mcp:

You can use [uv](https://github.com/astral-sh/uv) as a drop-in replacement for pip to create the virtual environment and install dependencies.

If you don't have `uv` installed, you can install it via (Linux and MacOS users):
If you don't have `uv` installed, you can install it following the instructions that you can find on the `README` of the project.

```bash
curl -Ls https://astral.sh/uv/install.sh | sh
```

To set up the environment:
If you want to develop, set up the environment using:

```bash
uv venv
Expand All @@ -185,36 +165,26 @@ This will create a virtual environment using `uv` and install the required depen

## Configuration

The application can be configured via the `app_config.yaml` file and environment variables.

### `app_config.yaml`

This file contains the main configuration for the application, including:

- **app**: Host, port, and log level for the MCP server.
- **sysdig**: The Sysdig Secure host to connect to.
- **mcp**: Transport protocol (stdio, sse, streamable-http), URL, host, and port for the MCP server.

> You can set the path for the app_config.yaml using the `APP_CONFIG_FILE=/path/to/app_config.yaml` env var. By default the app will search the file in the root of the app.

### Environment Variables

The following environment variables are required for configuring the Sysdig SDK:
The following environment variables are **required** for configuring the Sysdig SDK:

- `SYSDIG_HOST`: The URL of your Sysdig Secure instance (e.g., `https://us2.app.sysdig.com`).
- `SYSDIG_SECURE_TOKEN`: Your Sysdig Secure API token.

You can also set the following variables to override the default configuration:

- `MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`.
- `MCP_MOUNT_PATH`: The URL prefix for the Streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server`
- `LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO`
- `SYSDIG_MCP_LISTENING_PORT`: The port for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `8080`
- `SYSDIG_MCP_LISTENING_HOST`: The host for the server when it is deployed using remote protocols (`steamable-http`, `sse`). Defaults to: `localhost`

You can find your API token in the Sysdig Secure UI under **Settings > Sysdig Secure API**. Make sure to copy the token as it will not be shown again.

![API_TOKEN_CONFIG](./docs/assets/settings-config-token.png)
![API_TOKEN_SETTINGS](./docs/assets/api-token-copy.png)

You can set these variables in your shell or in a `.env` file.

You can also use `MCP_TRANSPORT` to override the transport protocol set in `app_config.yaml`.

> All of this env variables have precedence over the fields configured in the app_config.yaml.

## Running the Server

You can run the MCP server using either Docker, `uv` or install it in your K8s cluster with helm.
Expand Down Expand Up @@ -255,7 +225,6 @@ sysdig:
secureAPIToken: "<your_sysdig_secure_api_token>"
mcp:
transport: "streamable-http"
# You can set the Sysdig Tenant URL at this level or below in the app_config configmap
host: "https://us2.app.sysdig.com" # <your_sysdig_host> "https://eu1.app.sysdig.com"

configMap:
Expand Down Expand Up @@ -312,7 +281,7 @@ To use the MCP server with a client like Claude or Cursor, you need to provide t

When using the `sse` or `streamable-http` transport, the server requires a Bearer token for authentication. The token is passed in the `Authorization` header of the HTTP request.

Additionally, you can specify the Sysdig Secure host by providing the `X-Sysdig-Host` header. If this header is not present, the server will use the value from `app_config.yaml`.
Additionally, you can specify the Sysdig Secure host by providing the `X-Sysdig-Host` header. If this header is not present, the server will use the value from the env variable.

Example headers:

Expand All @@ -323,7 +292,7 @@ X-Sysdig-Host: <your_sysdig_host>

### URL

If you are running the server with the `sse` or `streamable-http` transport, the URL will be `http://<host>:<port>/sysdig-mcp-server/mcp`, where `<host>` and `<port>` are the values configured in `app_config.yaml` or the Docker run command.
If you are running the server with the `sse` or `streamable-http` transport, the URL will be `http://<host>:<port>/sysdig-mcp-server/mcp`.

For example, if you are running the server locally on port 8080, the URL will be `http://localhost:8080/sysdig-mcp-server/mcp`.

Expand Down
20 changes: 0 additions & 20 deletions app_config.yaml

This file was deleted.

23 changes: 13 additions & 10 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
from utils.app_config import get_app_config

# Register all tools so they attach to the MCP server
from utils.mcp_server import run_stdio, run_http
from utils.mcp_server import SysdigMCPServer

# Load environment variables from .env
load_dotenv()

app_config = get_app_config()

# Set up logging
logging.basicConfig(
format="%(asctime)s-%(process)d-%(levelname)s- %(message)s",
level=os.environ.get("LOGLEVEL", "ERROR"),
level=app_config.log_level(),
)
log = logging.getLogger(__name__)

# Load environment variables from .env
load_dotenv()

app_config = get_app_config()


def handle_signals():
def signal_handler(sig, frame):
Expand All @@ -40,19 +40,22 @@ def signal_handler(sig, frame):
def main():
# Choose transport: "stdio" or "sse" (HTTP/SSE)
handle_signals()
transport = os.environ.get("MCP_TRANSPORT", app_config["mcp"]["transport"]).lower()
transport = app_config.transport()
log.info("""
▄▖ ▌▘ ▖ ▖▄▖▄▖ ▄▖
▚ ▌▌▛▘▛▌▌▛▌ ▛▖▞▌▌ ▙▌ ▚ █▌▛▘▌▌█▌▛▘
▄▌▙▌▄▌▙▌▌▙▌ ▌▝ ▌▙▖▌ ▄▌▙▖▌ ▚▘▙▖▌
▄▌ ▄▌
""")

mcp_server = SysdigMCPServer(app_config=app_config)

if transport == "stdio":
# Run MCP server over STDIO (local)
run_stdio()
mcp_server.run_stdio()
else:
# Run MCP server over streamable HTTP by default
run_http()
mcp_server.run_http()


if __name__ == "__main__":
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
[project]
name = "sysdig-mcp-server"
version = "0.1.5"
version = "0.2.0"
description = "Sysdig MCP Server"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"mcp[cli]==1.10.0",
"mcp[cli]==1.12.4",
"python-dotenv>=1.1.0",
"pyyaml==6.0.2",
"sqlalchemy==2.0.36",
"sqlmodel==0.0.22",
"sysdig-sdk @ git+https://github.com/sysdiglabs/sysdig-sdk-python@e9b0d336c2f617f3bbd752416860f84eed160c41",
"sysdig-sdk-python @ git+https://github.com/sysdiglabs/sysdig-sdk-python@852ee2ccad12a8b445dd4732e7f3bd44d78a37f7",
"dask==2025.4.1",
"oauthlib==3.2.2",
"fastapi==0.116.1",
"fastmcp==2.5.1",
"fastmcp==2.11.3",
"requests",
]

Expand Down
49 changes: 45 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import json
import pytest
import os
from unittest.mock import patch
from utils.app_config import AppConfig
from unittest.mock import MagicMock, create_autospec, patch
from fastmcp.server.context import Context
from sysdig_client import SecureEventsApi, ApiClient, InventoryApi, VulnerabilityManagementApi
from utils.sysdig.legacy_sysdig_api import LegacySysdigApi
from fastmcp.server import FastMCP


def util_load_json(path):
Expand All @@ -26,7 +31,7 @@ def mock_success_response():
Fixture to mock the urllib3.PoolManager.request method

Yields:
MagicMock: A mocked request object that simulates a successful HTTP response.
MagicMock: A mocked request object that simulates a successful HTTP response.
"""
with patch("urllib3.PoolManager.request") as mock_request:
mock_resp = patch("urllib3.response.HTTPResponse").start()
Expand All @@ -42,5 +47,41 @@ def mock_creds():
"""
Fixture to set up mocked credentials.
"""
os.environ["SYSDIG_SECURE_TOKEN"] = "mocked_token"
os.environ["SYSDIG_HOST"] = "https://us2.app.sysdig.com"
os.environ["SYSDIG_MCP_SECURE_TOKEN"] = "mocked_token"
os.environ["SYSDIG_MCP_HOST"] = "https://us2.app.sysdig.com"


def mock_app_config() -> AppConfig:
"""
Utility function to create a mocked AppConfig instance.
Returns:
AppConfig: A mocked AppConfig instance.
"""
mock_cfg = create_autospec(AppConfig, instance=True)

mock_cfg.sysdig_endpoint.return_value = "https://us2.app.sysdig.com"
mock_cfg.transport.return_value = "stdio"
mock_cfg.log_level.return_value = "DEBUG"
mock_cfg.port.return_value = 8080

return mock_cfg


@pytest.fixture
def mock_context() -> Context:
"""
Utility function to create a mocked FastMCP context.
Returns:
Context: A mocked FastMCP context.
"""

ctx = Context(MagicMock(spec=FastMCP))

api_instances = {
"secure_events": SecureEventsApi(ApiClient()),
"vulnerability_management": VulnerabilityManagementApi(ApiClient()),
"inventory": InventoryApi(ApiClient()),
"legacy_sysdig_api": LegacySysdigApi(ApiClient()),
}
ctx.set_state("api_instances", api_instances)
return ctx
18 changes: 11 additions & 7 deletions tests/events_feed_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
Events Feed Test Module
"""

import os
from http import HTTPStatus
from tools.events_feed.tool import EventsFeedTools
from .conftest import util_load_json
from unittest.mock import MagicMock, AsyncMock
import os
from fastmcp.server.context import Context
from fastmcp.server import FastMCP
from .conftest import util_load_json, mock_app_config

# Get the absolute path of the current module file
module_path = os.path.abspath(__file__)
Expand All @@ -16,26 +18,28 @@

EVENT_INFO_RESPONSE = util_load_json(f"{module_directory}/test_data/events_feed/event_info_response.json")

ctx = Context(MagicMock(spec=FastMCP))

def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds) -> None:

def test_get_event_info(mock_success_response: MagicMock | AsyncMock, mock_creds, mock_context: Context) -> None:
"""Test the get_event_info tool method.
Args:
mock_success_response (MagicMock | AsyncMock): Mocked response object.
mock_creds: Mocked credentials.
"""
# Override the environment variable for MCP transport
os.environ["MCP_TRANSPORT"] = "stdio"
# Successful response
mock_success_response.return_value.json.return_value = EVENT_INFO_RESPONSE
mock_success_response.return_value.status_code = HTTPStatus.OK

tools_client = EventsFeedTools()
tools_client = EventsFeedTools(app_config=mock_app_config())

# Pass the mocked Context object
result: dict = tools_client.tool_get_event_info("12345")
result: dict = tools_client.tool_get_event_info(ctx=mock_context, event_id="12345")
results: dict = result["results"]

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