Skip to content

Feat/mcp for dashboard users#2

Merged
dadadadas111 merged 11 commits intomasterfrom
feat/test
Feb 23, 2026
Merged

Feat/mcp for dashboard users#2
dadadadas111 merged 11 commits intomasterfrom
feat/test

Conversation

@dadadadas111
Copy link
Owner

@dadadadas111 dadadadas111 commented Feb 23, 2026

Summary by CodeRabbit

  • Breaking Changes

    • Legacy authentication, admin configuration, and original MCP endpoints removed.
  • New Features

    • New MCP v2 endpoint with session-based API key handling.
    • New tools: init API key, find user ID, list devices, and simple device control.
  • Architecture Updates

    • Core MCP and API client functionality consolidated into a v2 module with a session store.

dadadadas111 and others added 8 commits February 13, 2026 11:51
Replace references to docker-compose.staging.yml with docker-compose.yml in .github/workflows/docker-build-staging.yml. This ensures the staging CI uses the generic compose file for both the docker-compose and docker compose command paths, simplifying configuration and avoiding filename mismatches.
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Warning

Rate limit exceeded

@dadadadas111 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 13 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 112f9b3 and 45879fc.

📒 Files selected for processing (3)
  • docs/api/TOOLS.md
  • src/mcp_v2/services/mcp-v2.service.ts
  • src/mcp_v2/tools/tools-list-v2.ts

Walkthrough

This PR removes the v1 Auth, Admin, API, and MCP modules and services, and introduces a new McpV2 module: controller, services (McpV2Service, ApiClientV2Service, SessionStoreService), tool definitions, and updates AppModule and startup logging accordingly.

Changes

Cohort / File(s) Summary
Removed Authentication
src/auth/auth.controller.ts, src/auth/auth.module.ts, src/auth/auth.service.ts
Deleted AuthController, AuthModule, and AuthService (login/refresh endpoints, DTOs, HTTP auth client, and related error handling).
Removed Admin
src/admin/admin.controller.ts, src/admin/admin.module.ts, src/admin/dto/update-config.dto.ts, src/admin/guards/admin-auth.guard.ts
Removed AdminController, AdminModule, DTOs, and AdminAuthGuard (admin endpoint, config DTOs, and header-based guard).
Removed API / MCP v1 & Infra
src/api/api.module.ts, src/api/controllers/mcp.controller.ts, src/mcp/mcp.module.ts, src/mcp/types/mcp.types.ts, src/mcp/services/api-client.service.ts, src/mcp/services/mcp.service.ts, src/mcp/services/redis.service.ts
Entire v1 MCP stack and supporting modules removed: MCP controller/service, API client, Redis session service, protocol types, and module wiring.
Added MCP v2 Module & Controller
src/mcp_v2/mcp-v2.module.ts, src/mcp_v2/controllers/mcp-v2.controller.ts
New McpV2Module and McpV2Controller added; controller manages per-session transports, delegates to McpV2Service, and integrates session lifecycle with SessionStore.
Added MCP v2 Services
src/mcp_v2/services/mcp-v2.service.ts, src/mcp_v2/services/api-client-v2.service.ts, src/mcp_v2/services/session-store.service.ts
New McpV2Service (creates MCP server, registers tools), ApiClientV2Service (typed HTTP client), and SessionStoreService (Redis-backed per-session API key storage with TTL).
Added Tool Definitions
src/mcp_v2/tools/tools-list-v2.ts
New Tool metadata and zod input schemas for init_api_key, find_user_id, list_devices, and control_device_simple.
App Bootstrap Changes
src/app.module.ts, src/main.ts
AppModule now imports McpV2Module instead of Auth/Api/Admin modules; main.ts logging switched from console to NestJS Logger.
Misc / Manifest
package.json
Manifest reference noted in diff analyzer (no code content changes detailed here).

Sequence Diagram

sequenceDiagram
    participant Client
    participant McpV2Controller
    participant SessionStoreService
    participant McpV2Service
    participant ApiClientV2Service
    participant Redis

    Client->>McpV2Controller: HTTP /mcp (mcp-session-id?, x-api-key?)
    McpV2Controller->>SessionStoreService: getApiKey(sessionId)
    SessionStoreService->>Redis: GET mcp_v2:session:{id}
    Redis-->>SessionStoreService: apiKey or null

    alt No stored apiKey
        McpV2Controller->>SessionStoreService: setApiKey(sessionId, x-api-key)
        SessionStoreService->>Redis: SETEX mcp_v2:session:{id} (TTL)
        Redis-->>SessionStoreService: OK
    end

    McpV2Controller->>McpV2Service: createServer() / route request to tool
    McpV2Service->>ApiClientV2Service: HTTP call (uses resolved apiKey)
    ApiClientV2Service-->>McpV2Service: API response
    McpV2Service-->>McpV2Controller: Tool result
    McpV2Controller->>Client: JSON-RPC response

    Note over McpV2Controller,SessionStoreService: On transport close -> SessionStoreService.delete(sessionId) -> Redis DEL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Delete test-mcp.js #1 — touches McpModule exports that were removed in this PR; likely related to MCP module refactor.

Poem

🐇 I hopped through old code, gave v1 a soft tap,
Seeds of v2 planted, sessions tucked in a nap,
Redis hums a lullaby, tools learn a new song,
I nibble a carrot and cheer: v2, hop along!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feat/mcp for dashboard users' is vague and does not clearly convey the specific changes made. While it mentions MCP, the changeset involves a massive architectural refactor including removal of auth/admin/api modules, replacement with McpV2Module, and complete service rewrites. Use a more descriptive title such as 'Refactor: Replace legacy MCP implementation with McpV2Module for dashboard users' to clearly communicate the major architectural changes in this changeset.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main.ts (1)

36-60: ⚠️ Potential issue | 🟡 Minor

Swagger auth definitions reference removed modules.

addBearerAuth (Firebase JWT) and addApiKey (x-admin-api-key) are still configured, but this PR removes the Auth and Admin modules. The new MCP v2 controller uses x-api-key instead. These stale Swagger security schemes will confuse API consumers.

Consider updating these to reflect the actual x-api-key header used by McpV2Controller, or removing them if Swagger is excluded for the MCP endpoint (which uses @ApiExcludeEndpoint()).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.ts` around lines 36 - 60, The Swagger security schemes are stale:
remove the call to DocumentBuilder.addBearerAuth(...) and the addApiKey(...)
that registers 'x-admin-api-key', and replace them with a single addApiKey(...)
entry for the actual header used by McpV2Controller (name: 'x-api-key', in:
'header', description: 'API key for MCP v2 access') or drop all auth definitions
entirely if the MCP endpoints are excluded from Swagger via
`@ApiExcludeEndpoint`() — update the DocumentBuilder usage accordingly to reflect
only the current x-api-key scheme.
🧹 Nitpick comments (6)
src/mcp_v2/controllers/mcp-v2.controller.ts (3)

44-44: Unnecessary non-null assertion on apiKey.

apiKey is already validated as truthy on Line 33 (the function returns early if falsy). The ! assertion on Line 44 is redundant.

Suggested fix
-            await this.sessionStore.setApiKey(newSessionId, apiKey!);
+            await this.sessionStore.setApiKey(newSessionId, apiKey);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/controllers/mcp-v2.controller.ts` at line 44, The non-null
assertion on apiKey is redundant; update the controller code that calls
this.sessionStore.setApiKey(newSessionId, apiKey!) to pass apiKey directly
(remove the trailing '!') since apiKey is already checked earlier, e.g., in the
method where newSessionId and apiKey are used; keep the call as
this.sessionStore.setApiKey(newSessionId, apiKey) so the types remain consistent
and no needless assertion is present.

17-20: @ApiExcludeEndpoint() overrides the other Swagger decorators.

@ApiExcludeEndpoint() removes this endpoint from the generated Swagger document, making @ApiOperation and @ApiResponse on Lines 18-19 dead code. Either remove the Swagger decorators or remove @ApiExcludeEndpoint() depending on the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/controllers/mcp-v2.controller.ts` around lines 17 - 20, The
decorators on the MCP v2 controller are contradictory: `@ApiExcludeEndpoint`()
hides the endpoint from Swagger while `@ApiOperation` and `@ApiResponse` on the same
handler are therefore ineffective; decide the intent and make them consistent—if
you want the endpoint documented, remove `@ApiExcludeEndpoint`() from the
controller (retain `@ApiOperation` and `@ApiResponse`), otherwise remove the
`@ApiOperation` and `@ApiResponse` decorators (keep `@ApiExcludeEndpoint`) so there
are no dead annotations; look for the decorators on the method where `@All`(),
`@ApiOperation`, `@ApiResponse`, and `@ApiExcludeEndpoint` are applied to update
accordingly.

29-63: No horizontal scaling: session state is process-local.

The transports Map is local to this controller instance. In a multi-instance deployment, a returning client with a valid mcp-session-id could hit a different instance that doesn't have the transport, causing it to fall into the "new session" path and fail with Missing x-api-key.

If multi-instance deployment is planned, consider sticky sessions (e.g., via load balancer affinity on mcp-session-id) or document this as a single-instance constraint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/controllers/mcp-v2.controller.ts` around lines 29 - 63, The
transports Map (this.transports) is process-local so a returning sessionId may
not be found on other instances; update the handler to avoid silently treating a
missing transport as a "new session" and either (A) require/declare sticky
sessions: when sessionId is present but this.transports.has(sessionId) is false,
respond with a clear 410/400 JSON-RPC error referencing the missing session and
instruct the client/operator to use load-balancer affinity on mcp-session-id, or
(B) implement a distributed session-to-instance registry (reuse sessionStore to
persist the owning instance id when onsessioninitialized in
StreamableHTTPServerTransport and, on a missing transport in transport lookup,
proxy/forward the request to the recorded instance or return a redirect/error).
Modify the code paths around sessionId lookup, the transport creation block, and
onsessioninitialized/transport.onclose to set and remove the instance id in the
shared sessionStore so other instances can detect and handle missing transports.
src/main.ts (1)

12-21: enableCors is computed but never used — CORS is always enabled.

Line 13 computes enableCors from the config, but it's never checked before app.enableCors(...) on Line 16. CORS is unconditionally applied regardless of the ENABLE_CORS setting.

Suggested fix

Either remove the dead variable (as the comment says "always enable for MCP compatibility") or actually gate the call:

-  const enableCors = configService.get<string>('ENABLE_CORS') !== 'false';
-  const origins = configService.get<string>('CORS_ORIGINS')?.split(',') || ['*'];
-
-  app.enableCors({
+  const origins = configService.get<string>('CORS_ORIGINS')?.split(',') || ['*'];
+  app.enableCors({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.ts` around lines 12 - 21, The variable enableCors is computed but
never used, so update the logic around app.enableCors: either remove the unused
enableCors variable and its comment if CORS should always be enabled, or use
enableCors to gate the call to app.enableCors (e.g. if (!enableCors) skip
calling app.enableCors). Locate the computation of enableCors and the
app.enableCors(...) invocation in main.ts and implement the chosen option so the
ENABLE_CORS config actually controls whether app.enableCors is invoked.
src/mcp_v2/services/mcp-v2.service.ts (1)

89-115: Magic command numbers reduce readability.

The numeric command codes (1, 28, 29, 20, 17) are hardcoded without explanation. Consider extracting them as named constants for maintainability:

Example
const COMMAND_CODES = {
  SWITCH: 1,
  BRIGHTNESS: 28,
  KELVIN: 29,
  TEMPERATURE: 20,
  MODE: 17,
} as const;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 89 - 115, Replace the
magic numeric command codes used when building the command array for args.action
by extracting them into clearly named constants (e.g., SWITCH, BRIGHTNESS,
KELVIN, TEMPERATURE, MODE) and then use those constants when constructing the
command arrays (replace occurrences of 1, 28, 29, 20, 17 with the corresponding
named constants); update the switch handling of args.action and the command
variable to reference these constants so the intent is clear and maintainability
improved.
src/mcp_v2/mcp-v2.module.ts (1)

9-14: Module wiring looks clean overall.

Two minor observations:

  1. ConfigModule is already isGlobal: true in AppModule (see src/app.module.ts Line 12), so the import here is redundant (harmless, but unnecessary).
  2. HttpModule is imported bare here, while AppModule registers it with timeout: 30000 and maxRedirects: 5. This means ApiClientV2Service gets an unconfigured HttpService instance. This is currently fine because ApiClientV2Service sets its own per-request timeout, but it's worth being aware of if you later rely on module-level Axios defaults.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/mcp-v2.module.ts` around lines 9 - 14, Remove the redundant import
of ConfigModule from the MCP V2 module since it is already globally imported in
AppModule. Additionally, consider importing HttpModule with the same
configuration used in AppModule (timeout and maxRedirects) to ensure consistent
HttpService settings for ApiClientV2Service, especially if default Axios
settings might be relied on later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mcp_v2/controllers/mcp-v2.controller.ts`:
- Line 12: The in-memory transports Map (transports: Map<string,
StreamableHTTPServerTransport>) can leak if sessions never trigger onclose; add
a periodic cleanup task that sweeps the Map and evicts stale entries: for each
key, check the session TTL/state in Redis (the same session key used by
McpServer) and remove/close the StreamableHTTPServerTransport and its McpServer
when the Redis TTL indicates expiry or the session no longer exists, or
alternatively attach a max-age timestamp to each Map entry and remove entries
older than that age; ensure cleanup runs on a timer (e.g., setInterval) and
properly calls transport.close()/server.shutdown() to release resources.

In `@src/mcp_v2/services/api-client-v2.service.ts`:
- Around line 57-61: The catch block in ApiClientV2Service currently throws
axiosError.response?.data or the raw axiosError, which loses HTTP status and
original stack; update the catch in the method handling requests (the catch that
casts to AxiosError) to construct and throw a structured error object (or a
custom Error subclass) that includes status: axiosError.response?.status,
message: axiosError.response?.data?.message || axiosError.message, context: {
method, path }, details: axiosError.response?.data, and preserve the original
error via an originalError or cause property so the stack trace remains
available; ensure callers in McpV2Service can inspect .status and .details
instead of relying on response data alone.

In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 28-41: Extract the repeated userId resolution into a private
helper (e.g., resolveUserIdFromFindApi) that accepts apiKey and payload/data,
calls this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data
}), and returns the normalized userId by applying the fallback chain
(resp?.userId || resp?.user_id || resp?.data?.userId) or throws a clear error;
then replace the inline logic inside each server.registerTool handlers
(ToolsListV2.find_user_id and the other two handlers) to call
this.resolveUserIdFromFindApi(sessionApiKey, data) after obtaining sessionId and
apiKey from this.sessionStore.getApiKey, and return the existing { content:
[...] } using the helper result.

In `@src/mcp_v2/services/session-store.service.ts`:
- Around line 31-33: Guard the onModuleDestroy method so it safely handles a
partially-initialized Redis client: check that this.client exists and is in a
state that supports quit (e.g., non-null and not already closed) before calling
this.client.quit(), and wrap the quit call in a try/catch to swallow or log any
errors; update the onModuleDestroy implementation to reference the existing
onModuleInit/client initialization flow and ensure no unguarded .quit() calls
occur if onModuleInit failed.
- Around line 22-27: The Redis client initialization in the constructor uses
this.client = new Redis(...) and only one-time 'ready'/'error' listeners, so it
needs a retryStrategy, maxRetriesPerRequest and a persistent error listener to
avoid unbounded buffering and unhandled post-init errors; update the Redis
options passed to new Redis(...) to include a retryStrategy (mirror the logic
from src/mcp/services/redis.service.ts), set maxRetriesPerRequest to a sane
value (and consider enableOfflineQueue=false if appropriate), and replace the
one-time 'error' listener with a persistent this.client.on('error', handler)
that logs/handles errors after init while keeping the existing init Promise
resolution using once('ready')/once('error').

In `@src/mcp_v2/tools/tools-list-v2.ts`:
- Around line 13-16: The list_devices inputSchema is missing the data field that
the handler in mcp-v2.service.ts destructures as { userId, data }, so add a data
property to the list_devices inputSchema (alongside userId) that accepts the
shape used by the handler (e.g., an object with email and/or phone fields) so
clients can send fallback lookup info and the SDK validation won't strip it;
update the z.object for list_devices.inputSchema to include data with
appropriate z types/description to match the email/phone resolution logic in the
list_devices handler.

---

Outside diff comments:
In `@src/main.ts`:
- Around line 36-60: The Swagger security schemes are stale: remove the call to
DocumentBuilder.addBearerAuth(...) and the addApiKey(...) that registers
'x-admin-api-key', and replace them with a single addApiKey(...) entry for the
actual header used by McpV2Controller (name: 'x-api-key', in: 'header',
description: 'API key for MCP v2 access') or drop all auth definitions entirely
if the MCP endpoints are excluded from Swagger via `@ApiExcludeEndpoint`() —
update the DocumentBuilder usage accordingly to reflect only the current
x-api-key scheme.

---

Nitpick comments:
In `@src/main.ts`:
- Around line 12-21: The variable enableCors is computed but never used, so
update the logic around app.enableCors: either remove the unused enableCors
variable and its comment if CORS should always be enabled, or use enableCors to
gate the call to app.enableCors (e.g. if (!enableCors) skip calling
app.enableCors). Locate the computation of enableCors and the
app.enableCors(...) invocation in main.ts and implement the chosen option so the
ENABLE_CORS config actually controls whether app.enableCors is invoked.

In `@src/mcp_v2/controllers/mcp-v2.controller.ts`:
- Line 44: The non-null assertion on apiKey is redundant; update the controller
code that calls this.sessionStore.setApiKey(newSessionId, apiKey!) to pass
apiKey directly (remove the trailing '!') since apiKey is already checked
earlier, e.g., in the method where newSessionId and apiKey are used; keep the
call as this.sessionStore.setApiKey(newSessionId, apiKey) so the types remain
consistent and no needless assertion is present.
- Around line 17-20: The decorators on the MCP v2 controller are contradictory:
`@ApiExcludeEndpoint`() hides the endpoint from Swagger while `@ApiOperation` and
`@ApiResponse` on the same handler are therefore ineffective; decide the intent
and make them consistent—if you want the endpoint documented, remove
`@ApiExcludeEndpoint`() from the controller (retain `@ApiOperation` and
`@ApiResponse`), otherwise remove the `@ApiOperation` and `@ApiResponse` decorators
(keep `@ApiExcludeEndpoint`) so there are no dead annotations; look for the
decorators on the method where `@All`(), `@ApiOperation`, `@ApiResponse`, and
`@ApiExcludeEndpoint` are applied to update accordingly.
- Around line 29-63: The transports Map (this.transports) is process-local so a
returning sessionId may not be found on other instances; update the handler to
avoid silently treating a missing transport as a "new session" and either (A)
require/declare sticky sessions: when sessionId is present but
this.transports.has(sessionId) is false, respond with a clear 410/400 JSON-RPC
error referencing the missing session and instruct the client/operator to use
load-balancer affinity on mcp-session-id, or (B) implement a distributed
session-to-instance registry (reuse sessionStore to persist the owning instance
id when onsessioninitialized in StreamableHTTPServerTransport and, on a missing
transport in transport lookup, proxy/forward the request to the recorded
instance or return a redirect/error). Modify the code paths around sessionId
lookup, the transport creation block, and onsessioninitialized/transport.onclose
to set and remove the instance id in the shared sessionStore so other instances
can detect and handle missing transports.

In `@src/mcp_v2/mcp-v2.module.ts`:
- Around line 9-14: Remove the redundant import of ConfigModule from the MCP V2
module since it is already globally imported in AppModule. Additionally,
consider importing HttpModule with the same configuration used in AppModule
(timeout and maxRedirects) to ensure consistent HttpService settings for
ApiClientV2Service, especially if default Axios settings might be relied on
later.

In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 89-115: Replace the magic numeric command codes used when building
the command array for args.action by extracting them into clearly named
constants (e.g., SWITCH, BRIGHTNESS, KELVIN, TEMPERATURE, MODE) and then use
those constants when constructing the command arrays (replace occurrences of 1,
28, 29, 20, 17 with the corresponding named constants); update the switch
handling of args.action and the command variable to reference these constants so
the intent is clear and maintainability improved.
ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d90b6a and 92ca436.

📒 Files selected for processing (22)
  • src/admin/admin.controller.ts
  • src/admin/admin.module.ts
  • src/admin/dto/update-config.dto.ts
  • src/admin/guards/admin-auth.guard.ts
  • src/api/api.module.ts
  • src/api/controllers/mcp.controller.ts
  • src/app.module.ts
  • src/auth/auth.controller.ts
  • src/auth/auth.module.ts
  • src/auth/auth.service.ts
  • src/main.ts
  • src/mcp/mcp.module.ts
  • src/mcp/services/api-client.service.ts
  • src/mcp/services/mcp.service.ts
  • src/mcp/services/redis.service.ts
  • src/mcp/types/mcp.types.ts
  • src/mcp_v2/controllers/mcp-v2.controller.ts
  • src/mcp_v2/mcp-v2.module.ts
  • src/mcp_v2/services/api-client-v2.service.ts
  • src/mcp_v2/services/mcp-v2.service.ts
  • src/mcp_v2/services/session-store.service.ts
  • src/mcp_v2/tools/tools-list-v2.ts
💤 Files with no reviewable changes (14)
  • src/admin/dto/update-config.dto.ts
  • src/auth/auth.controller.ts
  • src/admin/admin.controller.ts
  • src/mcp/services/mcp.service.ts
  • src/mcp/mcp.module.ts
  • src/api/api.module.ts
  • src/auth/auth.service.ts
  • src/admin/guards/admin-auth.guard.ts
  • src/admin/admin.module.ts
  • src/api/controllers/mcp.controller.ts
  • src/mcp/services/redis.service.ts
  • src/mcp/services/api-client.service.ts
  • src/auth/auth.module.ts
  • src/mcp/types/mcp.types.ts

@ApiTags('MCP Protocol V2')
@Controller('mcp')
export class McpV2Controller {
private readonly transports = new Map<string, StreamableHTTPServerTransport>();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

In-memory transport Map can leak if sessions are never properly closed.

If a client disconnects without triggering onclose (network drop, crash, etc.), the transport and its associated McpServer remain in the Map indefinitely. Over time this causes unbounded memory growth.

Consider adding a periodic cleanup sweep that evicts transports whose sessions have expired in Redis (the TTL is already tracked there), or set a max-age on entries in the Map.

Also applies to: 49-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/controllers/mcp-v2.controller.ts` at line 12, The in-memory
transports Map (transports: Map<string, StreamableHTTPServerTransport>) can leak
if sessions never trigger onclose; add a periodic cleanup task that sweeps the
Map and evicts stale entries: for each key, check the session TTL/state in Redis
(the same session key used by McpServer) and remove/close the
StreamableHTTPServerTransport and its McpServer when the Redis TTL indicates
expiry or the session no longer exists, or alternatively attach a max-age
timestamp to each Map entry and remove entries older than that age; ensure
cleanup runs on a timer (e.g., setInterval) and properly calls
transport.close()/server.shutdown() to release resources.

Comment on lines +57 to +61
} catch (error) {
const axiosError = error as AxiosError;
this.logger.error(`API Error [${method} ${path}]:`, axiosError.response?.data || axiosError.message);
throw axiosError.response?.data || axiosError;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Error propagation loses HTTP status and stack trace.

throw axiosError.response?.data || axiosError discards the HTTP status code and original stack trace. Callers (the MCP tool handlers) have no way to distinguish a 401 (bad API key) from a 404 or 500, making error handling in McpV2Service imprecise.

Consider wrapping the error in a structured object that preserves status and context:

Suggested fix
     } catch (error) {
       const axiosError = error as AxiosError;
       this.logger.error(`API Error [${method} ${path}]:`, axiosError.response?.data || axiosError.message);
-      throw axiosError.response?.data || axiosError;
+      throw {
+        status: axiosError.response?.status,
+        message: axiosError.message,
+        data: axiosError.response?.data,
+      };
     }

Based on learnings, API client errors should throw structured error objects with status, message, context, and details properties.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/api-client-v2.service.ts` around lines 57 - 61, The catch
block in ApiClientV2Service currently throws axiosError.response?.data or the
raw axiosError, which loses HTTP status and original stack; update the catch in
the method handling requests (the catch that casts to AxiosError) to construct
and throw a structured error object (or a custom Error subclass) that includes
status: axiosError.response?.status, message: axiosError.response?.data?.message
|| axiosError.message, context: { method, path }, details:
axiosError.response?.data, and preserve the original error via an originalError
or cause property so the stack trace remains available; ensure callers in
McpV2Service can inspect .status and .details instead of relying on response
data alone.

Comment on lines +28 to +41
server.registerTool('find_user_id', ToolsListV2.find_user_id, async ({ data }, extra) => {
const sessionId = extra?.sessionId;
if (!sessionId) throw new Error('Missing sessionId');

const apiKey = await this.sessionStore.getApiKey(sessionId);
if (!apiKey) throw new Error('Missing session API key');

// Call the upstream API to find user id by email/phone
const resp: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data });
const userId = resp?.userId || resp?.user_id || resp?.data?.userId;
if (!userId) throw new Error('UserId not found');

return { content: [{ type: 'text', text: JSON.stringify({ userId }) }] };
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Duplicate userId resolution logic — extract a helper.

The pattern of resolving userId from data via the findUserId API call is repeated across all three tool handlers (Lines 36-37, 53-54, 74-75), including the same fallback chain (resp?.userId || resp?.user_id || resp?.data?.userId). This violates DRY and makes the response-shape fallbacks harder to maintain.

Suggested refactor — extract a private helper
+ private async resolveUserId(apiKey: string, data: string): Promise<string> {
+   const resp: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data });
+   const userId = resp?.userId || resp?.user_id || resp?.data?.userId;
+   if (!userId) throw new Error('UserId not found');
+   return userId;
+ }

Then replace the inline calls in each handler:

-      const resp: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data });
-      const userId = resp?.userId || resp?.user_id || resp?.data?.userId;
-      if (!userId) throw new Error('UserId not found');
+      const userId = await this.resolveUserId(apiKey, data);

Also applies to: 44-62, 65-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 28 - 41, Extract the
repeated userId resolution into a private helper (e.g.,
resolveUserIdFromFindApi) that accepts apiKey and payload/data, calls
this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data }), and
returns the normalized userId by applying the fallback chain (resp?.userId ||
resp?.user_id || resp?.data?.userId) or throws a clear error; then replace the
inline logic inside each server.registerTool handlers (ToolsListV2.find_user_id
and the other two handlers) to call this.resolveUserIdFromFindApi(sessionApiKey,
data) after obtaining sessionId and apiKey from this.sessionStore.getApiKey, and
return the existing { content: [...] } using the helper result.

Comment on lines +22 to +27
this.client = new Redis({ host, port, password, db });
await new Promise<void>((resolve, reject) => {
this.client.once('ready', () => resolve());
this.client.once('error', (e) => reject(e));
setTimeout(() => reject(new Error('Redis connect timeout')), 5000);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Redis client lacks retry strategy and ongoing error handling.

Compared to the existing src/mcp/services/redis.service.ts, this Redis client has no retryStrategy or maxRetriesPerRequest, and no listener for post-init errors. If Redis becomes temporarily unavailable after startup:

  • ioredis will buffer commands by default, potentially causing memory buildup.
  • Unhandled 'error' events after the once('error') fires during init can crash the process.

Consider adding a retry strategy and a persistent error listener:

Suggested improvement
-    this.client = new Redis({ host, port, password, db });
+    this.client = new Redis({
+      host,
+      port,
+      password,
+      db,
+      retryStrategy: (times) => Math.min(times * 50, 2000),
+      maxRetriesPerRequest: 3,
+    });
+    this.client.on('error', (err) => {
+      this.logger.error('Redis error', err.message);
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.client = new Redis({ host, port, password, db });
await new Promise<void>((resolve, reject) => {
this.client.once('ready', () => resolve());
this.client.once('error', (e) => reject(e));
setTimeout(() => reject(new Error('Redis connect timeout')), 5000);
});
this.client = new Redis({
host,
port,
password,
db,
retryStrategy: (times) => Math.min(times * 50, 2000),
maxRetriesPerRequest: 3,
});
this.client.on('error', (err) => {
this.logger.error('Redis error', err.message);
});
await new Promise<void>((resolve, reject) => {
this.client.once('ready', () => resolve());
this.client.once('error', (e) => reject(e));
setTimeout(() => reject(new Error('Redis connect timeout')), 5000);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/session-store.service.ts` around lines 22 - 27, The Redis
client initialization in the constructor uses this.client = new Redis(...) and
only one-time 'ready'/'error' listeners, so it needs a retryStrategy,
maxRetriesPerRequest and a persistent error listener to avoid unbounded
buffering and unhandled post-init errors; update the Redis options passed to new
Redis(...) to include a retryStrategy (mirror the logic from
src/mcp/services/redis.service.ts), set maxRetriesPerRequest to a sane value
(and consider enableOfflineQueue=false if appropriate), and replace the one-time
'error' listener with a persistent this.client.on('error', handler) that
logs/handles errors after init while keeping the existing init Promise
resolution using once('ready')/once('error').

Comment on lines +31 to +33
async onModuleDestroy() {
await this.client.quit();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard onModuleDestroy against uninitialized client.

If onModuleInit fails (e.g., Redis timeout), this.client may be partially initialized. Calling .quit() on it could throw.

Suggested fix
   async onModuleDestroy() {
-    await this.client.quit();
+    await this.client?.quit();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async onModuleDestroy() {
await this.client.quit();
}
async onModuleDestroy() {
await this.client?.quit();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/session-store.service.ts` around lines 31 - 33, Guard the
onModuleDestroy method so it safely handles a partially-initialized Redis
client: check that this.client exists and is in a state that supports quit
(e.g., non-null and not already closed) before calling this.client.quit(), and
wrap the quit call in a try/catch to swallow or log any errors; update the
onModuleDestroy implementation to reference the existing onModuleInit/client
initialization flow and ensure no unguarded .quit() calls occur if onModuleInit
failed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/mcp_v2/services/mcp-v2.service.ts (1)

90-92: Duplicate userId resolution logic — already flagged in a prior review.

The inline findUserId call + multi-shape fallback (r?.userId || r?.user_id || r?.data?.userId) is repeated here (lines 90-92) and again at lines 110-112, identical to the pattern already raised in the previous review for find_user_id. The prior suggestion to extract a private resolveUserId(apiKey, data) helper still applies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 90 - 92, Extract a
private helper method, e.g. private async resolveUserId(apiKey, data):
Promise<string>, that performs the post to '/api/v2.0/iot-core/user/findUserId'
via this.apiClient.post(apiPath, apiKey, { data }), normalizes the response into
a single uid using the existing shape checks (r?.userId || r?.user_id ||
r?.data?.userId) and throws the same Error('UserId not found') when missing;
then replace the duplicated inline blocks in this service with calls to
this.resolveUserId(apiKey, data) to centralize logic and remove the repeated
fallback code.
🧹 Nitpick comments (2)
src/mcp_v2/services/mcp-v2.service.ts (2)

56-66: init_api_key throws raw errors while every other tool returns structured { isError: true } objects.

All other handlers are wrapped by withApiKey, which catches thrown errors and maps them to { isError: true, content: [...] }. init_api_key sits outside that wrapper and throws directly. If the MCP SDK's internal handler dispatch doesn't catch and re-wrap thrown errors the same way, the client will receive an unstructured error response. Consider wrapping the body in a try/catch that mirrors the withApiKey convention:

♻️ Proposed fix — consistent error-response shape
-    server.registerTool('init_api_key', ToolsListV2.init_api_key, async (args: any, extra: any) => {
-      const apiKey = args?.apiKey;
-      const sessionId = extra?.sessionId;
-      if (!sessionId) throw new Error('Missing sessionId');
-      if (!apiKey) throw new Error('apiKey is required');
-
-      await this.sessionStore.setApiKey(sessionId, apiKey);
-      this.logger.log(`Initialized apiKey for session ${sessionId}`);
-
-      return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'apiKey initialized for session' }) }] } as any;
-    });
+    server.registerTool('init_api_key', ToolsListV2.init_api_key, async (args: any, extra: any) => {
+      try {
+        const apiKey = args?.apiKey;
+        const sessionId = extra?.sessionId;
+        if (!sessionId) return { isError: true, content: [{ type: 'text', text: 'Missing sessionId in request' }] } as any;
+        if (!apiKey) return { isError: true, content: [{ type: 'text', text: 'apiKey is required' }] } as any;
+
+        await this.sessionStore.setApiKey(sessionId, apiKey);
+        this.logger.log(`Initialized apiKey for session ${sessionId}`);
+
+        return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'apiKey initialized for session' }) }] } as any;
+      } catch (err: any) {
+        this.logger.error(`init_api_key error: ${err?.message || err}`, err?.stack);
+        return { isError: true, content: [{ type: 'text', text: `Error: ${err?.message || err}` }] } as any;
+      }
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 56 - 66, The init_api_key
tool handler currently throws raw Errors (e.g., "Missing sessionId", "apiKey is
required") instead of returning the structured error object used elsewhere;
update the async handler passed to server.registerTool('init_api_key',
ToolsListV2.init_api_key, ...) to wrap its body in try/catch, call
this.sessionStore.setApiKey(...) and this.logger.log(...) in the try, and in
catch return the same shaped response used by withApiKey (an object with
isError: true and content: [...] containing the error message) so clients always
receive a consistent error-response shape.

50-52: withApiKey catch block logs nothing server-side — errors are silently swallowed.

Any exception thrown inside a withApiKey-wrapped handler (including upstream API failures, programming errors, and thrown validation errors) is caught here and returned to the client as a text message, but never logged with this.logger. This makes production debugging very difficult.

♻️ Proposed fix — add server-side error logging
         } catch (err: any) {
+          this.logger.error(`Tool handler error: ${err?.message || err}`, err?.stack);
           return { isError: true, content: [{ type: 'text', text: `Error: ${err?.message || err}` }] } as any;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 50 - 52, The catch in the
withApiKey-wrapped handler currently returns the error to the client but never
logs it; update the catch block inside withApiKey (in mcp-v2.service.ts) to call
this.logger.error with the error and its stack/metadata (e.g.,
this.logger.error('withApiKey handler error', err)) before returning the
existing { isError... } response so server-side exceptions are recorded for
debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 83-99: The handler registered for 'list_devices' destructures {
userId, data } but ToolsListV2.list_devices.inputSchema does not include data,
so the SDK strips it and the fallback branch that resolves email/phone is dead;
either add a data field to the list_devices inputSchema in tools-list-v2.ts
(e.g., include data: z.any()/object similar to control_device_simple) so the
handler can receive and use data, or remove the unreachable fallback code in the
server.registerTool(...) handler in mcp-v2.service.ts (the block that calls
this.apiClient.post('/api/v2.0/iot-core/user/findUserId', ...) and checks for
uid) and rely solely on userId being required/validated.

---

Duplicate comments:
In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 90-92: Extract a private helper method, e.g. private async
resolveUserId(apiKey, data): Promise<string>, that performs the post to
'/api/v2.0/iot-core/user/findUserId' via this.apiClient.post(apiPath, apiKey, {
data }), normalizes the response into a single uid using the existing shape
checks (r?.userId || r?.user_id || r?.data?.userId) and throws the same
Error('UserId not found') when missing; then replace the duplicated inline
blocks in this service with calls to this.resolveUserId(apiKey, data) to
centralize logic and remove the repeated fallback code.

---

Nitpick comments:
In `@src/mcp_v2/services/mcp-v2.service.ts`:
- Around line 56-66: The init_api_key tool handler currently throws raw Errors
(e.g., "Missing sessionId", "apiKey is required") instead of returning the
structured error object used elsewhere; update the async handler passed to
server.registerTool('init_api_key', ToolsListV2.init_api_key, ...) to wrap its
body in try/catch, call this.sessionStore.setApiKey(...) and
this.logger.log(...) in the try, and in catch return the same shaped response
used by withApiKey (an object with isError: true and content: [...] containing
the error message) so clients always receive a consistent error-response shape.
- Around line 50-52: The catch in the withApiKey-wrapped handler currently
returns the error to the client but never logs it; update the catch block inside
withApiKey (in mcp-v2.service.ts) to call this.logger.error with the error and
its stack/metadata (e.g., this.logger.error('withApiKey handler error', err))
before returning the existing { isError... } response so server-side exceptions
are recorded for debugging.
ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92ca436 and f2d6cbc.

📒 Files selected for processing (3)
  • src/mcp_v2/controllers/mcp-v2.controller.ts
  • src/mcp_v2/services/mcp-v2.service.ts
  • src/mcp_v2/tools/tools-list-v2.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/mcp_v2/tools/tools-list-v2.ts
  • src/mcp_v2/controllers/mcp-v2.controller.ts

Comment on lines +83 to +99
server.registerTool(
'list_devices',
ToolsListV2.list_devices,
withApiKey(async ({ userId, data }, apiKey) => {
// If admin provided email/phone in `data`, resolve it first
let uid = userId;
if (!uid && data) {
const r: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data });
uid = r?.userId || r?.user_id || r?.data?.userId;
if (!uid) throw new Error('UserId not found');
}

if (!uid) throw new Error('userId is required');

const devices: any = await this.apiClient.get(`/device/${uid}`, apiKey);
return { content: [{ type: 'text', text: JSON.stringify({ total: Array.isArray(devices) ? devices.length : 0, devices }) }] } as any;
}),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

data is absent from ToolsListV2.list_devices's schema — the email/phone fallback path is dead code.

The handler destructures { userId, data } from args, but the input schema for list_devices only defines userId (via .partial()), with no data field:

// tools-list-v2.ts
list_devices: {
  inputSchema: z.object({ userId: z.string().describe('End-user userId') }).partial(),
},

The SDK validates and strips inputs against the registered schema, so data will always be undefined here. The fallback branch on lines 89-93 can never execute, and any call that omits userId will unconditionally hit the throw new Error('userId is required') on line 95.

Either add data to the schema (like control_device_simple does) or remove the dead fallback:

🐛 Option A — add `data` to the `list_devices` schema in tools-list-v2.ts
 list_devices: {
   description: 'List devices for a given end-user `userId`.',
-  inputSchema: z.object({ userId: z.string().describe('End-user userId') }).partial(),
+  inputSchema: z.object({
+    userId: z.string().describe('End-user userId').optional(),
+    data: z.string().optional().describe('End-user email or phone (resolved if userId absent)'),
+  }),
 },
🐛 Option B — remove the unreachable fallback in the handler
-      withApiKey(async ({ userId, data }, apiKey) => {
-        // If admin provided email/phone in `data`, resolve it first
-        let uid = userId;
-        if (!uid && data) {
-          const r: any = await this.apiClient.post('/api/v2.0/iot-core/user/findUserId', apiKey, { data });
-          uid = r?.userId || r?.user_id || r?.data?.userId;
-          if (!uid) throw new Error('UserId not found');
-        }
-
-        if (!uid) throw new Error('userId is required');
+      withApiKey(async ({ userId }, apiKey) => {
+        if (!userId) throw new Error('userId is required');
+        const uid = userId;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_v2/services/mcp-v2.service.ts` around lines 83 - 99, The handler
registered for 'list_devices' destructures { userId, data } but
ToolsListV2.list_devices.inputSchema does not include data, so the SDK strips it
and the fallback branch that resolves email/phone is dead; either add a data
field to the list_devices inputSchema in tools-list-v2.ts (e.g., include data:
z.any()/object similar to control_device_simple) so the handler can receive and
use data, or remove the unreachable fallback code in the
server.registerTool(...) handler in mcp-v2.service.ts (the block that calls
this.apiClient.post('/api/v2.0/iot-core/user/findUserId', ...) and checks for
uid) and rely solely on userId being required/validated.

@dadadadas111 dadadadas111 merged commit ea08ba4 into master Feb 23, 2026
6 checks passed
dadadadas111 added a commit that referenced this pull request Feb 23, 2026
This was referenced Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant