Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export function useAgentsBaseURL() {

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

return baseUrl;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import KratosLogoutButton from "./KratosLogoutButton";

type UserProfileDropdownProps = {
openKubeConfigModal: () => void;
openMcpTokenModal: () => void;
};

export function KratosUserProfileDropdown({
openKubeConfigModal
openKubeConfigModal,
openMcpTokenModal
}: UserProfileDropdownProps) {
const { user } = useUser();
const userNavigation = [{ name: "Your Profile", href: "/profile-settings" }];
Expand Down Expand Up @@ -75,6 +77,14 @@ export function KratosUserProfileDropdown({
Download kubeconfig
</button>
</MenuItem>
<MenuItem>
<button
onClick={openMcpTokenModal}
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"
>
Setup MCP
</button>
</MenuItem>
<MenuItem>
<VersionInfo />
</MenuItem>
Expand Down
175 changes: 174 additions & 1 deletion src/components/Tokens/Add/TokenDisplayModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ import { useState } from "react";
import { FaCopy, FaEye, FaEyeSlash } from "react-icons/fa";
import { CreateTokenResponse } from "../../../api/services/tokens";
import { Button } from "../../../ui/Buttons/Button";
import { JSONViewer } from "../../../ui/Code/JSONViewer";
import { Modal } from "../../../ui/Modal";
import { Tab, Tabs } from "../../../ui/Tabs/Tabs";
import { toastSuccess } from "../../Toast/toast";
import { TokenFormValues } from "./CreateTokenForm";
import { useAgentsBaseURL } from "../../../components/Agents/InstalAgentInstruction/useAgentsBaseURL";
import CodeBlock from "@flanksource-ui/ui/Code/CodeBlock";

type Props = {
isOpen: boolean;
onClose: () => void;
tokenResponse: CreateTokenResponse;
formValues?: TokenFormValues;
isMcp?: boolean;
};

export default function TokenDisplayModal({
isOpen,
onClose,
tokenResponse,
formValues
formValues,
isMcp = false
}: Props) {
const [showToken, setShowToken] = useState(false);

Expand Down Expand Up @@ -77,6 +83,15 @@ export default function TokenDisplayModal({
</div>
</div>

{isMcp && (
<div className="rounded-md border border-green-200 bg-green-50 p-4">
<h4 className="mb-2 font-medium text-green-800">
MCP Client Setup:
</h4>
<McpSetupTabs token={tokenResponse.payload.token} />
</div>
)}

<div className="rounded-md border border-blue-200 bg-blue-50 p-4">
<h4 className="mb-2 font-medium text-blue-800">
Usage Instructions:
Expand Down Expand Up @@ -115,3 +130,161 @@ export default function TokenDisplayModal({
</Modal>
);
}

type McpSetupTabsProps = {
token: string;
};

function McpSetupTabs({ token }: McpSetupTabsProps) {
const [activeTab, setActiveTab] = useState<string>("claude-desktop");

const basicAuth = `Basic ${Buffer.from(`token:${token}`).toString("base64")}`;
const baseUrl = useAgentsBaseURL() + "/mcp";

const mcpConfigs = {
"claude-desktop": {
label: "Claude Desktop",
config: `{
"mcpServers": {
"mission-control": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-http",
"${baseUrl}"
],
"env": {
"AUTHORIZATION": "${basicAuth}"
}
}
}
}`
},
"claude-code": {
label: "Claude Code",
config: `{
"name": "mission-control",
"type": "http",
"url": "${baseUrl}",
"headers": {
"Authorization": "${basicAuth}"
}
}`
},
"vscode-copilot": {
label: "VS Code Copilot",
config: `{
"github.copilot.mcp.servers": {
"mission-control": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-http",
"${baseUrl}"
],
"env": {
"AUTHORIZATION": "${basicAuth}"
}
}
}
}`
},
cline: {
label: "Cline",
config: `{
"cline.mcpServers": {
"mission-control": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-http",
"${baseUrl}"
],
"env": {
"AUTHORIZATION": "${basicAuth}"
}
}
}
}`
},
continue: {
label: "Continue.dev",
config: `{
"mcpServers": [
{
"name": "mission-control",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-http",
"${baseUrl}"
],
"env": {
"AUTHORIZATION": "${basicAuth}"
}
}
]
}`
},
zed: {
label: "Zed Editor",
config: `{
"assistant": {
"mcp": {
"servers": {
"mission-control": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-http",
"${baseUrl}"
],
"env": {
"AUTHORIZATION": "${basicAuth}"
}
}
}
}
}
}`
},
direct: {
label: "Direct HTTP",
config: `curl -X POST ${baseUrl} \\
-H "Authorization: ${basicAuth}" \\
-H "Content-Type: application/json" \\
-d '{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {}
},
"id": 1
}'`
}
};

return (
<div className="mt-4">
<Tabs activeTab={activeTab} onSelectTab={setActiveTab}>
{Object.entries(mcpConfigs).map(([key, { label, config }]) => (
<Tab key={key} label={label} value={key} className="p-4">
<div className="max-h-64 overflow-y-auto">
{key === "direct" ? (
<CodeBlock code={config} language="bash" />
) : (
<JSONViewer
code={config}
format="json"
showLineNo
hideCopyButton={false}
/>
)}
</div>
</Tab>
))}
</Tabs>
</div>
);
}
5 changes: 3 additions & 2 deletions src/components/Tokens/TokenDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { FaCircleNotch, FaTrash } from "react-icons/fa";
import { Age } from "@flanksource-ui/ui/Age";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { deleteToken, Token } from "../../api/services/tokens";
import { Avatar } from "../../ui/Avatar";
Expand Down Expand Up @@ -36,7 +37,7 @@ export default function TokenDetailsModal({
});

const formattedCreatedAt = useMemo(() => {
return new Date(token.created_at).toLocaleString();
return new Date(token.created_at).toISOString();
}, [token.created_at]);

const handleDeleteToken = () => {
Expand Down Expand Up @@ -78,7 +79,7 @@ export default function TokenDetailsModal({
<label className="text-sm font-medium text-gray-700">
Created At
</label>
<div className="text-sm text-gray-900">{formattedCreatedAt}</div>
<Age from={formattedCreatedAt} />
</div>
</div>

Expand Down
39 changes: 38 additions & 1 deletion src/components/Users/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { OrganizationSwitcher, UserButton } from "@clerk/nextjs";
import { useState } from "react";
import { IoMdDownload } from "react-icons/io";
import { IoMdAirplane, IoMdDownload } from "react-icons/io";
import { CreateTokenResponse } from "../../api/services/tokens";
import { KratosUserProfileDropdown } from "../Authentication/Kratos/KratosUserProfileDropdown";
import useDetermineAuthSystem from "../Authentication/useDetermineAuthSystem";
import AddKubeConfigModal from "../KubeConfig/AddKubeConfigModal";
import CreateTokenForm, {
TokenFormValues
} from "../Tokens/Add/CreateTokenForm";
import TokenDisplayModal from "../Tokens/Add/TokenDisplayModal";

export function UserProfileDropdown() {
const authSystem = useDetermineAuthSystem();
const [isDownloadKubeConfigModalOpen, setIsDownloadKubeConfigModalOpen] =
useState(false);
const [isMcpTokenModalOpen, setIsMcpTokenModalOpen] = useState(false);
const [isMcpTokenDisplayModalOpen, setIsMcpTokenDisplayModalOpen] =
useState(false);
const [mcpTokenResponse, setMcpTokenResponse] =
useState<CreateTokenResponse>();
const [mcpTokenFormValues, setMcpTokenFormValues] =
useState<TokenFormValues>();

return (
<>
Expand All @@ -27,18 +39,43 @@ export function UserProfileDropdown() {
labelIcon={<IoMdDownload />}
onClick={() => setIsDownloadKubeConfigModalOpen(true)}
/>
<UserButton.Action
label="Setup MCP"
labelIcon={<IoMdAirplane />}
onClick={() => setIsMcpTokenModalOpen(true)}
/>
</UserButton.MenuItems>
</UserButton>
</div>
) : (
<KratosUserProfileDropdown
openKubeConfigModal={() => setIsDownloadKubeConfigModalOpen(true)}
openMcpTokenModal={() => setIsMcpTokenModalOpen(true)}
/>
)}
<AddKubeConfigModal
isOpen={isDownloadKubeConfigModalOpen}
onClose={() => setIsDownloadKubeConfigModalOpen(false)}
/>
<CreateTokenForm
isOpen={isMcpTokenModalOpen}
onClose={() => setIsMcpTokenModalOpen(false)}
onSuccess={(response, formValues) => {
setMcpTokenResponse(response);
setMcpTokenFormValues(formValues);
setIsMcpTokenModalOpen(false);
setIsMcpTokenDisplayModalOpen(true);
}}
/>
{mcpTokenResponse && (
<TokenDisplayModal
isOpen={isMcpTokenDisplayModalOpen}
onClose={() => setIsMcpTokenDisplayModalOpen(false)}
tokenResponse={mcpTokenResponse}
formValues={mcpTokenFormValues}
isMcp={true}
/>
)}
</>
);
}
Loading
Loading