feat: Introduce new A2A and MCP API routes, enhance dashboard UI, update READMEs, and add E2E tests.

This commit is contained in:
diegosouzapw
2026-03-05 11:16:56 -03:00
parent c38a58fc98
commit 21135407af
58 changed files with 17980 additions and 8998 deletions

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}