-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/mcp #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/mcp #2
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request introduces a Model Context Protocol (MCP) server to enable external clients to interact with home automation features through a standardized protocol. The implementation includes authentication, tool definitions for controlling home devices, and comprehensive test coverage for the new functionality.
Key changes:
- Added MCP server with authentication-protected endpoints for home automation control
- Implemented six MCP tools for toggling lights/doors/heating, setting temperature, retrieving state, and fetching history
- Added client authentication utility with database-backed credential verification
- Included comprehensive test suites for authentication and MCP tools
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
src/utils/auth.ts |
New authentication utility for verifying client credentials against database |
src/mcp/tools.ts |
MCP tool handlers for home automation operations with API call integration |
src/mcp/server.ts |
MCP server implementation with authentication, CORS, and endpoint routing |
src/mcp/index.ts |
Export module for MCP tools and authorization setter |
tests/utils/auth.test.ts |
Comprehensive test suite for authentication utility |
tests/mcp/tools.test.ts |
Test suite covering all MCP tool handlers and error scenarios |
index.ts |
Main application updated to start MCP server and display endpoint in banner |
package.json |
Added MCP SDK and zod dependencies, plus MCP server run script |
Dockerfile |
Exposed port 3001 for MCP server |
compose.yaml |
Mapped MCP server port 3001 |
prisma/db.ts |
Removed explanatory comments |
tests/rules/engine.test.ts |
Removed French comment |
tests/routes/ws.test.ts |
Removed French comments |
tests/routes/features.test.ts |
Removed French comments |
tests/mocks/prisma.ts |
Removed explanatory comments |
tests/integration/e2e.test.ts |
Removed comment |
bun.lock |
Updated lock file with new dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| const authResult = await verifyClientAuth(authorization); | ||
| if (!authResult.valid) { | ||
| return new Response(JSON.stringify({ error: authResult.error }), { |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error response from the MCP server exposes the specific error message from the authentication failure. This could leak information about the authentication system to potential attackers. Consider returning a generic error message like "Authentication failed" without exposing the specific reason (e.g., "Authorization header missing" vs "Invalid credentials").
| return new Response(JSON.stringify({ error: authResult.error }), { | |
| console.warn( | |
| `MCP authentication failed for request from ${req.headers.get("x-forwarded-for") || "unknown IP"}: ${ | |
| authResult.error || "Unknown authentication error" | |
| }`, | |
| ); | |
| return new Response(JSON.stringify({ error: "Authentication failed" }), { |
|
|
||
| if (import.meta.main) { | ||
| initRuleEngine(); | ||
| startMcpServer(); |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MCP server is started without error handling. If startMcpServer() throws an error or fails to start, it could crash the entire application or leave it in an inconsistent state. Consider wrapping the call in a try-catch block and handling potential startup failures gracefully, possibly with fallback behavior or proper error logging.
| startMcpServer(); | |
| try { | |
| startMcpServer(); | |
| } catch (error) { | |
| console.error("Failed to start MCP server", error); | |
| } |
| } | ||
| console.log(`MCP request from authenticated client: ${authResult.clientId}`); | ||
|
|
||
| setAuthorization(authorization || ""); |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The authorization header is stored in a module-level variable via setAuthorization before being used in tool handlers. This creates a race condition where concurrent requests could overwrite each other's authorization values. Since the MCP server can handle multiple requests concurrently, one client's credentials could be used for another client's request. The authorization should be passed through the request context or as a parameter to the tool handlers.
| headers: { ...corsHeaders, "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
| console.log(`MCP request from authenticated client: ${authResult.clientId}`); |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging the authenticated client ID on every MCP request could create excessive log entries and make it difficult to identify actual security issues. Consider using a different log level (e.g., debug) for routine successful authentications, or only log authentication events on first connection or failures.
| console.log(`MCP request from authenticated client: ${authResult.clientId}`); | |
| console.debug(`MCP request from authenticated client: ${authResult.clientId}`); |
| export async function startMcpServer() { | ||
| const MCP_PORT = Number(process.env.MCP_PORT) || 3001; | ||
|
|
||
| const server = new McpServer({ | ||
| name: "myhouse-os", | ||
| version: "1.0.0", | ||
| }); | ||
|
|
||
| for (const [name, tool] of Object.entries(tools)) { | ||
| server.tool(name, tool.description, tool.inputSchema.shape, async (args) => { | ||
| try { | ||
| const result = await tool.handler(args as Parameters<typeof tool.handler>[0]); | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: JSON.stringify(result, null, 2), | ||
| }, | ||
| ], | ||
| }; | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : "Unknown error"; | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: JSON.stringify({ error: message }, null, 2), | ||
| }, | ||
| ], | ||
| isError: true, | ||
| }; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const transport = new WebStandardStreamableHTTPServerTransport(); | ||
|
|
||
| await server.connect(transport); | ||
|
|
||
| Bun.serve({ | ||
| port: MCP_PORT, | ||
| async fetch(req) { | ||
| const url = new URL(req.url); | ||
|
|
||
| if (req.method === "OPTIONS") { | ||
| return new Response(null, { status: 204, headers: corsHeaders }); | ||
| } | ||
|
|
||
| if (url.pathname === "/health") { | ||
| return new Response(JSON.stringify({ status: "ok", server: "myhouse-os-mcp" }), { | ||
| headers: { ...corsHeaders, "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
|
|
||
| if (url.pathname === "/mcp") { | ||
| const authorization = req.headers.get("authorization"); | ||
|
|
||
| const authResult = await verifyClientAuth(authorization); | ||
| if (!authResult.valid) { | ||
| return new Response(JSON.stringify({ error: authResult.error }), { | ||
| status: 401, | ||
| headers: { ...corsHeaders, "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
| console.log(`MCP request from authenticated client: ${authResult.clientId}`); | ||
|
|
||
| setAuthorization(authorization || ""); | ||
|
|
||
| const response = await transport.handleRequest(req); | ||
|
|
||
| const newHeaders = new Headers(response.headers); | ||
| for (const [key, value] of Object.entries(corsHeaders)) { | ||
| newHeaders.set(key, value); | ||
| } | ||
|
|
||
| return new Response(response.body, { | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| headers: newHeaders, | ||
| }); | ||
| } | ||
|
|
||
| return new Response("Not Found", { status: 404, headers: corsHeaders }); | ||
| }, | ||
| }); | ||
|
|
||
| console.log(`MCP server running on http://localhost:${MCP_PORT}`); | ||
| console.log(`MCP endpoint: http://localhost:${MCP_PORT}/mcp`); | ||
| console.log(`Health check: http://localhost:${MCP_PORT}/health`); | ||
| } |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no tests for the MCP server implementation in src/mcp/server.ts. This file contains critical authentication logic, request handling, and server initialization code that should have test coverage. Consider adding integration tests that verify the authentication flow, CORS headers, endpoint routing, and error handling.
|
|
||
| const PORT_BUN_SERVER = process.env.PORT_BUN_SERVER || 3000; | ||
| const PORT_WEB_SERVER = process.env.PORT_WEB_SERVER || 8080; | ||
| const PORT_MCP_SERVER = process.env.MCP_PORT || 3001; |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The environment variable name is inconsistent. The code reads process.env.MCP_PORT but the banner displays PORT_MCP_SERVER which suggests there might be confusion about the correct variable name. Either both should use MCP_PORT or both should use PORT_MCP_SERVER to avoid confusion when configuring the application.
| const PORT_MCP_SERVER = process.env.MCP_PORT || 3001; | |
| const PORT_MCP_SERVER = process.env.PORT_MCP_SERVER || 3001; |
| console.log(`Unauthorized access attempt by unknown client: ${clientId}`); | ||
| return { valid: false, error: "Invalid credentials" }; | ||
| } | ||
|
|
||
| try { | ||
| const decryptedToken = decrypt(client.ClientToken); | ||
| if (decryptedToken !== clientToken) { | ||
| console.log(`Invalid token provided for client: ${clientId}`); | ||
| return { valid: false, error: "Invalid credentials" }; | ||
| } | ||
| } catch (error) { | ||
| console.error(`Token verification failed for ${clientId}:`, error); |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging sensitive information (client ID) on failed authentication attempts could facilitate enumeration attacks. An attacker could use this to determine which client IDs exist in the system. Consider using a generic log message that doesn't reveal whether the client ID exists or the token is invalid.
| console.log(`Unauthorized access attempt by unknown client: ${clientId}`); | |
| return { valid: false, error: "Invalid credentials" }; | |
| } | |
| try { | |
| const decryptedToken = decrypt(client.ClientToken); | |
| if (decryptedToken !== clientToken) { | |
| console.log(`Invalid token provided for client: ${clientId}`); | |
| return { valid: false, error: "Invalid credentials" }; | |
| } | |
| } catch (error) { | |
| console.error(`Token verification failed for ${clientId}:`, error); | |
| console.log("Unauthorized access attempt with unknown client credentials"); | |
| return { valid: false, error: "Invalid credentials" }; | |
| } | |
| try { | |
| const decryptedToken = decrypt(client.ClientToken); | |
| if (decryptedToken !== clientToken) { | |
| console.log("Invalid token provided for client authentication"); | |
| return { valid: false, error: "Invalid credentials" }; | |
| } | |
| } catch (error) { | |
| console.error("Token verification failed during client authentication:", error); |
| console.log(`Unauthorized access attempt by unknown client: ${clientId}`); | ||
| return { valid: false, error: "Invalid credentials" }; | ||
| } | ||
|
|
||
| try { | ||
| const decryptedToken = decrypt(client.ClientToken); | ||
| if (decryptedToken !== clientToken) { | ||
| console.log(`Invalid token provided for client: ${clientId}`); | ||
| return { valid: false, error: "Invalid credentials" }; | ||
| } | ||
| } catch (error) { | ||
| console.error(`Token verification failed for ${clientId}:`, error); |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging sensitive information (client ID) on failed token verification could facilitate enumeration attacks. Similar to the previous issue, this allows attackers to determine which client IDs are valid. Consider using a generic error log message instead.
| console.log(`Unauthorized access attempt by unknown client: ${clientId}`); | |
| return { valid: false, error: "Invalid credentials" }; | |
| } | |
| try { | |
| const decryptedToken = decrypt(client.ClientToken); | |
| if (decryptedToken !== clientToken) { | |
| console.log(`Invalid token provided for client: ${clientId}`); | |
| return { valid: false, error: "Invalid credentials" }; | |
| } | |
| } catch (error) { | |
| console.error(`Token verification failed for ${clientId}:`, error); | |
| console.log("Unauthorized access attempt by unknown client"); | |
| return { valid: false, error: "Invalid credentials" }; | |
| } | |
| try { | |
| const decryptedToken = decrypt(client.ClientToken); | |
| if (decryptedToken !== clientToken) { | |
| console.log("Invalid token provided during client authentication"); | |
| return { valid: false, error: "Invalid credentials" }; | |
| } | |
| } catch (error) { | |
| console.error("Token verification failed during client authentication:", error); |
| apiCall("/toggle/light", "GET"), | ||
| apiCall("/toggle/door", "GET"), | ||
| apiCall("/toggle/heat", "GET"), |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GET requests to toggle endpoints (/toggle/light, /toggle/door, /toggle/heat) in the get_home_state handler is semantically incorrect. Based on typical REST conventions, GET requests should not modify state. If these endpoints return the current state without changing it, the naming is misleading (they're called "toggle" but used to get state). If they do change state, using GET violates REST principles and could lead to unintended state changes.
| apiCall("/toggle/light", "GET"), | |
| apiCall("/toggle/door", "GET"), | |
| apiCall("/toggle/heat", "GET"), | |
| apiCall("/toggle/light", "POST"), | |
| apiCall("/toggle/door", "POST"), | |
| apiCall("/toggle/heat", "POST"), |
| import { setAuthorization, tools } from "./tools"; | ||
|
|
||
| const corsHeaders = { | ||
| "Access-Control-Allow-Origin": "*", |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CORS configuration allows all origins with "*", which is overly permissive for an MCP server that handles authenticated requests. This could expose the MCP endpoints to unauthorized cross-origin requests. Consider restricting the allowed origins to specific trusted domains or making this configurable via environment variables.
…formatting test: implement tests for wrapToolHandler and enhance verifyClientAuth error handling refactor: update mockPrisma methods to use unknown type for better type safety
This pull request introduces a new Model Context Protocol (MCP) server to the project, enabling external clients to interact with home automation features (like toggling lights, doors, and heating, as well as retrieving state and history) through a standardized protocol. It includes the MCP server implementation, tool definitions, authentication mechanisms, new dependencies, and comprehensive tests for the MCP tools. Additionally, the Docker and Compose configurations are updated to expose the new MCP server port.
The most important changes are:
MCP Server Integration:
src/mcp/server.ts, which exposes endpoints for MCP clients, registers home automation tools, handles CORS, and enforces authentication using a custom header. The server listens on port 3001 and provides/mcpand/healthendpoints.index.ts) to start the MCP server alongside the main API server, and display its endpoint in the startup banner. [1] [2] [3]Tooling and Protocol Implementation:
src/mcp/tools.ts, providing handlers for toggling light/door/heat, setting temperature, retrieving home state, and fetching event history. Each tool makes authenticated API calls to the main server.src/mcp/index.tsfor use by the server.Authentication:
src/utils/auth.tsto verify client credentials for MCP requests by checking a client’s ID and token against the database.Testing:
tests/mcp/tools.test.tsto verify all MCP tool handlers, including their success and error cases, as well as correct header usage.Configuration and Dependencies:
@modelcontextprotocol/sdk) and input validation (zod) inpackage.json, and provided a script for running the MCP server. [1] [2]These changes collectively enable secure, protocol-driven automation control and monitoring, and provide a solid foundation for future MCP-based integrations.