@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.
Files changed (61) hide show
  1. package/lib/components/Buttons/ConnectMCPButton.js +43 -14
  2. package/lib/components/MCP/McpConnectionTabs.d.ts +7 -0
  3. package/lib/components/MCP/McpConnectionTabs.js +151 -0
  4. package/lib/components/Markdown/Markdown.js +1 -1
  5. package/lib/core/constants/accordion.d.ts +3 -0
  6. package/lib/core/constants/accordion.js +7 -0
  7. package/lib/core/constants/index.d.ts +1 -0
  8. package/lib/core/constants/index.js +1 -0
  9. package/lib/core/constants/mcp.d.ts +2 -0
  10. package/lib/core/constants/mcp.js +8 -1
  11. package/lib/core/hooks/use-active-section-id.js +3 -1
  12. package/lib/core/hooks/use-connect-mcp-button.d.ts +1 -1
  13. package/lib/core/hooks/use-connect-mcp-button.js +16 -13
  14. package/lib/core/styles/global.js +46 -39
  15. package/lib/core/types/catalog.d.ts +2 -0
  16. package/lib/core/types/l10n.d.ts +1 -1
  17. package/lib/core/types/mcp.d.ts +1 -1
  18. package/lib/core/utils/mcp.d.ts +5 -0
  19. package/lib/core/utils/mcp.js +35 -0
  20. package/lib/icons/CodexIcon/CodexIcon.d.ts +25 -0
  21. package/lib/icons/CodexIcon/CodexIcon.js +22 -0
  22. package/lib/layouts/McpNotAllowed.js +5 -3
  23. package/lib/markdoc/components/Accordion/Accordion.d.ts +9 -0
  24. package/lib/markdoc/components/Accordion/Accordion.js +132 -0
  25. package/lib/markdoc/components/Accordion/AccordionGroup.d.ts +5 -0
  26. package/lib/markdoc/components/Accordion/AccordionGroup.js +29 -0
  27. package/lib/markdoc/components/Accordion/variables.d.ts +1 -0
  28. package/lib/markdoc/components/Accordion/variables.js +40 -0
  29. package/lib/markdoc/components/default.d.ts +2 -0
  30. package/lib/markdoc/components/default.js +2 -0
  31. package/lib/markdoc/default.d.ts +4 -0
  32. package/lib/markdoc/default.js +4 -0
  33. package/lib/markdoc/tags/accordion-group.d.ts +2 -0
  34. package/lib/markdoc/tags/accordion-group.js +10 -0
  35. package/lib/markdoc/tags/accordion.d.ts +2 -0
  36. package/lib/markdoc/tags/accordion.js +14 -0
  37. package/lib/markdoc/tags/connect-mcp.js +2 -1
  38. package/package.json +2 -2
  39. package/src/components/Buttons/ConnectMCPButton.tsx +44 -14
  40. package/src/components/MCP/McpConnectionTabs.tsx +263 -0
  41. package/src/components/Markdown/Markdown.tsx +2 -1
  42. package/src/core/constants/accordion.ts +4 -0
  43. package/src/core/constants/index.ts +1 -0
  44. package/src/core/constants/mcp.ts +10 -0
  45. package/src/core/hooks/use-active-section-id.ts +3 -1
  46. package/src/core/hooks/use-connect-mcp-button.ts +23 -15
  47. package/src/core/styles/global.ts +8 -1
  48. package/src/core/types/catalog.ts +3 -0
  49. package/src/core/types/l10n.ts +17 -0
  50. package/src/core/types/mcp.ts +1 -1
  51. package/src/core/utils/mcp.ts +39 -0
  52. package/src/icons/CodexIcon/CodexIcon.tsx +23 -0
  53. package/src/layouts/McpNotAllowed.tsx +4 -2
  54. package/src/markdoc/components/Accordion/Accordion.tsx +128 -0
  55. package/src/markdoc/components/Accordion/AccordionGroup.tsx +41 -0
  56. package/src/markdoc/components/Accordion/variables.ts +37 -0
  57. package/src/markdoc/components/default.ts +2 -0
  58. package/src/markdoc/default.ts +4 -0
  59. package/src/markdoc/tags/accordion-group.ts +8 -0
  60. package/src/markdoc/tags/accordion.ts +12 -0
  61. 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 = ['cursor', 'vscode', 'copy'],
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 { isCopied, visibleOptions, handleAction } = useConnectMCPButton({ options });
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
- // When copy is the primary action there is no dropdown item to show the copied state, so the
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
- setIsOpen(false);
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 === 'copy' && isCopied;
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
- --button-gap: var(--connect-mcp-button-gap);
167
- border-top-right-radius: 0;
168
- border-bottom-right-radius: 0;
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
- border-top-left-radius: 0;
173
- border-bottom-left-radius: 0;
174
- border-left: none;
175
- padding-inline: var(--connect-mcp-button-chevron-padding-inline);
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 {
@@ -0,0 +1,4 @@
1
+ export const ACCORDION_COMPONENT_NAME = 'Markdoc/Accordion/Accordion';
2
+ export const ACCORDION_GROUP_COMPONENT_NAME = 'Markdoc/Accordion/AccordionGroup';
3
+
4
+ export const ACCORDION_SELECTOR = `[data-component-name='${ACCORDION_COMPONENT_NAME}']`;
@@ -1,3 +1,4 @@
1
+ export * from './accordion';
1
2
  export * from './common';
2
3
  export * from './environments';
3
4
  export * from './code-walkthrough';
@@ -1 +1,11 @@
1
+ import type { MCPOption } from '../types/mcp';
2
+
1
3
  export const DEFAULT_MCP_SERVER_NAME = 'mcp-server';
4
+
5
+ export const DEFAULT_MCP_OPTIONS: MCPOption[] = [
6
+ 'cursor',
7
+ 'vscode',
8
+ 'claude-code',
9
+ 'codex',
10
+ 'copy',
11
+ ];
@@ -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
- isCopied: boolean;
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 = ['cursor', 'vscode', 'copy'],
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 [isCopied, setIsCopied] = useState(false);
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(() => setIsCopied(false), COPIED_RESET_TIMEOUT),
37
+ debounce(() => setCopiedOption(null), COPIED_RESET_TIMEOUT),
36
38
  [],
37
39
  );
38
40
 
39
41
  const handleAction = useCallback(
40
42
  (action: MCPOption) => {
41
- if (action === 'copy') {
42
- const config = {
43
- [serverName]: {
44
- url: serverUrl,
45
- description: 'MCP Server',
46
- },
47
- };
48
- ClipboardService.copyCustom(JSON.stringify(config, null, 2));
49
- setIsCopied(true);
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<Exclude<MCPOption, 'copy'>, string> = {
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
- isCopied,
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;
@@ -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'
@@ -1,6 +1,6 @@
1
1
  export type MCPClientType = 'cursor' | 'vscode';
2
2
 
3
- export type MCPOption = 'cursor' | 'vscode' | 'copy';
3
+ export type MCPOption = 'cursor' | 'vscode' | 'claude-code' | 'codex' | 'copy';
4
4
 
5
5
  export type McpConnectionParams = {
6
6
  serverName: string;
@@ -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
+ }