diff --git a/frontend/src/app/workspaces/[workspaceId]/integrations/[providerId]/page.tsx b/frontend/src/app/workspaces/[workspaceId]/integrations/[providerId]/page.tsx index 31b44254ec..3ca7c9e284 100644 --- a/frontend/src/app/workspaces/[workspaceId]/integrations/[providerId]/page.tsx +++ b/frontend/src/app/workspaces/[workspaceId]/integrations/[providerId]/page.tsx @@ -103,6 +103,15 @@ export default function ProviderDetailPage() { type ProviderDetailTab = "overview" | "configuration" +/** + * Check if a provider is an MCP (Model Context Protocol) provider. + * MCP providers don't require user-provided client credentials. + */ +function isMCPProvider(provider: ProviderRead): boolean { + // MCP providers follow the naming convention of ending with "_mcp" + return provider.metadata.id.endsWith("_mcp") +} + function ProviderDetailContent({ provider }: { provider: ProviderRead }) { const workspaceId = useWorkspaceId() const router = useRouter() @@ -110,11 +119,14 @@ function ProviderDetailContent({ provider }: { provider: ProviderRead }) { const [errorMessage, setErrorMessage] = useState("") const [_showConnectPrompt, setShowConnectPrompt] = useState(false) const providerId = provider.metadata.id + const isMCP = isMCPProvider(provider) // Get active tab from URL query params, default to "overview" + // For MCP providers, always use "overview" since there's no configuration tab const activeTab = ( searchParams && - ["overview", "configuration"].includes(searchParams.get("tab") || "") + ["overview", "configuration"].includes(searchParams.get("tab") || "") && + !isMCP // Don't allow configuration tab for MCP providers ? (searchParams.get("tab") ?? "overview") : "overview" ) as ProviderDetailTab @@ -294,11 +306,21 @@ function ProviderDetailContent({ provider }: { provider: ProviderRead }) { variant="outline" size="sm" className="h-[22px] px-2 py-0 text-xs font-medium" - onClick={() => handleTabChange("configuration")} - disabled={!isEnabled} + onClick={ + isMCP + ? handleOAuthConnect + : () => handleTabChange("configuration") + } + disabled={!isEnabled || (isMCP && connectProviderIsPending)} > - - Configure + {isMCP && connectProviderIsPending ? ( + + ) : isMCP ? ( + + ) : ( + + )} + {isMCP ? "Connect" : "Configure"} )} @@ -307,15 +329,6 @@ function ProviderDetailContent({ provider }: { provider: ProviderRead }) { - {errorMessage && ( - - - - {errorMessage} - - - )} - {/* Tabs */} Overview - - - Configuration - + {!isMCP && ( + + + Configuration + + )} @@ -514,54 +529,56 @@ function ProviderDetailContent({ provider }: { provider: ProviderRead }) { - - {/* Configuration Form */} -
- - {testConnectionIsPending ? ( - <> - - Testing... - - ) : ( - <> - - Test connection - - )} - - ) : ( - - ) - ) : null - } - /> -
-
+ {!isMCP && ( + + {/* Configuration Form */} +
+ + {testConnectionIsPending ? ( + <> + + Testing... + + ) : ( + <> + + Test connection + + )} + + ) : ( + + ) + ) : null + } + /> +
+
+ )}
) diff --git a/frontend/src/components/provider-config-form.tsx b/frontend/src/components/provider-config-form.tsx index 7b44427e5d..8ebee33d25 100644 --- a/frontend/src/components/provider-config-form.tsx +++ b/frontend/src/components/provider-config-form.tsx @@ -86,6 +86,15 @@ interface ProviderConfigFormProps { additionalButtons?: React.ReactNode } +/** + * Check if a provider is an MCP (Model Context Protocol) provider. + * MCP providers don't require user-provided client credentials. + */ +function isMCPProvider(provider: ProviderRead): boolean { + // MCP providers follow the naming convention of ending with "_mcp" + return provider.metadata.id.endsWith("_mcp") +} + export function ProviderConfigForm({ provider, onSuccess, @@ -98,6 +107,7 @@ export function ProviderConfigForm({ grant_type: grantType, } = provider const workspaceId = useWorkspaceId() + const isMCP = isMCPProvider(provider) const { integration, integrationIsLoading, @@ -171,6 +181,42 @@ export function ProviderConfigForm({ return } + // For MCP providers, show a simplified message + if (isMCP) { + return ( +
+ + + MCP OAuth Provider + + +

+ This is a Model Context Protocol (MCP) provider that uses + server-managed OAuth credentials. No client configuration is + required - simply click "Connect" to authenticate. +

+ {defaultScopes && defaultScopes.length > 0 && ( +
+ +
+ {defaultScopes.map((scope) => ( + + {scope} + + ))} +
+

+ The authorization server will determine the granted scopes + based on your permissions. +

+
+ )} +
+
+
+ ) + } + return (
{/* Current Configuration Summary */} diff --git a/tracecat/integrations/providers/__init__.py b/tracecat/integrations/providers/__init__.py index c242eb20f3..602fc709c7 100644 --- a/tracecat/integrations/providers/__init__.py +++ b/tracecat/integrations/providers/__init__.py @@ -2,20 +2,32 @@ from tracecat.integrations.models import ProviderKey from tracecat.integrations.providers.base import BaseOAuthProvider +from tracecat.integrations.providers.github.mcp import GitHubMCPProvider +from tracecat.integrations.providers.linear.mcp import LinearMCPProvider from tracecat.integrations.providers.microsoft.graph import ( MicrosoftGraphACProvider, MicrosoftGraphCCProvider, ) +from tracecat.integrations.providers.microsoft.mcp import MicrosoftLearnMCPProvider from tracecat.integrations.providers.microsoft.teams import ( MicrosoftTeamsACProvider, MicrosoftTeamsCCProvider, ) +from tracecat.integrations.providers.notion.mcp import NotionMCPProvider +from tracecat.integrations.providers.runreveal.mcp import RunRevealMCPProvider +from tracecat.integrations.providers.sentry.mcp import SentryMCPProvider _PROVIDER_CLASSES: list[type[BaseOAuthProvider]] = [ MicrosoftGraphACProvider, MicrosoftGraphCCProvider, MicrosoftTeamsACProvider, MicrosoftTeamsCCProvider, + MicrosoftLearnMCPProvider, + GitHubMCPProvider, + LinearMCPProvider, + NotionMCPProvider, + RunRevealMCPProvider, + SentryMCPProvider, ] diff --git a/tracecat/integrations/providers/base.py b/tracecat/integrations/providers/base.py index 59cc9f173f..935eb7f16d 100644 --- a/tracecat/integrations/providers/base.py +++ b/tracecat/integrations/providers/base.py @@ -2,7 +2,9 @@ from abc import ABC from typing import Any, ClassVar, Self, cast +from urllib.parse import urlparse +import httpx from authlib.integrations.httpx_client import AsyncOAuth2Client from pydantic import BaseModel, SecretStr @@ -63,10 +65,13 @@ def __init__( client_kwargs = { "client_id": self.client_id, "client_secret": self.client_secret, - "scope": " ".join(self.requested_scopes), "grant_type": self.grant_type, } + # Only add scope if not empty + if self.requested_scopes: + client_kwargs["scope"] = " ".join(self.requested_scopes) + # Let subclasses add grant-specific parameters client_kwargs.update(self._get_client_kwargs()) @@ -269,3 +274,113 @@ async def get_client_credentials_token(self) -> TokenResponse: error=str(e), ) raise + + +class MCPAuthProvider(AuthorizationCodeOAuthProvider): + """Base OAuth provider for Model Context Protocol (MCP) servers using OAuth 2.1. + + MCP OAuth follows OAuth 2.1 standards with: + - PKCE required for authorization code flow + - Resource parameter to identify the MCP server + - Flexible scope handling (server determines granted scopes) + - Dynamic discovery of OAuth endpoints + - Optional dynamic client registration + """ + + _mcp_server_uri: ClassVar[str] + + def __init__(self, **kwargs): + """Initialize MCP provider with dynamic endpoint discovery.""" + # Initialize logger early for discovery + self.logger = logger.bind(service=f"{self.__class__.__name__}") + + # Discover OAuth endpoints before parent initialization + self._discover_oauth_endpoints() + super().__init__(**kwargs) + + @property + def authorization_endpoint(self) -> str: + """Return the discovered authorization endpoint.""" + return self._discovered_auth_endpoint + + @property + def token_endpoint(self) -> str: + """Return the discovered token endpoint.""" + return self._discovered_token_endpoint + + def _get_base_url(self) -> str: + """Extract base URL from MCP server URI.""" + parsed = urlparse(self._mcp_server_uri) + return f"{parsed.scheme}://{parsed.netloc}" + + def _discover_oauth_endpoints(self) -> None: + """Discover OAuth endpoints from .well-known configuration with fallback support.""" + base_url = self._get_base_url() + discovery_url = f"{base_url}/.well-known/oauth-authorization-server" + + try: + # Synchronous discovery during initialization + with httpx.Client() as client: + response = client.get(discovery_url, timeout=10.0) + response.raise_for_status() + discovery_doc = response.json() + + # Store discovered endpoints as instance variables + self._discovered_auth_endpoint = discovery_doc["authorization_endpoint"] + self._discovered_token_endpoint = discovery_doc["token_endpoint"] + + # Store registration endpoint if available + self._registration_endpoint = discovery_doc.get("registration_endpoint") + + self.logger.info( + "Discovered OAuth endpoints", + provider=self.id, + authorization=self._discovered_auth_endpoint, + token=self._discovered_token_endpoint, + ) + except Exception as e: + # Check if subclass provides fallback endpoints + if hasattr(self, "_fallback_auth_endpoint") and hasattr( + self, "_fallback_token_endpoint" + ): + self._discovered_auth_endpoint = self._fallback_auth_endpoint + self._discovered_token_endpoint = self._fallback_token_endpoint + self.logger.info( + "Using fallback OAuth endpoints", + provider=self.id, + authorization=self._discovered_auth_endpoint, + token=self._discovered_token_endpoint, + ) + else: + self.logger.error( + "Failed to discover OAuth endpoints", + provider=self.id, + error=str(e), + discovery_url=discovery_url, + ) + raise ValueError( + f"Could not discover OAuth endpoints from {discovery_url} " + f"and no fallback endpoints provided" + ) from e + + def _use_pkce(self) -> bool: + """PKCE is mandatory for OAuth 2.1 compliance.""" + return True + + def _get_additional_authorize_params(self) -> dict[str, Any]: + """Add MCP-specific authorization parameters. + + The resource parameter identifies the MCP server that the token will be used with. + """ + params = super()._get_additional_authorize_params() + params["resource"] = self._mcp_server_uri + return params + + def _get_additional_token_params(self) -> dict[str, Any]: + """Add MCP-specific token exchange parameters. + + The resource parameter must be included in token requests per MCP spec. + """ + params = super()._get_additional_token_params() + params["resource"] = self._mcp_server_uri + return params diff --git a/tracecat/integrations/providers/github/mcp.py b/tracecat/integrations/providers/github/mcp.py new file mode 100644 index 0000000000..a40354826f --- /dev/null +++ b/tracecat/integrations/providers/github/mcp.py @@ -0,0 +1,52 @@ +"""GitHub Copilot MCP OAuth integration using Model Context Protocol.""" + +from typing import ClassVar + +from tracecat.integrations.models import ProviderMetadata, ProviderScopes +from tracecat.integrations.providers.base import MCPAuthProvider + + +class GitHubMCPProvider(MCPAuthProvider): + """GitHub Copilot OAuth provider for Model Context Protocol integration. + + This provider enables integration with GitHub Copilot's MCP server for: + - Code assistance and suggestions + - Repository context understanding + - Development workflow automation + + Uses fallback OAuth endpoints since GitHub doesn't support discovery. + """ + + id: ClassVar[str] = "github_mcp" + + # MCP server endpoint + _mcp_server_uri: ClassVar[str] = "https://api.githubcopilot.com/mcp" + + # Fallback OAuth endpoints (GitHub doesn't support discovery) + _fallback_auth_endpoint: ClassVar[str] = "https://github.com/login/oauth/authorize" + _fallback_token_endpoint: ClassVar[str] = ( + "https://github.com/login/oauth/access_token" + ) + + # No default scopes - authorization server determines based on user permissions + scopes: ClassVar[ProviderScopes] = ProviderScopes(default=[]) + + # Provider metadata + metadata: ClassVar[ProviderMetadata] = ProviderMetadata( + id="github_mcp", + name="GitHub Copilot MCP", + description="GitHub Copilot MCP provider for repo and code access", + enabled=True, + requires_config=False, + setup_instructions=( + "Connect to GitHub Copilot MCP to enable AI-powered code assistance and repository context. " + "Permissions are automatically determined based on your GitHub account and organization settings." + ), + setup_steps=[ + "Click 'Connect' to begin OAuth authorization", + "Authenticate with your GitHub account", + "Review and approve the OAuth client permissions", + "Complete authorization to enable MCP integration", + ], + api_docs_url="https://docs.github.com/en/copilot", + ) diff --git a/tracecat/integrations/providers/linear/mcp.py b/tracecat/integrations/providers/linear/mcp.py new file mode 100644 index 0000000000..ea616ee8f0 --- /dev/null +++ b/tracecat/integrations/providers/linear/mcp.py @@ -0,0 +1,46 @@ +"""Linear MCP OAuth integration using Model Context Protocol.""" + +from typing import ClassVar + +from tracecat.integrations.models import ProviderMetadata, ProviderScopes +from tracecat.integrations.providers.base import MCPAuthProvider + + +class LinearMCPProvider(MCPAuthProvider): + """Linear OAuth provider for Model Context Protocol integration. + + This provider enables integration with Linear's MCP server for: + - Accessing and managing issues, projects, and teams + - Running GraphQL queries against Linear's API + - Automating workflows and issue management + + OAuth endpoints are automatically discovered from the server. + """ + + id: ClassVar[str] = "linear_mcp" + + # MCP server endpoint - OAuth endpoints discovered automatically + _mcp_server_uri: ClassVar[str] = "https://mcp.linear.app/mcp" + + # No default scopes - authorization server determines based on user permissions + scopes: ClassVar[ProviderScopes] = ProviderScopes(default=[]) + + # Provider metadata + metadata: ClassVar[ProviderMetadata] = ProviderMetadata( + id="linear_mcp", + name="Linear MCP", + description="Linear MCP providerfor issue tracking and project management", + enabled=True, + requires_config=False, + setup_instructions=( + "Connect to Linear MCP to access issues, projects, and teams. " + "Permissions are automatically determined based on your Linear workspace access." + ), + setup_steps=[ + "Click 'Connect' to begin OAuth authorization", + "Select your Linear workspace if prompted", + "Review and approve the OAuth client permissions", + "Complete authorization to enable MCP integration", + ], + api_docs_url="https://linear.app/docs/mcp", + ) diff --git a/tracecat/integrations/providers/microsoft/mcp.py b/tracecat/integrations/providers/microsoft/mcp.py new file mode 100644 index 0000000000..b8db4695b8 --- /dev/null +++ b/tracecat/integrations/providers/microsoft/mcp.py @@ -0,0 +1,56 @@ +"""Microsoft Learn MCP OAuth integration using Model Context Protocol.""" + +from typing import ClassVar + +from tracecat.integrations.models import ProviderMetadata, ProviderScopes +from tracecat.integrations.providers.base import MCPAuthProvider + + +class MicrosoftLearnMCPProvider(MCPAuthProvider): + """Microsoft Learn OAuth provider for Model Context Protocol integration. + + This provider enables integration with Microsoft Learn's MCP server for: + - Real-time access to official Microsoft documentation + - AI-powered documentation search and retrieval + - Technical knowledge from Microsoft's documentation library + + Uses Microsoft Entra ID (Azure AD) for authentication. + Uses fallback OAuth endpoints since discovery is not supported. + """ + + id: ClassVar[str] = "microsoft_learn_mcp" + + # MCP server endpoint + _mcp_server_uri: ClassVar[str] = "https://learn.microsoft.com/api/mcp" + + # Microsoft Entra ID OAuth endpoints (fallback since discovery isn't supported) + _fallback_auth_endpoint: ClassVar[str] = ( + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" + ) + _fallback_token_endpoint: ClassVar[str] = ( + "https://login.microsoftonline.com/common/oauth2/v2.0/token" + ) + + # No default scopes - authorization server determines based on user permissions + scopes: ClassVar[ProviderScopes] = ProviderScopes(default=[]) + + # Provider metadata + metadata: ClassVar[ProviderMetadata] = ProviderMetadata( + id="microsoft_learn_mcp", + name="Microsoft Learn MCP", + description="Microsoft Learn MCP provider for Learn knowlege services.", + enabled=True, + requires_config=False, + setup_instructions=( + "Connect to Microsoft Learn MCP to access real-time Microsoft documentation. " + "This integration provides AI assistance with official Microsoft technical documentation. " + "Authentication is handled through Microsoft Entra ID (Azure AD)." + ), + setup_steps=[ + "Click 'Connect' to begin OAuth authorization", + "Sign in with your Microsoft account", + "Review and approve the OAuth permissions", + "Complete authorization to enable Microsoft Learn MCP integration", + ], + api_docs_url="https://learn.microsoft.com/en-us/training/support/mcp", + ) diff --git a/tracecat/integrations/providers/notion/mcp.py b/tracecat/integrations/providers/notion/mcp.py new file mode 100644 index 0000000000..dbd83e09dc --- /dev/null +++ b/tracecat/integrations/providers/notion/mcp.py @@ -0,0 +1,46 @@ +"""Notion MCP OAuth integration using Model Context Protocol.""" + +from typing import ClassVar + +from tracecat.integrations.models import ProviderMetadata, ProviderScopes +from tracecat.integrations.providers.base import MCPAuthProvider + + +class NotionMCPProvider(MCPAuthProvider): + """Notion OAuth provider for Model Context Protocol integration. + + This provider enables AI-powered integration with Notion workspaces for: + - Reading and writing pages, databases, and comments + - AI-optimized Markdown-based content retrieval + - Dynamic workspace access based on user permissions + + OAuth endpoints are automatically discovered from the server. + """ + + id: ClassVar[str] = "notion_mcp" + + # MCP server endpoint - OAuth endpoints discovered automatically + _mcp_server_uri: ClassVar[str] = "https://mcp.notion.com/mcp" + + # No default scopes - authorization server determines based on user permissions + scopes: ClassVar[ProviderScopes] = ProviderScopes(default=[]) + + # Provider metadata + metadata: ClassVar[ProviderMetadata] = ProviderMetadata( + id="notion_mcp", + name="Notion MCP", + description="Notion MCP provider for Notion workspace access", + enabled=True, + requires_config=False, + setup_instructions=( + "Connect to Notion MCP to enable AI tools to interact with your Notion workspace. " + "Full read and write access to pages, databases, and comments based on your permissions." + ), + setup_steps=[ + "Click 'Connect' to begin OAuth authorization", + "Select your Notion workspace", + "Review and approve the permissions", + "Complete authorization to enable MCP integration", + ], + api_docs_url="https://developers.notion.com/docs/mcp", + ) diff --git a/tracecat/integrations/providers/runreveal/mcp.py b/tracecat/integrations/providers/runreveal/mcp.py new file mode 100644 index 0000000000..88716df0f6 --- /dev/null +++ b/tracecat/integrations/providers/runreveal/mcp.py @@ -0,0 +1,47 @@ +"""RunReveal MCP OAuth integration using Model Context Protocol.""" + +from typing import ClassVar + +from tracecat.integrations.models import ProviderMetadata, ProviderScopes +from tracecat.integrations.providers.base import MCPAuthProvider + + +class RunRevealMCPProvider(MCPAuthProvider): + """RunReveal OAuth provider for Model Context Protocol integration. + + This provider enables integration with RunReveal's MCP server for: + - Running queries and detections + - Accessing table schemas + - Managing detection configurations + + Permissions are determined by the user's role in the selected workspace. + OAuth endpoints are automatically discovered from the server. + """ + + id: ClassVar[str] = "runreveal_mcp" + + # MCP server endpoint - OAuth endpoints discovered automatically + _mcp_server_uri: ClassVar[str] = "https://api.runreveal.com/mcp" + + # No default scopes - authorization server determines based on user/workspace permissions + scopes: ClassVar[ProviderScopes] = ProviderScopes(default=[]) + + # Provider metadata + metadata: ClassVar[ProviderMetadata] = ProviderMetadata( + id="runreveal_mcp", + name="RunReveal MCP", + description="RunReveal MCP provider for security data analysis", + enabled=True, + requires_config=False, + setup_instructions=( + "Connect to RunReveal MCP to access queries, detections, and table schemas. " + "Permissions are automatically determined based on your workspace role." + ), + setup_steps=[ + "Click 'Connect' to begin OAuth authorization", + "Select your RunReveal workspace", + "Review and approve the OAuth client permissions", + "Complete authorization to enable MCP integration", + ], + api_docs_url="https://docs.runreveal.com/ai-chat/model-context-protocol", + ) diff --git a/tracecat/integrations/providers/sentry/mcp.py b/tracecat/integrations/providers/sentry/mcp.py new file mode 100644 index 0000000000..a11fb29ca2 --- /dev/null +++ b/tracecat/integrations/providers/sentry/mcp.py @@ -0,0 +1,48 @@ +"""Sentry MCP OAuth integration using Model Context Protocol.""" + +from typing import ClassVar + +from tracecat.integrations.models import ProviderMetadata, ProviderScopes +from tracecat.integrations.providers.base import MCPAuthProvider + + +class SentryMCPProvider(MCPAuthProvider): + """Sentry OAuth provider for Model Context Protocol integration. + + This provider enables integration with Sentry's MCP server for: + - Accessing and managing error tracking and performance monitoring + - Querying issues, events, and performance data + - Managing projects, teams, and organizations + - Analyzing error patterns and performance metrics + + OAuth endpoints are automatically discovered from the server. + """ + + id: ClassVar[str] = "sentry_mcp" + + # MCP server endpoint - OAuth endpoints discovered automatically + _mcp_server_uri: ClassVar[str] = "https://mcp.sentry.dev/mcp" + + # No default scopes - authorization server determines based on user permissions + scopes: ClassVar[ProviderScopes] = ProviderScopes(default=[]) + + # Provider metadata + metadata: ClassVar[ProviderMetadata] = ProviderMetadata( + id="sentry_mcp", + name="Sentry MCP", + description="Sentry MCP provider for issues tracking and performance monitoring", + enabled=True, + requires_config=False, + setup_instructions=( + "Connect to Sentry MCP to access issues and performance monitoring. " + "Permissions are automatically determined based on your Sentry organization access." + ), + setup_steps=[ + "Click 'Connect' to begin OAuth authorization", + "Authenticate with your Sentry account", + "Select your Sentry organization if prompted", + "Review and approve the OAuth client permissions", + "Complete authorization to enable MCP integration", + ], + api_docs_url="https://docs.sentry.io/product/sentry-mcp/", + )