]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(mcp): split MCP definitions by spec concern — Resources, Tools, ToolSchemas
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 24 Mar 2026 00:58:12 +0000 (01:58 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 24 Mar 2026 00:58:12 +0000 (01:58 +0100)
Rename MCPResourceHandlers.ts to MCPResources.ts (contains only MCP
Resources: station-list, station-by-id, template-list, OCPP schemas).

Extract log tools (readCombinedLog, readErrorLog) and their helpers
(getLogFilePath, tailFile) into MCPTools.ts (MCP Tools per spec).

MCPToolSchemas.ts unchanged (tool schema definitions).

Directory now reflects MCP spec separation of concerns:
- MCPResources.ts  — MCP Resources (spec §Resources)
- MCPTools.ts      — MCP Tools with custom logic (spec §Tools)
- MCPToolSchemas.ts — Tool schema definitions for OCPP commands

src/charging-station/ui-server/mcp/MCPResources.ts [moved from src/charging-station/ui-server/mcp/MCPResourceHandlers.ts with 59% similarity]
src/charging-station/ui-server/mcp/MCPTools.ts [new file with mode: 0644]
src/charging-station/ui-server/mcp/index.ts

similarity index 59%
rename from src/charging-station/ui-server/mcp/MCPResourceHandlers.ts
rename to src/charging-station/ui-server/mcp/MCPResources.ts
index ec13451602b4064f50de142349e28c3b19ebddca..516f68ead6b825164ffc613707a40b3ef165f197 100644 (file)
@@ -1,60 +1,11 @@
 import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
-import { open, readdir, readFile, stat } from 'node:fs/promises'
+import { readdir, readFile } from 'node:fs/promises'
 import { dirname, join, resolve } from 'node:path'
 import { fileURLToPath } from 'node:url'
-import { z } from 'zod'
 
 import type { AbstractUIServer } from '../AbstractUIServer.js'
 
-import { ConfigurationSection, type LogConfiguration, OCPPVersion } from '../../../types/index.js'
-import { Configuration } from '../../../utils/Configuration.js'
-
-const MAX_TAIL_LINES = 5000
-const DEFAULT_TAIL_LINES = 200
-const TAIL_BYTES = 65_536
-
-const getLogFilePath = (configField: 'errorFile' | 'file', date?: string): string | undefined => {
-  const logConfig = Configuration.getConfigurationSection<LogConfiguration>(
-    ConfigurationSection.log
-  )
-  const relativePath = logConfig[configField]
-  if (relativePath == null) {
-    return undefined
-  }
-  if (logConfig.rotate !== true) {
-    return resolve(relativePath)
-  }
-  const now = new Date()
-  const localDate =
-    date ??
-    `${now.getFullYear().toString()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
-  const dir = dirname(resolve(relativePath))
-  const baseName = configField === 'file' ? `combined-${localDate}.log` : `error-${localDate}.log`
-  return join(dir, baseName)
-}
-
-const tailFile = async (
-  filePath: string,
-  maxLines: number
-): Promise<{ lines: string[]; totalLines: number }> => {
-  const fileStat = await stat(filePath)
-  const fileHandle = await open(filePath, 'r')
-  try {
-    const fullContent = fileStat.size <= TAIL_BYTES
-    const readSize = Math.min(TAIL_BYTES, fileStat.size)
-    const position = Math.max(0, fileStat.size - readSize)
-    const buffer = Buffer.alloc(readSize)
-    await fileHandle.read(buffer, 0, readSize, position)
-    const allLines = buffer.toString('utf8').split('\n')
-    if (position > 0) {
-      allLines.shift()
-    }
-    const totalLines = fullContent ? allLines.length : -1
-    return { lines: allLines.slice(-maxLines), totalLines }
-  } finally {
-    await fileHandle.close()
-  }
-}
+import { OCPPVersion } from '../../../types/index.js'
 
 export const registerMCPResources = (server: McpServer, uiServer: AbstractUIServer): void => {
   server.registerResource(
@@ -238,72 +189,3 @@ export const registerMCPSchemaResources = (server: McpServer): void => {
     }
   )
 }
-
-const registerLogReadTool = (
-  server: McpServer,
-  name: string,
-  configField: 'errorFile' | 'file',
-  description: string
-): void => {
-  const label = configField === 'file' ? 'Log' : 'Error log'
-  server.registerTool(
-    name,
-    {
-      annotations: { readOnlyHint: true },
-      description,
-      inputSchema: {
-        date: z
-          .string()
-          .regex(/^\d{4}-\d{2}-\d{2}$/)
-          .optional()
-          .describe('Log file date in YYYY-MM-DD format. Defaults to current local date'),
-        tail: z
-          .number()
-          .int()
-          .positive()
-          .max(MAX_TAIL_LINES)
-          .default(DEFAULT_TAIL_LINES)
-          .describe('Number of lines to return from the end of the log'),
-      },
-    },
-    async ({ date, tail }) => {
-      try {
-        const logPath = getLogFilePath(configField, date)
-        if (logPath == null) {
-          return {
-            content: [{ text: `${label} file not configured`, type: 'text' as const }],
-            isError: true,
-          }
-        }
-        const { lines, totalLines } = await tailFile(logPath, tail)
-        const meta =
-          totalLines >= 0
-            ? `Showing last ${String(lines.length)} of ${String(totalLines)} lines`
-            : `Showing last ${String(lines.length)} lines`
-        return {
-          content: [{ text: `${meta}\n\n${lines.join('\n')}`, type: 'text' as const }],
-        }
-      } catch {
-        return {
-          content: [{ text: `${label} file not available`, type: 'text' as const }],
-          isError: true,
-        }
-      }
-    }
-  )
-}
-
-export const registerMCPLogTools = (server: McpServer): void => {
-  registerLogReadTool(
-    server,
-    'readCombinedLog',
-    'file',
-    'Read recent entries from the combined simulator log file. Returns the last N lines (default 200, max 5000). Optionally specify a date (YYYY-MM-DD) for rotated log files.'
-  )
-  registerLogReadTool(
-    server,
-    'readErrorLog',
-    'errorFile',
-    'Read recent entries from the error log file. Returns the last N lines (default 200, max 5000). Optionally specify a date (YYYY-MM-DD) for rotated log files.'
-  )
-}
diff --git a/src/charging-station/ui-server/mcp/MCPTools.ts b/src/charging-station/ui-server/mcp/MCPTools.ts
new file mode 100644 (file)
index 0000000..22d5aef
--- /dev/null
@@ -0,0 +1,124 @@
+import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
+
+import { open, stat } from 'node:fs/promises'
+import { dirname, join, resolve } from 'node:path'
+import { z } from 'zod'
+
+import { ConfigurationSection, type LogConfiguration } from '../../../types/index.js'
+import { Configuration } from '../../../utils/Configuration.js'
+
+const MAX_TAIL_LINES = 5000
+const DEFAULT_TAIL_LINES = 200
+const TAIL_BYTES = 65_536
+
+const getLogFilePath = (configField: 'errorFile' | 'file', date?: string): string | undefined => {
+  const logConfig = Configuration.getConfigurationSection<LogConfiguration>(
+    ConfigurationSection.log
+  )
+  const relativePath = logConfig[configField]
+  if (relativePath == null) {
+    return undefined
+  }
+  if (logConfig.rotate !== true) {
+    return resolve(relativePath)
+  }
+  const now = new Date()
+  const localDate =
+    date ??
+    `${now.getFullYear().toString()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
+  const dir = dirname(resolve(relativePath))
+  const baseName = configField === 'file' ? `combined-${localDate}.log` : `error-${localDate}.log`
+  return join(dir, baseName)
+}
+
+const tailFile = async (
+  filePath: string,
+  maxLines: number
+): Promise<{ lines: string[]; totalLines: number }> => {
+  const fileStat = await stat(filePath)
+  const fileHandle = await open(filePath, 'r')
+  try {
+    const fullContent = fileStat.size <= TAIL_BYTES
+    const readSize = Math.min(TAIL_BYTES, fileStat.size)
+    const position = Math.max(0, fileStat.size - readSize)
+    const buffer = Buffer.alloc(readSize)
+    await fileHandle.read(buffer, 0, readSize, position)
+    const allLines = buffer.toString('utf8').split('\n')
+    if (position > 0) {
+      allLines.shift()
+    }
+    const totalLines = fullContent ? allLines.length : -1
+    return { lines: allLines.slice(-maxLines), totalLines }
+  } finally {
+    await fileHandle.close()
+  }
+}
+
+const registerLogReadTool = (
+  server: McpServer,
+  name: string,
+  configField: 'errorFile' | 'file',
+  description: string
+): void => {
+  const label = configField === 'file' ? 'Log' : 'Error log'
+  server.registerTool(
+    name,
+    {
+      annotations: { readOnlyHint: true },
+      description,
+      inputSchema: {
+        date: z
+          .string()
+          .regex(/^\d{4}-\d{2}-\d{2}$/)
+          .optional()
+          .describe('Log file date in YYYY-MM-DD format. Defaults to current local date'),
+        tail: z
+          .number()
+          .int()
+          .positive()
+          .max(MAX_TAIL_LINES)
+          .default(DEFAULT_TAIL_LINES)
+          .describe('Number of lines to return from the end of the log'),
+      },
+    },
+    async ({ date, tail }) => {
+      try {
+        const logPath = getLogFilePath(configField, date)
+        if (logPath == null) {
+          return {
+            content: [{ text: `${label} file not configured`, type: 'text' as const }],
+            isError: true,
+          }
+        }
+        const { lines, totalLines } = await tailFile(logPath, tail)
+        const meta =
+          totalLines >= 0
+            ? `Showing last ${String(lines.length)} of ${String(totalLines)} lines`
+            : `Showing last ${String(lines.length)} lines`
+        return {
+          content: [{ text: `${meta}\n\n${lines.join('\n')}`, type: 'text' as const }],
+        }
+      } catch {
+        return {
+          content: [{ text: `${label} file not available`, type: 'text' as const }],
+          isError: true,
+        }
+      }
+    }
+  )
+}
+
+export const registerMCPLogTools = (server: McpServer): void => {
+  registerLogReadTool(
+    server,
+    'readCombinedLog',
+    'file',
+    'Read recent entries from the combined simulator log file. Returns the last N lines (default 200, max 5000). Optionally specify a date (YYYY-MM-DD) for rotated log files.'
+  )
+  registerLogReadTool(
+    server,
+    'readErrorLog',
+    'errorFile',
+    'Read recent entries from the error log file. Returns the last N lines (default 200, max 5000). Optionally specify a date (YYYY-MM-DD) for rotated log files.'
+  )
+}
index ad2469de86f57a73d3ce817ac5acf74ace27e464..434a14de025f00da08de4cfb652ea2e43451c846 100644 (file)
@@ -1,7 +1,4 @@
-export {
-  registerMCPLogTools,
-  registerMCPResources,
-  registerMCPSchemaResources,
-} from './MCPResourceHandlers.js'
+export { registerMCPResources, registerMCPSchemaResources } from './MCPResources.js'
+export { registerMCPLogTools } from './MCPTools.js'
 export { mcpToolSchemas, ocppSchemaMapping } from './MCPToolSchemas.js'
 export type { MCPToolSchema } from './MCPToolSchemas.js'