]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(ui-server): add MCP transport and deprecate HTTP (#1746)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 23 Mar 2026 23:59:59 +0000 (00:59 +0100)
committerGitHub <noreply@github.com>
Mon, 23 Mar 2026 23:59:59 +0000 (00:59 +0100)
commit8fd32d22c9e6f4fe3c63878b488f1eef67aab19d
treeb62b93e8b2f9c7c0b2947583b6560fd2a749a76e
parent6f95d3c36218d1f1ec82a96f1b7013b3f8fd0e71
feat(ui-server): add MCP transport and deprecate HTTP (#1746)

* feat(types): add MCP application protocol type

* chore(deps): add @modelcontextprotocol/sdk and zod

* refactor(ui-server): deprecate UIHttpServer in favor of MCP transport

* feat(ui-server): add MCP tool zod schemas for all procedures

* feat(ui-server): add UIMCPServer transport and MCP resource handlers

* feat(ui-server): integrate UIMCPServer into UIServerFactory

* test(ui-server): add MCP factory integration tests

* test(ui-server): add UIMCPServer unit tests

* test(ui-server): add MCP endpoint integration tests and fix lint

* refactor(ui-server): extract HttpMethod enum to shared UIServerUtils

* docs: restructure UI Protocol section — MCP first, remove redundant API listing

* fix(ui-server): address PR review — security hardening and schema corrections

- Send 404 + destroy for non-/mcp requests instead of hanging socket
- Add rate limiting (429) matching UIHttpServer behavior
- Add body size limit (createBodySizeLimiter) to prevent DoS
- Register res.on('close') before handleRequest to prevent race
- Return 400 for JSON parse errors, 500 for server errors
- Fix setSupervisionUrl schema: supervisionUrl → url
- Fix stopTransaction schema: transactionIds string[] → transactionId number

* refactor(tests): use HttpMethod enum instead of string literals

* docs: remove redundant MCP discoverability statement

* docs: restructure MCP section for agent config, update uiServer type in config table

* feat(ui-server): log deprecation warning when HTTP transport is used

* fix(ui-server): harmonize log levels and message patterns with existing transports

* test(ui-server): align MCP tests with style guide

- Use createLoggerMocks() for log assertions instead of trivial checks
- Add server.stop() to factory fallback tests to prevent resource leaks
- Replace fragile globalThis.clearTimeout override with t.mock.method

* docs: restore PDU acronym definition in WebSocket Protocol section

* fix(ui-server): use UI protocol version for MCP server version

* fix(ui-server): derive log file paths from Configuration instead of hardcoding

* perf(ui-server): tail-read log files instead of loading entire file into memory

* refactor(ui-server): convert log resources to tools with tail parameter

Follow MCP best practices: resources are for static snapshots,
tools are for parameterized on-demand data.

- Replace log://combined and log://error resources with
  readCombinedLog and readErrorLog tools
- Add tail parameter (default 200, max 5000) following the
  filesystem server head/tail pattern
- Add readOnlyHint annotation per MCP tool spec
- Return totalLines metadata in response for context
- Keep station and template resources (small, static data)

* feat(ui-server): expose OCPP JSON schemas as MCP resource templates

Add schema://ocpp/{version} listing and schema://ocpp/{version}/{command}
resource templates that serve the existing JSON schema files from
src/assets/json-schemas/ocpp/ on demand.

LLM agents can discover available OCPP commands per version and read
the full JSON schema for any command without bloating tools/list.
Schemas are read from disk on demand — no memory duplication with
the AJV validators already loaded by OCPP services.

* feat(ui-server): add version-aware OCPP tool schemas with JSON Schema injection

- Add ocppSchemaMapping linking ProcedureName to JSON Schema files
- 16 OCPP tools now have ocpp16Payload/ocpp20Payload fields with
  version affinity in descriptions
- Intercept tools/list to inject full JSON Schemas from disk
  into inputSchema.properties (no Zod duplication, DRY)
- Add payload flattening: extract versioned payload, spread flat
- Add mutual exclusivity: reject if both payloads provided
- Add pre-flight version check: clear error on version mismatch
- Runtime guard: graceful fallback if SDK internal API changes

* refactor(ui-server): use OCPPVersion enum instead of string literals

* refactor(ui-server): improve MCP code elegance and project rule compliance

- Replace as-unknown-as cast with Reflect.get() for SDK handler access
- Replace generic Error with BaseError in stop/readRequestBody
- Extract createToolErrorResponse/createToolResponse static helpers (DRY)
- Extract closeTransportSafely for duplicate transport cleanup
- Extract registerLogReadTool factory for duplicate log tools (DRY)
- Remove unnecessary String() conversions in resource handlers

* fix(ui-server): fix body size double-counting and schema resource path

- readRequestBody: pass chunk.length (not accumulated total) to
  createBodySizeLimiter — matches UIHttpServer pattern
- getSchemaBaseDir: use correct relative path instead of broken
  resolve().includes('assets') condition (always true)

* refactor(ui-server): consolidate version fallback guards and clarify README version docs

* fix(ui-server): harden MCP schema resources against path traversal and fix transport leak

- Add path traversal guard in schema resource handlers to validate resolved
  paths stay within the OCPP schema base directory
- Close MCP transport on 405 Method Not Allowed responses to prevent leak
- Fix log file path to use direct path when rotation is disabled
- Fix misleading Heartbeat tool description that referenced unsupported
  ocpp payload fields

* fix(ui-server): address remaining review findings and lint warnings

- Reorder stop() to delete-before-reject preventing re-entrant issues
- Use ephemeral port in integration test instead of fixed port 18999
- Document synchronous authenticate() assumption and SDK pattern deviation
- Add 'modelcontextprotocol' to cspell wordlist
- Convert JSDoc to inline comment to fix jsdoc lint warnings

* fix(ui-server): exact MCP path match, pin SDK version, log schema injection

- Use exact pathname match (url.pathname !== '/mcp') instead of
  startsWith to prevent unintended paths from reaching the MCP handler
- Pin @modelcontextprotocol/sdk to ~1.27.1 (patch-only) since the
  OCPP schema injection relies on SDK internals pinned to 1.27.x
- Add startup log confirming successful OCPP JSON schema injection

* test(ui-server): add comprehensive UIMCPServer unit tests

Cover private method logic missing from initial test suite:
- invokeProcedure: service null, dual payload, version mismatch,
  direct response, service error, 30s timeout (mock timers)
- checkVersionCompatibility: OCPP 1.6/2.0/2.0.1 matching, cross-version
  errors, hashIds filtering, all-stations fallback
- readRequestBody: valid JSON, payload too large, invalid JSON, stream error
- loadOcppSchemas: disk loading, cache entry validation

Refactor for elegance and guide compliance:
- Centralize Reflect.get wrappers in TestableUIMCPServer class
- Extract assertToolError() helper to eliminate repeated triplet
- Move createMockChargingStationDataWithVersion() to UIServerTestUtils
- Replace magic number with DEFAULT_MAX_PAYLOAD_SIZE import

* fix(ui-server): per-request McpServer factory, async iterator body reader, transport leak

- Create a new McpServer per HTTP request via createMcpServer() factory
  to fix concurrency bug: McpServer.connect() overwrites a single
  internal _transport field, causing cross-talk under concurrent requests.
  OCPP schema cache and UI service are pre-loaded at startup; tool
  registration is ~12µs per request (negligible).

- Replace event-listener readRequestBody with for-await-of async
  iterator pattern. Single settlement guaranteed by language semantics,
  eliminating the double-reject bug on payload-too-large.

- Close transport on mcpServer.connect() failure to prevent resource leak.

- Clean up both transport and McpServer on response close via unified
  cleanup callback.

* fix: align StatusNotification field name with OCPP 2.0.1 schema, fix asset paths

- Use 'connectorStatus' (OCPP 2.0.1 schema field name) instead of
  'status' (non-standard) in OCPP20RequestService.buildRequestPayload
  and both call sites in OCPP20IncomingRequestService

- Fix esbuild-relative asset paths for OCPP JSON schemas and MCP
  resource handlers — import.meta.url resolves to dist/start.js,
  not the original source tree depth

* refactor(ocpp): align payload builders to object-param API, fix StatusNotification field names

- Refactor buildStatusNotificationRequest from positional params to
  object-based StatusNotificationRequest input. Builder accepts both
  'status' (internal/OCPP 1.6) and 'connectorStatus' (OCPP 2.0.1)
  with consistent fallback order (connectorStatus ?? status).

- Refactor buildTransactionEvent from 3 overloads to single
  object-based OCPP20TransactionEventRequest input, removing
  positional param complexity and non-null assertions.

- Refactor sendAndSetConnectorStatus from positional params to
  object-based StatusNotificationRequest input. Callers pass
  version-correct field names: 'status' in OCPP 1.6 code,
  'connectorStatus' in OCPP 2.0 code, either in common code.

- Extract getSchemaBaseDir() as protected method on UIMCPServer
  for test overridability, replacing fragile symlink hack.

- Migrate all call sites (28 sendAndSetConnectorStatus, 60+
  buildTransactionEvent) and their corresponding tests.

* build: externalize @modelcontextprotocol/sdk and zod from esbuild bundle

Add @modelcontextprotocol/* and zod to esbuild external list for
consistency with all other production dependencies (ajv, ws, winston,
etc.) which are already externalized. Reduces bundle size and avoids
bundling the SDK's transitive dependency tree.

* fix(ui-server): fix SonarCloud quality gate — localeCompare sort, reduce cognitive complexity

- Add localeCompare to schema file sort in MCPResourceHandlers to
  ensure locale-aware alphabetical ordering (SonarCloud bug)

- Extract sendErrorResponse helper from handleMcpRequest to reduce
  cognitive complexity from 16 to below threshold (SonarCloud smell)

* chore: update .serena/project.yml with new default config fields

* refactor(ocpp): buildMeterValue resolves connectorId/evseId from transactionId

- Refactor buildMeterValue signature from (station, connectorId,
  transactionId, interval) to (station, transactionId, interval).
  connectorId and evseId are resolved internally from the unique
  transactionId via getConnectorIdByTransactionId/getEvseIdByTransactionId.

- Refactor getSampledValueTemplate to accept optional evseId. When
  provided, EVSE-level MeterValues templates take priority; falls back
  to merging connector-level templates from all EVSE connectors.

- Add MeterValues field to EvseStatus and EvseTemplate types to support
  EVSE-level meter value template definitions.

- Refactor handleMeterValues in BroadcastChannel from if/else to
  switch/case on OCPP version with explicit default error.

- Simplify OCPP20IncomingRequestService fallback MeterValues (no
  transaction = static empty meter value, skip buildMeterValue).

- Add buildMeterValue unit tests covering OCPP 1.6/2.0 resolution,
  unknown transactionId throw, EVSE-level template priority, and
  connector template merge fallback.

- Add TEST_TRANSACTION_ID_STRING constant for OCPP 2.0 tests, fix
  BroadcastChannel test to use string transactionId for OCPP 2.0.1.

* docs: add MCP Protocol to ToC, fix HTTP Protocol link, document EVSE-level MeterValues

- Add MCP Protocol entry to table of contents
- Fix HTTP Protocol anchor to match 'deprecated' heading
- Document EVSE-level MeterValues templates in Evses section examples
  with priority explanation (EVSE-level overrides connector-level)

* fix(ocpp): version-aware Authorize response mapping, complete cross-version types

- Fix commandResponseToResponseStatus for Authorize: use switch/case
  on OCPP version. OCPP 1.6 checks idTagInfo.status, OCPP 2.0 checks
  idTokenInfo.status. Previously only checked idTagInfo (1.6 field),
  causing MCP to report 'failure' for successful OCPP 2.0 authorizations.

- Make AuthorizeResponse and AuthorizeRequest cross-version union types
  (OCPP16 | OCPP20) consistent with AuthorizationStatus pattern.

- Make isIdTagRemoteAuthorized version-aware: sends idTag for OCPP 1.6,
  idToken for OCPP 2.0, and checks the version-correct response field.

- Use version-specific casts (OCPP16AuthorizeResponse,
  OCPP20AuthorizeResponse) in version-switched code paths instead of
  agnostic union type.

* refactor(ocpp): harmonize version switch/case in auth functions

- Refactor isIdTagAuthorizedUnified and isIdTagAuthorized from if/else
  on OCPP version to switch/case with explicit VERSION_16, VERSION_20/201
  cases and separate default fallback.

- Remove unnecessary optional chaining on stationInfo inside
  version-switched cases where stationInfo is guaranteed non-null.

- Separate VERSION_16 from default in all auth switch/case blocks
  for spec-compliant version handling.

* fix(ocpp): buildMeterValue from transactionId, log date locale fix, MeterValues EVSE templates

- Refactor buildMeterValue to resolve connectorId/evseId from
  transactionId internally. Returns empty MeterValue when transactionId
  is undefined (MCP pass-through with provided meterValue).

- Fix log file date resolution: use local date instead of UTC to match
  Winston DailyRotateFile naming convention.

- Add optional date parameter to readCombinedLog/readErrorLog MCP tools
  for accessing rotated log files by date (YYYY-MM-DD).

- Add MeterValues field to EvseStatus/EvseTemplate types for EVSE-level
  meter value template definitions. getSampledValueTemplate checks
  EVSE-level templates first, falls back to merging connector templates.

- Refactor handleMeterValues to switch/case on OCPP version with
  explicit default error.

- Make AuthorizeRequest/AuthorizeResponse cross-version union types.
  Fix commandResponseToResponseStatus and isIdTagRemoteAuthorized with
  version-aware switch/case for OCPP 1.6 (idTagInfo) vs 2.0 (idTokenInfo).

- Add integration tests for readCombinedLog tool (default date, explicit
  date, missing file error). Add buildMeterValue unit tests.
40 files changed:
.serena/project.yml
README.md
eslint.config.js
package.json
pnpm-lock.yaml
scripts/bundle.js
src/charging-station/ChargingStation.ts
src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20RequestService.ts
src/charging-station/ocpp/2.0/OCPP20ResponseService.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIMCPServer.ts [new file with mode: 0644]
src/charging-station/ui-server/UIServerFactory.ts
src/charging-station/ui-server/UIServerUtils.ts
src/charging-station/ui-server/mcp/MCPResourceHandlers.ts [new file with mode: 0644]
src/charging-station/ui-server/mcp/MCPToolSchemas.ts [new file with mode: 0644]
src/charging-station/ui-server/mcp/index.ts [new file with mode: 0644]
src/types/Evse.ts
src/types/UIProtocol.ts
src/types/ocpp/Transaction.ts
tests/charging-station/ChargingStationTestConstants.ts
tests/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-CallChain.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts
tests/charging-station/ocpp/OCPPServiceUtils-connectorStatus.test.ts
tests/charging-station/ocpp/OCPPServiceUtils-meterValues.test.ts [new file with mode: 0644]
tests/charging-station/ui-server/UIHttpServer.test.ts
tests/charging-station/ui-server/UIMCPServer.integration.test.ts [new file with mode: 0644]
tests/charging-station/ui-server/UIMCPServer.test.ts [new file with mode: 0644]
tests/charging-station/ui-server/UIServerFactory.test.ts [new file with mode: 0644]
tests/charging-station/ui-server/UIServerTestUtils.ts