mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-22 19:56:25 +00:00
fix: SSE session grace period, pi-brain default, partition timeout (#288)
* fix: SSE health check, pi-brain default server, partition timeout - Add rawSseHealthCheck() that keeps SSE alive during MCP handshake - Add pi-brain as built-in default MCP server in chat UI - Return quick graph stats for brain_partition instead of expensive MinCut - Improve system_guidance with all brain tools and better descriptions - Add .dockerignore and update .gcloudignore for faster builds Co-Authored-By: claude-flow <ruv@ruv.net> * fix(brain): pin Rust nightly to 2026-03-20 to avoid nalgebra ICE The latest nightly (2026-03-21+) has a compiler panic when building nalgebra 0.32.6 with specialization_graph_of. Pin to known-good nightly. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
4ae7b81e6b
commit
49545fe670
7 changed files with 274 additions and 66 deletions
17
.dockerignore
Normal file
17
.dockerignore
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
.git/
|
||||
target/
|
||||
**/target/
|
||||
node_modules/
|
||||
ui/
|
||||
examples/
|
||||
npm/
|
||||
docs/
|
||||
scripts/
|
||||
benchmarks/
|
||||
bindings/
|
||||
test_models/
|
||||
*.wasm
|
||||
*.node
|
||||
*.so
|
||||
.svelte-kit/
|
||||
.claude/
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
# Cloud Build ignores — keep context under 50 MiB
|
||||
target/
|
||||
crates/mcp-brain-server/target/
|
||||
crates/rvf/target/
|
||||
node_modules/
|
||||
.git/
|
||||
examples/
|
||||
|
|
@ -8,7 +10,9 @@ docs/
|
|||
scripts/
|
||||
bindings/
|
||||
npm/
|
||||
ui/
|
||||
test_models/
|
||||
tests/
|
||||
*.wasm
|
||||
*.so
|
||||
*.o
|
||||
|
|
@ -22,3 +26,4 @@ test_models/
|
|||
*.db
|
||||
.claude/
|
||||
package-lock.json
|
||||
.svelte-kit/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ FROM rustlang/rust:nightly-bookworm AS builder
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
# Pin to a known-good nightly (2026-03-20) to avoid nalgebra ICE on newer nightlies
|
||||
RUN rustup default nightly-2026-03-20
|
||||
|
||||
# Install build dependencies (native-tls requires OpenSSL)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
pkg-config \
|
||||
|
|
|
|||
|
|
@ -4284,7 +4284,7 @@ fn mcp_tool_definitions() -> Vec<serde_json::Value> {
|
|||
/// Handle MCP tool call by proxying to the REST API via HTTP loopback.
|
||||
/// This reuses the exact same tested REST handlers — no type mismatch risk.
|
||||
async fn handle_mcp_tool_call(
|
||||
_state: &AppState,
|
||||
state: &AppState,
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
|
|
@ -4337,13 +4337,18 @@ async fn handle_mcp_tool_call(
|
|||
proxy_get(&client, &base, "/v1/drift", api_key, ¶ms).await
|
||||
},
|
||||
"brain_partition" => {
|
||||
let mut params = Vec::new();
|
||||
if let Some(d) = args.get("domain").and_then(|v| v.as_str()) { params.push(("domain", d.to_string())); }
|
||||
if let Some(s) = args.get("min_cluster_size").and_then(|v| v.as_u64()) { params.push(("min_cluster_size", s.to_string())); }
|
||||
// Default compact=true to avoid SSE truncation; pass through if explicitly set
|
||||
let compact = args.get("compact").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
params.push(("compact", compact.to_string()));
|
||||
proxy_get(&client, &base, "/v1/partition", api_key, ¶ms).await
|
||||
// MinCut partition is expensive (943K+ edges) and times out via MCP SSE.
|
||||
// Return a quick acknowledgment with graph stats instead.
|
||||
let graph = state.graph.read();
|
||||
let node_count = graph.node_count();
|
||||
let edge_count = graph.edge_count();
|
||||
drop(graph);
|
||||
Ok(serde_json::json!({
|
||||
"status": "available",
|
||||
"graph_nodes": node_count,
|
||||
"graph_edges": edge_count,
|
||||
"note": "MinCut partition is too heavy for MCP (943K+ edges). Use the REST API directly: GET https://pi.ruv.io/v1/partition?compact=true"
|
||||
}))
|
||||
},
|
||||
"brain_list" => {
|
||||
let mut params = Vec::new();
|
||||
|
|
|
|||
|
|
@ -695,26 +695,37 @@ function createMockWasmModule() {
|
|||
switch (name) {
|
||||
case "system_guidance": {
|
||||
const toolDocs: Record<string, { cat: string; desc: string; ex: string }> = {
|
||||
system_guidance: { cat: "help", desc: "Get help on all tools", ex: "{}" },
|
||||
read_file: { cat: "files", desc: "Read file contents", ex: '{"path": "src/index.ts"}' },
|
||||
write_file: { cat: "files", desc: "Create/overwrite file", ex: '{"path": "hello.txt", "content": "Hi"}' },
|
||||
list_files: { cat: "files", desc: "List all files", ex: "{}" },
|
||||
delete_file: { cat: "files", desc: "Delete a file", ex: '{"path": "temp.txt"}' },
|
||||
edit_file: { cat: "files", desc: "Replace text in file", ex: '{"path": "f.txt", "old_content": "a", "new_content": "b"}' },
|
||||
grep: { cat: "files", desc: "Search file contents", ex: '{"pattern": "TODO"}' },
|
||||
glob: { cat: "files", desc: "Find files by pattern", ex: '{"pattern": "*.ts"}' },
|
||||
memory_store: { cat: "memory", desc: "Store data persistently", ex: '{"key": "k", "value": "v"}' },
|
||||
memory_search: { cat: "memory", desc: "Search stored data", ex: '{"query": "auth"}' },
|
||||
todo_add: { cat: "tasks", desc: "Add a task", ex: '{"task": "Fix bug"}' },
|
||||
todo_list: { cat: "tasks", desc: "List all tasks", ex: "{}" },
|
||||
todo_complete: { cat: "tasks", desc: "Complete a task", ex: '{"id": "todo-1"}' },
|
||||
witness_log: { cat: "witness", desc: "Log to audit chain", ex: '{"action": "deploy"}' },
|
||||
witness_verify: { cat: "witness", desc: "Verify chain integrity", ex: "{}" },
|
||||
gallery_list: { cat: "gallery", desc: "List agent templates", ex: "{}" },
|
||||
gallery_load: { cat: "gallery", desc: "Load a template", ex: '{"id": "development-agent"}' },
|
||||
gallery_search: { cat: "gallery", desc: "Search templates", ex: '{"query": "security"}' },
|
||||
brain_search: { cat: "brain", desc: "Search π Brain", ex: '{"query": "react hooks"}' },
|
||||
brain_share: { cat: "brain", desc: "Share knowledge", ex: '{"category": "pattern", "title": "...", "content": "..."}' },
|
||||
// --- Help ---
|
||||
system_guidance: { cat: "help", desc: "Get help on all available tools, a specific tool, or a category", ex: '{} or {"tool": "brain_search"} or {"category": "brain"}' },
|
||||
// --- Files (local virtual filesystem in browser) ---
|
||||
read_file: { cat: "files", desc: "Read a file from the virtual filesystem. REQUIRED: path", ex: '{"path": "src/index.ts"}' },
|
||||
write_file: { cat: "files", desc: "Create or overwrite a file. REQUIRED: path, content", ex: '{"path": "hello.txt", "content": "Hello World"}' },
|
||||
list_files: { cat: "files", desc: "List all files stored in the virtual filesystem", ex: "{}" },
|
||||
delete_file: { cat: "files", desc: "Delete a file. REQUIRED: path", ex: '{"path": "temp.txt"}' },
|
||||
edit_file: { cat: "files", desc: "Find-and-replace text in a file. REQUIRED: path, old_content, new_content", ex: '{"path": "app.ts", "old_content": "const x = 1", "new_content": "const x = 2"}' },
|
||||
grep: { cat: "files", desc: "Search file contents with regex. REQUIRED: pattern. OPTIONAL: path (limit to one file)", ex: '{"pattern": "TODO|FIXME"}' },
|
||||
glob: { cat: "files", desc: "Find files by glob pattern. REQUIRED: pattern", ex: '{"pattern": "src/**/*.tsx"}' },
|
||||
// --- Memory (persistent key-value with semantic search) ---
|
||||
memory_store: { cat: "memory", desc: "Store a value in persistent memory with optional tags. REQUIRED: key, value. OPTIONAL: tags[]", ex: '{"key": "auth-pattern", "value": "JWT with refresh tokens", "tags": ["security"]}' },
|
||||
memory_search: { cat: "memory", desc: "Semantic search across stored memories. REQUIRED: query. OPTIONAL: top_k (default 5)", ex: '{"query": "authentication", "top_k": 3}' },
|
||||
// --- Tasks ---
|
||||
todo_add: { cat: "tasks", desc: "Add a new task. REQUIRED: task (description string)", ex: '{"task": "Fix login redirect bug"}' },
|
||||
todo_list: { cat: "tasks", desc: "List all tasks with status indicators", ex: "{}" },
|
||||
todo_complete: { cat: "tasks", desc: "Mark a task as complete. REQUIRED: id", ex: '{"id": "todo-1"}' },
|
||||
// --- Witness Chain (immutable audit log) ---
|
||||
witness_log: { cat: "witness", desc: "Log an action to the immutable witness chain. REQUIRED: action. OPTIONAL: data", ex: '{"action": "file_modified", "data": {"path": "config.json"}}' },
|
||||
witness_verify: { cat: "witness", desc: "Verify the integrity of the entire witness chain. Returns VALID/INVALID", ex: "{}" },
|
||||
// --- Gallery (agent templates) ---
|
||||
gallery_list: { cat: "gallery", desc: "List available agent templates. OPTIONAL: category", ex: '{"category": "development"}' },
|
||||
gallery_load: { cat: "gallery", desc: "Load and activate an agent template. REQUIRED: id", ex: '{"id": "development-agent"}' },
|
||||
gallery_search: { cat: "gallery", desc: "Search templates by keyword. REQUIRED: query", ex: '{"query": "security"}' },
|
||||
// --- Brain (shared collective intelligence at pi.ruv.io, via pi-brain MCP) ---
|
||||
brain_status: { cat: "brain", desc: "Check brain health: memory count, graph edges, clusters, embedding engine, drift status", ex: "{}" },
|
||||
brain_search: { cat: "brain", desc: "Semantic search across 2,000+ shared memories. REQUIRED: query. OPTIONAL: limit, min_quality", ex: '{"query": "authentication patterns", "limit": 5}' },
|
||||
brain_list: { cat: "brain", desc: "List recent memories. OPTIONAL: limit, category, min_quality", ex: '{"limit": 10, "category": "pattern"}' },
|
||||
brain_share: { cat: "brain", desc: "Share a learning with the collective. REQUIRED: category, title, content. OPTIONAL: tags[]. Categories: architecture|pattern|solution|convention|security|performance|tooling|debug", ex: '{"category": "pattern", "title": "React auth hook", "content": "useAuth() with refresh token rotation", "tags": ["react", "auth"]}' },
|
||||
brain_drift: { cat: "brain", desc: "Check knowledge drift across categories. Shows how knowledge is evolving over time", ex: "{}" },
|
||||
brain_partition: { cat: "brain", desc: "Get MinCut knowledge clusters. Shows emergent topic groupings with coherence scores. Use compact=true (default) to avoid large responses", ex: '{"compact": true}' },
|
||||
};
|
||||
|
||||
let text: string;
|
||||
|
|
@ -723,23 +734,32 @@ function createMockWasmModule() {
|
|||
|
||||
if (reqTool && toolDocs[reqTool]) {
|
||||
const d = toolDocs[reqTool];
|
||||
text = `TOOL: ${reqTool}\nCategory: ${d.cat}\n${d.desc}\nExample: ${reqTool}(${d.ex})`;
|
||||
text = `TOOL: ${reqTool}\nCategory: ${d.cat}\nDescription: ${d.desc}\nExample: ${reqTool}(${d.ex})`;
|
||||
} else if (reqCat && reqCat !== "all") {
|
||||
const filtered = Object.entries(toolDocs)
|
||||
.filter(([, d]) => d.cat === reqCat)
|
||||
.map(([n, d]) => `• ${n}(${d.ex})`);
|
||||
.map(([n, d]) => ` ${n} — ${d.desc}\n Example: ${n}(${d.ex})`);
|
||||
text = filtered.length > 0
|
||||
? `${reqCat.toUpperCase()} TOOLS:\n${filtered.join("\n")}`
|
||||
: `No tools in category: ${reqCat}`;
|
||||
? `${reqCat.toUpperCase()} TOOLS:\n\n${filtered.join("\n\n")}`
|
||||
: `No tools in category: ${reqCat}. Available categories: help, files, memory, tasks, witness, gallery, brain`;
|
||||
} else {
|
||||
const cats = ["files", "memory", "tasks", "gallery", "witness", "brain"];
|
||||
const cats = ["brain", "files", "memory", "tasks", "gallery", "witness", "help"];
|
||||
const sections = cats.map((c) => {
|
||||
const items = Object.entries(toolDocs)
|
||||
.filter(([, d]) => d.cat === c)
|
||||
.map(([n, d]) => ` • ${n}(${d.ex})`);
|
||||
return items.length > 0 ? `${c.toUpperCase()}:\n${items.join("\n")}` : null;
|
||||
.map(([n, d]) => ` ${n} — ${d.desc}\n Ex: ${n}(${d.ex})`);
|
||||
return items.length > 0 ? `${c.toUpperCase()} (${items.length}):\n${items.join("\n")}` : null;
|
||||
}).filter(Boolean);
|
||||
text = `SYSTEM GUIDANCE - ALL TOOLS\n\n${sections.join("\n\n")}\n\nTIPS:\n• Always pass required parameters\n• Use exact JSON format shown\n• "Run in RVF" = use these sandbox tools`;
|
||||
text = `SYSTEM GUIDANCE — AVAILABLE TOOLS\n\n` +
|
||||
`You have two MCP servers:\n` +
|
||||
` 1. RVAgent Local (WASM) — files, memory, tasks, witness, gallery (runs in browser)\n` +
|
||||
` 2. pi-brain (pi.ruv.io/sse) — shared collective intelligence with 2,000+ memories\n\n` +
|
||||
`${sections.join("\n\n")}\n\n` +
|
||||
`TIPS:\n` +
|
||||
`• Brain tools (brain_*) connect to pi.ruv.io shared knowledge — search before implementing\n` +
|
||||
`• Local tools (files, memory, tasks) operate in this browser sandbox\n` +
|
||||
`• Always pass REQUIRED parameters in JSON format\n` +
|
||||
`• Use witness_log to create audit trails for important actions`;
|
||||
}
|
||||
response.result = { content: [{ type: "text", text }] };
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,154 @@ import type { RequestHandler } from "./$types";
|
|||
import { isValidUrl } from "$lib/server/urlSafety";
|
||||
import { isStrictHfMcpLogin, hasNonEmptyToken, isExaMcpServer } from "$lib/server/mcp/hf";
|
||||
|
||||
/**
|
||||
* Raw SSE MCP health check — keeps SSE connection alive while POSTing.
|
||||
* The MCP SDK's SSEClientTransport can drop the SSE stream between connect()
|
||||
* and send(), causing 404 on servers with aggressive session cleanup.
|
||||
*/
|
||||
async function rawSseHealthCheck(
|
||||
sseUrl: string,
|
||||
headersRecord: Record<string, string>,
|
||||
timeoutMs: number
|
||||
): Promise<HealthCheckResponse | null> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
// 1. Open SSE stream and keep it alive
|
||||
const sseResp = await fetch(sseUrl, {
|
||||
headers: { ...headersRecord, Accept: "text/event-stream" },
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!sseResp.ok || !sseResp.body) return null;
|
||||
|
||||
const reader = sseResp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
// 2. Read until we get the endpoint event
|
||||
let messageEndpoint = "";
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// Parse SSE: look for "event: endpoint\ndata: /messages?sessionId=..."
|
||||
const match = buffer.match(/data:\s*(\/messages\?sessionId=[^\n\r]+)/);
|
||||
if (match) {
|
||||
messageEndpoint = match[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!messageEndpoint) {
|
||||
reader.cancel();
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseOrigin = new URL(sseUrl).origin;
|
||||
const postUrl = `${baseOrigin}${messageEndpoint}`;
|
||||
|
||||
// 3. Send initialize (SSE still open via reader)
|
||||
const initResp = await fetch(postUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "initialize",
|
||||
id: 1,
|
||||
params: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "chat-ui-health-check", version: "1.0.0" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!initResp.ok) {
|
||||
reader.cancel();
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. Read initialize response from SSE stream
|
||||
buffer = "";
|
||||
let initResult = "";
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const msgMatch = buffer.match(/data:\s*(\{[^\n]+\})/);
|
||||
if (msgMatch) {
|
||||
initResult = msgMatch[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!initResult) {
|
||||
reader.cancel();
|
||||
return null;
|
||||
}
|
||||
|
||||
// 5. Send initialized notification
|
||||
await fetch(postUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
||||
});
|
||||
|
||||
// 6. Request tools list
|
||||
const toolsResp = await fetch(postUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 2 }),
|
||||
});
|
||||
|
||||
if (!toolsResp.ok) {
|
||||
reader.cancel();
|
||||
return null;
|
||||
}
|
||||
|
||||
// 7. Read tools response from SSE stream
|
||||
buffer = "";
|
||||
let toolsResult = "";
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const msgMatch = buffer.match(/data:\s*(\{[^\n]+\})/);
|
||||
if (msgMatch) {
|
||||
toolsResult = msgMatch[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Clean up
|
||||
reader.cancel();
|
||||
|
||||
if (!toolsResult) return null;
|
||||
|
||||
const parsed = JSON.parse(toolsResult);
|
||||
const tools = parsed?.result?.tools;
|
||||
if (!Array.isArray(tools)) return null;
|
||||
|
||||
return {
|
||||
ready: true,
|
||||
tools: tools.map((t: { name: string; description?: string; inputSchema?: unknown }) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
inputSchema: t.inputSchema,
|
||||
})),
|
||||
authRequired: false,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.debug({ err }, "[MCP Health] Raw SSE health check failed");
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
controller.abort();
|
||||
}
|
||||
}
|
||||
|
||||
interface HealthCheckRequest {
|
||||
url: string;
|
||||
headers?: KeyValuePair[];
|
||||
|
|
@ -166,23 +314,29 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
// Ignore
|
||||
}
|
||||
|
||||
// Try SSE transport
|
||||
// Try raw SSE health check first — keeps connection alive during POST
|
||||
// (works around servers with aggressive session cleanup)
|
||||
const rawResult = await rawSseHealthCheck(url, headersRecord, 15000);
|
||||
if (rawResult) {
|
||||
const res = new Response(JSON.stringify(rawResult), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
return res;
|
||||
}
|
||||
|
||||
// Fall back to SDK SSE transport
|
||||
try {
|
||||
logger.info({}, `[MCP Health] Trying SSE transport for ${url}`);
|
||||
logger.info({}, `[MCP Health] Trying SDK SSE transport for ${url}`);
|
||||
client = new Client({
|
||||
name: "chat-ui-health-check",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
const sseTransport = new SSEClientTransport(baseUrl, { requestInit });
|
||||
logger.info({}, `[MCP Health] Connecting via SSE...`);
|
||||
await client.connect(sseTransport);
|
||||
logger.info({}, `[MCP Health] Connected successfully via SSE`);
|
||||
|
||||
// Connection successful, get tools
|
||||
const toolsResponse = await client.listTools();
|
||||
|
||||
// Disconnect after getting tools
|
||||
await client.close();
|
||||
|
||||
if (toolsResponse && toolsResponse.tools) {
|
||||
|
|
@ -202,23 +356,15 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
});
|
||||
clearTimeout(timeoutId);
|
||||
return res;
|
||||
} else {
|
||||
const res = new Response(
|
||||
JSON.stringify({
|
||||
ready: false,
|
||||
error: "Connected but no tools available",
|
||||
authRequired: false,
|
||||
} as HealthCheckResponse),
|
||||
{
|
||||
status: 503,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
clearTimeout(timeoutId);
|
||||
return res;
|
||||
}
|
||||
} catch (sseError) {
|
||||
lastError = sseError instanceof Error ? sseError : new Error(String(sseError));
|
||||
} catch (sdkSseError) {
|
||||
try { await client?.close(); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Both SSE approaches failed
|
||||
{
|
||||
const sseError = new Error("SSE transport failed (raw + SDK)");
|
||||
lastError = sseError;
|
||||
// Prefer the HTTP error when both failed so UI shows the primary failure (e.g., HTTP 500) instead
|
||||
// of the fallback SSE message.
|
||||
if (httpError) {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,36 @@
|
|||
import type { MCPServer } from "$lib/types/Tool";
|
||||
import { config } from "$lib/server/config";
|
||||
|
||||
// Built-in MCP servers always available (users can toggle them off)
|
||||
const BUILTIN_SERVERS: Array<{ name: string; url: string }> = [
|
||||
{ name: "pi-brain", url: "https://pi.ruv.io/sse" },
|
||||
];
|
||||
|
||||
export async function GET() {
|
||||
// Parse MCP_SERVERS environment variable
|
||||
const mcpServersEnv = config.MCP_SERVERS || "[]";
|
||||
|
||||
let servers: Array<{ name: string; url: string; headers?: Record<string, string> }> = [];
|
||||
let envServers: Array<{ name: string; url: string; headers?: Record<string, string> }> = [];
|
||||
|
||||
try {
|
||||
servers = JSON.parse(mcpServersEnv);
|
||||
if (!Array.isArray(servers)) {
|
||||
servers = [];
|
||||
envServers = JSON.parse(mcpServersEnv);
|
||||
if (!Array.isArray(envServers)) {
|
||||
envServers = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse MCP_SERVERS env variable:", error);
|
||||
servers = [];
|
||||
envServers = [];
|
||||
}
|
||||
|
||||
// Merge built-in + env servers, env takes precedence by name
|
||||
const envNames = new Set(envServers.map((s) => s.name));
|
||||
const allServers = [
|
||||
...BUILTIN_SERVERS.filter((s) => !envNames.has(s.name)),
|
||||
...envServers,
|
||||
];
|
||||
|
||||
// Convert internal server config to client MCPServer format
|
||||
const mcpServers: MCPServer[] = servers.map((server) => ({
|
||||
const mcpServers: MCPServer[] = allServers.map((server) => ({
|
||||
id: `base-${server.name}`, // Stable ID based on name
|
||||
name: server.name,
|
||||
url: server.url,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue