195 lines
8.2 KiB
TypeScript
195 lines
8.2 KiB
TypeScript
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useState, useEffect } from "react";
|
|
import { Loader2, Server, Save, Copy } from "lucide-react";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
|
|
export function McpSettingsCard() {
|
|
const { t } = useTranslation();
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
const [formData, setFormData] = useState({
|
|
mcp_enabled: false,
|
|
mcp_port: "5001" // Default to main app port unless specific override logic is added
|
|
});
|
|
|
|
// Feature 1: Get User for API Key
|
|
const { data: user } = useQuery<any>({
|
|
queryKey: ['/api/user']
|
|
});
|
|
|
|
// Feature 2: API Key Mutations
|
|
const generateApiKeyMutation = useMutation({
|
|
mutationFn: async () => {
|
|
const res = await apiRequest("POST", "/api/user/apikey", {});
|
|
return res.json();
|
|
},
|
|
onSuccess: (data) => {
|
|
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
|
toast({ title: t('settings.mcp.generated') });
|
|
},
|
|
});
|
|
|
|
const revokeApiKeyMutation = useMutation({
|
|
mutationFn: async () => {
|
|
await apiRequest("DELETE", "/api/user/apikey");
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
|
toast({ title: t('settings.mcp.revoked') });
|
|
},
|
|
});
|
|
|
|
const copyToClipboard = (text: string) => {
|
|
navigator.clipboard.writeText(text);
|
|
toast({ title: t('settings.mcp.copied', "Copied!") });
|
|
}
|
|
|
|
const { data: settings, isLoading } = useQuery<any>({
|
|
queryKey: ['/api/admin/settings'],
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (settings) {
|
|
setFormData({
|
|
mcp_enabled: settings.mcp_enabled === true,
|
|
mcp_port: settings.mcp_port || "5001"
|
|
});
|
|
}
|
|
}, [settings]);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: async (data: any) => {
|
|
const res = await fetch("/api/admin/settings", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to save settings");
|
|
return res.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['/api/admin/settings'] });
|
|
toast({ title: t('settings.saved', 'Settings saved') });
|
|
},
|
|
onError: () => {
|
|
toast({ title: t('settings.error', 'Failed to save settings'), variant: "destructive" });
|
|
}
|
|
});
|
|
|
|
const handleSave = () => {
|
|
mutation.mutate({
|
|
mcp_enabled: formData.mcp_enabled,
|
|
mcp_port: formData.mcp_port
|
|
});
|
|
};
|
|
|
|
if (isLoading) return <div className="flex justify-center p-4"><Loader2 className="animate-spin" /></div>;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<Server className="h-5 w-5 text-primary" />
|
|
<CardTitle>{t('settings.mcp.title')}</CardTitle>
|
|
</div>
|
|
<CardDescription>
|
|
{t('settings.mcp.description')}
|
|
<span className="block mt-1 text-xs">{t('settings.mcp.descriptionDetail', "The MCP server runs on the same port as the application (/api/mcp).")}</span>
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="mcp_enabled" className="flex flex-col gap-1">
|
|
<span>{t('settings.mcp.enableLabel')}</span>
|
|
<span className="font-normal text-xs text-muted-foreground">
|
|
{t('settings.mcp.enableDesc')}
|
|
</span>
|
|
</Label>
|
|
<Switch
|
|
id="mcp_enabled"
|
|
checked={formData.mcp_enabled}
|
|
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, mcp_enabled: checked }))}
|
|
/>
|
|
</div>
|
|
|
|
{/* Status Indicator */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<p className="font-medium">{t('settings.mcp.status')}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-green-500">
|
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
|
<span className="font-medium">{t('settings.mcp.running')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Endpoint URL */}
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium">{t('settings.mcp.url')}</p>
|
|
<div className="flex gap-2">
|
|
<Input readOnly value={`${window.location.protocol}//${window.location.host}/api/mcp/sse`} />
|
|
<Button variant="outline" size="icon" onClick={() => copyToClipboard(`${window.location.protocol}//${window.location.host}/api/mcp/sse`)}>
|
|
<Copy className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* API Key Management */}
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium">{t('settings.mcp.apiKey')}</p>
|
|
{user?.apiKey ? (
|
|
<div className="flex gap-2">
|
|
<Input type="password" readOnly value={user.apiKey} />
|
|
<Button variant="outline" size="icon" onClick={() => copyToClipboard(user.apiKey!)}>
|
|
<Copy className="w-4 h-4" />
|
|
</Button>
|
|
<Button variant="destructive" onClick={() => revokeApiKeyMutation.mutate()} disabled={revokeApiKeyMutation.isPending}>
|
|
{t('settings.mcp.revoke')}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-muted-foreground">{t('settings.mcp.noKey')}</p>
|
|
<Button onClick={() => generateApiKeyMutation.mutate()} disabled={generateApiKeyMutation.isPending}>
|
|
{t('settings.mcp.generate')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-muted/50 p-3 rounded-lg text-sm text-muted-foreground">
|
|
{t('settings.mcp.instructions')}
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="mcp_port">{t('settings.mcp.portLabel')}</Label>
|
|
<Input
|
|
id="mcp_port"
|
|
value={formData.mcp_port}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, mcp_port: e.target.value }))}
|
|
placeholder="5001"
|
|
disabled
|
|
className="bg-muted"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('settings.mcp.portDesc')}
|
|
</p>
|
|
</div>
|
|
|
|
<Button onClick={handleSave} disabled={mutation.isPending}>
|
|
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
<Save className="mr-2 h-4 w-4" />
|
|
{t('settings.save', 'Save Changes')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|