mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
feat(admin): add mcp as main navigation item when enabled, and make it route-aware
This commit is contained in:
@@ -474,6 +474,7 @@ type SectionHeaderAccordionItemProps = {
|
|||||||
ActiveIcon?: any;
|
ActiveIcon?: any;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
renderHeaderRight?: (props: { open: boolean }) => React.ReactNode;
|
renderHeaderRight?: (props: { open: boolean }) => React.ReactNode;
|
||||||
|
scrollContainerRef?: React.RefObject<HTMLDivElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SectionHeaderAccordionItem = ({
|
export const SectionHeaderAccordionItem = ({
|
||||||
@@ -483,6 +484,7 @@ export const SectionHeaderAccordionItem = ({
|
|||||||
ActiveIcon = IconChevronUp,
|
ActiveIcon = IconChevronUp,
|
||||||
children,
|
children,
|
||||||
renderHeaderRight,
|
renderHeaderRight,
|
||||||
|
scrollContainerRef,
|
||||||
}: SectionHeaderAccordionItemProps) => (
|
}: SectionHeaderAccordionItemProps) => (
|
||||||
<div
|
<div
|
||||||
style={{ minHeight: 49 }}
|
style={{ minHeight: 49 }}
|
||||||
@@ -493,6 +495,8 @@ export const SectionHeaderAccordionItem = ({
|
|||||||
: "flex-initial cursor-pointer hover:bg-primary/5",
|
: "flex-initial cursor-pointer hover:bg-primary/5",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/** biome-ignore lint/a11y/noStaticElementInteractions: . */}
|
||||||
|
{/** biome-ignore lint/a11y/useKeyWithClickEvents: . */}
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-row bg-muted/10 border-muted border-b h-14 py-4 pr-4 pl-2 items-center gap-2",
|
"flex flex-row bg-muted/10 border-muted border-b h-14 py-4 pr-4 pl-2 items-center gap-2",
|
||||||
@@ -501,14 +505,12 @@ export const SectionHeaderAccordionItem = ({
|
|||||||
>
|
>
|
||||||
<IconButton Icon={open ? ActiveIcon : IconChevronDown} disabled={open} />
|
<IconButton Icon={open ? ActiveIcon : IconChevronDown} disabled={open} />
|
||||||
<h2 className="text-lg dark:font-bold font-semibold select-text">{title}</h2>
|
<h2 className="text-lg dark:font-bold font-semibold select-text">{title}</h2>
|
||||||
<div className="flex flex-grow" />
|
<div className="flex grow" />
|
||||||
{renderHeaderRight?.({ open })}
|
{renderHeaderRight?.({ open })}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
ref={scrollContainerRef}
|
||||||
"overflow-y-scroll transition-all",
|
className={twMerge("overflow-y-scroll transition-all", open ? " grow" : "h-0 opacity-0")}
|
||||||
open ? " flex-grow" : "h-0 opacity-0",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -518,14 +520,25 @@ export const SectionHeaderAccordionItem = ({
|
|||||||
export const RouteAwareSectionHeaderAccordionItem = ({
|
export const RouteAwareSectionHeaderAccordionItem = ({
|
||||||
routePattern,
|
routePattern,
|
||||||
identifier,
|
identifier,
|
||||||
|
renderHeaderRight,
|
||||||
...props
|
...props
|
||||||
}: Omit<SectionHeaderAccordionItemProps, "open" | "toggle"> & {
|
}: Omit<SectionHeaderAccordionItemProps, "open" | "toggle" | "renderHeaderRight"> & {
|
||||||
|
renderHeaderRight?: (props: { open: boolean; active: boolean }) => React.ReactNode;
|
||||||
// it's optional because it could be provided using the context
|
// it's optional because it could be provided using the context
|
||||||
routePattern?: string;
|
routePattern?: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { active, toggle } = useRoutePathState(routePattern, identifier);
|
const { active, toggle } = useRoutePathState(routePattern, identifier);
|
||||||
return <SectionHeaderAccordionItem {...props} open={active} toggle={toggle} />;
|
return (
|
||||||
|
<SectionHeaderAccordionItem
|
||||||
|
{...props}
|
||||||
|
open={active}
|
||||||
|
toggle={toggle}
|
||||||
|
renderHeaderRight={
|
||||||
|
renderHeaderRight && ((props) => renderHeaderRight?.({ open: props.open, active }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Separator = ({ className, ...props }: ComponentPropsWithoutRef<"hr">) => (
|
export const Separator = ({ className, ...props }: ComponentPropsWithoutRef<"hr">) => (
|
||||||
|
|||||||
@@ -30,28 +30,29 @@ import { useAppShellAdminOptions } from "ui/options";
|
|||||||
|
|
||||||
export function HeaderNavigation() {
|
export function HeaderNavigation() {
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
|
const { config } = useBknd();
|
||||||
|
|
||||||
const items: {
|
const items: {
|
||||||
label: string;
|
label: string;
|
||||||
href: string;
|
href: string;
|
||||||
Icon: any;
|
Icon?: any;
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}[] = [
|
}[] = [
|
||||||
/*{
|
|
||||||
label: "Base",
|
|
||||||
href: "#",
|
|
||||||
exact: true,
|
|
||||||
Icon: TbLayoutDashboard,
|
|
||||||
disabled: true,
|
|
||||||
tooltip: "Coming soon"
|
|
||||||
},*/
|
|
||||||
{ label: "Data", href: "/data", Icon: TbDatabase },
|
{ label: "Data", href: "/data", Icon: TbDatabase },
|
||||||
{ label: "Auth", href: "/auth", Icon: TbFingerprint },
|
{ label: "Auth", href: "/auth", Icon: TbFingerprint },
|
||||||
{ label: "Media", href: "/media", Icon: TbPhoto },
|
{ label: "Media", href: "/media", Icon: TbPhoto },
|
||||||
{ label: "Flows", href: "/flows", Icon: TbHierarchy2 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (import.meta.env.DEV || Object.keys(config.flows?.flows ?? {}).length > 0) {
|
||||||
|
items.push({ label: "Flows", href: "/flows", Icon: TbHierarchy2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.server.mcp.enabled) {
|
||||||
|
items.push({ label: "MCP", href: "/tools/mcp", Icon: McpIcon });
|
||||||
|
}
|
||||||
|
|
||||||
const activeItem = items.find((item) =>
|
const activeItem = items.find((item) =>
|
||||||
item.exact ? location === item.href : location.startsWith(item.href),
|
item.exact ? location === item.href : location.startsWith(item.href),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import { useBkndWindowContext } from "bknd/client";
|
|||||||
import ToolsRoutes from "./tools";
|
import ToolsRoutes from "./tools";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const TestRoutes = lazy(() => import("./test"));
|
let TestRoutes: any;
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
TestRoutes = lazy(() => import("./test"));
|
||||||
|
}
|
||||||
|
|
||||||
export function Routes({
|
export function Routes({
|
||||||
BkndWrapper,
|
BkndWrapper,
|
||||||
@@ -43,11 +46,13 @@ export function Routes({
|
|||||||
<Route path="/" nest>
|
<Route path="/" nest>
|
||||||
<Root>
|
<Root>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/test*" nest>
|
{TestRoutes && (
|
||||||
<Suspense fallback={null}>
|
<Route path="/test*" nest>
|
||||||
<TestRoutes />
|
<Suspense fallback={null}>
|
||||||
</Suspense>
|
<TestRoutes />
|
||||||
</Route>
|
</Suspense>
|
||||||
|
</Route>
|
||||||
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default function ToolsRoutes() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Route path="/" component={ToolsIndex} />
|
<Route path="/" component={ToolsIndex} />
|
||||||
<Route path="/mcp" component={ToolsMcp} />
|
<Route path="/mcp*" component={ToolsMcp} nest />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ import { Empty } from "ui/components/display/Empty";
|
|||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { appShellStore } from "ui/store";
|
import { appShellStore } from "ui/store";
|
||||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
|
import { RoutePathStateProvider } from "ui/hooks/use-route-path-state";
|
||||||
|
import { Route, Switch } from "wouter";
|
||||||
|
|
||||||
export default function ToolsMcp() {
|
export default function ToolsMcp() {
|
||||||
useBrowserTitle(["MCP UI"]);
|
useBrowserTitle(["MCP UI"]);
|
||||||
|
|
||||||
const { config } = useBknd();
|
const { config } = useBknd();
|
||||||
const feature = useMcpStore((state) => state.feature);
|
|
||||||
const setFeature = useMcpStore((state) => state.setFeature);
|
|
||||||
const content = useMcpStore((state) => state.content);
|
|
||||||
const openSidebar = appShellStore((store) => store.toggleSidebar("default"));
|
const openSidebar = appShellStore((store) => store.toggleSidebar("default"));
|
||||||
const mcpPath = config.server.mcp.path;
|
const mcpPath = config.server.mcp.path;
|
||||||
|
|
||||||
@@ -29,51 +28,57 @@ export default function ToolsMcp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-grow max-w-screen">
|
<RoutePathStateProvider path={"/:type?"} defaultIdentifier="tools">
|
||||||
<AppShell.SectionHeader>
|
<div className="flex flex-col flex-grow max-w-screen">
|
||||||
<div className="flex flex-row gap-4 items-center">
|
<AppShell.SectionHeader>
|
||||||
<McpIcon />
|
<div className="flex flex-row gap-4 items-center">
|
||||||
<AppShell.SectionHeaderTitle className="whitespace-nowrap truncate">
|
<McpIcon />
|
||||||
MCP UI
|
<AppShell.SectionHeaderTitle className="whitespace-nowrap truncate">
|
||||||
</AppShell.SectionHeaderTitle>
|
MCP UI
|
||||||
<div className="hidden md:flex flex-row gap-2 items-center bg-primary/5 rounded-full px-3 pr-3.5 py-2">
|
</AppShell.SectionHeaderTitle>
|
||||||
<TbWorld />
|
<div className="hidden md:flex flex-row gap-2 items-center bg-primary/5 rounded-full px-3 pr-3.5 py-2">
|
||||||
<div className="min-w-0 flex-1">
|
<TbWorld />
|
||||||
<span className="block truncate text-sm font-mono leading-none select-text">
|
<div className="min-w-0 flex-1">
|
||||||
{window.location.origin + mcpPath}
|
<span className="block truncate text-sm font-mono leading-none select-text">
|
||||||
</span>
|
{window.location.origin + mcpPath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AppShell.SectionHeader>
|
||||||
</AppShell.SectionHeader>
|
|
||||||
|
|
||||||
<div className="flex h-full">
|
<div className="flex grow h-full">
|
||||||
<AppShell.Sidebar>
|
<AppShell.Sidebar>
|
||||||
<Tools.Sidebar open={feature === "tools"} toggle={() => setFeature("tools")} />
|
<Tools.Sidebar />
|
||||||
<AppShell.SectionHeaderAccordionItem
|
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||||
title="Resources"
|
title="Resources"
|
||||||
open={feature === "resources"}
|
identifier="resources"
|
||||||
toggle={() => setFeature("resources")}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col flex-grow p-3 gap-3 justify-center items-center opacity-40">
|
|
||||||
<i>Resources</i>
|
|
||||||
</div>
|
|
||||||
</AppShell.SectionHeaderAccordionItem>
|
|
||||||
</AppShell.Sidebar>
|
|
||||||
{feature === "tools" && <Tools.Content />}
|
|
||||||
|
|
||||||
{!content && (
|
|
||||||
<Empty title="No tool selected" description="Please select a tool to continue.">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => openSidebar()}
|
|
||||||
className="block md:hidden"
|
|
||||||
>
|
>
|
||||||
Open Tools
|
<div className="flex flex-col flex-grow p-3 gap-3 justify-center items-center opacity-40">
|
||||||
</Button>
|
<i>Resources</i>
|
||||||
</Empty>
|
</div>
|
||||||
)}
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||||
|
</AppShell.Sidebar>
|
||||||
|
|
||||||
|
<Switch>
|
||||||
|
<Route path="/tools/:toolName?" component={Tools.Content} />
|
||||||
|
<Route path="*">
|
||||||
|
<Empty
|
||||||
|
title="No tool selected"
|
||||||
|
description="Please select a tool to continue."
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => openSidebar()}
|
||||||
|
className="block md:hidden"
|
||||||
|
>
|
||||||
|
Open Tools
|
||||||
|
</Button>
|
||||||
|
</Empty>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RoutePathStateProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,16 @@ import { combine } from "zustand/middleware";
|
|||||||
|
|
||||||
import type { ToolJson } from "jsonv-ts/mcp";
|
import type { ToolJson } from "jsonv-ts/mcp";
|
||||||
|
|
||||||
const FEATURES = ["tools", "resources"] as const;
|
|
||||||
export type Feature = (typeof FEATURES)[number];
|
|
||||||
|
|
||||||
export const useMcpStore = create(
|
export const useMcpStore = create(
|
||||||
combine(
|
combine(
|
||||||
{
|
{
|
||||||
tools: [] as ToolJson[],
|
tools: [] as ToolJson[],
|
||||||
feature: "tools" as Feature | null,
|
|
||||||
content: null as ToolJson | null,
|
|
||||||
history: [] as { type: "request" | "response"; data: any }[],
|
history: [] as { type: "request" | "response"; data: any }[],
|
||||||
historyLimit: 50,
|
historyLimit: 50,
|
||||||
historyVisible: false,
|
historyVisible: false,
|
||||||
},
|
},
|
||||||
(set) => ({
|
(set) => ({
|
||||||
setTools: (tools: ToolJson[]) => set({ tools }),
|
setTools: (tools: ToolJson[]) => set({ tools }),
|
||||||
setFeature: (feature: Feature) => set({ feature }),
|
|
||||||
setContent: (content: ToolJson | null) => set({ content }),
|
|
||||||
addHistory: (type: "request" | "response", data: any) =>
|
addHistory: (type: "request" | "response", data: any) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
history: [{ type, data }, ...state.history.slice(0, state.historyLimit - 1)],
|
history: [{ type, data }, ...state.history.slice(0, state.historyLimit - 1)],
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { AppShell } from "ui/layouts/AppShell";
|
|||||||
import { TbHistory, TbHistoryOff, TbRefresh } from "react-icons/tb";
|
import { TbHistory, TbHistoryOff, TbRefresh } from "react-icons/tb";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { JsonViewer, JsonViewerTabs, type JsonViewerTabsRef } from "ui/components/code/JsonViewer";
|
import { JsonViewer, JsonViewerTabs, type JsonViewerTabsRef } from "ui/components/code/JsonViewer";
|
||||||
import { twMerge } from "ui/elements/mocks/tailwind-merge";
|
|
||||||
import { Field, Form } from "ui/components/form/json-schema-form";
|
import { Field, Form } from "ui/components/form/json-schema-form";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
@@ -13,17 +12,18 @@ import { appShellStore } from "ui/store";
|
|||||||
import { Icon } from "ui/components/display/Icon";
|
import { Icon } from "ui/components/display/Icon";
|
||||||
import { useMcpClient } from "./hooks/use-mcp-client";
|
import { useMcpClient } from "./hooks/use-mcp-client";
|
||||||
import { Tooltip } from "@mantine/core";
|
import { Tooltip } from "@mantine/core";
|
||||||
|
import { Link } from "ui/components/wouter/Link";
|
||||||
|
import { useParams } from "wouter";
|
||||||
|
|
||||||
export function Sidebar({ open, toggle }) {
|
export function Sidebar() {
|
||||||
const client = useMcpClient();
|
const client = useMcpClient();
|
||||||
const closeSidebar = appShellStore((store) => store.closeSidebar("default"));
|
const closeSidebar = appShellStore((store) => store.closeSidebar("default"));
|
||||||
const tools = useMcpStore((state) => state.tools);
|
const tools = useMcpStore((state) => state.tools);
|
||||||
const setTools = useMcpStore((state) => state.setTools);
|
const setTools = useMcpStore((state) => state.setTools);
|
||||||
const setContent = useMcpStore((state) => state.setContent);
|
|
||||||
const content = useMcpStore((state) => state.content);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [query, setQuery] = useState<string>("");
|
const [query, setQuery] = useState<string>("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(undefined!);
|
||||||
|
|
||||||
const handleRefresh = useCallback(async () => {
|
const handleRefresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -39,15 +39,22 @@ export function Sidebar({ open, toggle }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleRefresh();
|
handleRefresh().then(() => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
const selectedTool = scrollContainerRef.current.querySelector(".active");
|
||||||
|
if (selectedTool) {
|
||||||
|
selectedTool.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell.SectionHeaderAccordionItem
|
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||||
|
scrollContainerRef={scrollContainerRef}
|
||||||
title="Tools"
|
title="Tools"
|
||||||
open={open}
|
identifier="tools"
|
||||||
toggle={toggle}
|
renderHeaderRight={({ active }) => (
|
||||||
renderHeaderRight={() => (
|
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-row gap-2 items-center">
|
||||||
{error && (
|
{error && (
|
||||||
<Tooltip label={error}>
|
<Tooltip label={error}>
|
||||||
@@ -57,7 +64,7 @@ export function Sidebar({ open, toggle }) {
|
|||||||
<span className="flex-inline bg-primary/10 px-2 py-1.5 rounded-xl text-sm font-mono leading-none">
|
<span className="flex-inline bg-primary/10 px-2 py-1.5 rounded-xl text-sm font-mono leading-none">
|
||||||
{tools.length}
|
{tools.length}
|
||||||
</span>
|
</span>
|
||||||
<IconButton Icon={TbRefresh} disabled={!open || loading} onClick={handleRefresh} />
|
<IconButton Icon={TbRefresh} disabled={!active || loading} onClick={handleRefresh} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -76,12 +83,11 @@ export function Sidebar({ open, toggle }) {
|
|||||||
return (
|
return (
|
||||||
<AppShell.SidebarLink
|
<AppShell.SidebarLink
|
||||||
key={tool.name}
|
key={tool.name}
|
||||||
className={twMerge(
|
className="flex flex-col items-start h-auto py-3 gap-px"
|
||||||
"flex flex-col items-start h-auto py-3 gap-px",
|
as={Link}
|
||||||
content?.name === tool.name ? "active" : "",
|
href={`/tools/${tool.name}`}
|
||||||
)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setContent(tool);
|
//setContent(tool);
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -92,32 +98,34 @@ export function Sidebar({ open, toggle }) {
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</AppShell.SectionHeaderAccordionItem>
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Content() {
|
export function Content() {
|
||||||
const content = useMcpStore((state) => state.content);
|
const { toolName } = useParams();
|
||||||
|
const tools = useMcpStore((state) => state.tools);
|
||||||
|
const tool = tools.find((tool) => tool.name === toolName);
|
||||||
const addHistory = useMcpStore((state) => state.addHistory);
|
const addHistory = useMcpStore((state) => state.addHistory);
|
||||||
const [payload, setPayload] = useState<object>(getTemplate(content?.inputSchema));
|
const [payload, setPayload] = useState<object>(getTemplate(tool?.inputSchema));
|
||||||
const [result, setResult] = useState<object | null>(null);
|
const [result, setResult] = useState<object | null>(null);
|
||||||
const historyVisible = useMcpStore((state) => state.historyVisible);
|
const historyVisible = useMcpStore((state) => state.historyVisible);
|
||||||
const setHistoryVisible = useMcpStore((state) => state.setHistoryVisible);
|
const setHistoryVisible = useMcpStore((state) => state.setHistoryVisible);
|
||||||
const client = useMcpClient();
|
const client = useMcpClient();
|
||||||
const jsonViewerTabsRef = useRef<JsonViewerTabsRef>(null);
|
const jsonViewerTabsRef = useRef<JsonViewerTabsRef>(null);
|
||||||
const hasInputSchema =
|
const hasInputSchema =
|
||||||
content?.inputSchema && Object.keys(content.inputSchema.properties ?? {}).length > 0;
|
tool?.inputSchema && Object.keys(tool.inputSchema.properties ?? {}).length > 0;
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPayload(getTemplate(content?.inputSchema));
|
setPayload(getTemplate(tool?.inputSchema));
|
||||||
setResult(null);
|
setResult(null);
|
||||||
}, [content]);
|
}, [toolName]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(async () => {
|
const handleSubmit = useCallback(async () => {
|
||||||
if (!content?.name) return;
|
if (!tool?.name) return;
|
||||||
const request = {
|
const request = {
|
||||||
name: content.name,
|
name: tool.name,
|
||||||
arguments: payload,
|
arguments: payload,
|
||||||
};
|
};
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
@@ -131,7 +139,7 @@ export function Content() {
|
|||||||
});
|
});
|
||||||
}, [payload]);
|
}, [payload]);
|
||||||
|
|
||||||
if (!content) return null;
|
if (!tool) return null;
|
||||||
|
|
||||||
let readableResult = result;
|
let readableResult = result;
|
||||||
try {
|
try {
|
||||||
@@ -144,11 +152,11 @@ export function Content() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
className="flex flex-grow flex-col min-w-0 max-w-screen"
|
className="flex grow flex-col min-w-0 max-w-screen"
|
||||||
key={content.name}
|
key={tool.name}
|
||||||
schema={{
|
schema={{
|
||||||
title: "InputSchema",
|
title: "InputSchema",
|
||||||
...content?.inputSchema,
|
...tool?.inputSchema,
|
||||||
}}
|
}}
|
||||||
validateOn="submit"
|
validateOn="submit"
|
||||||
initialValues={payload}
|
initialValues={payload}
|
||||||
@@ -163,12 +171,12 @@ export function Content() {
|
|||||||
right={
|
right={
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<IconButton
|
<IconButton
|
||||||
Icon={historyVisible ? TbHistory : TbHistoryOff}
|
Icon={historyVisible ? TbHistoryOff : TbHistory}
|
||||||
onClick={() => setHistoryVisible(!historyVisible)}
|
onClick={() => setHistoryVisible(!historyVisible)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!content?.name || isPending}
|
disabled={!tool?.name || isPending}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
@@ -181,19 +189,19 @@ export function Content() {
|
|||||||
<span className="opacity-50">
|
<span className="opacity-50">
|
||||||
Tools <span className="opacity-70">/</span>
|
Tools <span className="opacity-70">/</span>
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
<span className="truncate">{content?.name}</span>
|
<span className="truncate">{tool?.name}</span>
|
||||||
</AppShell.SectionHeaderTitle>
|
</AppShell.SectionHeaderTitle>
|
||||||
</AppShell.SectionHeader>
|
</AppShell.SectionHeader>
|
||||||
<div className="flex flex-grow flex-row w-vw">
|
<div className="flex grow flex-row w-vw">
|
||||||
<div
|
<div
|
||||||
className="flex flex-grow flex-col max-w-full"
|
className="flex grow flex-col max-w-full"
|
||||||
style={{
|
style={{
|
||||||
width: "calc(100% - var(--sidebar-width-right) - 1px)",
|
width: "calc(100% - var(--sidebar-width-right) - 1px)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppShell.Scrollable>
|
<AppShell.Scrollable>
|
||||||
<div key={JSON.stringify(content)} className="flex flex-col py-4 px-5 gap-4">
|
<div key={JSON.stringify(tool)} className="flex flex-col py-4 px-5 gap-4">
|
||||||
<p className="text-primary/80">{content?.description}</p>
|
<p className="text-primary/80">{tool?.description}</p>
|
||||||
|
|
||||||
{hasInputSchema && <Field name="" />}
|
{hasInputSchema && <Field name="" />}
|
||||||
<JsonViewerTabs
|
<JsonViewerTabs
|
||||||
@@ -209,7 +217,7 @@ export function Content() {
|
|||||||
},
|
},
|
||||||
Result: { json: readableResult, title: "Result" },
|
Result: { json: readableResult, title: "Result" },
|
||||||
Configuration: {
|
Configuration: {
|
||||||
json: content ?? null,
|
json: tool ?? null,
|
||||||
title: "Configuration",
|
title: "Configuration",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -234,7 +242,7 @@ const History = () => {
|
|||||||
<>
|
<>
|
||||||
<AppShell.SectionHeader>History</AppShell.SectionHeader>
|
<AppShell.SectionHeader>History</AppShell.SectionHeader>
|
||||||
<AppShell.Scrollable>
|
<AppShell.Scrollable>
|
||||||
<div className="flex flex-col flex-grow p-3 gap-1">
|
<div className="flex grow flex-col p-3 gap-1">
|
||||||
{history.map((item, i) => (
|
{history.map((item, i) => (
|
||||||
<JsonViewer
|
<JsonViewer
|
||||||
key={`${item.type}-${i}`}
|
key={`${item.type}-${i}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user