@@ -376,7 +243,7 @@ export default function ApiEndpointsTab() {
)}
- {section === "catalog" && catalog && (
+ {catalog && (
<>
{/* Search & filter */}
@@ -647,232 +514,6 @@ export default function ApiEndpointsTab() {
)}
>
)}
-
- {/* ═══ WEBHOOKS ═══ */}
- {section === "webhooks" && (
- <>
-
-
-
-
webhook
-
-
Event Webhooks
-
- Receive HTTP callbacks when events occur in OmniRoute
-
-
-
- {!showAddWebhook && (
-
setShowAddWebhook(true)}
- className="flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-lg
- bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
- >
- add
- Add Webhook
-
- )}
-
-
- {/* Add webhook form */}
- {showAddWebhook && (
-
-
-
-
- Events
-
-
- setWhEvents(["*"])}
- className={`px-2 py-0.5 text-[10px] font-medium rounded transition-colors
- ${
- whEvents.includes("*")
- ? "bg-primary/10 text-primary"
- : "bg-black/5 dark:bg-white/5 text-text-muted"
- }`}
- >
- All events
-
- {WEBHOOK_EVENTS.map((ev) => (
- {
- if (whEvents.includes("*")) {
- setWhEvents([ev]);
- } else if (whEvents.includes(ev)) {
- setWhEvents(whEvents.filter((e) => e !== ev));
- } else {
- setWhEvents([...whEvents, ev]);
- }
- }}
- className={`px-2 py-0.5 text-[10px] font-medium rounded transition-colors
- ${
- whEvents.includes(ev) || whEvents.includes("*")
- ? "bg-primary/10 text-primary"
- : "bg-black/5 dark:bg-white/5 text-text-muted"
- }`}
- >
- {ev}
-
- ))}
-
-
-
-
- Save
-
- setShowAddWebhook(false)}
- className="px-3 py-1 text-xs font-medium rounded-lg
- bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
- >
- Cancel
-
-
-
- )}
-
- {/* Webhooks list */}
- {webhooksLoading ? (
- Loading...
- ) : webhooks.length === 0 ? (
-
-
- webhook
-
-
- No webhooks configured. Add one to receive event notifications.
-
-
- ) : (
-
- {webhooks.map((wh) => (
-
-
-
- {wh.url}
- {wh.failure_count > 0 && (
-
- {wh.failure_count} failures
-
- )}
-
-
- {wh.description && (
- {wh.description}
- )}
-
- Events: {wh.events.join(", ")}
-
- {wh.last_triggered_at && (
-
- Last: {new Date(wh.last_triggered_at).toLocaleString()}
- {wh.last_status ? ` (${wh.last_status})` : ""}
-
- )}
-
-
-
- testWebhook(wh.id)}
- disabled={testingWebhookId === wh.id}
- className="p-1 rounded hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
- title="Send test event"
- >
-
- {testingWebhookId === wh.id ? "sync" : "send"}
-
-
- toggleWebhook(wh)}
- className="p-1 rounded hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
- title={wh.enabled ? "Disable" : "Enable"}
- >
-
- {wh.enabled ? "toggle_on" : "toggle_off"}
-
-
- deleteWebhook(wh.id)}
- className="p-1 rounded hover:bg-red-500/10 transition-colors"
- title="Delete"
- >
-
- delete
-
-
-
-
- ))}
-
- )}
-
-
- {/* Webhook signature info */}
-
-
- vpn_key
-
Webhook Signatures
-
-
- Each webhook delivery includes an{" "}
-
- X-Webhook-Signature
- {" "}
- header signed with HMAC-SHA256 using the webhook secret. Verify the signature to
- ensure the payload is authentic.
-
-
-
- {`const crypto = require('crypto');\nconst sig = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');\nif (sig !== req.headers['x-webhook-signature']) throw new Error('Invalid signature');`}
-
-
-
- >
- )}
);
}
diff --git a/src/app/(dashboard)/dashboard/endpoint/page.tsx b/src/app/(dashboard)/dashboard/endpoint/page.tsx
index 4d5d159e3e..740a86fbbc 100644
--- a/src/app/(dashboard)/dashboard/endpoint/page.tsx
+++ b/src/app/(dashboard)/dashboard/endpoint/page.tsx
@@ -1,424 +1,5 @@
-"use client";
-
-import { useState, useEffect, useCallback } from "react";
-import { Card, SegmentedControl } from "@/shared/components";
import EndpointPageClient from "./EndpointPageClient";
-import McpDashboardPage from "./components/MCPDashboard";
-import A2ADashboardPage from "./components/A2ADashboard";
-import ApiEndpointsTab from "./ApiEndpointsTab";
-import { useTranslations } from "next-intl";
-import { copyToClipboard } from "@/shared/utils/clipboard";
-type ServiceStatus = {
- online: boolean;
- loading: boolean;
-};
-
-type McpTransport = "stdio" | "sse" | "streamable-http";
-
-/* ────── Toggle Switch ────── */
-function ServiceToggle({
- label,
- status,
- enabled,
- onToggle,
- toggling,
-}: {
- label: string;
- status: ServiceStatus;
- enabled: boolean;
- onToggle: () => void;
- toggling: boolean;
-}) {
- const online = enabled && status.online;
- const loading = enabled && status.loading;
-
- return (
-
-
-
- {loading ? "..." : online ? "Online" : "Offline"}
-
-
-
-
-
-
-
- {toggling ? "..." : enabled ? "ON" : "OFF"}
-
-
- );
-}
-
-function DisabledServicePanel({ title, description }: { title: string; description: string }) {
- return (
-
-
-
-
-
-
-
-
-
- {title}
-
-
- {description}
-
-
-
-
- );
-}
-
-/* ────── Transport Selector ────── */
-function TransportSelector({
- value,
- onChange,
- disabled,
- baseUrl,
-}: {
- value: McpTransport;
- onChange: (t: McpTransport) => void;
- disabled: boolean;
- baseUrl: string;
-}) {
- const options: { value: McpTransport; label: string; desc: string }[] = [
- { value: "stdio", label: "stdio", desc: "Local — IDE spawns process via omniroute --mcp" },
- { value: "sse", label: "SSE", desc: "Remote — Server-Sent Events over HTTP" },
- {
- value: "streamable-http",
- label: "Streamable HTTP",
- desc: "Remote — Modern bidirectional HTTP",
- },
- ];
-
- const urlMap: Record
= {
- stdio: "omniroute --mcp",
- sse: `${baseUrl}/api/mcp/sse`,
- "streamable-http": `${baseUrl}/api/mcp/stream`,
- };
-
- return (
-
-
-
- swap_horiz
-
-
- Transport Mode
-
-
-
-
- {options.map((opt) => (
- onChange(opt.value)}
- disabled={disabled}
- className="flex flex-col items-start px-4 py-2.5 rounded-lg border transition-all duration-200 text-left"
- style={{
- borderColor: value === opt.value ? "var(--color-primary)" : "var(--color-border)",
- background:
- value === opt.value
- ? "rgba(var(--color-primary-rgb, 99,102,241), 0.1)"
- : "transparent",
- opacity: disabled ? 0.5 : 1,
- cursor: disabled ? "wait" : "pointer",
- }}
- >
-
- {opt.label}
-
-
- {opt.desc}
-
-
- ))}
-
-
- {/* Connection info */}
-
-
- {value === "stdio" ? "terminal" : "link"}
-
-
- {urlMap[value]}
-
- {value !== "stdio" && (
- void copyToClipboard(urlMap[value])}
- title="Copy URL"
- >
- Copy
-
- )}
-
-
- );
-}
-
-/* ────── Main Page ────── */
export default function EndpointPage() {
- const [activeTab, setActiveTab] = useState("endpoint-proxy");
- const t = useTranslations("endpoints");
-
- const [mcpStatus, setMcpStatus] = useState({ online: false, loading: true });
- const [a2aStatus, setA2aStatus] = useState({ online: false, loading: true });
- const [mcpEnabled, setMcpEnabled] = useState(false);
- const [a2aEnabled, setA2aEnabled] = useState(false);
- const [mcpToggling, setMcpToggling] = useState(false);
- const [a2aToggling, setA2aToggling] = useState(false);
- const [mcpTransport, setMcpTransport] = useState("stdio");
- const [transportSaving, setTransportSaving] = useState(false);
-
- const [baseUrl, setBaseUrl] = useState("");
-
- // Detect base URL from browser
- useEffect(() => {
- if (typeof window !== "undefined") {
- setBaseUrl(`${window.location.protocol}//${window.location.host}`);
- }
- }, []);
-
- // Fetch initial settings
- useEffect(() => {
- const fetchSettings = async () => {
- try {
- const res = await fetch("/api/settings");
- if (res.ok) {
- const data = await res.json();
- setMcpEnabled(!!data.mcpEnabled);
- setA2aEnabled(!!data.a2aEnabled);
- setMcpTransport((data.mcpTransport as McpTransport) || "stdio");
- }
- } catch {
- // defaults stay
- }
- };
- void fetchSettings();
- }, []);
-
- const patchSetting = useCallback(async (body: Record) => {
- return fetch("/api/settings", {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
- });
- }, []);
-
- const toggleService = useCallback(
- async (service: "mcp" | "a2a") => {
- const setToggling = service === "mcp" ? setMcpToggling : setA2aToggling;
- const setEnabled = service === "mcp" ? setMcpEnabled : setA2aEnabled;
- const currentlyEnabled = service === "mcp" ? mcpEnabled : a2aEnabled;
- const newValue = !currentlyEnabled;
-
- setToggling(true);
- try {
- const res = await patchSetting({
- [service === "mcp" ? "mcpEnabled" : "a2aEnabled"]: newValue,
- });
- if (res.ok) setEnabled(newValue);
- } catch {
- // keep current state
- } finally {
- setToggling(false);
- }
- },
- [mcpEnabled, a2aEnabled, patchSetting]
- );
-
- const changeTransport = useCallback(
- async (newTransport: McpTransport) => {
- setTransportSaving(true);
- try {
- const res = await patchSetting({ mcpTransport: newTransport });
- if (res.ok) setMcpTransport(newTransport);
- } catch {
- // keep current
- } finally {
- setTransportSaving(false);
- }
- },
- [patchSetting]
- );
-
- const refreshMcpStatus = useCallback(async () => {
- setMcpStatus((prev) => ({ ...prev, loading: true }));
- try {
- const res = await fetch("/api/mcp/status");
- if (res.ok) {
- const data = await res.json();
- setMcpStatus({ online: !!data.online, loading: false });
- } else {
- setMcpStatus({ online: false, loading: false });
- }
- } catch {
- setMcpStatus({ online: false, loading: false });
- }
- }, []);
-
- const refreshA2aStatus = useCallback(async () => {
- setA2aStatus((prev) => ({ ...prev, loading: true }));
- try {
- const res = await fetch("/api/a2a/status");
- if (res.ok) {
- const data = await res.json();
- setA2aStatus({ online: data.status === "ok", loading: false });
- } else {
- setA2aStatus({ online: false, loading: false });
- }
- } catch {
- setA2aStatus({ online: false, loading: false });
- }
- }, []);
-
- useEffect(() => {
- const load = () => {
- void refreshMcpStatus();
- void refreshA2aStatus();
- };
- load();
- const interval = setInterval(load, 30000);
- return () => clearInterval(interval);
- }, [refreshMcpStatus, refreshA2aStatus]);
-
- return (
-
-
-
-
- {activeTab === "mcp" && (
- void toggleService("mcp")}
- toggling={mcpToggling}
- />
- )}
- {activeTab === "a2a" && (
- void toggleService("a2a")}
- toggling={a2aToggling}
- />
- )}
-
-
- {/* Transport selector for MCP */}
- {activeTab === "mcp" && mcpEnabled && (
-
void changeTransport(t)}
- disabled={transportSaving}
- baseUrl={baseUrl}
- />
- )}
-
- {activeTab === "endpoint-proxy" && }
- {activeTab === "mcp" && }
- {activeTab === "a2a" &&
- (a2aEnabled ? (
-
- ) : (
-
- ))}
- {activeTab === "api-endpoints" && }
-
- );
+ return ;
}
diff --git a/src/app/(dashboard)/dashboard/mcp/page.tsx b/src/app/(dashboard)/dashboard/mcp/page.tsx
index 68349a0792..aa7aad0e58 100644
--- a/src/app/(dashboard)/dashboard/mcp/page.tsx
+++ b/src/app/(dashboard)/dashboard/mcp/page.tsx
@@ -1,7 +1,332 @@
"use client";
+import { useState, useEffect, useCallback } from "react";
+import { Card } from "@/shared/components";
+import { copyToClipboard } from "@/shared/utils/clipboard";
import McpDashboardPage from "../endpoint/components/MCPDashboard";
+type ServiceStatus = { online: boolean; loading: boolean };
+type McpTransport = "stdio" | "sse" | "streamable-http";
+
+function ServiceToggle({
+ label,
+ status,
+ enabled,
+ onToggle,
+ toggling,
+}: {
+ label: string;
+ status: ServiceStatus;
+ enabled: boolean;
+ onToggle: () => void;
+ toggling: boolean;
+}) {
+ const online = enabled && status.online;
+ const loading = enabled && status.loading;
+
+ return (
+
+
+
+ {loading ? "..." : online ? "Online" : "Offline"}
+
+
+
+
+
+
+
+ {toggling ? "..." : enabled ? "ON" : "OFF"}
+
+
+ );
+}
+
+function TransportSelector({
+ value,
+ onChange,
+ disabled,
+ baseUrl,
+}: {
+ value: McpTransport;
+ onChange: (t: McpTransport) => void;
+ disabled: boolean;
+ baseUrl: string;
+}) {
+ const options: { value: McpTransport; label: string; desc: string }[] = [
+ { value: "stdio", label: "stdio", desc: "Local — IDE spawns process via omniroute --mcp" },
+ { value: "sse", label: "SSE", desc: "Remote — Server-Sent Events over HTTP" },
+ {
+ value: "streamable-http",
+ label: "Streamable HTTP",
+ desc: "Remote — Modern bidirectional HTTP",
+ },
+ ];
+
+ const urlMap: Record = {
+ stdio: "omniroute --mcp",
+ sse: `${baseUrl}/api/mcp/sse`,
+ "streamable-http": `${baseUrl}/api/mcp/stream`,
+ };
+
+ return (
+
+
+
+ swap_horiz
+
+
+ Transport Mode
+
+
+
+
+ {options.map((opt) => (
+ onChange(opt.value)}
+ disabled={disabled}
+ className="flex flex-col items-start px-4 py-2.5 rounded-lg border transition-all duration-200 text-left"
+ style={{
+ borderColor: value === opt.value ? "var(--color-primary)" : "var(--color-border)",
+ background:
+ value === opt.value
+ ? "rgba(var(--color-primary-rgb, 99,102,241), 0.1)"
+ : "transparent",
+ opacity: disabled ? 0.5 : 1,
+ cursor: disabled ? "wait" : "pointer",
+ }}
+ >
+
+ {opt.label}
+
+
+ {opt.desc}
+
+
+ ))}
+
+
+
+
+ {value === "stdio" ? "terminal" : "link"}
+
+
+ {urlMap[value]}
+
+ {value !== "stdio" && (
+ void copyToClipboard(urlMap[value])}
+ title="Copy URL"
+ >
+ Copy
+
+ )}
+
+
+ );
+}
+
+function DisabledPanel() {
+ return (
+
+
+
+
+
+
+
+
+
+ MCP is disabled
+
+
+ Enable MCP above to configure transport mode and view server telemetry.
+
+
+
+
+ );
+}
+
export default function McpPage() {
- return ;
+ const [mcpStatus, setMcpStatus] = useState({ online: false, loading: true });
+ const [mcpEnabled, setMcpEnabled] = useState(false);
+ const [mcpToggling, setMcpToggling] = useState(false);
+ const [mcpTransport, setMcpTransport] = useState("stdio");
+ const [transportSaving, setTransportSaving] = useState(false);
+ const [baseUrl, setBaseUrl] = useState("");
+
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ setBaseUrl(`${window.location.protocol}//${window.location.host}`);
+ }
+ }, []);
+
+ const patchSetting = useCallback(async (body: Record) => {
+ return fetch("/api/settings", {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+ }, []);
+
+ useEffect(() => {
+ const fetchSettings = async () => {
+ try {
+ const res = await fetch("/api/settings");
+ if (res.ok) {
+ const data = await res.json();
+ setMcpEnabled(!!data.mcpEnabled);
+ setMcpTransport((data.mcpTransport as McpTransport) || "stdio");
+ }
+ } catch {
+ // defaults stay
+ }
+ };
+ void fetchSettings();
+ }, []);
+
+ const refreshStatus = useCallback(async () => {
+ setMcpStatus((prev) => ({ ...prev, loading: true }));
+ try {
+ const res = await fetch("/api/mcp/status");
+ setMcpStatus({ online: res.ok ? !!(await res.json()).online : false, loading: false });
+ } catch {
+ setMcpStatus({ online: false, loading: false });
+ }
+ }, []);
+
+ useEffect(() => {
+ void refreshStatus();
+ const interval = setInterval(() => void refreshStatus(), 30000);
+ return () => clearInterval(interval);
+ }, [refreshStatus]);
+
+ const toggleMcp = useCallback(async () => {
+ const newValue = !mcpEnabled;
+ setMcpToggling(true);
+ try {
+ const res = await patchSetting({ mcpEnabled: newValue });
+ if (res.ok) setMcpEnabled(newValue);
+ } catch {
+ // keep current
+ } finally {
+ setMcpToggling(false);
+ }
+ }, [mcpEnabled, patchSetting]);
+
+ const changeTransport = useCallback(
+ async (newTransport: McpTransport) => {
+ setTransportSaving(true);
+ try {
+ const res = await patchSetting({ mcpTransport: newTransport });
+ if (res.ok) setMcpTransport(newTransport);
+ } catch {
+ // keep current
+ } finally {
+ setTransportSaving(false);
+ }
+ },
+ [patchSetting]
+ );
+
+ return (
+
+
+ void toggleMcp()}
+ toggling={mcpToggling}
+ />
+
+
+ {mcpEnabled && (
+
void changeTransport(t)}
+ disabled={transportSaving}
+ baseUrl={baseUrl}
+ />
+ )}
+
+ {mcpEnabled ? : }
+
+ );
}