open-swe/agent/integrations/datadog_mcp.py
Johannes du Plessis f539962c73
feat: server-side Datadog/LangSmith observability tools + team creds [closes OPE-54] (#1476)
* feat: server-side Datadog/LangSmith observability tools + team creds

Add team-wide observability credential settings (Datadog DD_SITE/API/APP
keys, LangSmith API key) stored encrypted server-side, with an admin
dashboard section to connect/disconnect each provider. When connected,
get_agent loads read-only observability tools server-side: Datadog via its
hosted MCP server (langchain-mcp-adapters, toolsets=core) and LangSmith
read tools (langsmith_get_trace, langsmith_list_runs). Credentials live in
the LangGraph server process and are never exposed to the sandbox.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: address review on observability tools

Address PR review feedback:
- Authorize observability tools per triggering user (admins + the
  OBSERVABILITY_AUTHORIZED_EMAILS allowlist) so prompt-injected runs from
  untrusted contributors can't reach team Datadog/LangSmith data.
- Use the documented Datadog MCP auth headers DD_API_KEY / DD_APPLICATION_KEY.
- Store each provider's credentials under its own store key to avoid a
  read-modify-write race dropping the other provider on concurrent saves.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: async email resolution in observability authorization gate

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-10 11:07:42 -07:00

68 lines
2.2 KiB
Python

"""Server-side Datadog tools backed by Datadog's hosted MCP server.
Credentials live in team settings (encrypted at rest) and are attached as
``DD_API_KEY`` / ``DD_APPLICATION_KEY`` headers to the MCP connection, which runs
in the LangGraph server process. The sandbox never holds Datadog credentials.
The default toolset is ``core`` (query-oriented: logs, metrics, traces,
dashboards, monitors, incidents, hosts, services, events). Override via
``DATADOG_MCP_TOOLSETS``.
"""
from __future__ import annotations
import logging
import os
from datetime import timedelta
from langchain_core.tools import BaseTool
from ..dashboard.team_credentials import DatadogCredentials, get_datadog_credentials
logger = logging.getLogger(__name__)
DEFAULT_DATADOG_TOOLSETS = "core"
_MCP_TIMEOUT_SECONDS = 30.0
def _toolsets() -> str:
return os.environ.get("DATADOG_MCP_TOOLSETS", DEFAULT_DATADOG_TOOLSETS).strip() or (
DEFAULT_DATADOG_TOOLSETS
)
async def _build_mcp_tools(creds: DatadogCredentials) -> list[BaseTool]:
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient(
{
"datadog": {
"transport": "streamable_http",
"url": creds.mcp_url(_toolsets()),
"headers": {
"DD_API_KEY": creds.api_key,
"DD_APPLICATION_KEY": creds.app_key,
},
"timeout": timedelta(seconds=_MCP_TIMEOUT_SECONDS),
}
}
)
return await client.get_tools()
async def load_datadog_tools() -> list[BaseTool]:
"""Return Datadog MCP tools when the team has connected Datadog, else ``[]``.
Failures (no credentials, unreachable MCP server) degrade to an empty list so
the agent still starts without Datadog tools.
"""
creds = await get_datadog_credentials()
if creds is None:
return []
try:
tools = await _build_mcp_tools(creds)
except Exception: # noqa: BLE001
logger.warning("Failed to load Datadog MCP tools", exc_info=True)
return []
logger.info("Loaded %d Datadog MCP tool(s) (toolsets=%s)", len(tools), _toolsets())
return tools