mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-22 20:00:29 +01:00
Compare commits
3 Commits
json-featu
...
feat/mcp-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4572f3c0b | ||
|
|
bdd155e59a | ||
|
|
498bf54204 |
52
frontend/public/locales/en-GB/mcpServer.json
Normal file
52
frontend/public/locales/en-GB/mcpServer.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"page_title": "SigNoz MCP Server",
|
||||
"page_subtitle": "Connect AI assistants like Claude, Cursor, VS Code, and Codex to your SigNoz data via the Model Context Protocol. Authenticate from your MCP client with a service-account API key.",
|
||||
|
||||
"fallback_title": "MCP Server is available on SigNoz Cloud",
|
||||
"fallback_body": "This in-product setup guide is for SigNoz Cloud. Self-hosted users can follow the docs to run the MCP server against their instance.",
|
||||
"fallback_docs_link": "View MCP Server docs",
|
||||
|
||||
"region_warning_prefix": "We couldn't detect your Cloud region from this URL. Find it at ",
|
||||
"region_warning_link": "Settings → Ingestion",
|
||||
"region_warning_suffix": " (the label between your workspace name and signoz.cloud in the ingestion URL), then enter it here.",
|
||||
"region_input_label": "SigNoz Cloud region",
|
||||
"region_input_placeholder": "Your SigNoz Cloud region",
|
||||
|
||||
"step1_title": "Configure your client",
|
||||
"step1_description": "Add SigNoz to your MCP client. Use a one-click install where available, or copy the config for manual setup. On first connect, the client will open a SigNoz authorization page — use the instance URL and API key from step 2.",
|
||||
"step1_manual_fallback": "Or copy the config below for manual setup.",
|
||||
"step1_client_docs_suffix": " setup docs",
|
||||
"step1_add_to_client_prefix": "Add to ",
|
||||
|
||||
"client_cursor_install_label": "Add to Cursor",
|
||||
"client_vscode_install_label": "Add to VS Code",
|
||||
"client_claude_desktop_instructions": "Open Claude Desktop, go to Settings → Connectors → Add custom connector, and paste the endpoint URL above. Claude Desktop does not read remote MCP servers from claude_desktop_config.json — the connector UI is the only supported path.",
|
||||
"client_other_instructions": "Most MCP clients that support remote HTTP servers will accept the endpoint URL above. Add it as a new MCP server in your client and paste your SigNoz API key when the client prompts for authentication. See the docs for client-specific instructions.",
|
||||
|
||||
"step2_title": "Authenticate from your client",
|
||||
"step2_description": "On first connect, your client opens a SigNoz authorization page asking for two values:",
|
||||
"step2_instance_url_label": "SigNoz Instance URL",
|
||||
"step2_api_key_label": "API Key",
|
||||
"step2_admin_cta": "Create service account",
|
||||
"step2_admin_helper": "Create a service account, then add a new key inside it — paste that key into the API Key field.",
|
||||
"step2_viewer_helper": "Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.",
|
||||
|
||||
"use_cases_title": "What you can do with it",
|
||||
"use_cases_item_1": "Ask your AI assistant to investigate a spiking error rate.",
|
||||
"use_cases_item_2": "Debug a slow service by walking through recent traces.",
|
||||
"use_cases_item_3": "Summarize an alert and suggest likely root causes.",
|
||||
"use_cases_item_4": "Generate dashboards or queries from a natural-language description.",
|
||||
"use_cases_docs_link": "See more use cases",
|
||||
|
||||
"copy_tooltip_enabled": "Copy to clipboard",
|
||||
"copy_tooltip_disabled": "Enter your Cloud region first",
|
||||
"copy_aria_endpoint": "Copy MCP endpoint",
|
||||
"copy_aria_instance_url": "Copy SigNoz instance URL",
|
||||
"copy_aria_snippet_prefix": "Copy ",
|
||||
"copy_aria_snippet_suffix": " config",
|
||||
|
||||
"toast_endpoint_copied": "Endpoint copied to clipboard",
|
||||
"toast_snippet_copied": "Snippet copied to clipboard",
|
||||
"toast_instance_url_copied": "Instance URL copied to clipboard",
|
||||
"toast_region_required": "Enter your Cloud region before copying"
|
||||
}
|
||||
@@ -16,5 +16,6 @@
|
||||
"roles": "Roles",
|
||||
"role_details": "Role Details",
|
||||
"members": "Members",
|
||||
"service_accounts": "Service Accounts"
|
||||
"service_accounts": "Service Accounts",
|
||||
"mcp_server": "MCP Server"
|
||||
}
|
||||
|
||||
@@ -53,5 +53,6 @@
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
}
|
||||
|
||||
52
frontend/public/locales/en/mcpServer.json
Normal file
52
frontend/public/locales/en/mcpServer.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"page_title": "SigNoz MCP Server",
|
||||
"page_subtitle": "Connect AI assistants like Claude, Cursor, VS Code, and Codex to your SigNoz data via the Model Context Protocol. Authenticate from your MCP client with a service-account API key.",
|
||||
|
||||
"fallback_title": "MCP Server is available on SigNoz Cloud",
|
||||
"fallback_body": "This in-product setup guide is for SigNoz Cloud. Self-hosted users can follow the docs to run the MCP server against their instance.",
|
||||
"fallback_docs_link": "View MCP Server docs",
|
||||
|
||||
"region_warning_prefix": "We couldn't detect your Cloud region from this URL. Find it at ",
|
||||
"region_warning_link": "Settings → Ingestion",
|
||||
"region_warning_suffix": " (the label between your workspace name and signoz.cloud in the ingestion URL), then enter it here.",
|
||||
"region_input_label": "SigNoz Cloud region",
|
||||
"region_input_placeholder": "Your SigNoz Cloud region",
|
||||
|
||||
"step1_title": "Configure your client",
|
||||
"step1_description": "Add SigNoz to your MCP client. Use a one-click install where available, or copy the config for manual setup. On first connect, the client will open a SigNoz authorization page — use the instance URL and API key from step 2.",
|
||||
"step1_manual_fallback": "Or copy the config below for manual setup.",
|
||||
"step1_client_docs_suffix": " setup docs",
|
||||
"step1_add_to_client_prefix": "Add to ",
|
||||
|
||||
"client_cursor_install_label": "Add to Cursor",
|
||||
"client_vscode_install_label": "Add to VS Code",
|
||||
"client_claude_desktop_instructions": "Open Claude Desktop, go to Settings → Connectors → Add custom connector, and paste the endpoint URL above. Claude Desktop does not read remote MCP servers from claude_desktop_config.json — the connector UI is the only supported path.",
|
||||
"client_other_instructions": "Most MCP clients that support remote HTTP servers will accept the endpoint URL above. Add it as a new MCP server in your client and paste your SigNoz API key when the client prompts for authentication. See the docs for client-specific instructions.",
|
||||
|
||||
"step2_title": "Authenticate from your client",
|
||||
"step2_description": "On first connect, your client opens a SigNoz authorization page asking for two values:",
|
||||
"step2_instance_url_label": "SigNoz Instance URL",
|
||||
"step2_api_key_label": "API Key",
|
||||
"step2_admin_cta": "Create service account",
|
||||
"step2_admin_helper": "Create a service account, then add a new key inside it — paste that key into the API Key field.",
|
||||
"step2_viewer_helper": "Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.",
|
||||
|
||||
"use_cases_title": "What you can do with it",
|
||||
"use_cases_item_1": "Ask your AI assistant to investigate a spiking error rate.",
|
||||
"use_cases_item_2": "Debug a slow service by walking through recent traces.",
|
||||
"use_cases_item_3": "Summarize an alert and suggest likely root causes.",
|
||||
"use_cases_item_4": "Generate dashboards or queries from a natural-language description.",
|
||||
"use_cases_docs_link": "See more use cases",
|
||||
|
||||
"copy_tooltip_enabled": "Copy to clipboard",
|
||||
"copy_tooltip_disabled": "Enter your Cloud region first",
|
||||
"copy_aria_endpoint": "Copy MCP endpoint",
|
||||
"copy_aria_instance_url": "Copy SigNoz instance URL",
|
||||
"copy_aria_snippet_prefix": "Copy ",
|
||||
"copy_aria_snippet_suffix": " config",
|
||||
|
||||
"toast_endpoint_copied": "Endpoint copied to clipboard",
|
||||
"toast_snippet_copied": "Snippet copied to clipboard",
|
||||
"toast_instance_url_copied": "Instance URL copied to clipboard",
|
||||
"toast_region_required": "Enter your Cloud region before copying"
|
||||
}
|
||||
@@ -16,5 +16,6 @@
|
||||
"roles": "Roles",
|
||||
"role_details": "Role Details",
|
||||
"members": "Members",
|
||||
"service_accounts": "Service Accounts"
|
||||
"service_accounts": "Service Accounts",
|
||||
"mcp_server": "MCP Server"
|
||||
}
|
||||
|
||||
@@ -76,5 +76,6 @@
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ const ROUTES = {
|
||||
HOME_PAGE: '/',
|
||||
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
|
||||
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',
|
||||
MCP_SERVER: '/settings/mcp-server',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
.mcp-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
max-width: 55rem;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--l0-foreground);
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
border: 0.0625rem solid var(--l3-background);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
&__card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--l0-foreground);
|
||||
}
|
||||
|
||||
&__step-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-robin-400);
|
||||
color: var(--l0-background);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__card-description {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
&__endpoint-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__endpoint-field {
|
||||
flex: 1;
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
&__endpoint-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 0.0625rem solid var(--l3-background);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--l2-background);
|
||||
font-family: var(--font-family-monospace, monospace);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--l0-foreground);
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
cursor: pointer;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
&__region-warning {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border: 0.0625rem solid var(--bg-amber-500);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--bg-amber-500-a10, rgba(255, 193, 7, 0.08));
|
||||
font-size: 0.75rem;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.125rem;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--bg-amber-500);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__region-card,
|
||||
&__cta-card {
|
||||
padding: 1rem 1.25rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
&__auth-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
&__auth-field-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--l2-foreground);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
&__info-banner-inline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-left: 0.1875rem solid var(--bg-robin-400);
|
||||
background: var(--l2-background);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--bg-robin-400);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__endpoint-input {
|
||||
margin-top: 0.5rem;
|
||||
font-family: var(--font-family-monospace, monospace);
|
||||
}
|
||||
|
||||
&__cta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__helper-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.125rem;
|
||||
}
|
||||
|
||||
&__tabs-container {
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__snippet-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&__install-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&__snippet-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__use-cases {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
&-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
|
||||
li {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--l1-foreground);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
border: 0.0625rem dashed var(--l3-background);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--l1-background);
|
||||
max-width: 40rem;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--l0-foreground);
|
||||
}
|
||||
|
||||
&-body {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
536
frontend/src/container/MCPServerSettings/MCPServerSettings.tsx
Normal file
536
frontend/src/container/MCPServerSettings/MCPServerSettings.tsx
Normal file
@@ -0,0 +1,536 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { TFunction, useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button, Input, Tabs, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import LearnMore from 'components/LearnMore/LearnMore';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import {
|
||||
Copy,
|
||||
Download,
|
||||
Info,
|
||||
KeyRound,
|
||||
Sparkles,
|
||||
TriangleAlert,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import {
|
||||
docsUrl,
|
||||
MCP_CLIENTS,
|
||||
MCP_DOCS_URL,
|
||||
MCP_USE_CASES_URL,
|
||||
McpClient,
|
||||
} from './clients';
|
||||
import {
|
||||
buildMcpEndpoint,
|
||||
getCloudRegion,
|
||||
normalizeRegion,
|
||||
parseRegionFromUrl,
|
||||
} from './getCloudRegion';
|
||||
|
||||
import './MCPServerSettings.styles.scss';
|
||||
|
||||
const ANALYTICS = {
|
||||
PAGE_VIEWED: 'MCP Settings: Page viewed',
|
||||
CREATE_SA_CLICKED: 'MCP Settings: Create service account clicked',
|
||||
CLIENT_TAB_SELECTED: 'MCP Settings: Client tab selected',
|
||||
SNIPPET_COPIED: 'MCP Settings: Client snippet copied',
|
||||
ONE_CLICK_INSTALL_CLICKED: 'MCP Settings: One-click install clicked',
|
||||
INSTANCE_URL_COPIED: 'MCP Settings: Instance URL copied',
|
||||
DOCS_LINK_CLICKED: 'MCP Settings: Docs link clicked',
|
||||
} as const;
|
||||
|
||||
const ENDPOINT_PLACEHOLDER = 'https://mcp.<region>.signoz.cloud/mcp';
|
||||
|
||||
function NotCloudFallback(): JSX.Element {
|
||||
const { t } = useTranslation('mcpServer');
|
||||
const onClick = useCallback(() => {
|
||||
logEvent(ANALYTICS.DOCS_LINK_CLICKED, { target: 'fallback' });
|
||||
}, []);
|
||||
return (
|
||||
<div className="mcp-settings">
|
||||
<div className="mcp-settings__fallback">
|
||||
<div className="mcp-settings__fallback-title">
|
||||
<Sparkles size={18} /> {t('fallback_title')}
|
||||
</div>
|
||||
<Typography.Text className="mcp-settings__fallback-body">
|
||||
{t('fallback_body')}
|
||||
</Typography.Text>
|
||||
<LearnMore
|
||||
text={t('fallback_docs_link')}
|
||||
url={MCP_DOCS_URL}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CopyIconButtonProps {
|
||||
ariaLabel: string;
|
||||
onCopy: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function CopyIconButton({
|
||||
ariaLabel,
|
||||
onCopy,
|
||||
disabled,
|
||||
}: CopyIconButtonProps): JSX.Element {
|
||||
const { t } = useTranslation('mcpServer');
|
||||
const tooltipTitle = disabled
|
||||
? t('copy_tooltip_disabled')
|
||||
: t('copy_tooltip_enabled');
|
||||
const button = (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
className="mcp-settings__copy-btn"
|
||||
icon={<Copy size={14} />}
|
||||
onClick={onCopy}
|
||||
/>
|
||||
);
|
||||
// Ant Design Tooltip doesn't reliably surface for a disabled Button —
|
||||
// wrap in a span so hover/focus still reaches the Tooltip.
|
||||
return (
|
||||
<Tooltip title={tooltipTitle}>
|
||||
{disabled ? <span>{button}</span> : button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
CopyIconButton.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
interface RegionFallbackCardProps {
|
||||
manualRegion: string;
|
||||
onRegionChange: (value: string) => void;
|
||||
onIngestionLinkClick: () => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
function RegionFallbackCard({
|
||||
manualRegion,
|
||||
onRegionChange,
|
||||
onIngestionLinkClick,
|
||||
t,
|
||||
}: RegionFallbackCardProps): JSX.Element {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => onRegionChange(e.target.value),
|
||||
[onRegionChange],
|
||||
);
|
||||
return (
|
||||
<div className="mcp-settings__card mcp-settings__region-card">
|
||||
<div className="mcp-settings__region-warning">
|
||||
<TriangleAlert size={14} />
|
||||
<span>
|
||||
{t('region_warning_prefix')}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
className="mcp-settings__inline-link"
|
||||
onClick={onIngestionLinkClick}
|
||||
>
|
||||
{t('region_warning_link')}
|
||||
</Button>
|
||||
{t('region_warning_suffix')}
|
||||
</span>
|
||||
</div>
|
||||
<label
|
||||
className="mcp-settings__auth-field-label"
|
||||
htmlFor="mcp-settings-manual-region"
|
||||
>
|
||||
{t('region_input_label')}
|
||||
</label>
|
||||
<Input
|
||||
id="mcp-settings-manual-region"
|
||||
className="mcp-settings__endpoint-input"
|
||||
size="small"
|
||||
value={manualRegion}
|
||||
placeholder={t('region_input_placeholder')}
|
||||
aria-label={t('region_input_label')}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ClientTabsProps {
|
||||
endpoint: string;
|
||||
activeTab: string;
|
||||
onTabChange: (key: string) => void;
|
||||
onCopySnippet: (clientKey: string, snippet: string) => void;
|
||||
onInstallClick: (clientKey: string) => void;
|
||||
onDocsLinkClick: (target: string) => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
interface ClientTabChildrenProps {
|
||||
client: McpClient;
|
||||
endpoint: string;
|
||||
onCopySnippet: (clientKey: string, snippet: string) => void;
|
||||
onInstallClick: (clientKey: string) => void;
|
||||
onDocsLinkClick: (target: string) => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
function ClientTabChildren({
|
||||
client,
|
||||
endpoint,
|
||||
onCopySnippet,
|
||||
onInstallClick,
|
||||
onDocsLinkClick,
|
||||
t,
|
||||
}: ClientTabChildrenProps): JSX.Element {
|
||||
const snippet = client.snippet
|
||||
? client.snippet(endpoint || ENDPOINT_PLACEHOLDER)
|
||||
: null;
|
||||
const installHref =
|
||||
client.installUrl && endpoint ? client.installUrl(endpoint) : null;
|
||||
|
||||
const handleInstallClick = useCallback(() => onInstallClick(client.key), [
|
||||
onInstallClick,
|
||||
client.key,
|
||||
]);
|
||||
const handleDocsClick = useCallback(
|
||||
() => onDocsLinkClick(`client-${client.key}`),
|
||||
[onDocsLinkClick, client.key],
|
||||
);
|
||||
const handleSnippetCopy = useCallback(() => {
|
||||
if (snippet) {
|
||||
onCopySnippet(client.key, snippet);
|
||||
}
|
||||
}, [onCopySnippet, client.key, snippet]);
|
||||
|
||||
const installLabel = client.installLabelKey
|
||||
? t(client.installLabelKey)
|
||||
: `${t('step1_add_to_client_prefix')}${client.label}`;
|
||||
const instructions = client.instructionsKey ? t(client.instructionsKey) : '';
|
||||
|
||||
return (
|
||||
<div className="mcp-settings__snippet-wrapper">
|
||||
{client.installUrl && (
|
||||
<div className="mcp-settings__install-row">
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!installHref}
|
||||
icon={<Download size={14} />}
|
||||
href={installHref ?? undefined}
|
||||
onClick={handleInstallClick}
|
||||
>
|
||||
{installLabel}
|
||||
</Button>
|
||||
<Typography.Text className="mcp-settings__helper-text">
|
||||
{t('step1_manual_fallback')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
{snippet !== null ? (
|
||||
<div className="mcp-settings__endpoint-value mcp-settings__snippet">
|
||||
<pre className="mcp-settings__snippet-pre">{snippet}</pre>
|
||||
<CopyIconButton
|
||||
ariaLabel={`${t('copy_aria_snippet_prefix')}${client.label}${t(
|
||||
'copy_aria_snippet_suffix',
|
||||
)}`}
|
||||
disabled={!endpoint}
|
||||
onCopy={handleSnippetCopy}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Text className="mcp-settings__card-description">
|
||||
{instructions}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<LearnMore
|
||||
text={`${client.label}${t('step1_client_docs_suffix')}`}
|
||||
url={docsUrl(client.docsPath)}
|
||||
onClick={handleDocsClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientTabs({
|
||||
endpoint,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
onCopySnippet,
|
||||
onInstallClick,
|
||||
onDocsLinkClick,
|
||||
t,
|
||||
}: ClientTabsProps): JSX.Element {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
MCP_CLIENTS.map((client: McpClient) => ({
|
||||
key: client.key,
|
||||
label: client.label,
|
||||
children: (
|
||||
<ClientTabChildren
|
||||
client={client}
|
||||
endpoint={endpoint}
|
||||
onCopySnippet={onCopySnippet}
|
||||
onInstallClick={onInstallClick}
|
||||
onDocsLinkClick={onDocsLinkClick}
|
||||
t={t}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[endpoint, onCopySnippet, onInstallClick, onDocsLinkClick, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className="mcp-settings__tabs-container"
|
||||
activeKey={activeTab}
|
||||
onChange={onTabChange}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface AuthCardProps {
|
||||
isAdmin: boolean;
|
||||
instanceUrl: string;
|
||||
onCopyInstanceUrl: () => void;
|
||||
onCreateServiceAccount: () => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
function AuthCard({
|
||||
isAdmin,
|
||||
instanceUrl,
|
||||
onCopyInstanceUrl,
|
||||
onCreateServiceAccount,
|
||||
t,
|
||||
}: AuthCardProps): JSX.Element {
|
||||
return (
|
||||
<section className="mcp-settings__card mcp-settings__cta-card">
|
||||
<h3 className="mcp-settings__card-title">
|
||||
<span className="mcp-settings__step-badge">2</span> {t('step2_title')}
|
||||
</h3>
|
||||
<Typography.Text className="mcp-settings__card-description">
|
||||
{t('step2_description')}
|
||||
</Typography.Text>
|
||||
<div className="mcp-settings__auth-field">
|
||||
<Typography.Text className="mcp-settings__auth-field-label">
|
||||
{t('step2_instance_url_label')}
|
||||
</Typography.Text>
|
||||
<div className="mcp-settings__endpoint-value">
|
||||
<span data-testid="mcp-instance-url">{instanceUrl}</span>
|
||||
<CopyIconButton
|
||||
ariaLabel={t('copy_aria_instance_url')}
|
||||
onCopy={onCopyInstanceUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mcp-settings__auth-field">
|
||||
<Typography.Text className="mcp-settings__auth-field-label">
|
||||
{t('step2_api_key_label')}
|
||||
</Typography.Text>
|
||||
{isAdmin ? (
|
||||
<div className="mcp-settings__cta-row">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<KeyRound size={14} />}
|
||||
onClick={onCreateServiceAccount}
|
||||
>
|
||||
{t('step2_admin_cta')}
|
||||
</Button>
|
||||
<Typography.Text className="mcp-settings__helper-text">
|
||||
{t('step2_admin_helper')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mcp-settings__info-banner-inline">
|
||||
<Info size={14} />
|
||||
<Typography.Text className="mcp-settings__helper-text">
|
||||
{t('step2_viewer_helper')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface UseCasesCardProps {
|
||||
onDocsLinkClick: (target: string) => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
function UseCasesCard({ onDocsLinkClick, t }: UseCasesCardProps): JSX.Element {
|
||||
const handleClick = useCallback(() => onDocsLinkClick('use-cases'), [
|
||||
onDocsLinkClick,
|
||||
]);
|
||||
return (
|
||||
<section className="mcp-settings__card mcp-settings__use-cases">
|
||||
<h3 className="mcp-settings__card-title">{t('use_cases_title')}</h3>
|
||||
<ul className="mcp-settings__use-cases-list">
|
||||
<li>{t('use_cases_item_1')}</li>
|
||||
<li>{t('use_cases_item_2')}</li>
|
||||
<li>{t('use_cases_item_3')}</li>
|
||||
<li>{t('use_cases_item_4')}</li>
|
||||
</ul>
|
||||
<LearnMore
|
||||
text={t('use_cases_docs_link')}
|
||||
url={MCP_USE_CASES_URL}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MCPServerSettings(): JSX.Element {
|
||||
const { t } = useTranslation('mcpServer');
|
||||
const { user } = useAppContext();
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
const { notifications } = useNotifications();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const isAdmin = user.role === USER_ROLES.ADMIN;
|
||||
const instanceUrl = window.location.origin;
|
||||
|
||||
const { data: globalConfig } = useGetGlobalConfig();
|
||||
const regionFromHost = useMemo(() => getCloudRegion(), []);
|
||||
const regionFromIngestion = useMemo(
|
||||
() =>
|
||||
globalConfig?.data?.ingestion_url
|
||||
? parseRegionFromUrl(globalConfig.data.ingestion_url)
|
||||
: null,
|
||||
[globalConfig?.data?.ingestion_url],
|
||||
);
|
||||
|
||||
const autoDetectedRegion = regionFromHost.region ?? regionFromIngestion;
|
||||
|
||||
const [manualRegion, setManualRegion] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<string>(MCP_CLIENTS[0]?.key ?? '');
|
||||
|
||||
const resolvedRegion: string | null =
|
||||
autoDetectedRegion ?? normalizeRegion(manualRegion);
|
||||
|
||||
const endpoint = resolvedRegion ? buildMcpEndpoint(resolvedRegion) : '';
|
||||
|
||||
// Fire once on mount so we reliably capture every visit, even if the
|
||||
// globalConfig fetch is slow or fails. Region is best-effort from the
|
||||
// hostname at mount time; if ingestion_url resolves later we skip logging
|
||||
// again to avoid double-fires.
|
||||
useEffect(() => {
|
||||
logEvent(ANALYTICS.PAGE_VIEWED, {
|
||||
isCloudUser,
|
||||
role: user.role,
|
||||
region: regionFromHost.region,
|
||||
isAutoDetected: Boolean(regionFromHost.region),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleCopySnippet = useCallback(
|
||||
(clientKey: string, snippet: string) => {
|
||||
if (!endpoint) {
|
||||
notifications.warning({ message: t('toast_region_required') });
|
||||
return;
|
||||
}
|
||||
copyToClipboard(snippet);
|
||||
notifications.success({ message: t('toast_snippet_copied') });
|
||||
logEvent(ANALYTICS.SNIPPET_COPIED, { client: clientKey });
|
||||
},
|
||||
[endpoint, copyToClipboard, notifications, t],
|
||||
);
|
||||
|
||||
const handleCreateServiceAccount = useCallback(() => {
|
||||
logEvent(ANALYTICS.CREATE_SA_CLICKED, {});
|
||||
history.push(
|
||||
`${ROUTES.SERVICE_ACCOUNTS_SETTINGS}?${SA_QUERY_PARAMS.CREATE_SA}=true`,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleCopyInstanceUrl = useCallback(() => {
|
||||
copyToClipboard(instanceUrl);
|
||||
notifications.success({ message: t('toast_instance_url_copied') });
|
||||
logEvent(ANALYTICS.INSTANCE_URL_COPIED, {});
|
||||
}, [copyToClipboard, instanceUrl, notifications, t]);
|
||||
|
||||
const handleDocsLinkClick = useCallback((target: string) => {
|
||||
logEvent(ANALYTICS.DOCS_LINK_CLICKED, { target });
|
||||
}, []);
|
||||
|
||||
const handleIngestionLinkClick = useCallback(() => {
|
||||
logEvent(ANALYTICS.DOCS_LINK_CLICKED, { target: 'ingestion-settings' });
|
||||
history.push(ROUTES.INGESTION_SETTINGS);
|
||||
}, []);
|
||||
|
||||
const handleInstallClick = useCallback((clientKey: string) => {
|
||||
logEvent(ANALYTICS.ONE_CLICK_INSTALL_CLICKED, { client: clientKey });
|
||||
}, []);
|
||||
|
||||
const handleTabChange = useCallback((key: string) => {
|
||||
setActiveTab(key);
|
||||
logEvent(ANALYTICS.CLIENT_TAB_SELECTED, { client: key });
|
||||
}, []);
|
||||
|
||||
if (!isCloudUser) {
|
||||
return <NotCloudFallback />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mcp-settings" data-testid="mcp-settings">
|
||||
<header className="mcp-settings__header">
|
||||
<h2 className="mcp-settings__header-title">
|
||||
<Sparkles size={20} /> {t('page_title')}
|
||||
</h2>
|
||||
<Typography.Text className="mcp-settings__header-subtitle">
|
||||
{t('page_subtitle')}
|
||||
</Typography.Text>
|
||||
</header>
|
||||
|
||||
{!autoDetectedRegion && (
|
||||
<RegionFallbackCard
|
||||
manualRegion={manualRegion}
|
||||
onRegionChange={setManualRegion}
|
||||
onIngestionLinkClick={handleIngestionLinkClick}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
<section className="mcp-settings__card">
|
||||
<h3 className="mcp-settings__card-title">
|
||||
<span className="mcp-settings__step-badge">1</span> {t('step1_title')}
|
||||
</h3>
|
||||
<Typography.Text className="mcp-settings__card-description">
|
||||
{t('step1_description')}
|
||||
</Typography.Text>
|
||||
<ClientTabs
|
||||
endpoint={endpoint}
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
onCopySnippet={handleCopySnippet}
|
||||
onInstallClick={handleInstallClick}
|
||||
onDocsLinkClick={handleDocsLinkClick}
|
||||
t={t}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AuthCard
|
||||
isAdmin={isAdmin}
|
||||
instanceUrl={instanceUrl}
|
||||
onCopyInstanceUrl={handleCopyInstanceUrl}
|
||||
onCreateServiceAccount={handleCreateServiceAccount}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<UseCasesCard onDocsLinkClick={handleDocsLinkClick} t={t} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MCPServerSettings;
|
||||
111
frontend/src/container/MCPServerSettings/clients.ts
Normal file
111
frontend/src/container/MCPServerSettings/clients.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { DOCS_BASE_URL } from 'constants/app';
|
||||
|
||||
export interface McpClient {
|
||||
key: string;
|
||||
// `label` is the client brand name (Cursor, VS Code, Claude Desktop …).
|
||||
// Brand names are not translated.
|
||||
label: string;
|
||||
docsPath: string;
|
||||
snippet: ((endpoint: string) => string) | null;
|
||||
// i18n key under the `mcpServer` namespace. Resolved at render time via t().
|
||||
instructionsKey?: string;
|
||||
installUrl?: (endpoint: string) => string;
|
||||
// i18n key for the install button label. Falls back to
|
||||
// `step1_add_to_client_prefix` + `label` when not set.
|
||||
installLabelKey?: string;
|
||||
}
|
||||
|
||||
function b64url(input: string): string {
|
||||
if (typeof btoa === 'function') {
|
||||
return btoa(input);
|
||||
}
|
||||
// fallback for non-browser TS contexts (never hit at runtime)
|
||||
return Buffer.from(input, 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
export const MCP_CLIENTS: McpClient[] = [
|
||||
{
|
||||
key: 'cursor',
|
||||
label: 'Cursor',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#cursor',
|
||||
snippet: (endpoint): string =>
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
signoz: {
|
||||
url: endpoint,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
installUrl: (endpoint): string => {
|
||||
const config = b64url(JSON.stringify({ url: endpoint }));
|
||||
return `cursor://anysphere.cursor-deeplink/mcp/install?name=SigNoz&config=${config}`;
|
||||
},
|
||||
installLabelKey: 'client_cursor_install_label',
|
||||
},
|
||||
{
|
||||
key: 'vscode',
|
||||
label: 'VS Code',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#vs-code',
|
||||
snippet: (endpoint): string =>
|
||||
JSON.stringify(
|
||||
{
|
||||
servers: {
|
||||
signoz: {
|
||||
type: 'http',
|
||||
url: endpoint,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
installUrl: (endpoint): string => {
|
||||
const payload = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
name: 'signoz',
|
||||
config: { type: 'http', url: endpoint },
|
||||
}),
|
||||
);
|
||||
return `vscode:mcp/install?${payload}`;
|
||||
},
|
||||
installLabelKey: 'client_vscode_install_label',
|
||||
},
|
||||
{
|
||||
key: 'claude-desktop',
|
||||
label: 'Claude Desktop',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#claude-desktop',
|
||||
snippet: null,
|
||||
instructionsKey: 'client_claude_desktop_instructions',
|
||||
},
|
||||
{
|
||||
key: 'claude-code',
|
||||
label: 'Claude Code',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#claude-code',
|
||||
snippet: (endpoint): string =>
|
||||
`claude mcp add --scope user --transport http signoz ${endpoint}`,
|
||||
},
|
||||
{
|
||||
key: 'codex',
|
||||
label: 'Codex',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#codex',
|
||||
snippet: (endpoint): string => `codex mcp add signoz --url ${endpoint}`,
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
label: 'Other',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/',
|
||||
snippet: null,
|
||||
instructionsKey: 'client_other_instructions',
|
||||
},
|
||||
];
|
||||
|
||||
export function docsUrl(path: string): string {
|
||||
return `${DOCS_BASE_URL}${path}`;
|
||||
}
|
||||
|
||||
export const MCP_DOCS_URL = `${DOCS_BASE_URL}/docs/ai/signoz-mcp-server/`;
|
||||
export const MCP_USE_CASES_URL = `${DOCS_BASE_URL}/docs/ai/use-cases/`;
|
||||
51
frontend/src/container/MCPServerSettings/getCloudRegion.ts
Normal file
51
frontend/src/container/MCPServerSettings/getCloudRegion.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface CloudRegionResult {
|
||||
region: string | null;
|
||||
isKnown: boolean;
|
||||
}
|
||||
|
||||
const VALID_REGION_LABEL = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
export function parseRegionFromSignozCloudHost(host: string): string | null {
|
||||
const parts = host.split('.');
|
||||
const len = parts.length;
|
||||
// SigNoz Cloud tenant hosts follow `<tenant>.<region>.signoz.cloud`
|
||||
// (4 labels). 3-label hosts like `app.signoz.cloud` would wrongly
|
||||
// resolve to region=`app`, so require at least 4 labels.
|
||||
if (len < 4 || parts[len - 1] !== 'cloud' || parts[len - 2] !== 'signoz') {
|
||||
return null;
|
||||
}
|
||||
const region = parts[len - 3]?.toLowerCase() ?? '';
|
||||
if (!VALID_REGION_LABEL.test(region)) {
|
||||
return null;
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
export function parseRegionFromUrl(url: string): string | null {
|
||||
try {
|
||||
return parseRegionFromSignozCloudHost(new URL(url).hostname);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCloudRegion(hostname: string): CloudRegionResult {
|
||||
const region = parseRegionFromSignozCloudHost(hostname);
|
||||
return region ? { region, isKnown: true } : { region: null, isKnown: false };
|
||||
}
|
||||
|
||||
export function normalizeRegion(input: string): string | null {
|
||||
const value = input.trim().toLowerCase();
|
||||
if (!VALID_REGION_LABEL.test(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function buildMcpEndpoint(region: string): string {
|
||||
return `https://mcp.${region}.signoz.cloud/mcp`;
|
||||
}
|
||||
|
||||
export function getCloudRegion(): CloudRegionResult {
|
||||
return parseCloudRegion(window.location.hostname);
|
||||
}
|
||||
@@ -192,7 +192,8 @@ const onboardingConfigWithLinks = [
|
||||
'setup',
|
||||
],
|
||||
imgUrl: signozBrandLogoUrl,
|
||||
link: '/docs/ai/signoz-mcp-server/',
|
||||
link: '/settings/mcp-server',
|
||||
internalRedirect: true,
|
||||
},
|
||||
{
|
||||
dataSource: 'migrate-from-datadog',
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
Settings,
|
||||
Shield,
|
||||
Slack,
|
||||
Sparkles,
|
||||
Unplug,
|
||||
User,
|
||||
UserPlus,
|
||||
@@ -337,6 +338,13 @@ export const settingsNavSections: SettingsNavSection[] = [
|
||||
isEnabled: false,
|
||||
itemKey: 'integrations',
|
||||
},
|
||||
{
|
||||
key: ROUTES.MCP_SERVER,
|
||||
label: 'MCP Server',
|
||||
icon: <Sparkles size={16} />,
|
||||
isEnabled: false,
|
||||
itemKey: 'mcp-server',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ export const routesToSkip = [
|
||||
ROUTES.ORG_SETTINGS,
|
||||
ROUTES.MEMBERS_SETTINGS,
|
||||
ROUTES.SERVICE_ACCOUNTS_SETTINGS,
|
||||
ROUTES.MCP_SERVER,
|
||||
ROUTES.INGESTION_SETTINGS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.LOGS_PIPELINES,
|
||||
|
||||
@@ -75,6 +75,11 @@ function SettingsPage(): JSX.Element {
|
||||
}
|
||||
|
||||
if (isCloudUser) {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
isEnabled: item.key === ROUTES.MCP_SERVER ? true : item.isEnabled,
|
||||
}));
|
||||
|
||||
if (isAdmin) {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
|
||||
@@ -8,6 +8,7 @@ import GeneralSettings from 'container/GeneralSettings';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
|
||||
import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSettings';
|
||||
import MCPServerSettings from 'container/MCPServerSettings/MCPServerSettings';
|
||||
import MySettings from 'container/MySettings';
|
||||
import OrganizationSettings from 'container/OrganizationSettings';
|
||||
import RolesSettings from 'container/RolesSettings';
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
Pencil,
|
||||
Plus,
|
||||
Shield,
|
||||
Sparkles,
|
||||
User,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
@@ -205,6 +207,19 @@ export const serviceAccountsSettings = (
|
||||
},
|
||||
];
|
||||
|
||||
export const mcpServerSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: MCPServerSettings,
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<Sparkles size={16} /> {t('routes:mcp_server').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.MCP_SERVER,
|
||||
key: ROUTES.MCP_SERVER,
|
||||
},
|
||||
];
|
||||
|
||||
export const createAlertChannels = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: (): JSX.Element => (
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
generalSettings,
|
||||
ingestionSettings,
|
||||
keyboardShortcuts,
|
||||
mcpServerSettings,
|
||||
membersSettings,
|
||||
multiIngestionSettings,
|
||||
mySettings,
|
||||
@@ -79,6 +80,10 @@ export const getRoutes = (
|
||||
...createAlertChannels(t),
|
||||
...editAlertChannels(t),
|
||||
...keyboardShortcuts(t),
|
||||
// Route is registered for everyone so direct-URL visitors see the
|
||||
// in-page fallback. Sidebar visibility is still Cloud-only, gated in
|
||||
// Settings.tsx.
|
||||
...mcpServerSettings(t),
|
||||
);
|
||||
|
||||
return settings;
|
||||
|
||||
@@ -132,4 +132,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
METER_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
PUBLIC_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ALERT_TYPE_SELECTION: ['ADMIN', 'EDITOR'],
|
||||
MCP_SERVER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user