Skip to content
Open
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
1 change: 1 addition & 0 deletions mcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies = [
"opentelemetry-instrumentation-httpx>=0.46b0,<1.0.0", # Trace upstream API calls
"prometheus-client>=0.21.0,<1.0.0", # Export Prometheus metrics
"pydantic-settings>=2.0.0,<3.0.0", # Environment-driven configuration
"sentry-sdk>=2.63.0,<3.0.0", # Capture errors and tracebacks
]

[project.scripts]
Expand Down
8 changes: 8 additions & 0 deletions mcp/src/flagsmith_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
class Settings(BaseSettings):
model_config = {"use_attribute_docstrings": True}

environment: Literal["local", "dev", "staging", "production"] = Field(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to constrain this

Suggested change
environment: Literal["local", "dev", "staging", "production"] = Field(
environment: str = Field(

default="local",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep this default, I expect the Docker build to set it to something else. But IMO we simply shouldn't set it at all.

)
"""Deployment environment."""
flagsmith_api_url: str = Field(
default="https://api.flagsmith.com",
)
Expand Down Expand Up @@ -47,6 +51,10 @@ class Settings(BaseSettings):
)
"""Public base URL of this MCP server, advertised in OAuth protected-resource
metadata. Override for HTTP deployments behind a proxy/public hostname."""
sentry_dsn: str | None = Field(
default=None,
)
Comment on lines +54 to +56

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This can use HttpUrl

"""Sentry DSN for error reporting. Error capture is disabled when unset."""

@model_validator(mode="after")
def validate_stdio_token(self) -> "Settings":
Expand Down
7 changes: 6 additions & 1 deletion mcp/src/flagsmith_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
from flagsmith_mcp.metrics import PrometheusMiddleware
from flagsmith_mcp.middleware import RootRouterMiddleware
from flagsmith_mcp.oauth import FlagsmithResourceAuth
from flagsmith_mcp.telemetry import propagate_span_attributes, setup_telemetry
from flagsmith_mcp.telemetry import (
propagate_span_attributes,
setup_sentry,
setup_telemetry,
)

ROUTE_MAPS = [
RouteMap(tags={"mcp"}, mcp_type=MCPType.TOOL),
Expand Down Expand Up @@ -92,6 +96,7 @@ async def health(request: Request) -> PlainTextResponse:
def run() -> None:
settings = config.Settings()
setup_telemetry(settings)
setup_sentry(settings)
server = create_server(settings)
if settings.metrics_port is not None:
start_http_server(settings.metrics_port)
Expand Down
11 changes: 11 additions & 0 deletions mcp/src/flagsmith_mcp/telemetry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import httpx
import sentry_sdk
from common.core.logging import setup_logging
from common.core.otel import (
add_otel_trace_context,
Expand Down Expand Up @@ -70,3 +71,13 @@ def setup_telemetry(settings: config.Settings) -> None:
application_loggers=APPLICATION_LOGGERS,
otel_processors=otel_processors,
)


def setup_sentry(settings: config.Settings) -> None:
"""Initialise Sentry for error capture when a DSN is configured."""
if not settings.sentry_dsn:
return
sentry_sdk.init(
dsn=settings.sentry_dsn,
environment=settings.environment,
)
34 changes: 34 additions & 0 deletions mcp/tests/unit/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,40 @@ def test_client_info_span_processor__outside_request_context__service_identity_o
)


def test_setup_sentry__no_dsn__init_not_called(mocker: MockerFixture) -> None:
# Given
mocker.patch.dict(os.environ, {}, clear=True)
sentry_sdk_mock = mocker.patch.object(telemetry, "sentry_sdk", autospec=True)
empty_settings = config.Settings()

# When
telemetry.setup_sentry(empty_settings)

# Then
sentry_sdk_mock.init.assert_not_called()


def test_setup_sentry__dsn_set__initialises_error_capture(
mocker: MockerFixture,
) -> None:
# Given
mocker.patch.dict(os.environ, {}, clear=True)
sentry_sdk_mock = mocker.patch.object(telemetry, "sentry_sdk", autospec=True)
settings = config.Settings(
sentry_dsn="https://public@sentry.example/1",
environment="staging",
)

# When
telemetry.setup_sentry(settings)

# Then
sentry_sdk_mock.init.assert_called_once_with(
dsn="https://public@sentry.example/1",
environment="staging",
)


async def test_propagate_span_attributes__no_recording_span__headers_untouched() -> (
None
):
Expand Down
8 changes: 5 additions & 3 deletions mcp/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading