@redocly/theme 0.69.0-next.0 → 0.69.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Buttons/ConnectMCPButton.js +43 -14
- package/lib/components/MCP/McpConnectionTabs.d.ts +7 -0
- package/lib/components/MCP/McpConnectionTabs.js +151 -0
- package/lib/components/Markdown/Markdown.js +1 -1
- package/lib/core/constants/accordion.d.ts +3 -0
- package/lib/core/constants/accordion.js +7 -0
- package/lib/core/constants/index.d.ts +1 -0
- package/lib/core/constants/index.js +1 -0
- package/lib/core/constants/mcp.d.ts +2 -0
- package/lib/core/constants/mcp.js +8 -1
- package/lib/core/hooks/use-active-section-id.js +3 -1
- package/lib/core/hooks/use-connect-mcp-button.d.ts +1 -1
- package/lib/core/hooks/use-connect-mcp-button.js +16 -13
- package/lib/core/styles/global.js +46 -39
- package/lib/core/types/catalog.d.ts +2 -0
- package/lib/core/types/l10n.d.ts +1 -1
- package/lib/core/types/mcp.d.ts +1 -1
- package/lib/core/utils/mcp.d.ts +5 -0
- package/lib/core/utils/mcp.js +35 -0
- package/lib/icons/CodexIcon/CodexIcon.d.ts +25 -0
- package/lib/icons/CodexIcon/CodexIcon.js +22 -0
- package/lib/layouts/McpNotAllowed.js +5 -3
- package/lib/markdoc/components/Accordion/Accordion.d.ts +9 -0
- package/lib/markdoc/components/Accordion/Accordion.js +132 -0
- package/lib/markdoc/components/Accordion/AccordionGroup.d.ts +5 -0
- package/lib/markdoc/components/Accordion/AccordionGroup.js +29 -0
- package/lib/markdoc/components/Accordion/variables.d.ts +1 -0
- package/lib/markdoc/components/Accordion/variables.js +40 -0
- package/lib/markdoc/components/default.d.ts +2 -0
- package/lib/markdoc/components/default.js +2 -0
- package/lib/markdoc/default.d.ts +4 -0
- package/lib/markdoc/default.js +4 -0
- package/lib/markdoc/tags/accordion-group.d.ts +2 -0
- package/lib/markdoc/tags/accordion-group.js +10 -0
- package/lib/markdoc/tags/accordion.d.ts +2 -0
- package/lib/markdoc/tags/accordion.js +14 -0
- package/lib/markdoc/tags/connect-mcp.js +2 -1
- package/package.json +2 -2
- package/src/components/Buttons/ConnectMCPButton.tsx +44 -14
- package/src/components/MCP/McpConnectionTabs.tsx +263 -0
- package/src/components/Markdown/Markdown.tsx +2 -1
- package/src/core/constants/accordion.ts +4 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/constants/mcp.ts +10 -0
- package/src/core/hooks/use-active-section-id.ts +3 -1
- package/src/core/hooks/use-connect-mcp-button.ts +23 -15
- package/src/core/styles/global.ts +8 -1
- package/src/core/types/catalog.ts +3 -0
- package/src/core/types/l10n.ts +17 -0
- package/src/core/types/mcp.ts +1 -1
- package/src/core/utils/mcp.ts +39 -0
- package/src/icons/CodexIcon/CodexIcon.tsx +23 -0
- package/src/layouts/McpNotAllowed.tsx +4 -2
- package/src/markdoc/components/Accordion/Accordion.tsx +128 -0
- package/src/markdoc/components/Accordion/AccordionGroup.tsx +41 -0
- package/src/markdoc/components/Accordion/variables.ts +37 -0
- package/src/markdoc/components/default.ts +2 -0
- package/src/markdoc/default.ts +4 -0
- package/src/markdoc/tags/accordion-group.ts +8 -0
- package/src/markdoc/tags/accordion.ts +12 -0
- package/src/markdoc/tags/connect-mcp.ts +3 -1
|
@@ -9,11 +9,14 @@ import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMen
|
|
|
9
9
|
import { Button } from '@redocly/theme/components/Button/Button';
|
|
10
10
|
import { CursorIcon } from '@redocly/theme/icons/CursorIcon/CursorIcon';
|
|
11
11
|
import { VSCodeIcon } from '@redocly/theme/icons/VSCodeIcon/VSCodeIcon';
|
|
12
|
+
import { ClaudeIcon } from '@redocly/theme/icons/ClaudeIcon/ClaudeIcon';
|
|
13
|
+
import { CodexIcon } from '@redocly/theme/icons/CodexIcon/CodexIcon';
|
|
12
14
|
import { CopyIcon } from '@redocly/theme/icons/CopyIcon/CopyIcon';
|
|
13
15
|
import { CheckmarkFilledIcon } from '@redocly/theme/icons/CheckmarkFilledIcon/CheckmarkFilledIcon';
|
|
14
16
|
import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
|
|
15
17
|
import { ChevronUpIcon } from '@redocly/theme/icons/ChevronUpIcon/ChevronUpIcon';
|
|
16
18
|
import { useConnectMCPButton, useOutsideClick, useThemeHooks } from '@redocly/theme/core/hooks';
|
|
19
|
+
import { DEFAULT_MCP_OPTIONS } from '@redocly/theme/core/constants';
|
|
17
20
|
|
|
18
21
|
type MenuOption = {
|
|
19
22
|
key: MCPOption;
|
|
@@ -41,6 +44,22 @@ const MENU_OPTIONS: MenuOption[] = [
|
|
|
41
44
|
descriptionTranslationKey: 'page.actions.connectMcp.vscodeDescription',
|
|
42
45
|
descriptionDefault: 'Install MCP server on VS Code',
|
|
43
46
|
},
|
|
47
|
+
{
|
|
48
|
+
key: 'claude-code',
|
|
49
|
+
icon: ClaudeIcon,
|
|
50
|
+
titleTranslationKey: 'page.actions.connectMcp.claudeCode',
|
|
51
|
+
titleDefault: 'Connect to Claude Code',
|
|
52
|
+
descriptionTranslationKey: 'page.actions.connectMcp.claudeCodeDescription',
|
|
53
|
+
descriptionDefault: 'Copy the Claude Code CLI command',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'codex',
|
|
57
|
+
icon: CodexIcon,
|
|
58
|
+
titleTranslationKey: 'page.actions.connectMcp.codex',
|
|
59
|
+
titleDefault: 'Connect to Codex CLI',
|
|
60
|
+
descriptionTranslationKey: 'page.actions.connectMcp.codexDescription',
|
|
61
|
+
descriptionDefault: 'Copy the Codex CLI command',
|
|
62
|
+
},
|
|
44
63
|
{
|
|
45
64
|
key: 'copy',
|
|
46
65
|
icon: CopyIcon,
|
|
@@ -51,6 +70,9 @@ const MENU_OPTIONS: MenuOption[] = [
|
|
|
51
70
|
},
|
|
52
71
|
];
|
|
53
72
|
|
|
73
|
+
// Copy actions keep the dropdown open so the copied checkmark is visible.
|
|
74
|
+
const COPY_OPTIONS: MCPOption[] = ['claude-code', 'codex', 'copy'];
|
|
75
|
+
|
|
54
76
|
export type ConnectMCPButtonProps = {
|
|
55
77
|
placement?: 'top' | 'bottom';
|
|
56
78
|
alignment?: 'start' | 'end';
|
|
@@ -60,14 +82,14 @@ export type ConnectMCPButtonProps = {
|
|
|
60
82
|
export function ConnectMCPButton({
|
|
61
83
|
placement = 'bottom',
|
|
62
84
|
alignment = 'end',
|
|
63
|
-
options =
|
|
85
|
+
options = DEFAULT_MCP_OPTIONS,
|
|
64
86
|
}: ConnectMCPButtonProps): JSX.Element | null {
|
|
65
87
|
const { useTranslate } = useThemeHooks();
|
|
66
88
|
const { translate } = useTranslate();
|
|
67
89
|
const [isOpen, setIsOpen] = useState(false);
|
|
68
90
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
69
91
|
|
|
70
|
-
const {
|
|
92
|
+
const { copiedOption, visibleOptions, handleAction } = useConnectMCPButton({ options });
|
|
71
93
|
|
|
72
94
|
useOutsideClick(wrapperRef, () => setIsOpen(false));
|
|
73
95
|
|
|
@@ -78,13 +100,13 @@ export function ConnectMCPButton({
|
|
|
78
100
|
if (!primaryOption) return null;
|
|
79
101
|
|
|
80
102
|
const PrimaryIcon = primaryOption.icon;
|
|
81
|
-
|
|
82
|
-
// primary button itself must reflect it.
|
|
83
|
-
const showPrimaryCopied = primaryOption.key === 'copy' && isCopied;
|
|
103
|
+
const showPrimaryCopied = copiedOption === primaryOption.key;
|
|
84
104
|
|
|
85
105
|
const handleMenuAction = (key: MCPOption) => {
|
|
86
106
|
handleAction(key);
|
|
87
|
-
|
|
107
|
+
if (!COPY_OPTIONS.includes(key)) {
|
|
108
|
+
setIsOpen(false);
|
|
109
|
+
}
|
|
88
110
|
};
|
|
89
111
|
|
|
90
112
|
return (
|
|
@@ -121,7 +143,7 @@ export function ConnectMCPButton({
|
|
|
121
143
|
<DropdownMenu>
|
|
122
144
|
{dropdownOptions.map((option) => {
|
|
123
145
|
const Icon = option.icon;
|
|
124
|
-
const showCheckmark = option.key
|
|
146
|
+
const showCheckmark = copiedOption === option.key;
|
|
125
147
|
|
|
126
148
|
return (
|
|
127
149
|
<DropdownMenuItem key={option.key} onAction={() => handleMenuAction(option.key)}>
|
|
@@ -162,17 +184,25 @@ const SplitButtonGroup = styled.div`
|
|
|
162
184
|
align-items: stretch;
|
|
163
185
|
`;
|
|
164
186
|
|
|
187
|
+
/* Doubled selectors outrank the base button class in any style injection order,
|
|
188
|
+
and margin: 0 keeps themed button margins from splitting the group apart. */
|
|
165
189
|
const PrimaryButton = styled(Button)`
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
&& {
|
|
191
|
+
--button-gap: var(--connect-mcp-button-gap);
|
|
192
|
+
margin: 0;
|
|
193
|
+
border-top-right-radius: 0;
|
|
194
|
+
border-bottom-right-radius: 0;
|
|
195
|
+
}
|
|
169
196
|
`;
|
|
170
197
|
|
|
171
198
|
const ChevronButton = styled(Button)`
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
&& {
|
|
200
|
+
margin: 0;
|
|
201
|
+
border-top-left-radius: 0;
|
|
202
|
+
border-bottom-left-radius: 0;
|
|
203
|
+
border-left: none;
|
|
204
|
+
padding-inline: var(--connect-mcp-button-chevron-padding-inline);
|
|
205
|
+
}
|
|
176
206
|
`;
|
|
177
207
|
|
|
178
208
|
const DropdownWrapper = styled.div<{ $placement?: string; $alignment?: string }>`
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { JSX, ReactNode } from 'react';
|
|
5
|
+
import type { TFunction } from '@redocly/theme/core/types';
|
|
6
|
+
|
|
7
|
+
import { useMCPConfig, useThemeHooks } from '@redocly/theme/core/hooks';
|
|
8
|
+
import {
|
|
9
|
+
generateClaudeCodeCommand,
|
|
10
|
+
generateClaudeDesktopConfig,
|
|
11
|
+
generateCodexCommand,
|
|
12
|
+
generateMcpInstallPrompt,
|
|
13
|
+
} from '@redocly/theme/core/utils';
|
|
14
|
+
import { CodeBlock } from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
15
|
+
import { ClaudeIcon } from '@redocly/theme/icons/ClaudeIcon/ClaudeIcon';
|
|
16
|
+
import { ChatGptIcon } from '@redocly/theme/icons/ChatGptIcon/ChatGptIcon';
|
|
17
|
+
import { Tabs, TabsSize } from '@redocly/theme/markdoc/components/Tabs/Tabs';
|
|
18
|
+
import { TabItemFragment } from '@redocly/theme/markdoc/components/Tabs/TabItemFragment';
|
|
19
|
+
import { NumberedList } from '@redocly/theme/markdoc/components/NumberedList/NumberedList';
|
|
20
|
+
import { NumberedItem } from '@redocly/theme/markdoc/components/NumberedList/NumberedItem';
|
|
21
|
+
|
|
22
|
+
export type McpConnectionTab = 'claude' | 'chatgpt' | 'json';
|
|
23
|
+
|
|
24
|
+
export type McpConnectionTabsProps = {
|
|
25
|
+
options?: McpConnectionTab[];
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_TAB_OPTIONS: McpConnectionTab[] = ['claude', 'chatgpt', 'json'];
|
|
30
|
+
|
|
31
|
+
const TAB_LABELS: Record<
|
|
32
|
+
McpConnectionTab,
|
|
33
|
+
{ translationKey: string; defaultLabel: string; icon?: React.ComponentType }
|
|
34
|
+
> = {
|
|
35
|
+
claude: {
|
|
36
|
+
translationKey: 'page.mcpNotAllowed.tabs.claude',
|
|
37
|
+
defaultLabel: 'Claude',
|
|
38
|
+
icon: ClaudeIcon,
|
|
39
|
+
},
|
|
40
|
+
chatgpt: {
|
|
41
|
+
translationKey: 'page.mcpNotAllowed.tabs.chatgpt',
|
|
42
|
+
defaultLabel: 'ChatGPT',
|
|
43
|
+
icon: ChatGptIcon,
|
|
44
|
+
},
|
|
45
|
+
json: { translationKey: 'page.mcpNotAllowed.tabs.json', defaultLabel: 'JSON' },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type TabClient = { headline: string; content: ReactNode };
|
|
49
|
+
|
|
50
|
+
// The report control is docs feedback, not useful on the connection page.
|
|
51
|
+
const CODE_BLOCK_HEADER = { controls: { copy: {}, report: { hidden: true } } };
|
|
52
|
+
|
|
53
|
+
export function McpConnectionTabs({
|
|
54
|
+
options = DEFAULT_TAB_OPTIONS,
|
|
55
|
+
className,
|
|
56
|
+
}: McpConnectionTabsProps): JSX.Element | null {
|
|
57
|
+
const { useTranslate } = useThemeHooks();
|
|
58
|
+
const { translate } = useTranslate();
|
|
59
|
+
const { serverName, serverUrl, isMcpDisabled } = useMCPConfig();
|
|
60
|
+
// The SSR markup of error pages is cached with the build-time origin, so the
|
|
61
|
+
// tabs remount after hydration to pick up the real browser origin.
|
|
62
|
+
const [hydrated, setHydrated] = useState(false);
|
|
63
|
+
|
|
64
|
+
useEffect(() => setHydrated(true), []);
|
|
65
|
+
|
|
66
|
+
if (isMcpDisabled) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const connection = { serverName, url: serverUrl };
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<McpConnectionTabsWrapper
|
|
74
|
+
key={hydrated ? 'client' : 'ssr'}
|
|
75
|
+
data-component-name="MCP/McpConnectionTabs"
|
|
76
|
+
data-testid="mcp-connection-tabs"
|
|
77
|
+
className={className}
|
|
78
|
+
>
|
|
79
|
+
<Tabs size={TabsSize.SMALL}>
|
|
80
|
+
{options.map((option) => {
|
|
81
|
+
const { translationKey, defaultLabel, icon: TabIcon } = TAB_LABELS[option];
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<TabItemFragment
|
|
85
|
+
key={option}
|
|
86
|
+
label={translate(translationKey, defaultLabel)}
|
|
87
|
+
icon={TabIcon ? <TabIcon /> : undefined}
|
|
88
|
+
>
|
|
89
|
+
{renderTabContent(option, connection, translate)}
|
|
90
|
+
</TabItemFragment>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</Tabs>
|
|
94
|
+
</McpConnectionTabsWrapper>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderTabContent(
|
|
99
|
+
option: McpConnectionTab,
|
|
100
|
+
connection: { serverName: string; url: string },
|
|
101
|
+
translate: TFunction,
|
|
102
|
+
): ReactNode {
|
|
103
|
+
const terminalHint = (
|
|
104
|
+
<TabHint>
|
|
105
|
+
{translate('page.mcpNotAllowed.snippets.terminal.hint', 'Run this command in your terminal:')}
|
|
106
|
+
</TabHint>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const urlBlock = <CodeBlock lang="text" source={connection.url} header={CODE_BLOCK_HEADER} />;
|
|
110
|
+
|
|
111
|
+
const assistantPromptClient: TabClient = {
|
|
112
|
+
headline: translate('page.mcpNotAllowed.snippets.prompt.headline', 'Prompt'),
|
|
113
|
+
content: (
|
|
114
|
+
<>
|
|
115
|
+
<TabHint>
|
|
116
|
+
{translate(
|
|
117
|
+
'page.mcpNotAllowed.snippets.prompt.hint',
|
|
118
|
+
'Or paste this prompt into the chat to have it add the server for you:',
|
|
119
|
+
)}
|
|
120
|
+
</TabHint>
|
|
121
|
+
<CodeBlock
|
|
122
|
+
lang="text"
|
|
123
|
+
source={generateMcpInstallPrompt(connection)}
|
|
124
|
+
header={CODE_BLOCK_HEADER}
|
|
125
|
+
wrapContents
|
|
126
|
+
/>
|
|
127
|
+
</>
|
|
128
|
+
),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
switch (option) {
|
|
132
|
+
case 'claude':
|
|
133
|
+
return renderClientList([
|
|
134
|
+
{
|
|
135
|
+
headline: translate('page.mcpNotAllowed.snippets.claudeCode.headline', 'Claude Code'),
|
|
136
|
+
content: (
|
|
137
|
+
<>
|
|
138
|
+
{terminalHint}
|
|
139
|
+
<CodeBlock
|
|
140
|
+
lang="shell"
|
|
141
|
+
source={generateClaudeCodeCommand(connection)}
|
|
142
|
+
header={CODE_BLOCK_HEADER}
|
|
143
|
+
wrapContents
|
|
144
|
+
/>
|
|
145
|
+
</>
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
headline: translate(
|
|
150
|
+
'page.mcpNotAllowed.snippets.claudeDesktop.headline',
|
|
151
|
+
'Claude Desktop',
|
|
152
|
+
),
|
|
153
|
+
content: (
|
|
154
|
+
<>
|
|
155
|
+
<TabHint>
|
|
156
|
+
{translate(
|
|
157
|
+
'page.mcpNotAllowed.snippets.claudeDesktop.instruction',
|
|
158
|
+
'In Claude Desktop, open Settings → Developer → Edit Config, add this to the configuration file, and restart Claude Desktop:',
|
|
159
|
+
)}
|
|
160
|
+
</TabHint>
|
|
161
|
+
<CodeBlock
|
|
162
|
+
lang="json"
|
|
163
|
+
source={generateClaudeDesktopConfig(connection)}
|
|
164
|
+
header={CODE_BLOCK_HEADER}
|
|
165
|
+
/>
|
|
166
|
+
</>
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
case 'chatgpt':
|
|
171
|
+
return renderClientList([
|
|
172
|
+
{
|
|
173
|
+
headline: translate('page.mcpNotAllowed.snippets.codexCli.headline', 'Codex CLI'),
|
|
174
|
+
content: (
|
|
175
|
+
<>
|
|
176
|
+
{terminalHint}
|
|
177
|
+
<CodeBlock
|
|
178
|
+
lang="shell"
|
|
179
|
+
source={generateCodexCommand(connection)}
|
|
180
|
+
header={CODE_BLOCK_HEADER}
|
|
181
|
+
wrapContents
|
|
182
|
+
/>
|
|
183
|
+
</>
|
|
184
|
+
),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
headline: translate(
|
|
188
|
+
'page.mcpNotAllowed.snippets.chatgptApp.headline',
|
|
189
|
+
'ChatGPT desktop app',
|
|
190
|
+
),
|
|
191
|
+
content: (
|
|
192
|
+
<>
|
|
193
|
+
<TabHint>
|
|
194
|
+
{translate(
|
|
195
|
+
'page.mcpNotAllowed.snippets.chatgptApp.instruction',
|
|
196
|
+
'In the ChatGPT desktop app, go to Settings → Plugins → MCPs and add a server with the "Streamable HTTP" type and this URL (restart the app afterwards):',
|
|
197
|
+
)}
|
|
198
|
+
</TabHint>
|
|
199
|
+
{urlBlock}
|
|
200
|
+
</>
|
|
201
|
+
),
|
|
202
|
+
},
|
|
203
|
+
assistantPromptClient,
|
|
204
|
+
]);
|
|
205
|
+
case 'json':
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<TabHint>
|
|
209
|
+
{translate(
|
|
210
|
+
'page.mcpNotAllowed.snippets.json.hint',
|
|
211
|
+
'Add this server entry to your MCP client configuration file:',
|
|
212
|
+
)}
|
|
213
|
+
</TabHint>
|
|
214
|
+
<CodeBlock
|
|
215
|
+
lang="json"
|
|
216
|
+
source={JSON.stringify(
|
|
217
|
+
{ [connection.serverName]: { url: connection.url, description: 'MCP Server' } },
|
|
218
|
+
null,
|
|
219
|
+
2,
|
|
220
|
+
)}
|
|
221
|
+
header={CODE_BLOCK_HEADER}
|
|
222
|
+
/>
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderClientList(clients: TabClient[]): ReactNode {
|
|
229
|
+
return (
|
|
230
|
+
<NumberedList>
|
|
231
|
+
{clients.map((client, index) => (
|
|
232
|
+
<NumberedItem
|
|
233
|
+
key={index}
|
|
234
|
+
type="dot"
|
|
235
|
+
size="small"
|
|
236
|
+
headline={client.headline}
|
|
237
|
+
index={index + 1}
|
|
238
|
+
isLast={index === clients.length - 1}
|
|
239
|
+
>
|
|
240
|
+
{client.content}
|
|
241
|
+
</NumberedItem>
|
|
242
|
+
))}
|
|
243
|
+
</NumberedList>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const McpConnectionTabsWrapper = styled.div`
|
|
248
|
+
--md-numbered-list-margin: 0 0 var(--spacing-sm);
|
|
249
|
+
|
|
250
|
+
font-size: var(--page-mcp-405-snippets-font-size);
|
|
251
|
+
line-height: var(--page-mcp-405-snippets-line-height);
|
|
252
|
+
text-align: left;
|
|
253
|
+
|
|
254
|
+
/* The copy control floats over the code, so wrapped lines need room to clear it. */
|
|
255
|
+
pre[data-testid='source-code'] {
|
|
256
|
+
padding-right: var(--page-mcp-405-snippets-code-padding-right);
|
|
257
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
const TabHint = styled.p`
|
|
261
|
+
margin: 0 0 var(--spacing-xs);
|
|
262
|
+
color: var(--page-mcp-405-snippets-hint-text-color);
|
|
263
|
+
`;
|
|
@@ -7,6 +7,7 @@ import { markdownBaseTableCss } from '@redocly/theme/components/Markdown/styles/
|
|
|
7
7
|
import { markdownLinksCss } from '@redocly/theme/components/Markdown/styles/links';
|
|
8
8
|
import { headingAnchorCss } from '@redocly/theme/components/Markdown/styles/heading-anchor';
|
|
9
9
|
import {
|
|
10
|
+
ACCORDION_SELECTOR,
|
|
10
11
|
ANCHOR_CLASS,
|
|
11
12
|
H1_CLASS,
|
|
12
13
|
H2_CLASS,
|
|
@@ -172,7 +173,7 @@ export const Markdown = styled.main
|
|
|
172
173
|
margin: var(--md-hr-margin-vertical) 0;
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
details {
|
|
176
|
+
details:not(${ACCORDION_SELECTOR}) {
|
|
176
177
|
margin: 1.125em 0;
|
|
177
178
|
|
|
178
179
|
summary {
|
|
@@ -60,11 +60,13 @@ export function useActiveSectionId(
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
window.addEventListener('scroll', scrollListener, { capture: false });
|
|
63
|
-
setTimeout(() => {
|
|
63
|
+
const initialRead = setTimeout(() => {
|
|
64
64
|
scrollListener();
|
|
65
65
|
}, 10);
|
|
66
66
|
|
|
67
67
|
return () => {
|
|
68
|
+
clearTimeout(initialRead);
|
|
69
|
+
scrollListener.cancel();
|
|
68
70
|
window.removeEventListener('scroll', scrollListener);
|
|
69
71
|
};
|
|
70
72
|
}, [location, heightOffset, scrollListener]);
|
|
@@ -6,13 +6,15 @@ import type { MCPOption } from '../types';
|
|
|
6
6
|
import { useThemeHooks } from './use-theme-hooks';
|
|
7
7
|
import { useMCPConfig } from './use-mcp-config';
|
|
8
8
|
import { ClipboardService } from '../utils/clipboard-service';
|
|
9
|
+
import { DEFAULT_MCP_OPTIONS } from '../constants/mcp';
|
|
10
|
+
import { generateClaudeCodeCommand, generateCodexCommand } from '../utils/mcp';
|
|
9
11
|
|
|
10
12
|
export type McpButtonOptions = {
|
|
11
13
|
options?: MCPOption[];
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export type McpButtonSettings = {
|
|
15
|
-
|
|
17
|
+
copiedOption: MCPOption | null;
|
|
16
18
|
cursorUrl: string;
|
|
17
19
|
vscodeUrl: string;
|
|
18
20
|
triggerButtonText: string;
|
|
@@ -23,35 +25,41 @@ export type McpButtonSettings = {
|
|
|
23
25
|
const COPIED_RESET_TIMEOUT = 1000;
|
|
24
26
|
|
|
25
27
|
export function useConnectMCPButton({
|
|
26
|
-
options =
|
|
28
|
+
options = DEFAULT_MCP_OPTIONS,
|
|
27
29
|
}: McpButtonOptions = {}): McpButtonSettings {
|
|
28
30
|
const { useTranslate } = useThemeHooks();
|
|
29
31
|
const { translate } = useTranslate();
|
|
30
32
|
const { serverName, serverUrl, cursorUrl, vscodeUrl } = useMCPConfig();
|
|
31
|
-
const [
|
|
33
|
+
const [copiedOption, setCopiedOption] = useState<MCPOption | null>(null);
|
|
32
34
|
|
|
33
35
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
36
|
const resetCopied = useCallback(
|
|
35
|
-
debounce(() =>
|
|
37
|
+
debounce(() => setCopiedOption(null), COPIED_RESET_TIMEOUT),
|
|
36
38
|
[],
|
|
37
39
|
);
|
|
38
40
|
|
|
39
41
|
const handleAction = useCallback(
|
|
40
42
|
(action: MCPOption) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
const connection = { serverName, url: serverUrl };
|
|
44
|
+
const copyTextMap: Partial<Record<MCPOption, string>> = {
|
|
45
|
+
copy: JSON.stringify(
|
|
46
|
+
{ [serverName]: { url: serverUrl, description: 'MCP Server' } },
|
|
47
|
+
null,
|
|
48
|
+
2,
|
|
49
|
+
),
|
|
50
|
+
'claude-code': generateClaudeCodeCommand(connection),
|
|
51
|
+
codex: generateCodexCommand(connection),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const copyText = copyTextMap[action];
|
|
55
|
+
if (copyText !== undefined) {
|
|
56
|
+
ClipboardService.copyCustom(copyText);
|
|
57
|
+
setCopiedOption(action);
|
|
50
58
|
resetCopied();
|
|
51
59
|
return;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
const urlMap: Record<
|
|
62
|
+
const urlMap: Partial<Record<MCPOption, string>> = {
|
|
55
63
|
cursor: cursorUrl,
|
|
56
64
|
vscode: vscodeUrl,
|
|
57
65
|
};
|
|
@@ -69,7 +77,7 @@ export function useConnectMCPButton({
|
|
|
69
77
|
const visibleOptions = useMemo(() => options.filter(Boolean), [options]);
|
|
70
78
|
|
|
71
79
|
return {
|
|
72
|
-
|
|
80
|
+
copiedOption,
|
|
73
81
|
cursorUrl,
|
|
74
82
|
vscodeUrl,
|
|
75
83
|
triggerButtonText,
|
|
@@ -28,6 +28,7 @@ import { code } from '@redocly/theme/components/CodeBlock/variables';
|
|
|
28
28
|
import { productPicker } from '@redocly/theme/components/Product/variables';
|
|
29
29
|
import { markdown } from '@redocly/theme/components/Markdown/variables';
|
|
30
30
|
import { banner } from '@redocly/theme/components/Banner/variables';
|
|
31
|
+
import { markdownAccordion } from '@redocly/theme/markdoc/components/Accordion/variables';
|
|
31
32
|
import { markdownTabs } from '@redocly/theme/markdoc/components/Tabs/variables';
|
|
32
33
|
import { markdownNumberedList } from '@redocly/theme/markdoc/components/NumberedList/variables';
|
|
33
34
|
import { diagram } from '@redocly/theme/markdoc/components/Diagram/variables';
|
|
@@ -1049,8 +1050,8 @@ const pages = css`
|
|
|
1049
1050
|
|
|
1050
1051
|
--page-mcp-405-status-text-color: var(--text-color-helper);
|
|
1051
1052
|
--page-mcp-405-status-font-size: var(--font-size-lg); // @presenter FontSize
|
|
1052
|
-
--page-mcp-405-status-line-height: var(--line-height-lg); // @presenter LineHeight
|
|
1053
1053
|
--page-mcp-405-status-font-weight: var(--font-weight-semibold); // @presenter FontWeight
|
|
1054
|
+
--page-mcp-405-status-line-height: var(--line-height-lg); // @presenter LineHeight
|
|
1054
1055
|
|
|
1055
1056
|
--page-mcp-405-title-text-color: var(--text-color-primary);
|
|
1056
1057
|
--page-mcp-405-title-font-size: 42px; // @presenter FontSize
|
|
@@ -1066,6 +1067,11 @@ const pages = css`
|
|
|
1066
1067
|
--page-mcp-405-description-font-size-mobile: var(--font-size-lg);
|
|
1067
1068
|
--page-mcp-405-description-line-height-mobile: var(--line-height-lg);
|
|
1068
1069
|
|
|
1070
|
+
--page-mcp-405-snippets-font-size: var(--font-size-base); // @presenter FontSize
|
|
1071
|
+
--page-mcp-405-snippets-line-height: var(--line-height-base); // @presenter LineHeight
|
|
1072
|
+
--page-mcp-405-snippets-hint-text-color: var(--text-color-secondary);
|
|
1073
|
+
--page-mcp-405-snippets-code-padding-right: var(--spacing-xxl);
|
|
1074
|
+
|
|
1069
1075
|
// @tokens End
|
|
1070
1076
|
|
|
1071
1077
|
/**
|
|
@@ -1352,6 +1358,7 @@ export const styles = css<{ palette?: string }>`
|
|
|
1352
1358
|
${loadProgressBar}
|
|
1353
1359
|
${logo}
|
|
1354
1360
|
${markdown}
|
|
1361
|
+
${markdownAccordion}
|
|
1355
1362
|
${markdownTabs}
|
|
1356
1363
|
${markdownNumberedList}
|
|
1357
1364
|
${diagram}
|
|
@@ -212,6 +212,9 @@ export type BffCatalogEntityList = BffCatalogList<BffCatalogEntity>;
|
|
|
212
212
|
export type BffCatalogRelatedEntityList = BffCatalogList<BffCatalogRelatedEntity>;
|
|
213
213
|
|
|
214
214
|
export type Page = {
|
|
215
|
+
// Optional so existing code that builds a Page keeps compiling; the catalog BFF always emits both.
|
|
216
|
+
nextPage?: string | null;
|
|
217
|
+
previousPage?: string | null;
|
|
215
218
|
endCursor: string | null;
|
|
216
219
|
startCursor: string | null;
|
|
217
220
|
hasNextPage: boolean;
|
package/src/core/types/l10n.ts
CHANGED
|
@@ -136,6 +136,19 @@ export type TranslationKey =
|
|
|
136
136
|
| 'page.notFound.description'
|
|
137
137
|
| 'page.mcpNotAllowed.title'
|
|
138
138
|
| 'page.mcpNotAllowed.description'
|
|
139
|
+
| 'page.mcpNotAllowed.snippets.terminal.hint'
|
|
140
|
+
| 'page.mcpNotAllowed.snippets.claudeCode.headline'
|
|
141
|
+
| 'page.mcpNotAllowed.snippets.claudeDesktop.headline'
|
|
142
|
+
| 'page.mcpNotAllowed.snippets.claudeDesktop.instruction'
|
|
143
|
+
| 'page.mcpNotAllowed.snippets.chatgptApp.headline'
|
|
144
|
+
| 'page.mcpNotAllowed.snippets.chatgptApp.instruction'
|
|
145
|
+
| 'page.mcpNotAllowed.snippets.codexCli.headline'
|
|
146
|
+
| 'page.mcpNotAllowed.snippets.prompt.headline'
|
|
147
|
+
| 'page.mcpNotAllowed.snippets.prompt.hint'
|
|
148
|
+
| 'page.mcpNotAllowed.snippets.json.hint'
|
|
149
|
+
| 'page.mcpNotAllowed.tabs.claude'
|
|
150
|
+
| 'page.mcpNotAllowed.tabs.chatgpt'
|
|
151
|
+
| 'page.mcpNotAllowed.tabs.json'
|
|
139
152
|
| 'page.lastUpdated.timeago'
|
|
140
153
|
| 'page.lastUpdated.on'
|
|
141
154
|
| 'catalog.filters.placeholder'
|
|
@@ -271,6 +284,10 @@ export type TranslationKey =
|
|
|
271
284
|
| 'page.actions.connectMcp.cursorDescription'
|
|
272
285
|
| 'page.actions.connectMcp.vscode'
|
|
273
286
|
| 'page.actions.connectMcp.vscodeDescription'
|
|
287
|
+
| 'page.actions.connectMcp.claudeCode'
|
|
288
|
+
| 'page.actions.connectMcp.claudeCodeDescription'
|
|
289
|
+
| 'page.actions.connectMcp.codex'
|
|
290
|
+
| 'page.actions.connectMcp.codexDescription'
|
|
274
291
|
| 'page.actions.connectMcp.copyConfig'
|
|
275
292
|
| 'page.actions.connectMcp.copyConfigDescription'
|
|
276
293
|
| 'openapi.download.description.title'
|
package/src/core/types/mcp.ts
CHANGED
package/src/core/utils/mcp.ts
CHANGED
|
@@ -32,3 +32,42 @@ export function generateMCPDeepLink(
|
|
|
32
32
|
return generateCursorMCPDeepLink(config);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
// Server names can contain spaces (e.g. "Docs MCP server") which break bare CLI
|
|
37
|
+
// arguments, so commands use a slugified form while JSON configs keep the raw name.
|
|
38
|
+
export function slugifyMcpServerName(name: string): string {
|
|
39
|
+
return name
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
42
|
+
.replace(/^-+|-+$/g, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Claude Desktop's config file only launches stdio commands, so remote servers
|
|
46
|
+
// connect through the mcp-remote bridge.
|
|
47
|
+
export function generateClaudeDesktopConfig(config: McpConnectionParams): string {
|
|
48
|
+
return JSON.stringify(
|
|
49
|
+
{
|
|
50
|
+
mcpServers: {
|
|
51
|
+
[slugifyMcpServerName(config.serverName)]: {
|
|
52
|
+
command: 'npx',
|
|
53
|
+
args: ['-y', 'mcp-remote', config.url],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
null,
|
|
58
|
+
2,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function generateClaudeCodeCommand(config: McpConnectionParams): string {
|
|
63
|
+
return `claude mcp add --transport http ${slugifyMcpServerName(config.serverName)} ${config.url}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function generateCodexCommand(config: McpConnectionParams): string {
|
|
67
|
+
return `codex mcp add ${slugifyMcpServerName(config.serverName)} --url ${config.url}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pasted into an AI assistant (Claude Code, Codex) so it installs the server itself.
|
|
71
|
+
export function generateMcpInstallPrompt(config: McpConnectionParams): string {
|
|
72
|
+
return `Add the MCP server "${slugifyMcpServerName(config.serverName)}" with URL ${config.url} (streamable HTTP transport) to my MCP configuration, then verify it is connected.`;
|
|
73
|
+
}
|