Skip to content

Commit 4dd023e

Browse files
authored
feat: add MCP Setup wizard (#2603)
* feat: add MCP Setup modal * chore: add config blocks for mcp * chore: cleanup token display modal * chore: add syntax highlighting * chore: add /api to kratos base url * chore: fix tests and update snapshot
1 parent b7e8b2d commit 4dd023e

File tree

9 files changed

+332
-50
lines changed

9 files changed

+332
-50
lines changed

src/components/Agents/InstalAgentInstruction/useAgentsBaseURL.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export function useAgentsBaseURL() {
77

88
// if we are on the SaaS platform, we need to use the backend URL from the user
99
// profile, not the current URL
10-
const baseUrl = authSystem === "clerk" ? backendUrl : window.location.origin;
10+
const baseUrl =
11+
authSystem === "clerk" ? backendUrl : window.location.origin + "/api";
1112

1213
return baseUrl;
1314
}

src/components/Authentication/Kratos/KratosUserProfileDropdown.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import KratosLogoutButton from "./KratosLogoutButton";
1414

1515
type UserProfileDropdownProps = {
1616
openKubeConfigModal: () => void;
17+
openMcpTokenModal: () => void;
1718
};
1819

1920
export function KratosUserProfileDropdown({
20-
openKubeConfigModal
21+
openKubeConfigModal,
22+
openMcpTokenModal
2123
}: UserProfileDropdownProps) {
2224
const { user } = useUser();
2325
const userNavigation = [{ name: "Your Profile", href: "/profile-settings" }];
@@ -75,6 +77,14 @@ export function KratosUserProfileDropdown({
7577
Download kubeconfig
7678
</button>
7779
</MenuItem>
80+
<MenuItem>
81+
<button
82+
onClick={openMcpTokenModal}
83+
className="block border-0 border-b border-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900"
84+
>
85+
Setup MCP
86+
</button>
87+
</MenuItem>
7888
<MenuItem>
7989
<VersionInfo />
8090
</MenuItem>

src/components/Tokens/Add/TokenDisplayModal.tsx

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,28 @@ import { useState } from "react";
22
import { FaCopy, FaEye, FaEyeSlash } from "react-icons/fa";
33
import { CreateTokenResponse } from "../../../api/services/tokens";
44
import { Button } from "../../../ui/Buttons/Button";
5+
import { JSONViewer } from "../../../ui/Code/JSONViewer";
56
import { Modal } from "../../../ui/Modal";
7+
import { Tab, Tabs } from "../../../ui/Tabs/Tabs";
68
import { toastSuccess } from "../../Toast/toast";
79
import { TokenFormValues } from "./CreateTokenForm";
10+
import { useAgentsBaseURL } from "../../../components/Agents/InstalAgentInstruction/useAgentsBaseURL";
11+
import CodeBlock from "@flanksource-ui/ui/Code/CodeBlock";
812

913
type Props = {
1014
isOpen: boolean;
1115
onClose: () => void;
1216
tokenResponse: CreateTokenResponse;
1317
formValues?: TokenFormValues;
18+
isMcp?: boolean;
1419
};
1520

1621
export default function TokenDisplayModal({
1722
isOpen,
1823
onClose,
1924
tokenResponse,
20-
formValues
25+
formValues,
26+
isMcp = false
2127
}: Props) {
2228
const [showToken, setShowToken] = useState(false);
2329

@@ -77,6 +83,15 @@ export default function TokenDisplayModal({
7783
</div>
7884
</div>
7985

86+
{isMcp && (
87+
<div className="rounded-md border border-green-200 bg-green-50 p-4">
88+
<h4 className="mb-2 font-medium text-green-800">
89+
MCP Client Setup:
90+
</h4>
91+
<McpSetupTabs token={tokenResponse.payload.token} />
92+
</div>
93+
)}
94+
8095
<div className="rounded-md border border-blue-200 bg-blue-50 p-4">
8196
<h4 className="mb-2 font-medium text-blue-800">
8297
Usage Instructions:
@@ -115,3 +130,161 @@ export default function TokenDisplayModal({
115130
</Modal>
116131
);
117132
}
133+
134+
type McpSetupTabsProps = {
135+
token: string;
136+
};
137+
138+
function McpSetupTabs({ token }: McpSetupTabsProps) {
139+
const [activeTab, setActiveTab] = useState<string>("claude-desktop");
140+
141+
const basicAuth = `Basic ${Buffer.from(`token:${token}`).toString("base64")}`;
142+
const baseUrl = useAgentsBaseURL() + "/mcp";
143+
144+
const mcpConfigs = {
145+
"claude-desktop": {
146+
label: "Claude Desktop",
147+
config: `{
148+
"mcpServers": {
149+
"mission-control": {
150+
"command": "npx",
151+
"args": [
152+
"-y",
153+
"@modelcontextprotocol/server-http",
154+
"${baseUrl}"
155+
],
156+
"env": {
157+
"AUTHORIZATION": "${basicAuth}"
158+
}
159+
}
160+
}
161+
}`
162+
},
163+
"claude-code": {
164+
label: "Claude Code",
165+
config: `{
166+
"name": "mission-control",
167+
"type": "http",
168+
"url": "${baseUrl}",
169+
"headers": {
170+
"Authorization": "${basicAuth}"
171+
}
172+
}`
173+
},
174+
"vscode-copilot": {
175+
label: "VS Code Copilot",
176+
config: `{
177+
"github.copilot.mcp.servers": {
178+
"mission-control": {
179+
"command": "npx",
180+
"args": [
181+
"-y",
182+
"@modelcontextprotocol/server-http",
183+
"${baseUrl}"
184+
],
185+
"env": {
186+
"AUTHORIZATION": "${basicAuth}"
187+
}
188+
}
189+
}
190+
}`
191+
},
192+
cline: {
193+
label: "Cline",
194+
config: `{
195+
"cline.mcpServers": {
196+
"mission-control": {
197+
"command": "npx",
198+
"args": [
199+
"-y",
200+
"@modelcontextprotocol/server-http",
201+
"${baseUrl}"
202+
],
203+
"env": {
204+
"AUTHORIZATION": "${basicAuth}"
205+
}
206+
}
207+
}
208+
}`
209+
},
210+
continue: {
211+
label: "Continue.dev",
212+
config: `{
213+
"mcpServers": [
214+
{
215+
"name": "mission-control",
216+
"command": "npx",
217+
"args": [
218+
"-y",
219+
"@modelcontextprotocol/server-http",
220+
"${baseUrl}"
221+
],
222+
"env": {
223+
"AUTHORIZATION": "${basicAuth}"
224+
}
225+
}
226+
]
227+
}`
228+
},
229+
zed: {
230+
label: "Zed Editor",
231+
config: `{
232+
"assistant": {
233+
"mcp": {
234+
"servers": {
235+
"mission-control": {
236+
"command": "npx",
237+
"args": [
238+
"-y",
239+
"@modelcontextprotocol/server-http",
240+
"${baseUrl}"
241+
],
242+
"env": {
243+
"AUTHORIZATION": "${basicAuth}"
244+
}
245+
}
246+
}
247+
}
248+
}
249+
}`
250+
},
251+
direct: {
252+
label: "Direct HTTP",
253+
config: `curl -X POST ${baseUrl} \\
254+
-H "Authorization: ${basicAuth}" \\
255+
-H "Content-Type: application/json" \\
256+
-d '{
257+
"jsonrpc": "2.0",
258+
"method": "initialize",
259+
"params": {
260+
"protocolVersion": "2024-11-05",
261+
"capabilities": {}
262+
},
263+
"id": 1
264+
}'`
265+
}
266+
};
267+
268+
return (
269+
<div className="mt-4">
270+
<Tabs activeTab={activeTab} onSelectTab={setActiveTab}>
271+
{Object.entries(mcpConfigs).map(([key, { label, config }]) => (
272+
<Tab key={key} label={label} value={key} className="p-4">
273+
<div className="max-h-64 overflow-y-auto">
274+
{key === "direct" ? (
275+
<CodeBlock code={config} language="bash" />
276+
) : (
277+
<JSONViewer
278+
code={config}
279+
format="json"
280+
showLineNo
281+
hideCopyButton={false}
282+
/>
283+
)}
284+
</div>
285+
</Tab>
286+
))}
287+
</Tabs>
288+
</div>
289+
);
290+
}

src/components/Tokens/TokenDetailsModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useMemo } from "react";
22
import { FaCircleNotch, FaTrash } from "react-icons/fa";
3+
import { Age } from "@flanksource-ui/ui/Age";
34
import { useMutation, useQueryClient } from "@tanstack/react-query";
45
import { deleteToken, Token } from "../../api/services/tokens";
56
import { Avatar } from "../../ui/Avatar";
@@ -36,7 +37,7 @@ export default function TokenDetailsModal({
3637
});
3738

3839
const formattedCreatedAt = useMemo(() => {
39-
return new Date(token.created_at).toLocaleString();
40+
return new Date(token.created_at).toISOString();
4041
}, [token.created_at]);
4142

4243
const handleDeleteToken = () => {
@@ -78,7 +79,7 @@ export default function TokenDetailsModal({
7879
<label className="text-sm font-medium text-gray-700">
7980
Created At
8081
</label>
81-
<div className="text-sm text-gray-900">{formattedCreatedAt}</div>
82+
<Age from={formattedCreatedAt} />
8283
</div>
8384
</div>
8485

src/components/Users/UserProfile.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import { OrganizationSwitcher, UserButton } from "@clerk/nextjs";
22
import { useState } from "react";
3-
import { IoMdDownload } from "react-icons/io";
3+
import { IoMdAirplane, IoMdDownload } from "react-icons/io";
4+
import { CreateTokenResponse } from "../../api/services/tokens";
45
import { KratosUserProfileDropdown } from "../Authentication/Kratos/KratosUserProfileDropdown";
56
import useDetermineAuthSystem from "../Authentication/useDetermineAuthSystem";
67
import AddKubeConfigModal from "../KubeConfig/AddKubeConfigModal";
8+
import CreateTokenForm, {
9+
TokenFormValues
10+
} from "../Tokens/Add/CreateTokenForm";
11+
import TokenDisplayModal from "../Tokens/Add/TokenDisplayModal";
712

813
export function UserProfileDropdown() {
914
const authSystem = useDetermineAuthSystem();
1015
const [isDownloadKubeConfigModalOpen, setIsDownloadKubeConfigModalOpen] =
1116
useState(false);
17+
const [isMcpTokenModalOpen, setIsMcpTokenModalOpen] = useState(false);
18+
const [isMcpTokenDisplayModalOpen, setIsMcpTokenDisplayModalOpen] =
19+
useState(false);
20+
const [mcpTokenResponse, setMcpTokenResponse] =
21+
useState<CreateTokenResponse>();
22+
const [mcpTokenFormValues, setMcpTokenFormValues] =
23+
useState<TokenFormValues>();
1224

1325
return (
1426
<>
@@ -27,18 +39,43 @@ export function UserProfileDropdown() {
2739
labelIcon={<IoMdDownload />}
2840
onClick={() => setIsDownloadKubeConfigModalOpen(true)}
2941
/>
42+
<UserButton.Action
43+
label="Setup MCP"
44+
labelIcon={<IoMdAirplane />}
45+
onClick={() => setIsMcpTokenModalOpen(true)}
46+
/>
3047
</UserButton.MenuItems>
3148
</UserButton>
3249
</div>
3350
) : (
3451
<KratosUserProfileDropdown
3552
openKubeConfigModal={() => setIsDownloadKubeConfigModalOpen(true)}
53+
openMcpTokenModal={() => setIsMcpTokenModalOpen(true)}
3654
/>
3755
)}
3856
<AddKubeConfigModal
3957
isOpen={isDownloadKubeConfigModalOpen}
4058
onClose={() => setIsDownloadKubeConfigModalOpen(false)}
4159
/>
60+
<CreateTokenForm
61+
isOpen={isMcpTokenModalOpen}
62+
onClose={() => setIsMcpTokenModalOpen(false)}
63+
onSuccess={(response, formValues) => {
64+
setMcpTokenResponse(response);
65+
setMcpTokenFormValues(formValues);
66+
setIsMcpTokenModalOpen(false);
67+
setIsMcpTokenDisplayModalOpen(true);
68+
}}
69+
/>
70+
{mcpTokenResponse && (
71+
<TokenDisplayModal
72+
isOpen={isMcpTokenDisplayModalOpen}
73+
onClose={() => setIsMcpTokenDisplayModalOpen(false)}
74+
tokenResponse={mcpTokenResponse}
75+
formValues={mcpTokenFormValues}
76+
isMcp={true}
77+
/>
78+
)}
4279
</>
4380
);
4481
}

0 commit comments

Comments
 (0)