import {
applyParsedAppConfigRequestSchema,
CatalogMCPServerItem,
CreateServerFromCatalogRequest,
createTargetServerRequestSchema,
createServerFromCatalogRequestSchema,
DynamicCapabilitiesStatusResponse,
initiateServerAuthRequestSchema,
ListSavedSetupsResponse,
MessageResponse,
SaveSetupResponse,
saveSetupRequestSchema,
TargetServerRequest,
} from "@mcpx/shared-model";
import { makeError } from "@mcpx/toolkit-core/data";
import { loggableError } from "@mcpx/toolkit-core/logging";
import express, { Router } from "express";
import { Logger } from "winston";
import z, { ZodError } from "zod/v4";
import { env } from "../env.js";
import {
AlreadyExistsError,
FailedToConnectToTargetServer,
InvalidConfigError,
NotFoundError,
} from "../errors.js";
import { targetServerSchema } from "../model/target-servers.js";
import { Services } from "../services/services.js";
import { redactEnv } from "../services/redact.js";
import {
resolveEnvToRuntime,
MissingRequiredEnvError,
} from "../services/env-resolver.js";
function buildTargetServerRequest(
server: CatalogMCPServerItem,
request: CreateServerFromCatalogRequest,
): TargetServerRequest {
const { config } = server;
if (config.type === "stdio") {
return {
type: "stdio",
name: server.name,
command: config.command,
args: config.args ?? [],
env: resolveEnvToRuntime(config.env, request.envValues ?? {}),
};
}
return {
type: config.type,
name: server.name,
url: config.url,
headers: config.headers,
};
}
export function buildControlPlaneRouter(
authGuard: express.RequestHandler,
services: Services,
logger: Logger,
): Router {
const router = Router();
if (!env.ENABLE_CONTROL_PLANE_REST) {
logger.debug(
"Control Plane REST API is disabled. Skipping control plane routes.",
);
return router;
}
router.get("/system-state", authGuard, async (_req, res) => {
const response = services.controlPlane.getSystemState();
res.status(200).json(response);
});
router.get("/app-config", authGuard, async (_req, res) => {
const payload = services.controlPlane.getAppConfig();
res.status(200).json(payload);
});
router.patch("/app-config", authGuard, async (req, res) => {
const parsed = applyParsedAppConfigRequestSchema.safeParse(req.body);
if (!parsed.success) {
handleInvalidRequestSchema(req.url, res, parsed.error, req.body, logger);
return;
}
const payload = parsed.data;
try {
const response = await services.controlPlane.patchAppConfig(payload);
res.status(200).json(response);
return;
} catch (e) {
if (e instanceof ZodError) {
handleInvalidRequestSchema(req.url, res, e, req.body, logger);
return;
}
if (e instanceof InvalidConfigError) {
logger.error("Invalid config in PATCH /app-config request", {
payload,
error: loggableError(e),
});
res.status(400).json({
message: "Invalid config",
error: loggableError(e).errorMessage,
});
return;
}
const error = loggableError(e);
logger.error("Error in PATCH /app-config request", { payload, error });
res
.status(500)
.json({ message: "Internal server error", error: error.errorMessage });
return;
}
});
router.post("/target-server", authGuard, async (req, res) => {
const parsed = createTargetServerRequestSchema.safeParse(req.body);
if (!parsed.success) {
handleInvalidRequestSchema(req.url, res, parsed.error, req.body, logger);
return;
}
const payload = parsed.data;
try {
const result = await services.controlPlane.addTargetServer(payload);
res.status(201).json(result);
} catch (e: unknown) {
const error = loggableError(e);
if (e instanceof FailedToConnectToTargetServer) {
res.status(400).json({
message: e.message,
error,
});
return;
}
if (e instanceof AlreadyExistsError) {
res
.status(409)
.json({ message: "Target server already exists", error });
return;
}
logger.error("Error creating target server", {
error,
payload: redactEnv(payload),
});
res.status(500).json({
message: "Internal server error",
error: error.errorMessage,
});
}
});
router.post(
"/catalog-item/:id/target-server",
authGuard,
async (req, res) => {
const catalogItemId = req.params["id"];
if (!catalogItemId) {
res.status(400).json({ message: "Catalog item ID is required" });
return;
}
const parsed = createServerFromCatalogRequestSchema.safeParse(req.body);
if (!parsed.success) {
handleInvalidRequestSchema(
req.url,
res,
parsed.error,
req.body,
logger,
);
return;
}
const catalogItem = services.catalogManager.getById(catalogItemId);
if (!catalogItem) {
res.status(404).json({ message: "Catalog item not found" });
return;
}
try {
const targetServerRequest = buildTargetServerRequest(
catalogItem.server,
parsed.data,
);
const result =
await services.controlPlane.addTargetServer(targetServerRequest);
res.status(201).json(result);
} catch (e: unknown) {
if (e instanceof MissingRequiredEnvError) {
res.status(400).json({
message: `Missing required environment variables [${e.missingKeys.join(", ")}]`,
missingKeys: e.missingKeys,
});
return;
}
const error = loggableError(e);
if (e instanceof FailedToConnectToTargetServer) {
res.status(400).json({ message: e.message, error });
return;
}
if (e instanceof AlreadyExistsError) {
res
.status(409)
.json({ message: "Target server already exists", error });
return;
}
logger.error("Error creating target server from catalog", {
error,
catalogItemId,
});
res.status(500).json({
message: "Internal server error",
error: error.errorMessage,
});
}
},
);
router.patch("/target-server/:name", authGuard, async (req, res) => {
const parsed = targetServerSchema.safeParse(req.body);
if (!parsed.success) {
handleInvalidRequestSchema(req.url, res, parsed.error, req.body, logger);
return;
}
const payload = parsed.data;
const name = req.params["name"];
if (!name) {
res.status(400).json({ message: "Target server name is required" });
return;
}
try {
const result = await services.controlPlane.updateTargetServer(
name,
payload,
);
res.status(200).json(result);
} catch (e) {
if (e instanceof NotFoundError) {
logger.error(`Target server ${name} not found`, {
payload: redactEnv(payload),
});
res.status(404).json({
message: `Target server ${name} not found`,
error: loggableError(e),
});
return;
}
const error = loggableError(e);
logger.error("Error updating target server", {
error,
payload: redactEnv(payload),
});
res.status(500).json({
message: "Internal server error",
error: error.errorMessage,
});
return;
}
});
router.delete("/target-server/:name", authGuard, async (req, res) => {
const name = req.params["name"];
if (!name) {
res.status(400).json({ message: "Target server name is required" });
return;
}
try {
await services.controlPlane.removeTargetServer(name);
res.status(200).json({ message: "Target server removed successfully" });
} catch (e) {
if (e instanceof NotFoundError) {
logger.error(`Target server ${name} not found for removal`, { name });
res.status(404).json({
message: `Target server ${name} not found`,
error: loggableError(e),
});
return;
}
const error = loggableError(e);
logger.error("Error removing target server", { error, name });
res.status(500).json({
message: "Internal server error",
error: error.errorMessage,
});
}
});
// In deprecation, use PUT config/target-server/:name/activate
router.put("/target-server/:name/activate", authGuard, async (req, res) => {
const name = req.params["name"];
if (!name) {
res.status(400).json({ message: "Target server name is required" });
return;
}
try {
await services.controlPlane.config.activateTargetServer(name);
logger.info("Activated target server", { name });
res.status(200).json({ message: "Target server activated successfully" });
} catch (e) {
const error = loggableError(e);
logger.error("Error activating target server", { error, name });
res.status(500).json(error);
}
});
// In deprecation, use PUT config/target-server/:name/deactivate
router.put("/target-server/:name/deactivate", authGuard, async (req, res) => {
const name = req.params["name"];
if (!name) {
res.status(400).json({ message: "Target server name is required" });
return;
}
try {
await services.controlPlane.config.deactivateTargetServer(name);
logger.info("Deactivated target server", { name });
res
.status(200)
.json({ message: "Target server deactivated successfully" });
} catch (e) {
const error = loggableError(e);
logger.error("Error deactivating target server", { error, name });
res.status(500).json(error);
}
});
// In deprecation, use GET config/target-servers/attributes
router.get("/target-servers/attributes", authGuard, async (_req, res) => {
try {
const targetServerAttributes =
services.controlPlane.config.getTargetServerAttributes();
res.status(200).json({ targetServerAttributes });
} catch (e) {
const error = loggableError(e);
logger.error("Error fetching target server attributes", { error });
res.status(500).json(error);
}
});
// Saved setups endpoints
router.post("/saved-setups", authGuard, async (req, res) => {
const parsed = saveSetupRequestSchema.safeParse(req.body);
if (!parsed.success) {
handleInvalidRequestSchema(req.url, res, parsed.error, req.body, logger);
return;
}
const { description } = parsed.data;
const currentSetup = services.setupManager.getCurrentSetup();
const result = await services.hubService.savedSetups.saveSetup({
...currentSetup,
description,
});
if (!result.success) {
logger.error("Failed to save setup", { error: result.error });
res.status(500).json({ message: result.error });
return;
}
res.status(201).json(result satisfies SaveSetupResponse);
});
router.get("/saved-setups", authGuard, async (_req, res) => {
const result = await services.hubService.savedSetups.listSavedSetups();
res.status(200).json(result satisfies ListSavedSetupsResponse);
});
router.delete("/saved-setups/:id", authGuard, async (req, res) => {
const savedSetupId = req.params["id"];
if (!savedSetupId) {
res.status(400).json({ message: "Saved setup ID is required" });
return;
}
const result =
await services.hubService.savedSetups.deleteSavedSetup(savedSetupId);
if (!result.success) {
const status = result.errorCode === "not_found" ? 404 : 500;
logger.error("Failed to delete saved setup", {
savedSetupId,
error: result.error,
});
res.status(status).json({ message: result.error });
return;
}
res.status(200).json({
message: "Saved setup deleted successfully",
} satisfies MessageResponse);
});
router.put("/saved-setups/:id", authGuard, async (req, res) => {
const savedSetupId = req.params["id"];
if (!savedSetupId) {
res.status(400).json({ message: "Saved setup ID is required" });
return;
}
const currentSetup = services.setupManager.getCurrentSetup();
const result = await services.hubService.savedSetups.updateSavedSetup({
savedSetupId,
...currentSetup,
});
if (!result.success) {
const status = result.errorCode === "not_found" ? 404 : 500;
logger.error("Failed to update saved setup", {
savedSetupId,
error: result.error,
});
res.status(status).json({ message: result.error });
return;
}
res.status(200).json({
message: "Saved setup updated successfully",
} satisfies MessageResponse);
});
router.post("/saved-setups/:id/restore", authGuard, async (req, res) => {
const savedSetupId = req.params["id"];
if (!savedSetupId) {
res.status(400).json({ message: "Saved setup ID is required" });
return;
}
// Get the saved setup from the list
const listResult = await services.hubService.savedSetups.listSavedSetups();
const savedSetup = listResult.setups.find((s) => s.id === savedSetupId);
if (!savedSetup) {
res.status(404).json({ message: "Saved setup not found" });
return;
}
// Apply the saved setup locally (this triggers setup-changed to Hub)
try {
await services.setupManager.applySetup({
source: "user",
setupId: savedSetupId,
targetServers: savedSetup.targetServers,
config: savedSetup.config,
});
res.status(200).json({
message: "Setup restored successfully",
} satisfies MessageResponse);
} catch (e) {
const error = loggableError(e);
logger.error("Failed to restore saved setup", { savedSetupId, error });
res.status(500).json({
message: "Failed to restore setup",
error: error.errorMessage,
});
}
});
router.post("/setup/reset", authGuard, async (_req, res) => {
try {
await services.setupManager.resetSetup();
// Should be already reflected by resetting SetupManager,
// but ensure system state is cleared too for edge cases of leftover state
services.systemStateTracker.resetTargetServers();
res.status(200).json({
message: "Setup reset successfully",
} satisfies MessageResponse);
} catch (e) {
const error = loggableError(e);
logger.error("Failed to reset setup", { error });
res.status(500).json({
message: "Failed to reset setup",
error: error.errorMessage,
});
}
});
router.post("/auth/initiate/:name", authGuard, async (req, res) => {
const name = req.params["name"];
if (!name) {
res.status(400).json({ message: "Target server name is required" });
return;
}
const parsedBody = initiateServerAuthRequestSchema.safeParse(req.body);
const callbackUrl = parsedBody.data?.callbackUrl
? parsedBody.data.callbackUrl
: undefined;
try {
// Try to reuse existing OAuth tokens first
try {
await services.upstreamHandler.reuseOAuthByName(name);
res.status(200).json({
msg: "Successfully reused OAuth tokens for target server",
targetServerName: name,
authorizationUrl: null,
userCode: null,
});
return;
} catch (_e) {
logger.info("Could not reuse OAuth tokens, will initiate new flow", {
targetServerName: name,
});
}
// Initiate new OAuth flow
const result = await services.upstreamHandler.initiateOAuthForServer(
name,
callbackUrl,
);
res.status(202).json({
msg: "Successfully initiated OAuth flow for target server",
targetServerName: name,
authorizationUrl: result.authorizationUrl,
userCode: result.userCode ?? null,
});
} catch (e) {
const error = makeError(e);
logger.error("Failed to initiate OAuth flow", {
targetServerName: name,
error: loggableError(error),
});
res
.status(500)
.json({ message: "Failed to initiate OAuth", error: loggableError(e) });
}
});
// Auth callback endpoint - redirects to OAuth callback
router.get(
"/auth/callback",
(req: express.Request, res: express.Response) => {
const { code, state, error } = req.query;
// Redirect to the OAuth callback endpoint with the same query parameters
const queryParams = new URLSearchParams();
if (code) queryParams.set("code", code as string);
if (state) queryParams.set("state", state as string);
if (error) queryParams.set("error", error as string);
const redirectUrl = `/oauth/callback${queryParams.toString() ? "?" + queryParams.toString() : ""}`;
res.redirect(redirectUrl);
},
);
// Dynamic capabilities endpoints
router.get(
"/dynamic-capabilities/:consumerTag",
authGuard,
async (req, res) => {
const consumerTag = validateParamExists(req, res, "consumerTag");
if (!consumerTag) return;
const enabled =
services.dynamicCapabilities.isDynamicCapabilitiesEnabled(consumerTag);
res.status(200).json({
consumerTag,
enabled,
} satisfies DynamicCapabilitiesStatusResponse);
},
);
router.put(
"/dynamic-capabilities/:consumerTag/enable",
authGuard,
async (req, res) => {
const consumerTag = validateParamExists(req, res, "consumerTag");
if (!consumerTag) return;
try {
await services.dynamicCapabilities.initializeDynamicCapabilities(
consumerTag,
);
res.status(200).json({
consumerTag,
enabled: true,
} satisfies DynamicCapabilitiesStatusResponse);
} catch (e) {
const error = loggableError(e);
logger.error("Failed to enable dynamic capabilities", {
consumerTag,
error,
});
res.status(500).json({
message: "Failed to enable dynamic capabilities",
error: error.errorMessage,
});
}
},
);
router.put(
"/dynamic-capabilities/:consumerTag/disable",
authGuard,
async (req, res) => {
const consumerTag = validateParamExists(req, res, "consumerTag");
if (!consumerTag) return;
try {
await services.dynamicCapabilities.cleanupDynamicCapabilities(
consumerTag,
);
res.status(200).json({
consumerTag,
enabled: false,
} satisfies DynamicCapabilitiesStatusResponse);
} catch (e) {
const error = loggableError(e);
logger.error("Failed to disable dynamic capabilities", {
consumerTag,
error,
});
res.status(500).json({
message: "Failed to disable dynamic capabilities",
error: error.errorMessage,
});
}
},
);
return router;
}
function handleInvalidRequestSchema(
name: string,
res: express.Response,
error: ZodError,
payload: unknown,
logger: Logger,
): void {
const treeifiedError = z.treeifyError(error);
logger.error(`Invalid schema in ${name} request`, {
payload,
error: treeifiedError,
});
res.status(400).json({
message: "Invalid request schema",
error: treeifiedError,
});
}
function validateParamExists(
req: express.Request,
res: express.Response,
paramName: string,
): string | null {
const paramValue = req.params[paramName];
if (!paramValue) {
res.status(400).json({ message: `param ${paramName} is required` });
return null;
}
return paramValue;
}