logging refactoring

This commit is contained in:
azykov@mail.ru
2026-07-09 10:32:10 +03:00
parent 812e36d0af
commit bc5b3572df
10 changed files with 152 additions and 70 deletions
+2
View File
@@ -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"
+74
View File
@@ -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):
+1 -1
View File
@@ -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 } } },
);
+2 -2
View File
@@ -1,4 +1,4 @@
export * from './ping';
export * from './listTools';
export * from './invokeTool';
// export * from './listTools';
// export * from './invokeTool';
export * from './testAddRemoveTool';
+2 -3
View File
@@ -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
}),
},
}
+2 -3
View File
@@ -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,
)
}],
})),
}),
}
+3 -3
View File
@@ -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}` }],
};
}
}),
},
};
+6 -7
View File
@@ -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" }] };
}),
},
};
+34 -30
View File
@@ -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<void> {
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);
server.registerTools(...Object.values(mcpTools));
+26 -21
View File
@@ -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<A extends any[]>(
name: string,
handler: (...args: A) => Promise<TextResult>,
): (...args: A) => Promise<TextResult> {
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<TextResult> => {
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 {