mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-17 01:08:27 +00:00
Feat: add MCP logs
This commit is contained in:
parent
256cdb8bdc
commit
cdb6788ef0
@ -38,9 +38,20 @@ export function getIcon(icon: IconType) {
|
|||||||
return ICON_MAPPING["integration"];
|
return ICON_MAPPING["integration"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getIconForAuthorise = (name: string, image?: string) => {
|
export const getIconForAuthorise = (
|
||||||
|
name: string,
|
||||||
|
size = 40,
|
||||||
|
image?: string,
|
||||||
|
) => {
|
||||||
if (image) {
|
if (image) {
|
||||||
return <img src={image} alt={name} className="h-[40px] w-[40px] rounded" />;
|
return (
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={name}
|
||||||
|
className="rounded"
|
||||||
|
style={{ height: size, width: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowerName = name.toLowerCase();
|
const lowerName = name.toLowerCase();
|
||||||
@ -48,8 +59,8 @@ export const getIconForAuthorise = (name: string, image?: string) => {
|
|||||||
if (lowerName in ICON_MAPPING) {
|
if (lowerName in ICON_MAPPING) {
|
||||||
const IconComponent = ICON_MAPPING[lowerName as IconType];
|
const IconComponent = ICON_MAPPING[lowerName as IconType];
|
||||||
|
|
||||||
return <IconComponent size={40} />;
|
return <IconComponent size={size} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <LayoutGrid size={40} />;
|
return <LayoutGrid size={size} />;
|
||||||
};
|
};
|
||||||
|
|||||||
127
apps/webapp/app/components/mcp/mcp-sessions-filters.tsx
Normal file
127
apps/webapp/app/components/mcp/mcp-sessions-filters.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { ListFilter, X } from "lucide-react";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverPortal,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
|
||||||
|
interface McpSessionsFiltersProps {
|
||||||
|
availableSources: Array<{ name: string; slug: string; count: number }>;
|
||||||
|
selectedSource?: string;
|
||||||
|
onSourceChange: (source?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterStep = "main" | "source";
|
||||||
|
|
||||||
|
export function McpSessionsFilters({
|
||||||
|
availableSources,
|
||||||
|
selectedSource,
|
||||||
|
onSourceChange,
|
||||||
|
}: McpSessionsFiltersProps) {
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const [step, setStep] = useState<FilterStep>("main");
|
||||||
|
|
||||||
|
// Only show first few sources, or "All sources" if none
|
||||||
|
const limitedSources = availableSources.slice(0, 5);
|
||||||
|
|
||||||
|
const selectedSourceName = availableSources.find(
|
||||||
|
(s) => s.slug === selectedSource,
|
||||||
|
)?.name;
|
||||||
|
|
||||||
|
const hasFilters = selectedSource;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-2 flex w-full items-center justify-start gap-2">
|
||||||
|
<Popover
|
||||||
|
open={popoverOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setPopoverOpen(open);
|
||||||
|
if (!open) setStep("main");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={popoverOpen}
|
||||||
|
className="justify-between"
|
||||||
|
>
|
||||||
|
<ListFilter className="mr-2 h-4 w-4" />
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverPortal>
|
||||||
|
<PopoverContent className="w-[220px] p-0" align="start">
|
||||||
|
{step === "main" && (
|
||||||
|
<div className="flex flex-col gap-1 p-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="justify-start"
|
||||||
|
onClick={() => setStep("source")}
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "source" && (
|
||||||
|
<div className="flex flex-col gap-1 p-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={() => {
|
||||||
|
onSourceChange(undefined);
|
||||||
|
setPopoverOpen(false);
|
||||||
|
setStep("main");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
All sources
|
||||||
|
</Button>
|
||||||
|
{limitedSources.map((source) => (
|
||||||
|
<Button
|
||||||
|
key={source.slug}
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-between"
|
||||||
|
onClick={() => {
|
||||||
|
onSourceChange(
|
||||||
|
source.slug === selectedSource
|
||||||
|
? undefined
|
||||||
|
: source.slug,
|
||||||
|
);
|
||||||
|
setPopoverOpen(false);
|
||||||
|
setStep("main");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{source.name}</span>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{source.count}
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</PopoverPortal>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* Active Filters */}
|
||||||
|
{hasFilters && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedSource && (
|
||||||
|
<Badge variant="secondary" className="h-7 gap-1 rounded px-2">
|
||||||
|
{selectedSourceName}
|
||||||
|
<X
|
||||||
|
className="hover:text-destructive h-3.5 w-3.5 cursor-pointer"
|
||||||
|
onClick={() => onSourceChange(undefined)}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
apps/webapp/app/components/mcp/mcp-sources-stats.tsx
Normal file
105
apps/webapp/app/components/mcp/mcp-sources-stats.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||||
|
import { Badge } from "../ui/badge";
|
||||||
|
import { LoaderCircle } from "lucide-react";
|
||||||
|
import { getIconForAuthorise } from "../icon-utils";
|
||||||
|
|
||||||
|
interface McpSourcesStatsProps {
|
||||||
|
sources: Array<{ name: string; slug: string; count: number }>;
|
||||||
|
activeSources?: string[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function McpSourcesStats({
|
||||||
|
sources,
|
||||||
|
activeSources = [],
|
||||||
|
isLoading,
|
||||||
|
}: McpSourcesStatsProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Top Sources</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<LoaderCircle className="text-primary h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSessions = sources.reduce((sum, source) => sum + source.count, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Top Sources</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-3 pb-4">
|
||||||
|
{sources.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm">No sources found</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{sources.slice(0, 5).map((source) => {
|
||||||
|
const percentage =
|
||||||
|
totalSessions > 0 ? (source.count / totalSessions) * 100 : 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={source.slug}
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{getIconForAuthorise(source.name.toLowerCase(), 16)}
|
||||||
|
<span className="mr-1 text-sm">{source.name}</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{source.count}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-2 w-16 overflow-hidden rounded-full bg-gray-200">
|
||||||
|
<div
|
||||||
|
className="bg-primary h-full transition-all duration-500"
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground w-10 text-right text-xs">
|
||||||
|
{percentage.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Current Active</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-3 pb-4">
|
||||||
|
{activeSources.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm">No active sources</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{activeSources.map((source) => (
|
||||||
|
<Badge
|
||||||
|
key={source}
|
||||||
|
variant="secondary"
|
||||||
|
className="rounded text-xs"
|
||||||
|
>
|
||||||
|
{getIconForAuthorise(source.toLowerCase(), 12)}
|
||||||
|
|
||||||
|
{source}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
273
apps/webapp/app/components/mcp/virtual-mcp-sessions-list.tsx
Normal file
273
apps/webapp/app/components/mcp/virtual-mcp-sessions-list.tsx
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
InfiniteLoader,
|
||||||
|
AutoSizer,
|
||||||
|
CellMeasurer,
|
||||||
|
CellMeasurerCache,
|
||||||
|
type Index,
|
||||||
|
type ListRowProps,
|
||||||
|
} from "react-virtualized";
|
||||||
|
import { type McpSessionItem } from "~/hooks/use-mcp-sessions";
|
||||||
|
import { ScrollManagedList } from "../virtualized-list";
|
||||||
|
import { Badge } from "../ui/badge";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Check, Copy } from "lucide-react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { Card, CardContent } from "../ui/card";
|
||||||
|
import { getIconForAuthorise } from "../icon-utils";
|
||||||
|
|
||||||
|
interface VirtualMcpSessionsListProps {
|
||||||
|
sessions: McpSessionItem[];
|
||||||
|
hasMore: boolean;
|
||||||
|
loadMore: () => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MCPUrlBox() {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [selectedSource, setSelectedSource] = useState<
|
||||||
|
"Claude" | "Cursor" | "Other"
|
||||||
|
>("Claude");
|
||||||
|
|
||||||
|
const getMcpURL = (source: "Claude" | "Cursor" | "Other") => {
|
||||||
|
const baseUrl = "https://core.heysol.ai/api/v1/mcp";
|
||||||
|
return `${baseUrl}?source=${source}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mcpURL = getMcpURL(selectedSource);
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(mcpURL);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="min-w-[400px] rounded-lg bg-transparent pt-1">
|
||||||
|
<CardContent className="pt-2 text-base">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-grayAlpha-100 flex space-x-1 rounded-lg p-1">
|
||||||
|
{(["Claude", "Cursor", "Other"] as const).map((source) => (
|
||||||
|
<Button
|
||||||
|
key={source}
|
||||||
|
onClick={() => setSelectedSource(source)}
|
||||||
|
variant="ghost"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 rounded-md px-3 py-1.5 transition-all",
|
||||||
|
selectedSource === source
|
||||||
|
? "bg-accent text-accent-foreground shadow-sm"
|
||||||
|
: "text-muted-foreground hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{source}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-background-3 flex items-center rounded">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="mcpURL"
|
||||||
|
value={mcpURL}
|
||||||
|
readOnly
|
||||||
|
className="bg-background-3 block w-full text-base"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="px-3"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function McpSessionItemRenderer(
|
||||||
|
props: ListRowProps,
|
||||||
|
sessions: McpSessionItem[],
|
||||||
|
cache: CellMeasurerCache,
|
||||||
|
) {
|
||||||
|
const { index, key, style, parent } = props;
|
||||||
|
const session = sessions[index];
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return (
|
||||||
|
<CellMeasurer
|
||||||
|
key={key}
|
||||||
|
cache={cache}
|
||||||
|
columnIndex={0}
|
||||||
|
parent={parent}
|
||||||
|
rowIndex={index}
|
||||||
|
>
|
||||||
|
<div key={key} style={style} className="p-4">
|
||||||
|
<div className="h-20 animate-pulse rounded bg-gray-200" />
|
||||||
|
</div>
|
||||||
|
</CellMeasurer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdAt = new Date(session.createdAt);
|
||||||
|
const deleted = !!session.deleted;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CellMeasurer
|
||||||
|
key={key}
|
||||||
|
cache={cache}
|
||||||
|
columnIndex={0}
|
||||||
|
parent={parent}
|
||||||
|
rowIndex={index}
|
||||||
|
>
|
||||||
|
<div key={key} style={style} className="px-0 py-2">
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group-hover:bg-grayAlpha-100 flex min-w-[0px] shrink grow items-start gap-2 rounded-md px-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"border-border flex w-full min-w-[0px] shrink flex-col border-b py-1",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-between gap-4">
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
|
{getIconForAuthorise(session.source.toLowerCase(), 18)}
|
||||||
|
|
||||||
|
<div className="inline-flex min-h-[24px] min-w-[0px] shrink cursor-pointer items-center justify-start">
|
||||||
|
<div className={cn("truncate text-left")}>
|
||||||
|
{session.source}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground flex shrink-0 items-center justify-end text-xs">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{!deleted && (
|
||||||
|
<Badge className="bg-success/20 text-success mr-2 rounded text-xs">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<div className="text-muted-foreground mr-3">
|
||||||
|
{createdAt.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{session.integrations.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{session.integrations.map((integration) => (
|
||||||
|
<Badge
|
||||||
|
key={integration}
|
||||||
|
variant="secondary"
|
||||||
|
className="rounded text-xs"
|
||||||
|
>
|
||||||
|
{getIconForAuthorise(integration, 12)}
|
||||||
|
{integration}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CellMeasurer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VirtualMcpSessionsList({
|
||||||
|
sessions,
|
||||||
|
hasMore,
|
||||||
|
loadMore,
|
||||||
|
isLoading,
|
||||||
|
}: VirtualMcpSessionsListProps) {
|
||||||
|
// Create a CellMeasurerCache instance using useRef to prevent recreation
|
||||||
|
const cacheRef = useRef<CellMeasurerCache | null>(null);
|
||||||
|
if (!cacheRef.current) {
|
||||||
|
cacheRef.current = new CellMeasurerCache({
|
||||||
|
defaultHeight: 100, // Default row height
|
||||||
|
fixedWidth: true, // Rows have fixed width but dynamic height
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const cache = cacheRef.current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cache.clearAll();
|
||||||
|
}, [sessions, cache]);
|
||||||
|
|
||||||
|
const isRowLoaded = ({ index }: { index: number }) => {
|
||||||
|
return !!sessions[index];
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMoreRows = async () => {
|
||||||
|
if (hasMore) {
|
||||||
|
return loadMore();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowRenderer = (props: ListRowProps) => {
|
||||||
|
return McpSessionItemRenderer(props, sessions, cache);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowHeight = ({ index }: Index) => {
|
||||||
|
return cache.getHeight(index, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemCount = hasMore ? sessions.length + 1 : sessions.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full grow overflow-hidden">
|
||||||
|
<AutoSizer className="h-full">
|
||||||
|
{({ width, height: autoHeight }) => (
|
||||||
|
<InfiniteLoader
|
||||||
|
isRowLoaded={isRowLoaded}
|
||||||
|
loadMoreRows={loadMoreRows}
|
||||||
|
rowCount={itemCount}
|
||||||
|
threshold={5}
|
||||||
|
>
|
||||||
|
{({ onRowsRendered, registerChild }) => (
|
||||||
|
<ScrollManagedList
|
||||||
|
ref={registerChild}
|
||||||
|
className="h-auto overflow-auto"
|
||||||
|
height={autoHeight}
|
||||||
|
width={width}
|
||||||
|
rowCount={itemCount}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
onRowsRendered={onRowsRendered}
|
||||||
|
rowRenderer={rowRenderer}
|
||||||
|
deferredMeasurementCache={cache}
|
||||||
|
overscanRowCount={10}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</InfiniteLoader>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="text-muted-foreground p-4 text-center text-sm">
|
||||||
|
Loading more sessions...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
apps/webapp/app/hooks/use-mcp-sessions.tsx
Normal file
116
apps/webapp/app/hooks/use-mcp-sessions.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
|
||||||
|
export interface McpSessionItem {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
integrations: string[];
|
||||||
|
createdAt: string;
|
||||||
|
deleted: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McpSessionsResponse {
|
||||||
|
sessions: McpSessionItem[];
|
||||||
|
totalCount: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
availableSources: Array<{ name: string; slug: string; count: number }>;
|
||||||
|
activeSources: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseMcpSessionsOptions {
|
||||||
|
endpoint: string;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMcpSessions({ endpoint, source }: UseMcpSessionsOptions) {
|
||||||
|
const fetcher = useFetcher<McpSessionsResponse>();
|
||||||
|
const [sessions, setSessions] = useState<McpSessionItem[]>([]);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
const [availableSources, setAvailableSources] = useState<
|
||||||
|
Array<{ name: string; slug: string; count: number }>
|
||||||
|
>([]);
|
||||||
|
const [activeSources, setActiveSources] = useState<string[]>([]);
|
||||||
|
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||||
|
|
||||||
|
const buildUrl = useCallback(
|
||||||
|
(pageNum: number) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("page", pageNum.toString());
|
||||||
|
params.set("limit", "10");
|
||||||
|
if (source) params.set("source", source);
|
||||||
|
return `${endpoint}?${params.toString()}`;
|
||||||
|
},
|
||||||
|
[endpoint, source],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (fetcher.state === "idle" && hasMore) {
|
||||||
|
fetcher.load(buildUrl(page + 1));
|
||||||
|
}
|
||||||
|
}, [hasMore, page, buildUrl]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setSessions([]);
|
||||||
|
setPage(1);
|
||||||
|
setHasMore(true);
|
||||||
|
setIsInitialLoad(true);
|
||||||
|
fetcher.load(buildUrl(1));
|
||||||
|
}, [buildUrl]);
|
||||||
|
|
||||||
|
// Effect to handle fetcher data
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetcher.state === "idle" && fetcher.data) {
|
||||||
|
const {
|
||||||
|
sessions: newSessions,
|
||||||
|
hasMore: newHasMore,
|
||||||
|
page: currentPage,
|
||||||
|
availableSources: sources,
|
||||||
|
activeSources: activeSourceNames,
|
||||||
|
} = fetcher.data;
|
||||||
|
|
||||||
|
if (currentPage === 1) {
|
||||||
|
// First page or reset
|
||||||
|
setSessions(newSessions);
|
||||||
|
setIsInitialLoad(false);
|
||||||
|
} else {
|
||||||
|
// Append to existing sessions
|
||||||
|
setSessions((prev) => [...prev, ...newSessions]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasMore(newHasMore);
|
||||||
|
setPage(currentPage);
|
||||||
|
setAvailableSources(sources);
|
||||||
|
setActiveSources(activeSourceNames);
|
||||||
|
}
|
||||||
|
}, [fetcher.data, fetcher.state]);
|
||||||
|
|
||||||
|
// Effect to reset when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
setSessions([]);
|
||||||
|
setPage(1);
|
||||||
|
setHasMore(true);
|
||||||
|
setIsInitialLoad(true);
|
||||||
|
fetcher.load(buildUrl(1));
|
||||||
|
}, [source, buildUrl]);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialLoad) {
|
||||||
|
fetcher.load(buildUrl(1));
|
||||||
|
}
|
||||||
|
}, [isInitialLoad, buildUrl]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessions,
|
||||||
|
hasMore,
|
||||||
|
loadMore,
|
||||||
|
reset,
|
||||||
|
availableSources,
|
||||||
|
activeSources,
|
||||||
|
isLoading: fetcher.state === "loading",
|
||||||
|
isInitialLoad,
|
||||||
|
};
|
||||||
|
}
|
||||||
81
apps/webapp/app/routes/api.v1.mcp.sessions.tsx
Normal file
81
apps/webapp/app/routes/api.v1.mcp.sessions.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { prisma } from "~/db.server";
|
||||||
|
import { createHybridLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
|
|
||||||
|
const SearchParamsSchema = z.object({
|
||||||
|
page: z.string().optional().default("1"),
|
||||||
|
limit: z.string().optional().default("10"),
|
||||||
|
source: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const loader = createHybridLoaderApiRoute(
|
||||||
|
{
|
||||||
|
searchParams: SearchParamsSchema,
|
||||||
|
findResource: async () => 1,
|
||||||
|
corsStrategy: "all",
|
||||||
|
allowJWT: true,
|
||||||
|
},
|
||||||
|
async ({ searchParams, authentication }) => {
|
||||||
|
const page = parseInt(searchParams.page);
|
||||||
|
const limit = parseInt(searchParams.limit);
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||||
|
const where = {
|
||||||
|
workspaceId: workspace?.id,
|
||||||
|
...(searchParams.source && { source: searchParams.source }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const [sessions, totalCount, sourcesResult, activeSources] =
|
||||||
|
await Promise.all([
|
||||||
|
prisma.mCPSession.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
}),
|
||||||
|
prisma.mCPSession.count({ where }),
|
||||||
|
prisma.mCPSession.groupBy({
|
||||||
|
by: ["source"],
|
||||||
|
where: { workspaceId: workspace?.id },
|
||||||
|
_count: { source: true },
|
||||||
|
orderBy: { _count: { source: "desc" } },
|
||||||
|
}),
|
||||||
|
// Get distinct active sources (where deleted is null)
|
||||||
|
prisma.mCPSession.findMany({
|
||||||
|
where: { deleted: null, workspaceId: workspace?.id },
|
||||||
|
select: { source: true },
|
||||||
|
distinct: ["source"],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const hasMore = skip + sessions.length < totalCount;
|
||||||
|
|
||||||
|
const availableSources = sourcesResult.map((item) => ({
|
||||||
|
name: item.source,
|
||||||
|
slug: item.source,
|
||||||
|
count: item._count.source,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const activeSourceNames = activeSources.map((item) => item.source);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
sessions: sessions.map((session) => ({
|
||||||
|
id: session.id,
|
||||||
|
source: session.source,
|
||||||
|
integrations: session.integrations,
|
||||||
|
createdAt: session.createdAt.toISOString(),
|
||||||
|
deleted: session.deleted?.toISOString(),
|
||||||
|
})),
|
||||||
|
totalCount,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasMore,
|
||||||
|
availableSources,
|
||||||
|
activeSources: activeSourceNames,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { loader };
|
||||||
@ -134,7 +134,7 @@ function parseSpec(spec: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CustomIntegrationContent({ integration }: { integration: any }) {
|
function CustomIntegrationContent({ integration }: { integration: any }) {
|
||||||
const memoryUrl = `https://core.heysol.ai/api/v1/mcp/memory?source=${integration.slug}`;
|
const memoryUrl = `https://core.heysol.ai/api/v1/mcp?source=${integration.slug}`;
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const copyToClipboard = async () => {
|
const copyToClipboard = async () => {
|
||||||
|
|||||||
@ -233,7 +233,7 @@ export default function OAuthAuthorize() {
|
|||||||
<Card className="bg-background-3 shadow-1 w-full max-w-md rounded-lg p-5">
|
<Card className="bg-background-3 shadow-1 w-full max-w-md rounded-lg p-5">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-center gap-4">
|
<div className="flex items-center justify-center gap-4">
|
||||||
{getIconForAuthorise(client.name, client.logoUrl)}
|
{getIconForAuthorise(client.name, 40, client.logoUrl)}
|
||||||
<ArrowRightLeft size={16} />
|
<ArrowRightLeft size={16} />
|
||||||
<Logo width={40} height={40} />
|
<Logo width={40} height={40} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export default function Onboarding() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getMemoryUrl = (source: "Claude" | "Cursor" | "Other") => {
|
const getMemoryUrl = (source: "Claude" | "Cursor" | "Other") => {
|
||||||
const baseUrl = "https://core.heysol.ai/api/v1/mcp/memory";
|
const baseUrl = "https://core.heysol.ai/api/v1/mcp";
|
||||||
return `${baseUrl}?Source=${source}`;
|
return `${baseUrl}?Source=${source}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
93
apps/webapp/app/routes/settings.mcp.tsx
Normal file
93
apps/webapp/app/routes/settings.mcp.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useMcpSessions } from "~/hooks/use-mcp-sessions";
|
||||||
|
import { McpSessionsFilters } from "~/components/mcp/mcp-sessions-filters";
|
||||||
|
import {
|
||||||
|
MCPUrlBox,
|
||||||
|
VirtualMcpSessionsList,
|
||||||
|
} from "~/components/mcp/virtual-mcp-sessions-list";
|
||||||
|
import { McpSourcesStats } from "~/components/mcp/mcp-sources-stats";
|
||||||
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
|
import { Database, LoaderCircle } from "lucide-react";
|
||||||
|
import { SettingSection } from "~/components/setting-section";
|
||||||
|
|
||||||
|
export default function McpSettings() {
|
||||||
|
const [selectedSource, setSelectedSource] = useState<string | undefined>();
|
||||||
|
|
||||||
|
const {
|
||||||
|
sessions,
|
||||||
|
hasMore,
|
||||||
|
loadMore,
|
||||||
|
availableSources,
|
||||||
|
activeSources,
|
||||||
|
isLoading,
|
||||||
|
isInitialLoad,
|
||||||
|
} = useMcpSessions({
|
||||||
|
endpoint: "/api/v1/mcp/sessions",
|
||||||
|
source: selectedSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex h-full w-3xl flex-col gap-4 px-4 pt-6">
|
||||||
|
<SettingSection
|
||||||
|
title="MCP Sessions"
|
||||||
|
description="View and manage Model Context Protocol sessions for integrations."
|
||||||
|
>
|
||||||
|
<div className="flex h-[calc(100vh_-_135px)] w-full flex-col items-center space-y-6">
|
||||||
|
{/* Top Sources Stats */}
|
||||||
|
<div className="flex w-full flex-col gap-4">
|
||||||
|
<MCPUrlBox />
|
||||||
|
<McpSourcesStats
|
||||||
|
sources={availableSources}
|
||||||
|
activeSources={activeSources}
|
||||||
|
isLoading={isInitialLoad}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isInitialLoad ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<LoaderCircle className="text-primary h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Filters */}
|
||||||
|
<McpSessionsFilters
|
||||||
|
availableSources={availableSources}
|
||||||
|
selectedSource={selectedSource}
|
||||||
|
onSourceChange={setSelectedSource}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Sessions List */}
|
||||||
|
<div className="flex h-full w-full space-y-4">
|
||||||
|
{sessions.length === 0 ? (
|
||||||
|
<Card className="bg-background-2 w-full">
|
||||||
|
<CardContent className="bg-background-2 flex w-full items-center justify-center py-16">
|
||||||
|
<div className="text-center">
|
||||||
|
<Database className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">
|
||||||
|
No MCP sessions found
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{selectedSource
|
||||||
|
? "Try adjusting your filters to see more results."
|
||||||
|
: "No MCP sessions are available yet."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<VirtualMcpSessionsList
|
||||||
|
sessions={sessions}
|
||||||
|
hasMore={hasMore}
|
||||||
|
loadMore={loadMore}
|
||||||
|
isLoading={isLoading}
|
||||||
|
height={600}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SettingSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,15 +1,4 @@
|
|||||||
import {
|
import { ArrowLeft, Code, Webhook, Cable } from "lucide-react";
|
||||||
ArrowLeft,
|
|
||||||
Brain,
|
|
||||||
Building,
|
|
||||||
Clock,
|
|
||||||
Code,
|
|
||||||
User,
|
|
||||||
Workflow,
|
|
||||||
Webhook,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@ -54,6 +43,7 @@ export default function Settings() {
|
|||||||
// { name: "Workspace", icon: Building },
|
// { name: "Workspace", icon: Building },
|
||||||
{ name: "API", icon: Code },
|
{ name: "API", icon: Code },
|
||||||
{ name: "Webhooks", icon: Webhook },
|
{ name: "Webhooks", icon: Webhook },
|
||||||
|
{ name: "MCP", icon: Cable },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@ -13,6 +13,9 @@ import { IntegrationLoader } from "~/utils/mcp/integration-loader";
|
|||||||
import { callMemoryTool, memoryTools } from "~/utils/mcp/memory";
|
import { callMemoryTool, memoryTools } from "~/utils/mcp/memory";
|
||||||
import { logger } from "~/services/logger.service";
|
import { logger } from "~/services/logger.service";
|
||||||
import { type Response, type Request } from "express";
|
import { type Response, type Request } from "express";
|
||||||
|
import { getUser } from "./session.server";
|
||||||
|
import { getUserById } from "~/models/user.server";
|
||||||
|
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
|
|
||||||
const QueryParams = z.object({
|
const QueryParams = z.object({
|
||||||
source: z.string().optional(),
|
source: z.string().optional(),
|
||||||
@ -99,7 +102,7 @@ async function createTransport(
|
|||||||
// Clean up old sessions (24+ hours) during new session initialization
|
// Clean up old sessions (24+ hours) during new session initialization
|
||||||
try {
|
try {
|
||||||
const [dbCleanupCount, memoryCleanupCount] = await Promise.all([
|
const [dbCleanupCount, memoryCleanupCount] = await Promise.all([
|
||||||
MCPSessionManager.cleanupOldSessions(),
|
MCPSessionManager.cleanupOldSessions(workspaceId),
|
||||||
TransportManager.cleanupOldSessions(),
|
TransportManager.cleanupOldSessions(),
|
||||||
]);
|
]);
|
||||||
if (dbCleanupCount > 0 || memoryCleanupCount > 0) {
|
if (dbCleanupCount > 0 || memoryCleanupCount > 0) {
|
||||||
@ -112,7 +115,12 @@ async function createTransport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store session in database
|
// Store session in database
|
||||||
await MCPSessionManager.upsertSession(sessionId, source, integrations);
|
await MCPSessionManager.upsertSession(
|
||||||
|
sessionId,
|
||||||
|
workspaceId,
|
||||||
|
source,
|
||||||
|
integrations,
|
||||||
|
);
|
||||||
|
|
||||||
// Store main transport
|
// Store main transport
|
||||||
TransportManager.setMainTransport(sessionId, transport);
|
TransportManager.setMainTransport(sessionId, transport);
|
||||||
@ -164,13 +172,17 @@ export const handleMCPRequest = async (
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const userId = authentication.userId;
|
const userId = authentication.userId;
|
||||||
const workspaceId = authentication.workspaceId;
|
const workspace = await getWorkspaceByUser(userId);
|
||||||
|
const workspaceId = workspace?.id as string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let transport: StreamableHTTPServerTransport;
|
let transport: StreamableHTTPServerTransport;
|
||||||
let currentSessionId = sessionId;
|
let currentSessionId = sessionId;
|
||||||
|
|
||||||
if (sessionId && (await MCPSessionManager.isSessionActive(sessionId))) {
|
if (
|
||||||
|
sessionId &&
|
||||||
|
(await MCPSessionManager.isSessionActive(sessionId, workspaceId))
|
||||||
|
) {
|
||||||
// Use existing session
|
// Use existing session
|
||||||
const sessionData = TransportManager.getSessionInfo(sessionId);
|
const sessionData = TransportManager.getSessionInfo(sessionId);
|
||||||
if (!sessionData.exists) {
|
if (!sessionData.exists) {
|
||||||
@ -214,10 +226,17 @@ export const handleMCPRequest = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleSessionRequest = async (req: Request, res: Response) => {
|
export const handleSessionRequest = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
workspaceId: string,
|
||||||
|
) => {
|
||||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||||
|
|
||||||
if (sessionId && (await MCPSessionManager.isSessionActive(sessionId))) {
|
if (
|
||||||
|
sessionId &&
|
||||||
|
(await MCPSessionManager.isSessionActive(sessionId, workspaceId))
|
||||||
|
) {
|
||||||
const sessionData = TransportManager.getSessionInfo(sessionId);
|
const sessionData = TransportManager.getSessionInfo(sessionId);
|
||||||
|
|
||||||
if (sessionData.exists) {
|
if (sessionData.exists) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export interface MCPSessionData {
|
|||||||
integrations: string[];
|
integrations: string[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
deleted?: Date;
|
deleted?: Date;
|
||||||
|
workspaceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MCPSessionManager {
|
export class MCPSessionManager {
|
||||||
@ -14,6 +15,7 @@ export class MCPSessionManager {
|
|||||||
*/
|
*/
|
||||||
static async upsertSession(
|
static async upsertSession(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
|
workspaceId: string,
|
||||||
source: string,
|
source: string,
|
||||||
integrations: string[],
|
integrations: string[],
|
||||||
): Promise<MCPSessionData> {
|
): Promise<MCPSessionData> {
|
||||||
@ -29,6 +31,7 @@ export class MCPSessionManager {
|
|||||||
data: {
|
data: {
|
||||||
source,
|
source,
|
||||||
integrations,
|
integrations,
|
||||||
|
workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -38,6 +41,7 @@ export class MCPSessionManager {
|
|||||||
id: sessionId,
|
id: sessionId,
|
||||||
source,
|
source,
|
||||||
integrations,
|
integrations,
|
||||||
|
workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -47,6 +51,7 @@ export class MCPSessionManager {
|
|||||||
source: session.source,
|
source: session.source,
|
||||||
integrations: session.integrations,
|
integrations: session.integrations,
|
||||||
createdAt: session.createdAt,
|
createdAt: session.createdAt,
|
||||||
|
workspaceId: session.workspaceId as string,
|
||||||
deleted: session.deleted || undefined,
|
deleted: session.deleted || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -79,6 +84,7 @@ export class MCPSessionManager {
|
|||||||
integrations: session.integrations,
|
integrations: session.integrations,
|
||||||
createdAt: session.createdAt,
|
createdAt: session.createdAt,
|
||||||
deleted: session.deleted || undefined,
|
deleted: session.deleted || undefined,
|
||||||
|
workspaceId: session.workspaceId || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,13 +109,14 @@ export class MCPSessionManager {
|
|||||||
/**
|
/**
|
||||||
* Clean up old sessions (older than 24 hours)
|
* Clean up old sessions (older than 24 hours)
|
||||||
*/
|
*/
|
||||||
static async cleanupOldSessions(): Promise<number> {
|
static async cleanupOldSessions(workspaceId: string): Promise<number> {
|
||||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const result = await prisma.mCPSession.updateMany({
|
const result = await prisma.mCPSession.updateMany({
|
||||||
where: {
|
where: {
|
||||||
createdAt: { lt: twentyFourHoursAgo },
|
createdAt: { lt: twentyFourHoursAgo },
|
||||||
deleted: null,
|
deleted: null,
|
||||||
|
workspaceId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
deleted: new Date(),
|
deleted: new Date(),
|
||||||
@ -122,9 +129,12 @@ export class MCPSessionManager {
|
|||||||
/**
|
/**
|
||||||
* Check if session is active (not deleted)
|
* Check if session is active (not deleted)
|
||||||
*/
|
*/
|
||||||
static async isSessionActive(sessionId: string): Promise<boolean> {
|
static async isSessionActive(
|
||||||
|
sessionId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
const session = await prisma.mCPSession.findUnique({
|
const session = await prisma.mCPSession.findUnique({
|
||||||
where: { id: sessionId },
|
where: { id: sessionId, workspaceId },
|
||||||
select: { deleted: true },
|
select: { deleted: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -920,88 +920,6 @@ paths:
|
|||||||
"204":
|
"204":
|
||||||
description: Space deleted successfully
|
description: Space deleted successfully
|
||||||
|
|
||||||
# MCP (Model Context Protocol)
|
|
||||||
/api/v1/mcp/memory:
|
|
||||||
post:
|
|
||||||
summary: MCP Memory Operations
|
|
||||||
description: |
|
|
||||||
MCP server endpoint for memory operations including:
|
|
||||||
- ingest: Add data to memory
|
|
||||||
- search: Search memory content
|
|
||||||
- get_spaces: Retrieve available spaces
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: mcp-session-id
|
|
||||||
in: header
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: MCP session identifier
|
|
||||||
- name: source
|
|
||||||
in: header
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: MCP integration source
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
description: MCP request payload
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: MCP response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
|
|
||||||
delete:
|
|
||||||
summary: MCP Session Cleanup
|
|
||||||
description: Clean up MCP session resources
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: mcp-session-id
|
|
||||||
in: header
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
"204":
|
|
||||||
description: Session cleaned up
|
|
||||||
|
|
||||||
/api/v1/mcp/{slug}:
|
|
||||||
parameters:
|
|
||||||
- name: slug
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: Integration slug identifier
|
|
||||||
|
|
||||||
post:
|
|
||||||
summary: MCP Integration Proxy
|
|
||||||
description: |
|
|
||||||
Proxy MCP requests to integration-specific servers.
|
|
||||||
Routes requests based on the slug to appropriate integration.
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: mcp-session-id
|
|
||||||
in: header
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: MCP session identifier
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Proxied MCP response
|
|
||||||
|
|
||||||
# Integration Management
|
# Integration Management
|
||||||
/api/v1/integrations:
|
/api/v1/integrations:
|
||||||
get:
|
get:
|
||||||
|
|||||||
@ -1235,88 +1235,6 @@ paths:
|
|||||||
"204":
|
"204":
|
||||||
description: Rule deleted
|
description: Rule deleted
|
||||||
|
|
||||||
# MCP (Model Context Protocol)
|
|
||||||
/api/v1/mcp/memory:
|
|
||||||
post:
|
|
||||||
summary: MCP Memory Operations
|
|
||||||
description: |
|
|
||||||
MCP server endpoint for memory operations including:
|
|
||||||
- ingest: Add data to memory
|
|
||||||
- search: Search memory content
|
|
||||||
- get_spaces: Retrieve available spaces
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: mcp-session-id
|
|
||||||
in: header
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: MCP session identifier
|
|
||||||
- name: source
|
|
||||||
in: header
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: MCP integration source
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
description: MCP request payload
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: MCP response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
|
|
||||||
delete:
|
|
||||||
summary: MCP Session Cleanup
|
|
||||||
description: Clean up MCP session resources
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: mcp-session-id
|
|
||||||
in: header
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
"204":
|
|
||||||
description: Session cleaned up
|
|
||||||
|
|
||||||
/api/v1/mcp/{slug}:
|
|
||||||
parameters:
|
|
||||||
- name: slug
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: Integration slug identifier
|
|
||||||
|
|
||||||
post:
|
|
||||||
summary: MCP Integration Proxy
|
|
||||||
description: |
|
|
||||||
Proxy MCP requests to integration-specific servers.
|
|
||||||
Routes requests based on the slug to appropriate integration.
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- name: mcp-session-id
|
|
||||||
in: header
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: MCP session identifier
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Proxied MCP response
|
|
||||||
|
|
||||||
# Integration Management
|
# Integration Management
|
||||||
/api/v1/integrations:
|
/api/v1/integrations:
|
||||||
get:
|
get:
|
||||||
|
|||||||
@ -24,8 +24,7 @@ async function init() {
|
|||||||
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
|
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
|
||||||
: await import("./build/server/index.js");
|
: await import("./build/server/index.js");
|
||||||
|
|
||||||
const { authenticateHybridRequest, handleMCPRequest, handleSessionRequest } =
|
const module = build.entry?.module;
|
||||||
build.entry.module;
|
|
||||||
|
|
||||||
remixHandler = createRequestHandler({ build });
|
remixHandler = createRequestHandler({ build });
|
||||||
|
|
||||||
@ -54,22 +53,28 @@ async function init() {
|
|||||||
app.use(morgan("tiny"));
|
app.use(morgan("tiny"));
|
||||||
|
|
||||||
app.get("/api/v1/mcp", async (req, res) => {
|
app.get("/api/v1/mcp", async (req, res) => {
|
||||||
const authenticationResult = await authenticateHybridRequest(req as any, {
|
const authenticationResult = await module.authenticateHybridRequest(
|
||||||
allowJWT: true,
|
req as any,
|
||||||
});
|
{
|
||||||
|
allowJWT: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!authenticationResult) {
|
if (!authenticationResult) {
|
||||||
res.status(401).json({ error: "Authentication required" });
|
res.status(401).json({ error: "Authentication required" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleSessionRequest(req, res);
|
await module.handleSessionRequest(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/v1/mcp", async (req, res) => {
|
app.post("/api/v1/mcp", async (req, res) => {
|
||||||
const authenticationResult = await authenticateHybridRequest(req as any, {
|
const authenticationResult = await module.authenticateHybridRequest(
|
||||||
allowJWT: true,
|
req as any,
|
||||||
});
|
{
|
||||||
|
allowJWT: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!authenticationResult) {
|
if (!authenticationResult) {
|
||||||
res.status(401).json({ error: "Authentication required" });
|
res.status(401).json({ error: "Authentication required" });
|
||||||
@ -85,7 +90,7 @@ async function init() {
|
|||||||
try {
|
try {
|
||||||
const parsedBody = JSON.parse(body);
|
const parsedBody = JSON.parse(body);
|
||||||
const queryParams = req.query; // Get query parameters from the request
|
const queryParams = req.query; // Get query parameters from the request
|
||||||
await handleMCPRequest(
|
await module.handleMCPRequest(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
parsedBody,
|
parsedBody,
|
||||||
@ -99,16 +104,19 @@ async function init() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.delete("/api/v1/mcp", async (req, res) => {
|
app.delete("/api/v1/mcp", async (req, res) => {
|
||||||
const authenticationResult = await authenticateHybridRequest(req as any, {
|
const authenticationResult = await module.authenticateHybridRequest(
|
||||||
allowJWT: true,
|
req as any,
|
||||||
});
|
{
|
||||||
|
allowJWT: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!authenticationResult) {
|
if (!authenticationResult) {
|
||||||
res.status(401).json({ error: "Authentication required" });
|
res.status(401).json({ error: "Authentication required" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleSessionRequest(req, res);
|
await module.handleSessionRequest(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.options("/api/v1/mcp", (_, res) => {
|
app.options("/api/v1/mcp", (_, res) => {
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "MCPSession" ADD COLUMN "workspaceId" TEXT;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "MCPSession" ADD CONSTRAINT "MCPSession_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@ -215,6 +215,9 @@ model MCPSession {
|
|||||||
source String
|
source String
|
||||||
integrations String[]
|
integrations String[]
|
||||||
|
|
||||||
|
workspace Workspace? @relation(references: [id], fields: [workspaceId])
|
||||||
|
workspaceId String?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
deleted DateTime?
|
deleted DateTime?
|
||||||
}
|
}
|
||||||
@ -635,6 +638,7 @@ model Workspace {
|
|||||||
OAuthRefreshToken OAuthRefreshToken[]
|
OAuthRefreshToken OAuthRefreshToken[]
|
||||||
RecallLog RecallLog[]
|
RecallLog RecallLog[]
|
||||||
Space Space[]
|
Space Space[]
|
||||||
|
MCPSession MCPSession[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuthenticationMethod {
|
enum AuthenticationMethod {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user