PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / admin / mcp / components / ConnectionConfig.js

ConnectionConfig.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/admin/mcp/components/ConnectionConfig.js

304 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState } from "react";
2 import { __ } from "@wordpress/i18n";
3 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4 import { toast } from "react-toastify";
5 import Swal from "sweetalert2";
6
7 // `connect` and `disconnect` stay in `utils/api` — they are API the driver and
8 // the read-only pairing path use (ADR-038/ADR-056) — but the page no longer
9 // calls them: the token is minted for it on the first status read.
10 import { getConnection, rotate } from "../utils/api";
11 import CopyField from "./CopyField";
12 import EditorsTab from "./EditorsTab";
13 import { IconChatGpt, IconClaude, IconExternal } from "./Icons";
14
15 export const CONNECTION_KEY = "bd-mcp-connection";
16
17 const LEARN_MORE_MCP = "https://modelcontextprotocol.io/docs/concepts/transports";
18 const LEARN_MORE_CHATGPT = "https://developers.openai.com/apps-sdk/deploy/connect-chatgpt";
19
20 const TABS = [
21 { id: "auth", label: __("OAuth", "betterdocs") },
22 { id: "others", label: __("Editors & CLI", "betterdocs") },
23 ];
24
25 const Steps = ({ steps }) => (
26 <ol className="bd-mcp-steps">
27 {steps.map((step, i) => (
28 <li key={step} className="bd-mcp-steps__item">
29 <span className="bd-mcp-steps__num">{i + 1}</span>
30 <span>{step}</span>
31 </li>
32 ))}
33 </ol>
34 );
35
36 /**
37 * The icon square. The brand accent arrives as a custom property so the
38 * stylesheet — not this component — decides what happens to it in dark mode,
39 * where ChatGPT's black and Cursor's near-black would otherwise vanish.
40 *
41 * `solid` marks a tile whose glyph is a vendor's own logo file rather than one
42 * of ours. It draws a plain white chip with a hairline border, so the logo keeps
43 * its own colour and every tile in the row gets one treatment (ADR-066). That
44 * chip stays light in dark mode too — a terracotta logo on a near-black square
45 * is unreadable.
46 */
47 const CardHead = ({ Icon, accent, accentDark, accentFgDark, tint, solid, name, badge, badgeTone, sub }) => (
48 <div className="bd-mcp-card__head">
49 <span
50 className={`bd-mcp-card__icon${solid ? " is-solid" : ""}`}
51 style={{
52 "--bd-accent": accent,
53 "--bd-accent-dark": accentDark || accent,
54 ...(accentFgDark ? { "--bd-accent-fg-dark": accentFgDark } : {}),
55 "--bd-accent-tint": tint || `${accent}14`,
56 }}
57 >
58 <Icon />
59 </span>
60 <div className="bd-mcp-card__headtext">
61 <span className="bd-mcp-card__title">
62 {name}
63 {badge && <span className={`bda-badge ${badgeTone || "slate"}`}>{badge}</span>}
64 </span>
65 {sub && <span className="bd-mcp-card__sub">{sub}</span>}
66 </div>
67 </div>
68 );
69
70 const AuthConnectorCard = ({ connector, endpoint }) => (
71 <div className="bda-card bd-mcp-card">
72 <CardHead
73 Icon={connector.Icon}
74 accent={connector.accent}
75 accentDark={connector.accentDark}
76 accentFgDark={connector.accentFgDark}
77 solid={connector.solid}
78 name={connector.name}
79 badge={__("OAuth", "betterdocs")}
80 badgeTone="green"
81 sub={__("Signs in with a one-time approval. No token to paste.", "betterdocs")}
82 />
83
84 <CopyField label={__("Connector URL", "betterdocs")} value={endpoint} />
85 <Steps steps={connector.steps} />
86
87 {connector.learnMore && (
88 <a
89 className="bd-mcp-link"
90 href={connector.learnMore}
91 target="_blank"
92 rel="noopener noreferrer"
93 >
94 {__("How to add a connector", "betterdocs")}
95 <IconExternal size={11} />
96 </a>
97 )}
98 </div>
99 );
100
101 export default function ConnectionConfig() {
102 const queryClient = useQueryClient();
103 const [tab, setTab] = useState("auth");
104
105 const { data, isLoading, isError } = useQuery({
106 queryKey: [CONNECTION_KEY],
107 queryFn: getConnection,
108 });
109
110 const refresh = (next) => {
111 if (next) queryClient.setQueryData([CONNECTION_KEY], (prev) => ({ ...prev, ...next }));
112 queryClient.invalidateQueries({ queryKey: [CONNECTION_KEY] });
113 };
114
115 const rotateMutation = useMutation({
116 mutationFn: () => rotate(null),
117 onSuccess: (next) => {
118 refresh(next);
119 toast.success(__("Connection token rotated. Reconnect your clients with the new one.", "betterdocs"));
120 },
121 onError: () => toast.error(__("Couldn't rotate the connection token.", "betterdocs")),
122 });
123
124 const confirmRotate = () =>
125 Swal.fire({
126 title: __("Rotate the connection token?", "betterdocs"),
127 text: __(
128 "A new token is generated immediately. Every client using the old one — Claude Code, Cursor, VS Code, Claude Desktop — stops working until you give it the new command. Apps connected through OAuth are unaffected.",
129 "betterdocs",
130 ),
131 icon: "warning",
132 showCancelButton: true,
133 confirmButtonText: __("Rotate token", "betterdocs"),
134 cancelButtonText: __("Cancel", "betterdocs"),
135 confirmButtonColor: "#00b884",
136 }).then((r) => r.isConfirmed && rotateMutation.mutate());
137
138 // The card head: the section's title and its "how it works" line on the
139 // left, the two tabs as the analytics segmented control on the right
140 // (ADR-062). The tabs only make sense once there is something to tab
141 // between, so the loading and error states below render the head alone.
142 const head = (withTabs) => (
143 <div className="bda-card-head bd-mcp-cardhead">
144 <div>
145 <h3 className="bda-card-title">{__("Add BetterDocs to your client", "betterdocs")}</h3>
146 <p className="bd-mcp-muted">
147 {__("Pick your assistant below — it opens or copies fully configured.", "betterdocs")}{" "}
148 <a className="bd-mcp-link" href={LEARN_MORE_MCP} target="_blank" rel="noopener noreferrer">
149 {__("How it works", "betterdocs")}
150 <IconExternal size={11} />
151 </a>
152 </p>
153 </div>
154 {withTabs && (
155 <div className="bda-segtabs bd-mcp-tabs" role="tablist">
156 {TABS.map((t) => (
157 <button
158 key={t.id}
159 type="button"
160 role="tab"
161 aria-selected={tab === t.id}
162 className={`bda-segtab${tab === t.id ? " active" : ""}`}
163 onClick={() => setTab(t.id)}
164 >
165 {t.label}
166 </button>
167 ))}
168 </div>
169 )}
170 </div>
171 );
172
173 // The token is minted by `GET /mcp/connection` itself (ADR-056), so the only
174 // states this section has are "asking" and "here it is".
175 if (isLoading) {
176 return (
177 <div className="bda-card bd-mcp-connect">
178 {head(false)}
179 <div className="bda-card-body">
180 <p className="bd-mcp-muted">{__("Preparing your connection…", "betterdocs")}</p>
181 </div>
182 </div>
183 );
184 }
185
186 if (isError || !data) {
187 return (
188 <div className="bda-card bd-mcp-connect">
189 {head(false)}
190 <div className="bda-card-body">
191 <div className="bd-mcp-alert is-error">
192 {__("Couldn't load the MCP connection status.", "betterdocs")}
193 </div>
194 </div>
195 </div>
196 );
197 }
198
199 const endpoint = data.mcp_endpoint || "";
200 const busy = rotateMutation.isLoading;
201
202 // A connected site whose stored token cannot be decrypted — the salt-rotation
203 // case. The pairing still authenticates the clients that already have it, so
204 // the honest answer is "rotate", not "you are not connected".
205 if (!data.token_available) {
206 return (
207 <div className="bda-card bd-mcp-connect">
208 {head(false)}
209 <div className="bda-card-body">
210 <div className="bd-mcp-alert is-warning">
211 <strong>{__("The stored copy of your connection token can't be read.", "betterdocs")}</strong>{" "}
212 {__(
213 "This happens when the WordPress security salts change. Clients you already connected keep working — but to see or share the token again you have to rotate it, which disconnects them.",
214 "betterdocs",
215 )}
216 <div className="bd-mcp-actions">
217 <button type="button" className="bda-btn primary sm" onClick={confirmRotate} disabled={busy}>
218 {__("Rotate token", "betterdocs")}
219 </button>
220 </div>
221 </div>
222 </div>
223 </div>
224 );
225 }
226
227 // The two connector assistants. Both need the endpoint URL and nothing
228 // else — they sign in through the browser — so this tab has never had a
229 // token on it, and the picker redesign left it untouched (ADR-057).
230 const authConnectors = [
231 {
232 id: "claude",
233 name: __("Claude (web, mobile & desktop)", "betterdocs"),
234 Icon: IconClaude,
235 accent: "#d97757",
236 solid: true,
237 steps: [
238 __("In Claude, open Settings → Connectors → Add Connector → Add custom connector.", "betterdocs"),
239 __("Paste the connector URL above and give it a name (e.g. BetterDocs).", "betterdocs"),
240 __("Click Add, then approve access when Claude asks you to sign in.", "betterdocs"),
241 ],
242 },
243 {
244 id: "chatgpt",
245 name: "ChatGPT",
246 Icon: IconChatGpt,
247 accent: "#000000",
248 // A solid black square vanishes on a dark card, so in dark mode the
249 // tile inverts: near-white square, near-black logomark — which is
250 // how OpenAI's own icon renders on a dark surface.
251 accentDark: "#e5e9f2",
252 accentFgDark: "#101828",
253 solid: true,
254 learnMore: LEARN_MORE_CHATGPT,
255 steps: [
256 __("In ChatGPT, open Settings → Connectors → Add New.", "betterdocs"),
257 __("Enter a name (e.g. BetterDocs).", "betterdocs"),
258 __("Paste the connector URL above.", "betterdocs"),
259 __("Save, then approve access when ChatGPT asks you to sign in.", "betterdocs"),
260 ],
261 },
262 ];
263
264 return (
265 <div className="bda-card bd-mcp-connect">
266 {head(true)}
267
268 <div className="bda-card-body">
269 <div className="bd-mcp-tabpanel">
270 {tab === "auth" && (
271 <div className="bd-mcp-grid">
272 {authConnectors.map((c) => (
273 <AuthConnectorCard key={c.id} connector={c} endpoint={endpoint} />
274 ))}
275 </div>
276 )}
277
278 {tab === "others" && <EditorsTab data={data} />}
279 </div>
280 </div>
281
282 {/* The card's footer row, below both panels (ADR-062). It used to
283 sit inside the Editors & CLI tab, because only those clients use
284 the connection token (ADR-057) — but as a card footer it reads as
285 a property of the whole card rather than of whichever tab is
286 open, and its own last clause ("browser sign-ins are unaffected")
287 is addressed to the reader of the OAuth tab. */}
288 <div className="bd-mcp-reset">
289 <p className="bd-mcp-reset__text">
290 {__(
291 "Rotate the connection token if it may have leaked. Clients that use the token will need the new one; browser sign-ins are unaffected.",
292 "betterdocs",
293 )}
294 </p>
295 <div className="bd-mcp-actions">
296 <button type="button" className="bda-btn ghost sm" onClick={confirmRotate} disabled={busy}>
297 {rotateMutation.isLoading ? __("Rotating…", "betterdocs") : __("Rotate token", "betterdocs")}
298 </button>
299 </div>
300 </div>
301 </div>
302 );
303 }
304