Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions app/mcp/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@ const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");

const clientsMap = new Map<string, McpClientData>();

// These functions are exported from a "use server" module, which makes every one of
// them callable directly over HTTP as a Next.js Server Action, unauthenticated, by
// anyone who has the action id (visible in the client bundle). isMcpEnabled() existed
// but nothing called it, so every action ran regardless of the ENABLE_MCP setting -
// including addMcpServer, which spawns a child process using a caller-supplied command.
// Every action here now checks this first.
async function assertMcpEnabled() {
if (!(await isMcpEnabled())) {
throw new Error("MCP is not enabled");
}
}

// 获取客户端状态
export async function getClientsStatus(): Promise<
Record<string, ServerStatusResponse>
> {
await assertMcpEnabled();
const config = await getMcpConfigFromFile();
const result: Record<string, ServerStatusResponse> = {};

Expand Down Expand Up @@ -76,18 +89,21 @@ export async function getClientsStatus(): Promise<

// 获取客户端工具
export async function getClientTools(clientId: string) {
await assertMcpEnabled();
return clientsMap.get(clientId)?.tools ?? null;
}

// 获取可用客户端数量
export async function getAvailableClientsCount() {
await assertMcpEnabled();
let count = 0;
clientsMap.forEach((map) => !map.errorMsg && count++);
return count;
}

// 获取所有客户端工具
export async function getAllTools() {
await assertMcpEnabled();
const result = [];
for (const [clientId, status] of clientsMap.entries()) {
result.push({
Expand Down Expand Up @@ -140,6 +156,7 @@ async function initializeSingleClient(

// 初始化系统
export async function initializeMcpSystem() {
await assertMcpEnabled();
logger.info("MCP Actions starting...");
try {
// 检查是否已有活跃的客户端
Expand All @@ -162,6 +179,7 @@ export async function initializeMcpSystem() {

// 添加服务器
export async function addMcpServer(clientId: string, config: ServerConfig) {
await assertMcpEnabled();
try {
const currentConfig = await getMcpConfigFromFile();
const isNewServer = !(clientId in currentConfig.mcpServers);
Expand Down Expand Up @@ -194,6 +212,7 @@ export async function addMcpServer(clientId: string, config: ServerConfig) {

// 暂停服务器
export async function pauseMcpServer(clientId: string) {
await assertMcpEnabled();
try {
const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId];
Expand Down Expand Up @@ -230,6 +249,7 @@ export async function pauseMcpServer(clientId: string) {

// 恢复服务器
export async function resumeMcpServer(clientId: string): Promise<void> {
await assertMcpEnabled();
try {
const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId];
Expand Down Expand Up @@ -284,6 +304,7 @@ export async function resumeMcpServer(clientId: string): Promise<void> {

// 移除服务器
export async function removeMcpServer(clientId: string) {
await assertMcpEnabled();
try {
const currentConfig = await getMcpConfigFromFile();
const { [clientId]: _, ...rest } = currentConfig.mcpServers;
Expand All @@ -309,6 +330,7 @@ export async function removeMcpServer(clientId: string) {

// 重启所有客户端
export async function restartAllClients() {
await assertMcpEnabled();
logger.info("Restarting all clients...");
try {
// 关闭所有客户端
Expand Down Expand Up @@ -338,6 +360,7 @@ export async function executeMcpAction(
clientId: string,
request: McpRequestMessage,
) {
await assertMcpEnabled();
try {
const client = clientsMap.get(clientId);
if (!client?.client) {
Expand All @@ -353,6 +376,7 @@ export async function executeMcpAction(

// 获取 MCP 配置文件
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
await assertMcpEnabled();
try {
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
return JSON.parse(configStr);
Expand Down
77 changes: 77 additions & 0 deletions test/mcp-actions-require-enabled.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
isMcpEnabled,
getClientsStatus,
getClientTools,
getAvailableClientsCount,
getAllTools,
initializeMcpSystem,
addMcpServer,
pauseMcpServer,
resumeMcpServer,
removeMcpServer,
restartAllClients,
executeMcpAction,
getMcpConfigFromFile,
} from "../app/mcp/actions";

// Regression coverage for the unauthenticated MCP server actions: every one of
// these functions is a Next.js Server Action, callable directly over HTTP with
// no session or auth check. addMcpServer in particular spawns a child process
// using a caller-supplied command, so leaving these ungated is remote code
// execution regardless of whether MCP was ever turned on (ENABLE_MCP unset,
// which is the default, is exactly the case exercised below).
describe("MCP server actions require MCP to be enabled", () => {
const ORIGINAL_ENABLE_MCP = process.env.ENABLE_MCP;

afterEach(() => {
if (ORIGINAL_ENABLE_MCP === undefined) {
delete process.env.ENABLE_MCP;
} else {
process.env.ENABLE_MCP = ORIGINAL_ENABLE_MCP;
}
});

describe("when ENABLE_MCP is unset (the default)", () => {
beforeEach(() => {
delete process.env.ENABLE_MCP;
});

test.each<[string, () => Promise<unknown>]>([
["getClientsStatus", () => getClientsStatus()],
["getClientTools", () => getClientTools("x")],
["getAvailableClientsCount", () => getAvailableClientsCount()],
["getAllTools", () => getAllTools()],
["initializeMcpSystem", () => initializeMcpSystem()],
[
"addMcpServer",
() =>
addMcpServer("attack", {
command: "touch",
args: ["/tmp/pwned"],
} as any),
],
["pauseMcpServer", () => pauseMcpServer("x")],
["resumeMcpServer", () => resumeMcpServer("x")],
["removeMcpServer", () => removeMcpServer("x")],
["restartAllClients", () => restartAllClients()],
["executeMcpAction", () => executeMcpAction("x", {} as any)],
["getMcpConfigFromFile", () => getMcpConfigFromFile()],
])("%s rejects instead of running", async (_name, callAction) => {
await expect(callAction()).rejects.toThrow("MCP is not enabled");
});
});

describe("when ENABLE_MCP=true", () => {
beforeEach(() => {
process.env.ENABLE_MCP = "true";
});

test("isMcpEnabled reports enabled", async () => {
expect(await isMcpEnabled()).toBe(true);
});

test("getMcpConfigFromFile proceeds instead of rejecting", async () => {
await expect(getMcpConfigFromFile()).resolves.toBeDefined();
});
});
});