mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-19 18:45:15 +02:00
connector-popup: surface trusted-tools UI in MCP edit view; consolidate disconnect
- Slot MCPTrustedTools in mcp-service-config (gated on connector.id > 0) so any connected MCP-backed connector exposes a revoke surface for approve_always grants. - Add new mcp-trusted-tools.tsx (audit + revoke list) and connectorsApiService.untrustMCPTool() that backs it. - Drop the redundant row-level Disconnect from ConnectorAccountsListView: Manage now leads to the edit view whose own Disconnect is the single source of truth. Remove the now-dead onDisconnect prop, confirm-flow state, and handleDisconnectFromList hook callback + return entry.
This commit is contained in:
parent
a22e0e915f
commit
5327f3348c
6 changed files with 101 additions and 78 deletions
|
|
@ -124,7 +124,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
||||||
handleStartEdit,
|
handleStartEdit,
|
||||||
handleSaveConnector,
|
handleSaveConnector,
|
||||||
handleDisconnectConnector,
|
handleDisconnectConnector,
|
||||||
handleDisconnectFromList,
|
|
||||||
handleBackFromEdit,
|
handleBackFromEdit,
|
||||||
handleBackFromConnect,
|
handleBackFromConnect,
|
||||||
handleBackFromYouTube,
|
handleBackFromYouTube,
|
||||||
|
|
@ -233,9 +232,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
||||||
indexingConnectorIds={indexingConnectorIds}
|
indexingConnectorIds={indexingConnectorIds}
|
||||||
onBack={handleBackFromMCPList}
|
onBack={handleBackFromMCPList}
|
||||||
onManage={handleStartEdit}
|
onManage={handleStartEdit}
|
||||||
onDisconnect={(connector) =>
|
|
||||||
handleDisconnectFromList(connector, () => refreshConnectors())
|
|
||||||
}
|
|
||||||
onAddAccount={handleAddNewMCPFromList}
|
onAddAccount={handleAddNewMCPFromList}
|
||||||
addButtonText="Add New MCP Server"
|
addButtonText="Add New MCP Server"
|
||||||
/>
|
/>
|
||||||
|
|
@ -247,9 +243,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
||||||
indexingConnectorIds={indexingConnectorIds}
|
indexingConnectorIds={indexingConnectorIds}
|
||||||
onBack={handleBackFromAccountsList}
|
onBack={handleBackFromAccountsList}
|
||||||
onManage={handleStartEdit}
|
onManage={handleStartEdit}
|
||||||
onDisconnect={(connector) =>
|
|
||||||
handleDisconnectFromList(connector, () => refreshConnectors())
|
|
||||||
}
|
|
||||||
onAddAccount={() => {
|
onAddAccount={() => {
|
||||||
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
|
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
|
||||||
const oauthConnector =
|
const oauthConnector =
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { CheckCircle2 } from "lucide-react";
|
import { CheckCircle2 } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import type { ConnectorConfigProps } from "../index";
|
import type { ConnectorConfigProps } from "../index";
|
||||||
|
import { MCPTrustedTools } from "./mcp-trusted-tools";
|
||||||
|
|
||||||
export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
|
export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
|
||||||
const serviceName = connector.config?.mcp_service as string | undefined;
|
const serviceName = connector.config?.mcp_service as string | undefined;
|
||||||
|
|
@ -11,7 +12,7 @@ export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
|
||||||
: "this service";
|
: "this service";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<div className="rounded-xl border border-border bg-emerald-500/5 p-4 flex items-start gap-3">
|
<div className="rounded-xl border border-border bg-emerald-500/5 p-4 flex items-start gap-3">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 shrink-0 mt-0.5">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 shrink-0 mt-0.5">
|
||||||
<CheckCircle2 className="size-4 text-emerald-500" />
|
<CheckCircle2 className="size-4 text-emerald-500" />
|
||||||
|
|
@ -23,6 +24,8 @@ export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{connector.id > 0 && <MCPTrustedTools connector={connector} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ShieldCheck, Trash2 } from "lucide-react";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
|
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||||
|
|
||||||
|
interface MCPTrustedToolsProps {
|
||||||
|
connector: SearchSourceConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Audit + revoke surface for tools promoted via in-chat "Always Allow". */
|
||||||
|
export const MCPTrustedTools: FC<MCPTrustedToolsProps> = ({ connector }) => {
|
||||||
|
const trustedTools = readTrustedTools(connector.config);
|
||||||
|
const [pending, setPending] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const handleRevoke = async (toolName: string) => {
|
||||||
|
setPending((prev) => new Set(prev).add(toolName));
|
||||||
|
try {
|
||||||
|
await connectorsApiService.untrustMCPTool(connector.id, toolName);
|
||||||
|
toast.success(`Removed ${toolName} from trusted tools`);
|
||||||
|
} catch {
|
||||||
|
toast.error(`Failed to remove ${toolName} from trusted tools`);
|
||||||
|
} finally {
|
||||||
|
setPending((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(toolName);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-medium text-sm sm:text-base flex items-center gap-2">
|
||||||
|
<ShieldCheck className="h-4 w-4" />
|
||||||
|
Trusted Tools
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-4">
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
Tools listed here skip the approval prompt during chat. Trust is granted by clicking
|
||||||
|
"Always Allow" on an approval card; revoke it here to require approval again.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{trustedTools.length === 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground/70 italic">
|
||||||
|
No trusted tools yet for this connector.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{trustedTools.map((toolName) => {
|
||||||
|
const isPending = pending.has(toolName);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={toolName}
|
||||||
|
className="flex items-center justify-between gap-3 rounded-lg px-3 py-2 hover:bg-muted/40 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-xs font-mono break-all">{toolName}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
|
||||||
|
onClick={() => handleRevoke(toolName)}
|
||||||
|
disabled={isPending}
|
||||||
|
aria-label={`Revoke trust for ${toolName}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
<span className="ml-1 hidden sm:inline">Revoke</span>
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function readTrustedTools(config: Record<string, unknown> | undefined | null): string[] {
|
||||||
|
const raw = config?.trusted_tools;
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
return raw.filter((item): item is string => typeof item === "string");
|
||||||
|
}
|
||||||
|
|
@ -1288,25 +1288,6 @@ export const useConnectorDialog = () => {
|
||||||
[editingConnector, searchSpaceId, deleteConnector, cameFromMCPList, setIsOpen]
|
[editingConnector, searchSpaceId, deleteConnector, cameFromMCPList, setIsOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDisconnectFromList = useCallback(
|
|
||||||
async (connector: SearchSourceConnector, refreshConnectors: () => void) => {
|
|
||||||
if (!searchSpaceId) return;
|
|
||||||
try {
|
|
||||||
await deleteConnector({ id: connector.id });
|
|
||||||
trackConnectorDeleted(Number(searchSpaceId), connector.connector_type, connector.id);
|
|
||||||
toast.success(`${connector.name} disconnected successfully`);
|
|
||||||
refreshConnectors();
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error disconnecting connector:", error);
|
|
||||||
toast.error("Failed to disconnect connector");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[searchSpaceId, deleteConnector]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle quick index (index with selected date range, or backend defaults if none selected)
|
// Handle quick index (index with selected date range, or backend defaults if none selected)
|
||||||
const handleQuickIndexConnector = useCallback(
|
const handleQuickIndexConnector = useCallback(
|
||||||
async (
|
async (
|
||||||
|
|
@ -1480,7 +1461,6 @@ export const useConnectorDialog = () => {
|
||||||
handleStartEdit,
|
handleStartEdit,
|
||||||
handleSaveConnector,
|
handleSaveConnector,
|
||||||
handleDisconnectConnector,
|
handleDisconnectConnector,
|
||||||
handleDisconnectFromList,
|
|
||||||
handleBackFromEdit,
|
handleBackFromEdit,
|
||||||
handleBackFromConnect,
|
handleBackFromConnect,
|
||||||
handleBackFromYouTube,
|
handleBackFromYouTube,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { ArrowLeft, Plus, RefreshCw, Server, Trash2 } from "lucide-react";
|
import { ArrowLeft, Plus, RefreshCw, Server } from "lucide-react";
|
||||||
import { type FC, useCallback, useState } from "react";
|
import { type FC, useCallback, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||||
|
|
@ -24,7 +24,6 @@ interface ConnectorAccountsListViewProps {
|
||||||
indexingConnectorIds: Set<number>;
|
indexingConnectorIds: Set<number>;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onManage: (connector: SearchSourceConnector) => void;
|
onManage: (connector: SearchSourceConnector) => void;
|
||||||
onDisconnect?: (connector: SearchSourceConnector) => Promise<void> | void;
|
|
||||||
onAddAccount: () => void;
|
onAddAccount: () => void;
|
||||||
isConnecting?: boolean;
|
isConnecting?: boolean;
|
||||||
addButtonText?: string;
|
addButtonText?: string;
|
||||||
|
|
@ -37,15 +36,12 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
||||||
indexingConnectorIds,
|
indexingConnectorIds,
|
||||||
onBack,
|
onBack,
|
||||||
onManage,
|
onManage,
|
||||||
onDisconnect,
|
|
||||||
onAddAccount,
|
onAddAccount,
|
||||||
isConnecting = false,
|
isConnecting = false,
|
||||||
addButtonText,
|
addButtonText,
|
||||||
}) => {
|
}) => {
|
||||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||||
const [reauthingId, setReauthingId] = useState<number | null>(null);
|
const [reauthingId, setReauthingId] = useState<number | null>(null);
|
||||||
const [confirmDisconnectId, setConfirmDisconnectId] = useState<number | null>(null);
|
|
||||||
const [disconnectingId, setDisconnectingId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// Get connector status
|
// Get connector status
|
||||||
const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus();
|
const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus();
|
||||||
|
|
@ -240,51 +236,6 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
||||||
/>
|
/>
|
||||||
Re-authenticate
|
Re-authenticate
|
||||||
</Button>
|
</Button>
|
||||||
) : isLive && onDisconnect ? (
|
|
||||||
confirmDisconnectId === connector.id ? (
|
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 text-[11px] px-3 rounded-lg font-medium shadow-xs"
|
|
||||||
onClick={async () => {
|
|
||||||
setDisconnectingId(connector.id);
|
|
||||||
setConfirmDisconnectId(null);
|
|
||||||
try {
|
|
||||||
await onDisconnect(connector);
|
|
||||||
} finally {
|
|
||||||
setDisconnectingId(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={disconnectingId === connector.id}
|
|
||||||
>
|
|
||||||
{disconnectingId === connector.id ? (
|
|
||||||
<RefreshCw className="size-3.5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Confirm"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 text-[11px] px-2 rounded-lg"
|
|
||||||
onClick={() => setConfirmDisconnectId(null)}
|
|
||||||
disabled={disconnectingId === connector.id}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-red-50 hover:text-red-700 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-red-950 dark:hover:text-red-400 shrink-0"
|
|
||||||
onClick={() => setConfirmDisconnectId(connector.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-3.5" />
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,13 @@ class ConnectorsApiService {
|
||||||
`/api/v1/obsidian/stats?vault_id=${encodeURIComponent(vaultId)}`
|
`/api/v1/obsidian/stats?vault_id=${encodeURIComponent(vaultId)}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Revoke a previously-trusted MCP tool so the next call asks again. */
|
||||||
|
untrustMCPTool = async (connectorId: number, toolName: string): Promise<void> => {
|
||||||
|
await baseApiService.post(`/api/v1/connectors/mcp/${connectorId}/untrust-tool`, undefined, {
|
||||||
|
body: { tool_name: toolName },
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObsidianStats {
|
export interface ObsidianStats {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue