mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Add SaaS MCP usage tab (#6590)
This commit is contained in:
@@ -3049,6 +3049,37 @@ impact = "Any applications or services currently using these keys will stop work
|
|||||||
title = "Refresh API Keys"
|
title = "Refresh API Keys"
|
||||||
warning = "⚠️ Warning: This action will generate new API keys and make your previous keys invalid."
|
warning = "⚠️ Warning: This action will generate new API keys and make your previous keys invalid."
|
||||||
|
|
||||||
|
[config.mcp]
|
||||||
|
description = "Model Context Protocol (MCP) lets AI assistants like Claude use your Stirling PDF tools directly. Connect a client once and your assistant can convert, edit, secure and process documents on your behalf."
|
||||||
|
navLabel = "MCP Server"
|
||||||
|
tip = "Every action your assistant runs is performed as your account and counts towards your usage, just like using Stirling PDF directly."
|
||||||
|
title = "MCP Server"
|
||||||
|
viewApiKeys = "View API keys"
|
||||||
|
|
||||||
|
[config.mcp.copy]
|
||||||
|
configLabel = "Config"
|
||||||
|
copied = "Copied"
|
||||||
|
copy = "Copy"
|
||||||
|
endpointLabel = "Endpoint URL"
|
||||||
|
tooltip = "Copy {{label}}"
|
||||||
|
tooltipCopied = "{{label}} copied"
|
||||||
|
|
||||||
|
[config.mcp.endpoint]
|
||||||
|
label = "Your MCP endpoint"
|
||||||
|
|
||||||
|
[config.mcp.setup]
|
||||||
|
addTo = "Add to"
|
||||||
|
hint = "Pick your client, paste the snippet into the file shown, then restart it. You'll sign in with your Stirling account on first use - no keys to copy."
|
||||||
|
title = "Connect your AI assistant"
|
||||||
|
|
||||||
|
[config.mcp.tools]
|
||||||
|
ai = "AI"
|
||||||
|
convert = "Convert"
|
||||||
|
misc = "Misc"
|
||||||
|
pages = "Pages"
|
||||||
|
security = "Security"
|
||||||
|
title = "What your assistant can do"
|
||||||
|
|
||||||
[config.overview]
|
[config.overview]
|
||||||
description = "Current application settings and configuration details."
|
description = "Current application settings and configuration details."
|
||||||
error = "Error"
|
error = "Error"
|
||||||
@@ -3210,7 +3241,6 @@ creditsRemaining = "{{current}} of {{total}} remaining"
|
|||||||
creditsThisMonth = "Monthly credits"
|
creditsThisMonth = "Monthly credits"
|
||||||
current = "Current Plan"
|
current = "Current Plan"
|
||||||
customPricing = "Custom"
|
customPricing = "Custom"
|
||||||
customWatchedFolders = "Custom Watched Folders"
|
|
||||||
dedicatedSupportSlas = "Dedicated support & SLAs"
|
dedicatedSupportSlas = "Dedicated support & SLAs"
|
||||||
enterpriseSubscription = "Enterprise"
|
enterpriseSubscription = "Enterprise"
|
||||||
everythingInCredits = "Everything in Credits, plus:"
|
everythingInCredits = "Everything in Credits, plus:"
|
||||||
@@ -3518,7 +3548,6 @@ addFiles = "Add Files"
|
|||||||
|
|
||||||
[fileManager]
|
[fileManager]
|
||||||
active = "Active"
|
active = "Active"
|
||||||
addToWatchedFolder = "Add to Watched Folder"
|
|
||||||
addToUpload = "Add to Upload"
|
addToUpload = "Add to Upload"
|
||||||
changesNotUploaded = "Changes not uploaded"
|
changesNotUploaded = "Changes not uploaded"
|
||||||
clearAll = "Clear All"
|
clearAll = "Clear All"
|
||||||
@@ -4563,10 +4592,8 @@ sendResetLink = "Send reset link"
|
|||||||
sessionExpired = "Your session has expired. Please sign in again."
|
sessionExpired = "Your session has expired. Please sign in again."
|
||||||
createAccount = "Create an account"
|
createAccount = "Create an account"
|
||||||
signin = "Sign in"
|
signin = "Sign in"
|
||||||
signInAnonymously = "Sign Up as a Guest"
|
|
||||||
signingIn = "Signing in..."
|
signingIn = "Signing in..."
|
||||||
signInWith = "Sign in with"
|
signInWith = "Sign in with"
|
||||||
subtitle = "Sign back in to Stirling PDF"
|
|
||||||
title = "Sign in"
|
title = "Sign in"
|
||||||
unexpectedError = "Unexpected error: {{message}}"
|
unexpectedError = "Unexpected error: {{message}}"
|
||||||
updatePassword = "Update password"
|
updatePassword = "Update password"
|
||||||
@@ -5782,7 +5809,6 @@ label = "SMTP Username"
|
|||||||
|
|
||||||
[quickAccess]
|
[quickAccess]
|
||||||
access = "Access"
|
access = "Access"
|
||||||
watchedFolders = "Watched Folders"
|
|
||||||
accessAddPerson = "Add another person"
|
accessAddPerson = "Add another person"
|
||||||
accessBack = "Back"
|
accessBack = "Back"
|
||||||
accessCopyLink = "Copy link"
|
accessCopyLink = "Copy link"
|
||||||
@@ -6873,6 +6899,7 @@ createFailed = "Failed to create signing request"
|
|||||||
|
|
||||||
[signup]
|
[signup]
|
||||||
accountCreatedSuccessfully = "Account created successfully! You can now sign in."
|
accountCreatedSuccessfully = "Account created successfully! You can now sign in."
|
||||||
|
alreadyHaveAccount = "I already have an account"
|
||||||
checkEmailConfirmation = "Check your email for a confirmation link to complete your registration."
|
checkEmailConfirmation = "Check your email for a confirmation link to complete your registration."
|
||||||
confirmPassword = "Confirm password"
|
confirmPassword = "Confirm password"
|
||||||
confirmPasswordPlaceholder = "Confirm password"
|
confirmPasswordPlaceholder = "Confirm password"
|
||||||
@@ -6898,7 +6925,6 @@ skip = "Skip"
|
|||||||
subtitle = "Join Stirling PDF to get started"
|
subtitle = "Join Stirling PDF to get started"
|
||||||
title = "Create an account"
|
title = "Create an account"
|
||||||
unexpectedError = "Unexpected error: {{message}}"
|
unexpectedError = "Unexpected error: {{message}}"
|
||||||
useEmailInstead = "Use Email Instead"
|
|
||||||
|
|
||||||
[sizes]
|
[sizes]
|
||||||
large = "Large"
|
large = "Large"
|
||||||
@@ -8172,20 +8198,14 @@ newFolder = "New folder"
|
|||||||
noFolders = "No watched folders yet"
|
noFolders = "No watched folders yet"
|
||||||
sidebarTitle = "Watched Folders"
|
sidebarTitle = "Watched Folders"
|
||||||
title = "Watched Folders"
|
title = "Watched Folders"
|
||||||
sidebarFiles = "My Files"
|
|
||||||
|
|
||||||
[watchedFolders.modal]
|
[watchedFolders.modal]
|
||||||
automation = "Automation"
|
automation = "Automation"
|
||||||
automationSaved = "Automation saved"
|
|
||||||
createFolder = "Create Folder"
|
createFolder = "Create Folder"
|
||||||
stepsSaved = "Steps saved — click Create Folder to finish"
|
|
||||||
automationRequired = "Add at least one configured step before saving."
|
automationRequired = "Add at least one configured step before saving."
|
||||||
color = "Accent color"
|
color = "Accent color"
|
||||||
createTitle = "New Watched Folder"
|
createTitle = "New Watched Folder"
|
||||||
description = "Description"
|
|
||||||
descriptionPlaceholder = "What does this folder do?"
|
|
||||||
editTitle = "Edit Watched Folder"
|
editTitle = "Edit Watched Folder"
|
||||||
icon = "Icon"
|
|
||||||
name = "Folder name"
|
name = "Folder name"
|
||||||
namePlaceholder = "My Watched Folder"
|
namePlaceholder = "My Watched Folder"
|
||||||
nameRequired = "Folder name is required"
|
nameRequired = "Folder name is required"
|
||||||
@@ -8193,17 +8213,13 @@ nameTooLong = "Folder name must be 50 characters or less"
|
|||||||
saveChanges = "Save Changes"
|
saveChanges = "Save Changes"
|
||||||
saveFailed = "Failed to save folder. Please try again."
|
saveFailed = "Failed to save folder. Please try again."
|
||||||
automationNameFallback = "Watched Folder automation"
|
automationNameFallback = "Watched Folder automation"
|
||||||
retryLabel = "Auto-retry"
|
|
||||||
maxRetries = "Max retries"
|
maxRetries = "Max retries"
|
||||||
maxRetriesDesc = "0 to disable"
|
|
||||||
retryDelay = "Delay (minutes)"
|
retryDelay = "Delay (minutes)"
|
||||||
outputLabel = "Output"
|
|
||||||
outputModeVersion = "Replace original?"
|
outputModeVersion = "Replace original?"
|
||||||
outputModeVersionDesc = "Output becomes a new version of the input file rather than a separate file"
|
outputModeVersionDesc = "Output becomes a new version of the input file rather than a separate file"
|
||||||
outputModeNewDesc = "Output is saved as a new separate file"
|
outputModeNewDesc = "Output is saved as a new separate file"
|
||||||
outputName = "Output filename prefix"
|
outputName = "Output filename prefix"
|
||||||
outputNameSuffix = "Suffix"
|
outputNameSuffix = "Suffix"
|
||||||
outputNamePlaceholderVersion = "Same as original"
|
|
||||||
sectionFolder = "Folder"
|
sectionFolder = "Folder"
|
||||||
sectionSourceOutput = "Source & Output"
|
sectionSourceOutput = "Source & Output"
|
||||||
sectionSteps = "Steps"
|
sectionSteps = "Steps"
|
||||||
@@ -8230,14 +8246,10 @@ positionSuffix = "Suffix"
|
|||||||
|
|
||||||
[watchedFolders.home]
|
[watchedFolders.home]
|
||||||
create = "Create your first Watched Folder"
|
create = "Create your first Watched Folder"
|
||||||
dropHere = "Drop to process"
|
|
||||||
editFolder = "Edit folder"
|
editFolder = "Edit folder"
|
||||||
empty = "No watched folders yet"
|
empty = "No watched folders yet"
|
||||||
file = "file"
|
file = "file"
|
||||||
files = "files"
|
files = "files"
|
||||||
openFolder = "Open folder"
|
|
||||||
title = "Watched Folders"
|
|
||||||
subtitle = "Folders that automatically process PDFs with your configured pipeline"
|
|
||||||
emptyTitle = "Automate your PDF workflows"
|
emptyTitle = "Automate your PDF workflows"
|
||||||
emptyDesc = "Set up a Watched Folder once. Drop PDFs in and they're automatically compressed, OCR'd, split, merged — whatever your pipeline does."
|
emptyDesc = "Set up a Watched Folder once. Drop PDFs in and they're automatically compressed, OCR'd, split, merged — whatever your pipeline does."
|
||||||
addAnother = "Add another Watched Folder"
|
addAnother = "Add another Watched Folder"
|
||||||
@@ -8245,7 +8257,6 @@ addAnotherDesc = "Automatically process files with a new pipeline"
|
|||||||
resume = "Resume"
|
resume = "Resume"
|
||||||
pause = "Pause"
|
pause = "Pause"
|
||||||
deleteFolder = "Delete folder"
|
deleteFolder = "Delete folder"
|
||||||
noSteps = "No automation steps configured"
|
|
||||||
|
|
||||||
[watchedFolders.card]
|
[watchedFolders.card]
|
||||||
edit = "Edit folder"
|
edit = "Edit folder"
|
||||||
@@ -8328,7 +8339,6 @@ previewLoadFailed = "Could not load file preview."
|
|||||||
back = "Back"
|
back = "Back"
|
||||||
dismiss = "Dismiss"
|
dismiss = "Dismiss"
|
||||||
retry = "Retry"
|
retry = "Retry"
|
||||||
view = "View"
|
|
||||||
download = "Download"
|
download = "Download"
|
||||||
downloadInput = "Download input"
|
downloadInput = "Download input"
|
||||||
downloadOutput = "Download output"
|
downloadOutput = "Download output"
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Paper,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
Alert,
|
||||||
|
Code,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
CopyButton,
|
||||||
|
Tabs,
|
||||||
|
Tooltip,
|
||||||
|
ThemeIcon,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||||
|
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||||
|
import { openAppSettings } from "@app/utils/appSettings";
|
||||||
|
|
||||||
|
/** Strip a single trailing slash so we can safely append paths. */
|
||||||
|
function trimTrailingSlash(url: string): string {
|
||||||
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A small copy-to-clipboard button that sits inline next to a URL/snippet. */
|
||||||
|
function CopyInline({ value, label }: { value: string; label: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<CopyButton value={value} timeout={1500}>
|
||||||
|
{({ copied, copy }) => (
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
copied
|
||||||
|
? t("config.mcp.copy.tooltipCopied", "{{label}} copied", {
|
||||||
|
label,
|
||||||
|
})
|
||||||
|
: t("config.mcp.copy.tooltip", "Copy {{label}}", { label })
|
||||||
|
}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="compact-xs"
|
||||||
|
variant="subtle"
|
||||||
|
color={copied ? "teal" : "gray"}
|
||||||
|
onClick={copy}
|
||||||
|
leftSection={
|
||||||
|
<LocalIcon
|
||||||
|
icon={copied ? "check-rounded" : "content-copy-rounded"}
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{copied
|
||||||
|
? t("config.mcp.copy.copied", "Copied")
|
||||||
|
: t("config.mcp.copy.copy", "Copy")}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</CopyButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaaS MCP guide: always shown, explains how to point an AI assistant at the
|
||||||
|
// OAuth-protected /mcp endpoint with per-client config.
|
||||||
|
export default function McpSection() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { config } = useAppConfig();
|
||||||
|
|
||||||
|
const baseUrl = useMemo(() => {
|
||||||
|
const raw =
|
||||||
|
config?.baseUrl ||
|
||||||
|
(typeof window !== "undefined" ? window.location.origin : "");
|
||||||
|
return trimTrailingSlash(raw);
|
||||||
|
}, [config?.baseUrl]);
|
||||||
|
|
||||||
|
const mcpUrl = `${baseUrl}/mcp`;
|
||||||
|
|
||||||
|
// Per-client connection snippets pointing at this deployment's /mcp endpoint.
|
||||||
|
const clients = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
value: "claude",
|
||||||
|
label: "Claude Desktop",
|
||||||
|
file: "claude_desktop_config.json",
|
||||||
|
config: JSON.stringify(
|
||||||
|
{ mcpServers: { "stirling-pdf": { type: "http", url: mcpUrl } } },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "codex",
|
||||||
|
label: "Codex CLI",
|
||||||
|
file: "~/.codex/config.toml",
|
||||||
|
config: `[mcp_servers.stirling-pdf]\nurl = "${mcpUrl}"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "vscode",
|
||||||
|
label: "VS Code",
|
||||||
|
file: ".vscode/mcp.json",
|
||||||
|
config: JSON.stringify(
|
||||||
|
{ servers: { "stirling-pdf": { type: "http", url: mcpUrl } } },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[mcpUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tools: { icon: string; key: string; fallback: string }[] = [
|
||||||
|
{
|
||||||
|
icon: "sync-alt-rounded",
|
||||||
|
key: "config.mcp.tools.convert",
|
||||||
|
fallback: "Convert",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "description-rounded",
|
||||||
|
key: "config.mcp.tools.pages",
|
||||||
|
fallback: "Pages",
|
||||||
|
},
|
||||||
|
{ icon: "lock", key: "config.mcp.tools.security", fallback: "Security" },
|
||||||
|
{
|
||||||
|
icon: "construction-rounded",
|
||||||
|
key: "config.mcp.tools.misc",
|
||||||
|
fallback: "Misc",
|
||||||
|
},
|
||||||
|
{ icon: "smart-toy-rounded", key: "config.mcp.tools.ai", fallback: "AI" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-section-container">
|
||||||
|
<Stack gap="md" className="settings-section-content">
|
||||||
|
<div>
|
||||||
|
<Group gap="sm" align="center">
|
||||||
|
<ThemeIcon variant="light" size="lg" radius="md">
|
||||||
|
<LocalIcon icon="smart-toy-rounded" width={22} height={22} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={600} size="lg">
|
||||||
|
{t("config.mcp.title", "MCP Server")}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" c="dimmed" mt={6}>
|
||||||
|
{t(
|
||||||
|
"config.mcp.description",
|
||||||
|
"Model Context Protocol (MCP) lets AI assistants like Claude use your Stirling PDF tools directly. Connect a client once and your assistant can convert, edit, secure and process documents on your behalf.",
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Endpoint */}
|
||||||
|
<Paper withBorder p="sm" radius="md">
|
||||||
|
<Group gap="xs" wrap="nowrap" align="center">
|
||||||
|
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
{t("config.mcp.endpoint.label", "Your MCP endpoint")}
|
||||||
|
</Text>
|
||||||
|
<Code style={{ overflowX: "auto" }}>{mcpUrl}</Code>
|
||||||
|
</Stack>
|
||||||
|
<CopyInline
|
||||||
|
value={mcpUrl}
|
||||||
|
label={t("config.mcp.copy.endpointLabel", "Endpoint URL")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Per-client setup */}
|
||||||
|
<Paper withBorder p="sm" radius="md">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
{t("config.mcp.setup.title", "Connect your AI assistant")}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t(
|
||||||
|
"config.mcp.setup.hint",
|
||||||
|
"Pick your client, paste the snippet into the file shown, then restart it. You'll sign in with your Stirling account on first use - no keys to copy.",
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Tabs defaultValue="claude" variant="pills" radius="md" mt={4}>
|
||||||
|
<Tabs.List>
|
||||||
|
{clients.map((c) => (
|
||||||
|
<Tabs.Tab key={c.value} value={c.value}>
|
||||||
|
{c.label}
|
||||||
|
</Tabs.Tab>
|
||||||
|
))}
|
||||||
|
</Tabs.List>
|
||||||
|
{clients.map((c) => (
|
||||||
|
<Tabs.Panel key={c.value} value={c.value} pt="sm">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t("config.mcp.setup.addTo", "Add to")}{" "}
|
||||||
|
<Code>{c.file}</Code>
|
||||||
|
</Text>
|
||||||
|
<CopyInline
|
||||||
|
value={c.config}
|
||||||
|
label={t("config.mcp.copy.configLabel", "Config")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Code block>{c.config}</Code>
|
||||||
|
</Stack>
|
||||||
|
</Tabs.Panel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Available tools */}
|
||||||
|
<div>
|
||||||
|
<Text fw={500} size="sm" mb="xs">
|
||||||
|
{t("config.mcp.tools.title", "What your assistant can do")}
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<Badge
|
||||||
|
key={tool.key}
|
||||||
|
variant="light"
|
||||||
|
color="gray"
|
||||||
|
radius="sm"
|
||||||
|
size="lg"
|
||||||
|
leftSection={
|
||||||
|
<LocalIcon icon={tool.icon} width={13} height={13} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(tool.key, tool.fallback)}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tip / cross-link */}
|
||||||
|
<Alert
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
icon={<LocalIcon icon="info-rounded" width="1rem" height="1rem" />}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap" gap="sm">
|
||||||
|
<Text size="sm">
|
||||||
|
{t(
|
||||||
|
"config.mcp.tip",
|
||||||
|
"Every action your assistant runs is performed as your account and counts towards your usage, just like using Stirling PDF directly.",
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
leftSection={
|
||||||
|
<LocalIcon icon="key-rounded" width={14} height={14} />
|
||||||
|
}
|
||||||
|
onClick={() => openAppSettings("api-keys")}
|
||||||
|
>
|
||||||
|
{t("config.mcp.viewApiKeys", "View API keys")}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import GeneralSection from "@app/components/shared/config/configSections/General
|
|||||||
import PasswordSecurity from "@app/components/shared/config/configSections/PasswordSecurity";
|
import PasswordSecurity from "@app/components/shared/config/configSections/PasswordSecurity";
|
||||||
import ApiKeys from "@app/components/shared/config/configSections/ApiKeys";
|
import ApiKeys from "@app/components/shared/config/configSections/ApiKeys";
|
||||||
import Plan from "@app/components/shared/config/configSections/Plan";
|
import Plan from "@app/components/shared/config/configSections/Plan";
|
||||||
|
import McpSection from "@app/components/shared/config/configSections/McpSection";
|
||||||
|
|
||||||
type OverviewComponent = React.ComponentType<{ onLogoutClick: () => void }>;
|
type OverviewComponent = React.ComponentType<{ onLogoutClick: () => void }>;
|
||||||
|
|
||||||
@@ -108,6 +109,44 @@ function appendBillingSection(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add an "MCP Server" tab in the Developer section. Always shown in SaaS;
|
||||||
|
// purely informational, so it appears for anonymous users too.
|
||||||
|
function appendMcpSection(
|
||||||
|
sections: ConfigNavSection[],
|
||||||
|
t: TFunction<"translation", undefined>,
|
||||||
|
): ConfigNavSection[] {
|
||||||
|
const hasMcp = sections.some((section) =>
|
||||||
|
section.items.some((item) => item.key === "mcp"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasMcp) {
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcpItem = {
|
||||||
|
key: "mcp" as const,
|
||||||
|
label: t("config.mcp.navLabel", "MCP Server"),
|
||||||
|
icon: "smart-toy-rounded",
|
||||||
|
component: <McpSection />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const developerIndex = sections.findIndex((section) =>
|
||||||
|
section.items.some(
|
||||||
|
(item) => item.key === "developer" || item.key === "api-keys",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (developerIndex === -1) {
|
||||||
|
return [...sections, { title: "Developer", items: [mcpItem] }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.map((section, index) =>
|
||||||
|
index === developerIndex
|
||||||
|
? { ...section, items: [...section.items, mcpItem] }
|
||||||
|
: section,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function createSaasConfigNavSections(
|
export function createSaasConfigNavSections(
|
||||||
Overview: OverviewComponent,
|
Overview: OverviewComponent,
|
||||||
onLogoutClick: () => void,
|
onLogoutClick: () => void,
|
||||||
@@ -151,6 +190,7 @@ export function createSaasConfigNavSections(
|
|||||||
|
|
||||||
sections = ensurePreferencesSection(sections);
|
sections = ensurePreferencesSection(sections);
|
||||||
sections = appendDeveloperSection(sections);
|
sections = appendDeveloperSection(sections);
|
||||||
|
sections = appendMcpSection(sections, t);
|
||||||
|
|
||||||
if (!isAnonymous) {
|
if (!isAnonymous) {
|
||||||
sections = appendBillingSection(sections, t);
|
sections = appendBillingSection(sections, t);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { VALID_NAV_KEYS as CORE_NAV_KEYS } from "@core/components/shared/config/types";
|
import { VALID_NAV_KEYS as CORE_NAV_KEYS } from "@core/components/shared/config/types";
|
||||||
|
|
||||||
// SaaS adds an "overview" account section. All other keys (including ones
|
// SaaS adds an "overview" account section and an "mcp" integrations tab. All
|
||||||
// SaaS doesn't render today) come from core - subtracting them here would
|
// other keys (including ones SaaS doesn't render today) come from core -
|
||||||
// just break the type union without affecting runtime nav.
|
// subtracting them here would just break the type union without affecting
|
||||||
export const VALID_NAV_KEYS = [...CORE_NAV_KEYS, "overview"] as const;
|
// runtime nav.
|
||||||
|
export const VALID_NAV_KEYS = [...CORE_NAV_KEYS, "overview", "mcp"] as const;
|
||||||
|
|
||||||
export type NavKey = (typeof VALID_NAV_KEYS)[number];
|
export type NavKey = (typeof VALID_NAV_KEYS)[number];
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ vi.mock("@app/auth/supabase", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Stub apiClient's heavy UI/util deps so importing it stays cheap. None are
|
||||||
|
// exercised here (toast, plan settings and handleHttpError paths aren't hit),
|
||||||
|
// and re-importing the real toast graph per test made these tests time out.
|
||||||
|
vi.mock("@app/components/toast", () => ({ alert: vi.fn() }));
|
||||||
|
vi.mock("@app/utils/appSettings", () => ({ openPlanSettings: vi.fn() }));
|
||||||
|
vi.mock("@app/services/httpErrorHandler", () => ({
|
||||||
|
handleHttpError: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("apiClient", () => {
|
describe("apiClient", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|||||||
Reference in New Issue
Block a user