mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
feat: Introduce new A2A and MCP API routes, enhance dashboard UI, update READMEs, and add E2E tests.
This commit is contained in:
36
src/app/api/a2a/status/route.ts
Normal file
36
src/app/api/a2a/status/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTaskManager } from "@/lib/a2a/taskManager";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const tm = getTaskManager();
|
||||
const stats = tm.getStats();
|
||||
|
||||
let agentCard: any = null;
|
||||
try {
|
||||
const agentModule = await import("@/app/.well-known/agent.json/route");
|
||||
const cardResponse = await agentModule.GET();
|
||||
agentCard = await cardResponse.json();
|
||||
} catch {
|
||||
agentCard = null;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
tasks: stats,
|
||||
agent: agentCard
|
||||
? {
|
||||
name: agentCard.name,
|
||||
description: agentCard.description,
|
||||
version: agentCard.version,
|
||||
url: agentCard.url,
|
||||
}
|
||||
: null,
|
||||
capabilities: agentCard?.capabilities || null,
|
||||
skills: Array.isArray(agentCard?.skills) ? agentCard.skills : [],
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load A2A status";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
15
src/app/api/a2a/tasks/[id]/cancel/route.ts
Normal file
15
src/app/api/a2a/tasks/[id]/cancel/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTaskManager } from "@/lib/a2a/taskManager";
|
||||
|
||||
export async function POST(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const tm = getTaskManager();
|
||||
const task = tm.cancelTask(id);
|
||||
return NextResponse.json({ task: { id: task.id, state: task.state } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to cancel A2A task";
|
||||
const status = message.includes("not found") ? 404 : 400;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
17
src/app/api/a2a/tasks/[id]/route.ts
Normal file
17
src/app/api/a2a/tasks/[id]/route.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTaskManager } from "@/lib/a2a/taskManager";
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const tm = getTaskManager();
|
||||
const task = tm.getTask(id);
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: `Task not found: ${id}` }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ task });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load A2A task";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
46
src/app/api/a2a/tasks/route.ts
Normal file
46
src/app/api/a2a/tasks/route.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTaskManager, type TaskState } from "@/lib/a2a/taskManager";
|
||||
|
||||
const VALID_TASK_STATES = new Set<TaskState>([
|
||||
"submitted",
|
||||
"working",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
]);
|
||||
|
||||
function parseIntParam(value: string | null, fallback: number): number {
|
||||
if (typeof value !== "string") return fallback;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const stateParam = searchParams.get("state");
|
||||
const skill = searchParams.get("skill") || undefined;
|
||||
const limit = Math.max(1, Math.min(200, parseIntParam(searchParams.get("limit"), 50)));
|
||||
const offset = Math.max(0, parseIntParam(searchParams.get("offset"), 0));
|
||||
|
||||
const state =
|
||||
typeof stateParam === "string" && VALID_TASK_STATES.has(stateParam as TaskState)
|
||||
? (stateParam as TaskState)
|
||||
: undefined;
|
||||
|
||||
const tm = getTaskManager();
|
||||
const total = tm.countTasks({ state, skill });
|
||||
const tasks = tm.listTasks({ state, skill, limit, offset });
|
||||
|
||||
return NextResponse.json({
|
||||
tasks,
|
||||
total,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to list A2A tasks";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
39
src/app/api/mcp/audit/route.ts
Normal file
39
src/app/api/mcp/audit/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { queryAuditEntries } from "@omniroute/open-sse/mcp-server/audit";
|
||||
|
||||
function parseBooleanParam(value: string | null): boolean | undefined {
|
||||
if (value === "true" || value === "1") return true;
|
||||
if (value === "false" || value === "0") return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseNumberParam(value: string | null, fallback: number): number {
|
||||
if (typeof value !== "string") return fallback;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = parseNumberParam(searchParams.get("limit"), 50);
|
||||
const offset = parseNumberParam(searchParams.get("offset"), 0);
|
||||
const tool = searchParams.get("tool") || undefined;
|
||||
const success = parseBooleanParam(searchParams.get("success"));
|
||||
const apiKeyId = searchParams.get("apiKeyId") || undefined;
|
||||
|
||||
const result = await queryAuditEntries({
|
||||
limit,
|
||||
offset,
|
||||
tool,
|
||||
success,
|
||||
apiKeyId,
|
||||
});
|
||||
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load MCP audit log";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
12
src/app/api/mcp/audit/stats/route.ts
Normal file
12
src/app/api/mcp/audit/stats/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAuditStats } from "@omniroute/open-sse/mcp-server/audit";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const stats = await getAuditStats();
|
||||
return NextResponse.json(stats);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load MCP audit stats";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
57
src/app/api/mcp/status/route.ts
Normal file
57
src/app/api/mcp/status/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAuditStats, queryAuditEntries } from "@omniroute/open-sse/mcp-server/audit";
|
||||
import {
|
||||
isMcpHeartbeatOnline,
|
||||
isProcessAlive,
|
||||
readMcpHeartbeat,
|
||||
resolveMcpHeartbeatPath,
|
||||
} from "@omniroute/open-sse/mcp-server/runtimeHeartbeat";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const [heartbeat, stats, lastCallPage] = await Promise.all([
|
||||
readMcpHeartbeat(),
|
||||
getAuditStats(),
|
||||
queryAuditEntries({ limit: 1, offset: 0 }),
|
||||
]);
|
||||
|
||||
const online = isMcpHeartbeatOnline(heartbeat, { requireLivePid: true });
|
||||
const lastCall = lastCallPage.entries[0] || null;
|
||||
const now = Date.now();
|
||||
const lastHeartbeatAtMs = heartbeat ? new Date(heartbeat.lastHeartbeatAt).getTime() : null;
|
||||
const startedAtMs = heartbeat ? new Date(heartbeat.startedAt).getTime() : null;
|
||||
const heartbeatAgeMs =
|
||||
typeof lastHeartbeatAtMs === "number" && Number.isFinite(lastHeartbeatAtMs)
|
||||
? Math.max(0, now - lastHeartbeatAtMs)
|
||||
: null;
|
||||
const uptimeMs =
|
||||
typeof startedAtMs === "number" && Number.isFinite(startedAtMs)
|
||||
? Math.max(0, now - startedAtMs)
|
||||
: null;
|
||||
|
||||
return NextResponse.json({
|
||||
status: online ? "online" : "offline",
|
||||
online,
|
||||
heartbeatPath: resolveMcpHeartbeatPath(),
|
||||
heartbeat: heartbeat
|
||||
? {
|
||||
...heartbeat,
|
||||
pidAlive: isProcessAlive(heartbeat.pid),
|
||||
heartbeatAgeMs,
|
||||
uptimeMs,
|
||||
}
|
||||
: null,
|
||||
activity: {
|
||||
totalCalls24h: stats.totalCalls,
|
||||
successRate: stats.successRate,
|
||||
avgDurationMs: stats.avgDurationMs,
|
||||
topTools: stats.topTools,
|
||||
lastCallAt: lastCall?.createdAt || null,
|
||||
lastCallTool: lastCall?.toolName || null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load MCP status";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
22
src/app/api/mcp/tools/route.ts
Normal file
22
src/app/api/mcp/tools/route.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCP_TOOLS, MCP_TOOL_MAP } from "@omniroute/open-sse/mcp-server/schemas/tools";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
return NextResponse.json({
|
||||
total: MCP_TOOLS.length,
|
||||
mappedTotal: Object.keys(MCP_TOOL_MAP).length,
|
||||
tools: MCP_TOOLS.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
scopes: [...tool.scopes],
|
||||
phase: tool.phase,
|
||||
auditLevel: tool.auditLevel,
|
||||
sourceEndpoints: [...tool.sourceEndpoints],
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load MCP tools";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user