From bc5b3572df02d25729aef53677d56f7d0b18e265 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Thu, 9 Jul 2026 10:32:10 +0300 Subject: [PATCH] logging refactoring --- package.json | 2 + pnpm-lock.yaml | 74 +++++++++++++++++++++++++++++++ src/mcpConnection.ts | 2 +- src/mcpTools/index.ts | 4 +- src/mcpTools/invokeTool.ts | 5 +-- src/mcpTools/listTools.ts | 5 +-- src/mcpTools/ping.ts | 6 +-- src/mcpTools/testAddRemoveTool.ts | 13 +++--- src/server.ts | 64 +++++++++++++------------- src/utils/logged.ts | 47 +++++++++++--------- 10 files changed, 152 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 8a12871..9040137 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ }, "type": "module", "dependencies": { + "@fastify/cors": "^11.2.0", "@modelcontextprotocol/sdk": "^1.13.1", + "fast-xml-parser": "^5.9.3", "fastify": "^5.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.25.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f43e766..730482e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,9 +207,15 @@ importers: .: dependencies: + '@fastify/cors': + specifier: ^11.2.0 + version: 11.2.0 '@modelcontextprotocol/sdk': specifier: ^1.13.1 version: 1.29.0(zod@3.25.76) + fast-xml-parser: + specifier: ^5.9.3 + version: 5.9.3 fastify: specifier: ^5.2.1 version: 5.9.0 @@ -444,6 +450,9 @@ packages: '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + '@fastify/cors@11.2.0': + resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} @@ -495,6 +504,9 @@ packages: '@cfworker/json-schema': optional: true + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -601,6 +613,9 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -850,6 +865,16 @@ packages: fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-xml-builder@1.2.1: + resolution: {integrity: sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==} + + fast-xml-parser@5.9.3: + resolution: {integrity: sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==} + hasBin: true + + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + fastify@5.9.0: resolution: {integrity: sha512-VMS5lE0zj+MZlJpQa3Qv5iGjfun0H2N7VRgoBwpcTNQ2bdIQpv7fDpb+HGteGbicBsGkzGS+X+hdx9mmrfWuHQ==} @@ -993,6 +1018,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-unsafe@1.0.1: + resolution: {integrity: sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1125,6 +1153,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.6.1: + resolution: {integrity: sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1296,6 +1328,9 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1373,6 +1408,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1517,6 +1556,11 @@ snapshots: ajv-formats: 3.0.1(ajv@8.20.0) fast-uri: 3.1.2 + '@fastify/cors@11.2.0': + dependencies: + fastify-plugin: 5.1.0 + toad-cache: 3.7.1 + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.0.3': @@ -1576,6 +1620,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@nodable/entities@2.2.0': {} + '@pinojs/redact@0.4.0': {} '@types/estree@1.0.9': {} @@ -1712,6 +1758,8 @@ snapshots: dependencies: color-convert: 2.0.1 + anynum@1.0.1: {} + argparse@2.0.1: {} atomic-sleep@1.0.0: {} @@ -2008,6 +2056,22 @@ snapshots: fast-uri@3.1.2: {} + fast-xml-builder@1.2.1: + dependencies: + path-expression-matcher: 1.6.1 + xml-naming: 0.1.0 + + fast-xml-parser@5.9.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.1 + is-unsafe: 1.0.1 + path-expression-matcher: 1.6.1 + strnum: 2.4.1 + xml-naming: 0.1.0 + + fastify-plugin@5.1.0: {} + fastify@5.9.0: dependencies: '@fastify/ajv-compiler': 4.0.5 @@ -2153,6 +2217,8 @@ snapshots: is-promise@4.0.0: {} + is-unsafe@1.0.1: {} + isexe@2.0.0: {} jose@6.2.3: {} @@ -2265,6 +2331,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.6.1: {} + path-key@3.1.1: {} path-to-regexp@8.4.2: {} @@ -2454,6 +2522,10 @@ snapshots: strip-json-comments@5.0.3: {} + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -2522,6 +2594,8 @@ snapshots: wrappy@1.0.2: {} + xml-naming@0.1.0: {} + yocto-queue@0.1.0: {} zod-to-json-schema@3.25.2(zod@3.25.76): diff --git a/src/mcpConnection.ts b/src/mcpConnection.ts index bf9625f..08ef71f 100644 --- a/src/mcpConnection.ts +++ b/src/mcpConnection.ts @@ -26,7 +26,7 @@ export default class McpSession { private buildMcpServer(): McpServer { const server = new McpServer( - { name: "mcp-ping-server", version: "1.0.0" }, + { name: "home-mcp-server", version: "1.0.0" }, { capabilities: { tools: { listChanged: true } } }, ); diff --git a/src/mcpTools/index.ts b/src/mcpTools/index.ts index 6898f88..2615c41 100644 --- a/src/mcpTools/index.ts +++ b/src/mcpTools/index.ts @@ -1,4 +1,4 @@ export * from './ping'; -export * from './listTools'; -export * from './invokeTool'; +// export * from './listTools'; +// export * from './invokeTool'; export * from './testAddRemoveTool'; diff --git a/src/mcpTools/invokeTool.ts b/src/mcpTools/invokeTool.ts index 692f672..7d7d42e 100644 --- a/src/mcpTools/invokeTool.ts +++ b/src/mcpTools/invokeTool.ts @@ -1,7 +1,6 @@ import z from "zod"; import { server } from "../server"; import { ToolDef } from "../types"; -import { logged } from "../utils/logged"; export const invokeToolTool: ToolDef = { title: "invoke_tool", @@ -12,10 +11,10 @@ export const invokeToolTool: ToolDef = { args: z.record(z.any()).default({}), }, }, - handler: logged('invoke_tool', async ({ name, args }) => { + handler: async ({ name, args }) => { const tool = server.tools[name]; if (!tool) return { isError: true, content: [{ type: "text", text: `Unknown tool: ${name}` }] }; return tool.handler(args); // dispatch to the real implementation - }), + }, } diff --git a/src/mcpTools/listTools.ts b/src/mcpTools/listTools.ts index 067e94e..f964323 100644 --- a/src/mcpTools/listTools.ts +++ b/src/mcpTools/listTools.ts @@ -2,7 +2,6 @@ import z from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { server } from "../server"; import { ToolDef } from "../types"; -import { logged } from "../utils/logged"; export const listToolsTool: ToolDef = { title: "list_tools", @@ -10,7 +9,7 @@ export const listToolsTool: ToolDef = { description: "Return the authoritative current list of available local tools with their schemas.", inputSchema: {}, }, - handler: logged('list_tools', async () => ({ + handler: async () => ({ content: [{ type: "text", text: JSON.stringify( @@ -26,5 +25,5 @@ export const listToolsTool: ToolDef = { 2, ) }], - })), + }), } diff --git a/src/mcpTools/ping.ts b/src/mcpTools/ping.ts index eb71fd1..f5ef1a4 100644 --- a/src/mcpTools/ping.ts +++ b/src/mcpTools/ping.ts @@ -1,6 +1,6 @@ import z from "zod"; import { ToolDef } from "../types" -import { isValidHost, logged } from "../utils/logged"; +import { isValidHost } from "../utils/logged"; import { exec } from "node:child_process"; import { promisify } from "node:util"; @@ -15,7 +15,7 @@ export const pingTool: ToolDef = { count: z.number().int().min(1).max(10).default(4).describe("Echo requests to send"), }, }, - handler: logged('ping', async ({ host, count }) => { + handler: async ({ host, count }) => { if (!isValidHost(host)) { return { isError: true, @@ -40,5 +40,5 @@ export const pingTool: ToolDef = { content: [{ type: "text", text: `ping failed:\n${message}` }], }; } - }), + }, }; diff --git a/src/mcpTools/testAddRemoveTool.ts b/src/mcpTools/testAddRemoveTool.ts index 96e177b..72915ae 100644 --- a/src/mcpTools/testAddRemoveTool.ts +++ b/src/mcpTools/testAddRemoveTool.ts @@ -1,5 +1,4 @@ import { ToolDef } from "../types" -import { logged } from "../utils/logged"; import { server } from "../server"; const dynamicTool: ToolDef = { @@ -8,9 +7,9 @@ const dynamicTool: ToolDef = { description: "Says hello", inputSchema: {}, }, - handler: logged('say_hello', async () => { + handler: async () => { return { content: [{ type: "text", text: "hello" }] }; - }), + }, } export const testAddTool: ToolDef = { @@ -19,10 +18,10 @@ export const testAddTool: ToolDef = { description: "Dynamically add a new tool in runtime", inputSchema: {}, }, - handler: logged('test_add_tool', async () => { + handler: async () => { server.registerTools(dynamicTool); return { content: [{ type: "text", text: "New tool added" }] }; - }), + }, }; export const testRemoveTool: ToolDef = { @@ -31,8 +30,8 @@ export const testRemoveTool: ToolDef = { description: "Dynamically remove a tool in runtime", inputSchema: {}, }, - handler: logged('test_remove_tool', async () => { + handler: async () => { server.unregisterTools(dynamicTool.title); return { content: [{ type: "text", text: "Tool removed" }] }; - }), + }, }; diff --git a/src/server.ts b/src/server.ts index ee31165..7b42d33 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,7 +2,9 @@ import Fastify, { type FastifyInstance } from "fastify"; import { ToolDef } from "./types"; import McpSession from "./mcpConnection"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; -import { pingTool, testAddTool, testRemoveTool, listToolsTool, invokeToolTool } from "./mcpTools"; +import * as mcpTools from "./mcpTools"; +import { logged } from "./utils/logged"; +import cors from '@fastify/cors'; export class Server { private readonly app: FastifyInstance; @@ -14,34 +16,20 @@ export class Server { this.port = port; this.app = Fastify({ trustProxy: true, - logger: { - serializers: { - req(request) { - return { - method: request.method, - url: request.url, - host: request.headers.host, - remoteAddress: request.ip, - headers: request.headers, // <-- full header set - }; - }, - }, - }, - // logger: { - // level: process.env.LOG_LEVEL ?? "info", - // ...(process.env.NODE_ENV !== "production" - // ? { - // transport: { - // target: "pino-pretty", - // options: { colorize: true, translateTime: "HH:MM:ss.l", ignore: "pid,hostname" }, - // }, - // } - // : {}), + // serializers: { + // req(request) { + // return { + // method: request.method, + // url: request.url, + // host: request.headers.host, + // remoteAddress: request.ip, + // headers: request.headers, // <-- full header set + // }; + // }, + // }, // }, }); - this.registerLogging(); - this.registerRoutes(); } private registerLogging(): void { @@ -73,8 +61,10 @@ export class Server { public registerTools(...tools: ToolDef[]): void { for (const tool of tools) { - if (!this.getTool(tool.title)) - this.tools[tool.title] = tool; + if (!this.getTool(tool.title)) { + // console.log(`Registering new tool ${tool.title}`); + this.tools[tool.title] = logged(tool); + } } this.emitToolListChange(); @@ -88,8 +78,11 @@ export class Server { } private emitToolListChange() { + const toolList = Object.values(this.tools); + console.log(`Advertising updated tool list: ${toolList.map((tool) => tool.title)}`); + for (const session of Object.values(this.sessions)) - session.handleToolListChange(Object.values(this.tools)); + session.handleToolListChange(toolList); } public getSession(sessionId: string): McpSession | undefined { @@ -101,6 +94,9 @@ export class Server { // underlying Node req/res it expects. private registerRoutes(): void { this.app.post("/mcp", async (request, reply) => { + reply.raw.setHeader('Access-Control-Allow-Origin', request.headers.origin ?? '*'); + reply.raw.setHeader('Access-Control-Allow-Credentials', 'true'); + reply.raw.setHeader('Vary', 'Origin'); let session: McpSession | undefined; @@ -157,10 +153,18 @@ export class Server { } async start(): Promise { + await this.app.register(cors, { + origin: true, + credentials: true, + }); + + await this.registerLogging(); + await this.registerRoutes(); + await this.app.listen({ port: this.port, host: "0.0.0.0" }); this.app.log.info(`MCP ping server listening on http://0.0.0.0:${this.port}/mcp`); } } export const server = new Server(); -server.registerTools(pingTool, listToolsTool, invokeToolTool, testAddTool, testRemoveTool); \ No newline at end of file +server.registerTools(...Object.values(mcpTools)); \ No newline at end of file diff --git a/src/utils/logged.ts b/src/utils/logged.ts index bbd4f04..8cab4d3 100644 --- a/src/utils/logged.ts +++ b/src/utils/logged.ts @@ -1,29 +1,34 @@ import { randomUUID } from "node:crypto"; -import { TextResult } from "../types"; +import { TextResult, ToolDef } from "../types"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function logged( - name: string, - handler: (...args: A) => Promise, -): (...args: A) => Promise { - return async (...args: A) => { - const id = randomUUID().slice(0, 8); - const t0 = process.hrtime.bigint(); - console.log(`[tool>] ${name} #${id} args=${JSON.stringify(args)}`); - try { - const result = await handler(...args); - const ms = Number(process.hrtime.bigint() - t0) / 1e6; - console.log( - `[tool<] ${name} #${id} ${ms.toFixed(1)}ms` + - `${result.isError ? " ERROR" : ""} result=${JSON.stringify(result).slice(0, 2000)}`, - ); - return result; - } catch (err) { - const ms = Number(process.hrtime.bigint() - t0) / 1e6; - console.error(`[tool!] ${name} #${id} ${ms.toFixed(1)}ms threw:`, err); - throw err; // let the SDK turn it into a JSON-RPC error + tool: ToolDef, +): ToolDef { + + console.dir(tool); + + return { + ...tool, + handler: async (...args: A): Promise => { + const id = randomUUID().slice(0, 8); + const t0 = process.hrtime.bigint(); + console.log(`[tool>] ${tool.title} #${id} args=${JSON.stringify(args)}`); + try { + const result = await tool.handler(...args); + const ms = Number(process.hrtime.bigint() - t0) / 1e6; + console.log( + `[tool<] ${tool.title} #${id} ${ms.toFixed(1)}ms` + + `${result.isError ? " ERROR" : ""} result=${JSON.stringify(result).slice(0, 2000)}`, + ); + return result; + } catch (err) { + const ms = Number(process.hrtime.bigint() - t0) / 1e6; + console.error(`[tool!] ${tool.title} #${id} ${ms.toFixed(1)}ms threw:`, err); + throw err; // let the SDK turn it into a JSON-RPC error + } } - }; + } } export function isValidHost(host: string): boolean {