Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions apps/web/scripts/migrate-single-to-multi-mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env node

/**
* Migration script to convert legacy single MCP server configuration to multi-server format
*
* Usage:
* node scripts/migrate-single-to-multi-mcp.ts
Copy link

@gedion gedion Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script migrate-single-to-multi-mcp.ts claims you can run it with node, but it’s a TypeScript file (.ts)! Of course, Node.js will just throw an error, because it can’t execute TypeScript out-of-the-box.

*
* This script reads the legacy environment variables:
* - NEXT_PUBLIC_MCP_SERVER_URL
* - NEXT_PUBLIC_MCP_AUTH_REQUIRED
*
* And generates the new NEXT_PUBLIC_MCP_SERVERS JSON configuration.
*
* The output can be copied to your .env file or environment configuration.
*
* Example output:
* NEXT_PUBLIC_MCP_SERVERS='{"default":{"type":"http","url":"http://localhost:3001","authProvider":{"type":"bearer"}}}'
*/

import { MCPServersConfig, MCPServerHTTPConfig } from "../src/types/mcp";

function migrateSingleToMultiMCP(): void {
const legacyUrl = process.env.NEXT_PUBLIC_MCP_SERVER_URL;
const authRequired = process.env.NEXT_PUBLIC_MCP_AUTH_REQUIRED === "true";

if (!legacyUrl) {
console.error("❌ No legacy MCP server configuration found.");
console.error(" NEXT_PUBLIC_MCP_SERVER_URL is not set.");
process.exit(1);
}

console.log("🔍 Found legacy MCP configuration:");
console.log(` URL: ${legacyUrl}`);
console.log(` Auth Required: ${authRequired}`);
console.log("");

// Create the new multi-server configuration
const serverConfig: MCPServerHTTPConfig = {
type: "http",
transport: "http",
url: legacyUrl,
};

// Add auth provider if authentication was required
if (authRequired) {
serverConfig.authProvider = {
type: "bearer",
};
}

const multiServerConfig: MCPServersConfig = {
default: serverConfig,
};

// Generate the JSON string
const jsonConfig = JSON.stringify(multiServerConfig);

console.log("✅ Generated multi-server configuration:");
console.log("");
console.log("Add the following to your .env file:");
console.log("=====================================");
console.log(`NEXT_PUBLIC_MCP_SERVERS='${jsonConfig}'`);
console.log("=====================================");
console.log("");
console.log("📝 Notes:");
console.log(" - The legacy server is now named 'default'");
console.log(" - You can add more servers by editing the JSON");
console.log(" - The legacy variables can be removed after migration");
console.log("");
console.log("Example with multiple servers:");
console.log("==============================");
const exampleConfig: MCPServersConfig = {
default: serverConfig,
"github-tools": {
type: "http",
transport: "http",
url: "https://api.github.com/mcp",
authProvider: {
type: "api-key",
apiKey: "your-api-key-here",
},
},
"local-stdio": {
type: "stdio",
transport: "stdio",
command: "node",
args: ["./local-mcp-server.js"],
},
};
console.log(
`NEXT_PUBLIC_MCP_SERVERS='${JSON.stringify(exampleConfig, null, 2)}'`,
);
}

// Run the migration
if (require.main === module) {
migrateSingleToMultiMCP();
}
47 changes: 47 additions & 0 deletions apps/web/src/app/api/oap_mcp/[server]/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextRequest } from "next/server";
import { proxyMultiServerRequest } from "../../proxy-request";

export const runtime = "edge";

function extractParamsFromUrl(req: NextRequest) {
const pathname = req.nextUrl?.pathname ?? new URL(req.url).pathname;
const afterBase = pathname.replace(/^\/api\/oap_mcp\//, "");
const segments = afterBase.split("/").filter(Boolean);
const [server, ...path] = segments;
return { server, path } as const;
}

export async function GET(req: NextRequest) {
const { server, path } = extractParamsFromUrl(req);
return proxyMultiServerRequest(req, { params: { server, path } });
}

export async function POST(req: NextRequest) {
const { server, path } = extractParamsFromUrl(req);
return proxyMultiServerRequest(req, { params: { server, path } });
}

export async function PUT(req: NextRequest) {
const { server, path } = extractParamsFromUrl(req);
return proxyMultiServerRequest(req, { params: { server, path } });
}

export async function PATCH(req: NextRequest) {
const { server, path } = extractParamsFromUrl(req);
return proxyMultiServerRequest(req, { params: { server, path } });
}

export async function DELETE(req: NextRequest) {
const { server, path } = extractParamsFromUrl(req);
return proxyMultiServerRequest(req, { params: { server, path } });
}

export async function HEAD(req: NextRequest) {
const { server, path } = extractParamsFromUrl(req);
return proxyMultiServerRequest(req, { params: { server, path } });
}

export async function OPTIONS(req: NextRequest) {
const { server, path } = extractParamsFromUrl(req);
return proxyMultiServerRequest(req, { params: { server, path } });
}
94 changes: 89 additions & 5 deletions apps/web/src/app/api/oap_mcp/proxy-request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,72 @@
import { getMCPServers } from "@/lib/environment/mcp-servers";
import { handleServerAuth } from "@/lib/mcp-auth";
import { NextRequest, NextResponse } from "next/server";
import { createServerClient } from "@supabase/ssr";

export async function proxyMultiServerRequest(
req: NextRequest,
{ params }: { params: { server: string; path: string[] } },
): Promise<Response> {
const servers = getMCPServers();
const serverConfig = servers[params.server];

if (!serverConfig) {
return NextResponse.json(
{ message: `Server ${params.server} not found` },
{ status: 404 },
);
}

if (serverConfig.type === "stdio") {
return NextResponse.json(
{ message: "STDIO transport not supported via proxy" },
{ status: 400 },
);
}

// Construct target URL
const path = params.path.join("/");
const targetUrl = new URL(serverConfig.url);
targetUrl.pathname = `${targetUrl.pathname}/mcp/${path}`;

// Handle authentication based on server config
const headers = new Headers();
req.headers.forEach((value, key) => {
if (key.toLowerCase() !== "host") {
headers.append(key, value);
}
});

// Apply server-specific auth
if (serverConfig.authProvider) {
const accessToken = await handleServerAuth(serverConfig, req);
if (accessToken) {
headers.set("Authorization", `Bearer ${accessToken}`);
}
}

// Apply custom headers
if (serverConfig.headers) {
Object.entries(serverConfig.headers).forEach(([key, value]) => {
headers.set(key, value);
});
}

// Make the proxied request
const response = await fetch(targetUrl.toString(), {
method: req.method,
headers,
body: req.body,
});

// Return the response
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}

// This will contain the object which contains the access token
const MCP_TOKENS = process.env.MCP_TOKENS;
const MCP_SERVER_URL = process.env.NEXT_PUBLIC_MCP_SERVER_URL;
Expand Down Expand Up @@ -89,6 +155,29 @@ async function getMcpAccessToken(supabaseToken: string, mcpServerUrl: URL) {
* @returns The response from the MCP server.
*/
export async function proxyRequest(req: NextRequest): Promise<Response> {
// Extract the path after '/api/oap_mcp/'
// Example: /api/oap_mcp/foo/bar -> /foo/bar
const url = new URL(req.url);
const path = url.pathname.replace(/^\/api\/oap_mcp/, "");

// Check if the first path segment might be a server name
const pathSegments = path.split("/").filter(Boolean);
if (pathSegments.length > 0) {
const servers = getMCPServers();
const potentialServerName = pathSegments[0];

// If the first segment matches a configured server, delegate to new proxy
if (servers[potentialServerName]) {
return proxyMultiServerRequest(req, {
params: {
server: potentialServerName,
path: pathSegments.slice(1),
},
});
}
}

// Legacy behavior - continue with single server logic
if (!MCP_SERVER_URL) {
return new Response(
JSON.stringify({
Expand All @@ -99,11 +188,6 @@ export async function proxyRequest(req: NextRequest): Promise<Response> {
);
}

// Extract the path after '/api/oap_mcp/'
// Example: /api/oap_mcp/foo/bar -> /foo/bar
const url = new URL(req.url);
const path = url.pathname.replace(/^\/api\/oap_mcp/, "");

// Construct the target URL
const targetUrlObj = new URL(MCP_SERVER_URL);
targetUrlObj.pathname = `${targetUrlObj.pathname}${targetUrlObj.pathname.endsWith("/") ? "" : "/"}mcp${path}${url.search}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
ConfigField,
ConfigFieldAgents,
ConfigFieldRAG,
ConfigFieldTool,
} from "@/features/chat/components/configuration-sidebar/config-field";
import { useSearchTools } from "@/hooks/use-search-tools";
import { useMCPContext } from "@/providers/MCP";
Expand All @@ -22,6 +21,7 @@ import {
import _ from "lodash";
import { useFetchPreselectedTools } from "@/hooks/use-fetch-preselected-tools";
import { Controller, useFormContext } from "react-hook-form";
import { ToolSelectionByServer } from "./tool-selection-by-server";

export function AgentFieldsFormLoading() {
return (
Expand Down Expand Up @@ -60,7 +60,8 @@ export function AgentFieldsForm({
config: Record<string, any>;
}>();

const { tools, setTools, getTools, cursor, loading } = useMCPContext();
const { tools, toolsByServer, setTools, getTools, getToolsByServer, cursor, loading } =
useMCPContext();
const { toolSearchTerm, debouncedSetSearchTerm, displayTools } =
useSearchTools(tools, {
preSelectedTools: toolConfigurations[0]?.default?.tools,
Expand Down Expand Up @@ -152,28 +153,27 @@ export function AgentFieldsForm({
/>
<div className="relative w-full flex-1 basis-[500px] rounded-md border-[1px] border-slate-200 px-4">
<div className="absolute inset-0 overflow-y-auto px-4">
{toolConfigurations[0]?.label
? displayTools.map((c) => (
<Controller
key={`tool-${c.name}`}
control={form.control}
name={`config.${toolConfigurations[0].label}`}
render={({ field: { value, onChange } }) => (
<ConfigFieldTool
key={`tool-${c.name}`}
id={c.name}
label={c.name}
description={c.description}
agentId={agentId}
toolId={toolConfigurations[0].label}
className="border-b-[1px] py-4"
value={value}
setValue={onChange}
/>
)}
{toolConfigurations[0]?.label && (
<Controller
control={form.control}
name={`config.${toolConfigurations[0].label}`}
render={({ field: { value, onChange } }) => (
<ToolSelectionByServer
toolsByServer={toolsByServer}
selectedTools={value?.tools || []}
onToolToggle={(toolName) => {
const currentTools = value?.tools || [];
const newTools = currentTools.includes(toolName)
? currentTools.filter(
(t: string) => t !== toolName,
)
: [...currentTools, toolName];
onChange({ ...value, tools: newTools });
}}
/>
))
: null}
)}
/>
)}
{displayTools.length === 0 && toolSearchTerm && (
<p className="my-4 w-full text-center text-sm text-slate-500">
No tools found matching "{toolSearchTerm}".
Expand All @@ -192,7 +192,7 @@ export function AgentFieldsForm({
onClick={async () => {
try {
setLoadingMore(true);
const moreTool = await getTools(cursor);
const moreTool = await getToolsByServer(cursor, cursor);
setTools((prevTools) => [
...prevTools,
...moreTool,
Expand Down
Loading
Loading