diff --git a/docs/configs/mcp.md b/docs/configs/mcp.md index 20182efbb..6a86469dc 100644 --- a/docs/configs/mcp.md +++ b/docs/configs/mcp.md @@ -26,9 +26,9 @@ http://your-homepage-instance/api/mcp ## Authentication -If Homepage auth is enabled with `HOMEPAGE_AUTH_ENABLED`, requests from an authenticated Homepage session are allowed. +The MCP endpoint requires authentication. Requests from an authenticated Homepage session are allowed when Homepage auth is enabled with `HOMEPAGE_AUTH_ENABLED`. -For MCP clients that cannot use the browser session, set `HOMEPAGE_MCP_TOKEN`. Requests can then include either of the following headers: +For MCP clients that cannot use the browser session, set `HOMEPAGE_MCP_TOKEN`. An MCP token is required when Homepage auth is not enabled. Requests can include either of the following headers: ```txt Authorization: Bearer your-token diff --git a/src/pages/api/mcp/index.js b/src/pages/api/mcp/index.js index b7971a432..b76a6b7a3 100644 --- a/src/pages/api/mcp/index.js +++ b/src/pages/api/mcp/index.js @@ -1,7 +1,7 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "pages/api/auth/[...nextauth]"; -import { handleMcpRequest, mcpAuthorized, mcpEnabled } from "utils/mcp/homepage-mcp"; +import { handleMcpRequest, mcpEnabled, mcpTokenAuthorized } from "utils/mcp/homepage-mcp"; async function hasHomepageSession(req, res) { if (!process.env.HOMEPAGE_AUTH_ENABLED) return false; @@ -13,7 +13,7 @@ export default async function handler(req, res) { return res.status(404).end("Not Found"); } - if (!mcpAuthorized(req) && !(await hasHomepageSession(req, res))) { + if (!mcpTokenAuthorized(req) && !(await hasHomepageSession(req, res))) { return res.status(401).json({ error: "Unauthorized" }); } diff --git a/src/pages/api/mcp/index.test.js b/src/pages/api/mcp/index.test.js index f41757903..266ea7fe3 100644 --- a/src/pages/api/mcp/index.test.js +++ b/src/pages/api/mcp/index.test.js @@ -69,6 +69,19 @@ describe("pages/api/mcp", () => { expect(res.status).toHaveBeenCalledWith(401); }); + it("rejects requests when neither Homepage auth nor an MCP token is configured", async () => { + process.env.HOMEPAGE_MCP_ENABLED = "true"; + delete process.env.HOMEPAGE_AUTH_ENABLED; + delete process.env.HOMEPAGE_MCP_TOKEN; + const handler = await loadHandler(); + const res = mockResponse(); + + await handler({ method: "POST", headers: {}, body: { jsonrpc: "2.0", id: 1, method: "tools/list" } }, res); + + expect(getServerSession).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); + }); + it("handles JSON-RPC requests when enabled and authorized", async () => { process.env.HOMEPAGE_MCP_ENABLED = "true"; process.env.HOMEPAGE_MCP_TOKEN = "secret"; @@ -143,10 +156,18 @@ describe("pages/api/mcp", () => { it("returns 202 for JSON-RPC notifications", async () => { process.env.HOMEPAGE_MCP_ENABLED = "true"; + process.env.HOMEPAGE_MCP_TOKEN = "secret"; const handler = await loadHandler(); const res = mockResponse(); - await handler({ method: "POST", headers: {}, body: { jsonrpc: "2.0", method: "notifications/initialized" } }, res); + await handler( + { + method: "POST", + headers: { authorization: "Bearer secret" }, + body: { jsonrpc: "2.0", method: "notifications/initialized" }, + }, + res, + ); expect(res.status).toHaveBeenCalledWith(202); expect(res.end).toHaveBeenCalledWith(); @@ -154,10 +175,11 @@ describe("pages/api/mcp", () => { it("rejects non-POST requests", async () => { process.env.HOMEPAGE_MCP_ENABLED = "true"; + process.env.HOMEPAGE_MCP_TOKEN = "secret"; const handler = await loadHandler(); const res = mockResponse(); - await handler({ method: "GET", headers: {}, body: {} }, res); + await handler({ method: "GET", headers: { authorization: "Bearer secret" }, body: {} }, res); expect(res.status).toHaveBeenCalledWith(405); expect(res.setHeader).toHaveBeenCalledWith("Allow", "POST"); diff --git a/src/utils/mcp/homepage-mcp.js b/src/utils/mcp/homepage-mcp.js index aa8eba466..0eea0a57b 100644 --- a/src/utils/mcp/homepage-mcp.js +++ b/src/utils/mcp/homepage-mcp.js @@ -61,10 +61,6 @@ function requiredToken() { return process.env.HOMEPAGE_MCP_TOKEN; } -function authEnabled() { - return Boolean(process.env.HOMEPAGE_AUTH_ENABLED); -} - function jsonRpcResult(id, result) { return { jsonrpc: "2.0", id, result }; } @@ -447,10 +443,6 @@ export function mcpTokenAuthorized(req) { return authHeader === `Bearer ${token}` || req.headers["x-homepage-mcp-token"] === token; } -export function mcpAuthorized(req) { - return mcpTokenAuthorized(req) || (!requiredToken() && !authEnabled()); -} - export function handleMcpRequest(message) { if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") { return jsonRpcError(message?.id, -32600, "Invalid JSON-RPC request"); diff --git a/src/utils/mcp/homepage-mcp.test.js b/src/utils/mcp/homepage-mcp.test.js index b0f87a610..7eb5b111c 100644 --- a/src/utils/mcp/homepage-mcp.test.js +++ b/src/utils/mcp/homepage-mcp.test.js @@ -617,19 +617,14 @@ describe("utils/mcp/homepage-mcp", () => { }); }); - it("checks MCP token and auth mode authorization", async () => { + it("requires a matching MCP token for token authorization", async () => { const mod = await loadMcpWithConfigDir(mkdtempSync(path.join(tmpdir(), "homepage-mcp-test-"))); expect(mod.mcpTokenAuthorized({ headers: {} })).toBe(false); - expect(mod.mcpAuthorized({ headers: {} })).toBe(true); - - process.env.HOMEPAGE_AUTH_ENABLED = "true"; - expect(mod.mcpAuthorized({ headers: {} })).toBe(false); process.env.HOMEPAGE_MCP_TOKEN = "secret"; expect(mod.mcpTokenAuthorized({ headers: { authorization: "Bearer secret" } })).toBe(true); expect(mod.mcpTokenAuthorized({ headers: { "x-homepage-mcp-token": "secret" } })).toBe(true); expect(mod.mcpTokenAuthorized({ headers: { authorization: "Bearer wrong" } })).toBe(false); - expect(mod.mcpAuthorized({ headers: { authorization: "Bearer secret" } })).toBe(true); }); });