From 9d738e3408daddd27d2182d7416dd22eb38c0cea Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 24 Mar 2026 01:58:12 +0100 Subject: [PATCH] =?utf8?q?refactor(mcp):=20split=20MCP=20definitions=20by?= =?utf8?q?=20spec=20concern=20=E2=80=94=20Resources,=20Tools,=20ToolSchema?= =?utf8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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 --- ...MCPResourceHandlers.ts => MCPResources.ts} | 122 +---------------- .../ui-server/mcp/MCPTools.ts | 124 ++++++++++++++++++ src/charging-station/ui-server/mcp/index.ts | 7 +- 3 files changed, 128 insertions(+), 125 deletions(-) rename src/charging-station/ui-server/mcp/{MCPResourceHandlers.ts => MCPResources.ts} (59%) create mode 100644 src/charging-station/ui-server/mcp/MCPTools.ts diff --git a/src/charging-station/ui-server/mcp/MCPResourceHandlers.ts b/src/charging-station/ui-server/mcp/MCPResources.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 ec134516..516f68ea 100644 --- a/src/charging-station/ui-server/mcp/MCPResourceHandlers.ts +++ b/src/charging-station/ui-server/mcp/MCPResources.ts @@ -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( - 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 index 00000000..22d5aef4 --- /dev/null +++ b/src/charging-station/ui-server/mcp/MCPTools.ts @@ -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( + 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.' + ) +} diff --git a/src/charging-station/ui-server/mcp/index.ts b/src/charging-station/ui-server/mcp/index.ts index ad2469de..434a14de 100644 --- a/src/charging-station/ui-server/mcp/index.ts +++ b/src/charging-station/ui-server/mcp/index.ts @@ -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' -- 2.43.0