drawio-mcp-server 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, jest } from "@jest/globals";
2
- import EventEmitter from "node:events";
2
+ import { EventEmitter } from "node:events";
3
3
  import { create_bus } from "./emitter_bus.js";
4
4
  import { bus_request_stream, bus_reply_stream } from "./types.js";
5
5
  import { create_logger } from "./standard_console_logger.js";
package/build/index.js CHANGED
@@ -9,9 +9,8 @@ import { bus_reply_stream, bus_request_stream, } from "./types.js";
9
9
  import { create_bus } from "./emitter_bus.js";
10
10
  import { default_tool } from "./tool.js";
11
11
  import { nanoid_id_generator } from "./nanoid_id_generator.js";
12
- import { create_logger } from "./mcp_console_logger.js";
12
+ import { create_logger, validLogLevels } from "./mcp_console_logger.js";
13
13
  const PORT = 3333;
14
- const log = create_logger();
15
14
  async function checkPortAvailable(port) {
16
15
  return new Promise((resolve) => {
17
16
  const server = createServer();
@@ -78,12 +77,18 @@ async function start_websocket_server() {
78
77
  // Create server instance
79
78
  const server = new McpServer({
80
79
  name: "drawio-mcp-server",
81
- version: "1.0.3",
80
+ version: "1.2.0",
81
+ }, {
82
82
  capabilities: {
83
83
  resources: {},
84
84
  tools: {},
85
+ logging: {
86
+ setLevels: true,
87
+ levels: validLogLevels,
88
+ },
85
89
  },
86
90
  });
91
+ const log = create_logger(server);
87
92
  const bus = create_bus(log)(emitter);
88
93
  const id_generator = nanoid_id_generator();
89
94
  const context = {
@@ -1,14 +1,133 @@
1
- export function create_logger() {
1
+ import { z } from "zod";
2
+ // Log levels (lower is more severe)
3
+ const LogLevelMap = {
4
+ emergency: 0,
5
+ alert: 1,
6
+ critical: 2,
7
+ error: 3,
8
+ warning: 4,
9
+ notice: 5,
10
+ info: 6,
11
+ debug: 7,
12
+ };
13
+ export const validLogLevels = Object.keys(LogLevelMap);
14
+ // Define the request schema for setLevels
15
+ const SetLevelsRequestSchema = z.object({
16
+ method: z.literal("logging/setLevels"),
17
+ params: z.object({
18
+ levels: z.record(z.string(), z.enum(validLogLevels).nullable()),
19
+ }),
20
+ });
21
+ // Define the request schema for setLevel
22
+ const SetLevelRequestSchema = z.object({
23
+ method: z.literal("logging/setLevel"),
24
+ params: z.object({
25
+ level: z.enum(validLogLevels),
26
+ }),
27
+ });
28
+ // Per-logger log levels (default is root `"."`)
29
+ let logLevels = {
30
+ ".": LogLevelMap.info, // Start with info level
31
+ };
32
+ // Helper: Effective log level for a logger
33
+ const getEffectiveLogLevel = (loggerName) => {
34
+ return loggerName in logLevels
35
+ ? logLevels[loggerName]
36
+ : (logLevels["."] ?? LogLevelMap.info);
37
+ };
38
+ // Helper: Should we log at this level?
39
+ const shouldLog = (level, loggerName) => {
40
+ const numericLevel = LogLevelMap[level];
41
+ const effectiveLevel = getEffectiveLogLevel(loggerName);
42
+ return numericLevel <= effectiveLevel;
43
+ };
44
+ // Helper: Actually send a log if allowed
45
+ const log = (server) => (level, loggerName, data) => {
46
+ if (!(level in LogLevelMap)) {
47
+ console.error(`Internal Error: Invalid log level used: ${level}`);
48
+ return;
49
+ }
50
+ if (shouldLog(level, loggerName)) {
51
+ server.server.sendLoggingMessage({
52
+ level,
53
+ logger: loggerName,
54
+ data,
55
+ });
56
+ }
57
+ };
58
+ export function create_logger(server) {
59
+ const log3 = log(server);
60
+ // 2. Register handler for logging/setLevels (only override requested loggers)
61
+ server.server.setRequestHandler(SetLevelsRequestSchema, async (request) => {
62
+ const newLevels = request.params.levels;
63
+ // Only update logLevels for specified keys; unset any set to null
64
+ for (const loggerName in newLevels) {
65
+ if (Object.prototype.hasOwnProperty.call(newLevels, loggerName)) {
66
+ const levelName = newLevels[loggerName];
67
+ if (levelName === null) {
68
+ if (loggerName !== ".") {
69
+ delete logLevels[loggerName]; // Remove override
70
+ log3("debug", "logging", {
71
+ message: `Reset log level for logger: ${loggerName}`,
72
+ });
73
+ }
74
+ }
75
+ else if (levelName &&
76
+ validLogLevels.includes(levelName)) {
77
+ logLevels[loggerName] = LogLevelMap[levelName];
78
+ log3("debug", "logging", {
79
+ message: `Set log level for logger '${loggerName}' to '${levelName}'`,
80
+ });
81
+ }
82
+ else {
83
+ log3("warning", "logging", {
84
+ message: `Invalid log level '${levelName}' received for logger '${loggerName}'`,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ return {};
90
+ });
91
+ // Register handler for logging/setLevel (sets root logger level)
92
+ server.server.setRequestHandler(SetLevelRequestSchema, async (request) => {
93
+ const log3 = log(server);
94
+ const levelName = request.params.level;
95
+ if (validLogLevels.includes(levelName)) {
96
+ logLevels["."] = LogLevelMap[levelName];
97
+ log3("debug", "logging", {
98
+ message: `Set root log level to '${levelName}'`,
99
+ });
100
+ }
101
+ else {
102
+ log3("warning", "logging", {
103
+ message: `Invalid log level '${levelName}' received`,
104
+ });
105
+ }
106
+ return {};
107
+ });
2
108
  return {
3
109
  log: (level, message, ...data) => {
4
- return data.length > 0
5
- ? console.error(`${level?.toUpperCase()}: ${message}`, ...data)
6
- : console.error(`${level?.toUpperCase()}: ${message}`);
110
+ log3(level, ".", { message, data });
111
+ // return server.server.sendLoggingMessage({
112
+ // // level: LogLevelMap[level?.toLowerCase()],
113
+ // level: level?.toLowerCase() as McpLogLevel,
114
+ // logger: "root",
115
+ // data,
116
+ // });
117
+ // return data.length > 0
118
+ // ? console.error(`${level?.toUpperCase()}: ${message}`, ...data)
119
+ // : console.error(`${level?.toUpperCase()}: ${message}`);
7
120
  },
8
121
  debug: (message, ...data) => {
9
- return data.length > 0
10
- ? console.error(`DEBUG: ${message}`, ...data)
11
- : console.error(`DEBUG: ${message}`);
122
+ log3("debug", ".", { message, data });
123
+ // return server.server.sendLoggingMessage({
124
+ // level: "debug" as McpLogLevel,
125
+ // logger: "root",
126
+ // data,
127
+ // });
128
+ // return data.length > 0
129
+ // ? console.error(`DEBUG: ${message}`, ...data)
130
+ // : console.error(`DEBUG: ${message}`);
12
131
  },
13
132
  };
14
133
  }
@@ -1,63 +1,196 @@
1
1
  import { jest } from "@jest/globals";
2
2
  import { create_logger } from "./mcp_console_logger.js";
3
3
  describe("create_logger", () => {
4
- let originalConsoleError;
5
- let mockConsoleError;
4
+ let mock_send_logging_message;
5
+ let mock_set_request_handler;
6
+ let mock_server;
6
7
  beforeEach(() => {
7
- // Save original console.error
8
- originalConsoleError = console.error;
9
- // Create mock for console.error
10
- mockConsoleError = jest.fn();
11
- console.error = mockConsoleError;
8
+ mock_send_logging_message = jest.fn();
9
+ mock_set_request_handler = jest.fn();
10
+ mock_server = {
11
+ server: {
12
+ sendLoggingMessage: mock_send_logging_message,
13
+ setRequestHandler: mock_set_request_handler,
14
+ },
15
+ };
12
16
  });
13
17
  afterEach(() => {
14
- // Restore original console.error
15
- console.error = originalConsoleError;
16
- // Clear all mocks
17
18
  jest.clearAllMocks();
18
19
  });
19
20
  it("should return a Logger object with log and debug methods", () => {
20
- const logger = create_logger();
21
+ const logger = create_logger(mock_server);
21
22
  expect(logger).toBeDefined();
22
23
  expect(typeof logger.log).toBe("function");
23
24
  expect(typeof logger.debug).toBe("function");
24
25
  });
25
- it("log method should call console.error with message and data", () => {
26
- const logger = create_logger();
27
- const testMessage = "test message";
28
- const testData = { key: "value" };
29
- const testLevel = "info";
30
- logger.log(testLevel, testMessage, testData);
31
- expect(mockConsoleError).toHaveBeenCalledTimes(1);
32
- expect(mockConsoleError).toHaveBeenCalledWith(`${testLevel.toUpperCase()}: ${testMessage}`, testData);
33
- });
34
- it("debug method should call console.error with message and data", () => {
35
- const logger = create_logger();
36
- const testMessage = "debug message";
37
- const testData = { debug: true };
38
- logger.debug(testMessage, testData);
39
- expect(mockConsoleError).toHaveBeenCalledTimes(1);
40
- expect(mockConsoleError).toHaveBeenCalledWith(`DEBUG: ${testMessage}`, testData);
41
- });
42
- it("should handle no additional data parameters", () => {
43
- const logger = create_logger();
44
- const testMessage = "message without data";
45
- logger.log("warn", testMessage);
46
- expect(mockConsoleError).toHaveBeenCalledWith(`WARN: ${testMessage}`);
47
- mockConsoleError.mockClear();
48
- logger.debug(testMessage);
49
- expect(mockConsoleError).toHaveBeenCalledWith(`DEBUG: ${testMessage}`);
50
- });
51
- it("should handle multiple data parameters", () => {
52
- const logger = create_logger();
53
- const testMessage = "message with multiple data";
26
+ it("log should call sendLoggingMessage with level and data (message is ignored by implementation)", () => {
27
+ const logger = create_logger(mock_server);
28
+ const test_message = "test message";
29
+ const test_data = { key: "value" };
30
+ const test_level = "info";
31
+ logger.log(test_level, test_message, test_data);
32
+ expect(mock_send_logging_message).toHaveBeenCalledTimes(1);
33
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
34
+ level: "info",
35
+ logger: ".",
36
+ data: { message: test_message, data: [test_data] },
37
+ });
38
+ });
39
+ it("debug should call sendLoggingMessage with debug level and data", async () => {
40
+ const logger = create_logger(mock_server);
41
+ const test_message = "debug message";
42
+ const test_data = { debug: true };
43
+ const set_level_handler = mock_set_request_handler.mock.calls[1][1];
44
+ await set_level_handler({
45
+ method: "logging/setLevel",
46
+ params: { level: "debug" },
47
+ });
48
+ mock_send_logging_message.mockClear();
49
+ logger.debug(test_message, test_data);
50
+ expect(mock_send_logging_message).toHaveBeenCalledTimes(1);
51
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
52
+ level: "debug",
53
+ logger: ".",
54
+ data: { message: test_message, data: [test_data] },
55
+ });
56
+ });
57
+ it("should handle no additional data parameters", async () => {
58
+ const logger = create_logger(mock_server);
59
+ const test_message = "message without data";
60
+ logger.log("warning", test_message);
61
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
62
+ level: "warning",
63
+ logger: ".",
64
+ data: { message: test_message, data: [] },
65
+ });
66
+ const set_level_handler = mock_set_request_handler.mock.calls[1][1];
67
+ await set_level_handler({
68
+ method: "logging/setLevel",
69
+ params: { level: "debug" },
70
+ });
71
+ mock_send_logging_message.mockClear();
72
+ logger.debug(test_message);
73
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
74
+ level: "debug",
75
+ logger: ".",
76
+ data: { message: test_message, data: [] },
77
+ });
78
+ });
79
+ it("should handle multiple data parameters", async () => {
80
+ const logger = create_logger(mock_server);
81
+ const test_message = "message with multiple data";
54
82
  const data1 = { key1: "value1" };
55
83
  const data2 = { key2: "value2" };
56
84
  const data3 = "string data";
57
- logger.log("error", testMessage, data1, data2, data3);
58
- expect(mockConsoleError).toHaveBeenCalledWith(`ERROR: ${testMessage}`, data1, data2, data3);
59
- mockConsoleError.mockClear();
60
- logger.debug(testMessage, data1, data2, data3);
61
- expect(mockConsoleError).toHaveBeenCalledWith(`DEBUG: ${testMessage}`, data1, data2, data3);
85
+ logger.log("error", test_message, data1, data2, data3);
86
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
87
+ level: "error",
88
+ logger: ".",
89
+ data: { message: test_message, data: [data1, data2, data3] },
90
+ });
91
+ const set_level_handler = mock_set_request_handler.mock.calls[1][1];
92
+ await set_level_handler({
93
+ method: "logging/setLevel",
94
+ params: { level: "debug" },
95
+ });
96
+ mock_send_logging_message.mockClear();
97
+ logger.debug(test_message, data1, data2, data3);
98
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
99
+ level: "debug",
100
+ logger: ".",
101
+ data: { message: test_message, data: [data1, data2, data3] },
102
+ });
103
+ });
104
+ it("should not log and print error if an invalid log level is used directly", () => {
105
+ const logger = create_logger(mock_server);
106
+ const console_error_spy = jest
107
+ .spyOn(console, "error")
108
+ .mockImplementation(() => { });
109
+ logger.log("invalid_level", "this should not be logged");
110
+ expect(console_error_spy).toHaveBeenCalledWith("Internal Error: Invalid log level used: invalid_level");
111
+ expect(mock_send_logging_message).not.toHaveBeenCalled();
112
+ console_error_spy.mockRestore();
113
+ });
114
+ it("should log a warning if logging/setLevel receives an invalid log level", async () => {
115
+ create_logger(mock_server);
116
+ const set_level_handler = mock_set_request_handler.mock.calls[1][1];
117
+ await set_level_handler({
118
+ method: "logging/setLevel",
119
+ params: { level: "not_a_level" },
120
+ });
121
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
122
+ level: "warning",
123
+ logger: "logging",
124
+ data: {
125
+ message: "Invalid log level 'not_a_level' received",
126
+ },
127
+ });
128
+ });
129
+ it("should support valid per-logger setLevels, and emit debug message", async () => {
130
+ create_logger(mock_server);
131
+ const set_levels_handler = mock_set_request_handler.mock.calls[0][1];
132
+ const set_level_handler = mock_set_request_handler.mock.calls[1][1];
133
+ await set_level_handler({
134
+ method: "logging/setLevel",
135
+ params: { level: "debug" },
136
+ });
137
+ mock_send_logging_message.mockClear();
138
+ await set_levels_handler({
139
+ method: "logging/setLevels",
140
+ params: { levels: { app: "error" } },
141
+ });
142
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
143
+ level: "debug",
144
+ logger: "logging",
145
+ data: {
146
+ message: "Set log level for logger 'app' to 'error'",
147
+ },
148
+ });
149
+ });
150
+ it("should reset per-logger level to default when null is provided", async () => {
151
+ create_logger(mock_server);
152
+ const set_levels_handler = mock_set_request_handler.mock.calls[0][1];
153
+ const set_level_handler = mock_set_request_handler.mock.calls[1][1];
154
+ await set_level_handler({
155
+ method: "logging/setLevel",
156
+ params: { level: "debug" },
157
+ });
158
+ mock_send_logging_message.mockClear();
159
+ await set_levels_handler({
160
+ method: "logging/setLevels",
161
+ params: { levels: { app: "warning" } },
162
+ });
163
+ await set_levels_handler({
164
+ method: "logging/setLevels",
165
+ params: { levels: { app: null } },
166
+ });
167
+ expect(mock_send_logging_message.mock.calls.some((call) => {
168
+ const arg = call[0];
169
+ return (arg.level === "debug" &&
170
+ arg.logger === "logging" &&
171
+ arg.data &&
172
+ arg.data.message === "Reset log level for logger: app");
173
+ })).toBe(true);
174
+ });
175
+ it("should log a warning for invalid per-logger level in setLevels", async () => {
176
+ create_logger(mock_server);
177
+ const set_levels_handler = mock_set_request_handler.mock.calls[0][1];
178
+ const set_level_handler = mock_set_request_handler.mock.calls[1][1];
179
+ await set_level_handler({
180
+ method: "logging/setLevel",
181
+ params: { level: "debug" },
182
+ });
183
+ mock_send_logging_message.mockClear();
184
+ await set_levels_handler({
185
+ method: "logging/setLevels",
186
+ params: { levels: { "my.logger": "invalid" } },
187
+ });
188
+ expect(mock_send_logging_message).toHaveBeenCalledWith({
189
+ level: "warning",
190
+ logger: "logging",
191
+ data: {
192
+ message: "Invalid log level 'invalid' received for logger 'my.logger'",
193
+ },
194
+ });
62
195
  });
63
196
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawio-mcp-server",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Provides Draw.io services to MCP Clients",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -27,30 +27,28 @@
27
27
  "author": "Ladislav Gazo",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "1.10.1",
30
+ "@modelcontextprotocol/sdk": "1.17.5",
31
31
  "nanoid": "5.1.5",
32
- "pino": "9.6.0",
33
- "pino-pretty": "13.0.0",
34
32
  "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0",
35
33
  "zod": "3.24.3"
36
34
  },
37
35
  "devDependencies": {
38
- "@jest/globals": "29.7.0",
39
- "@types/jest": "29.5.12",
40
- "@types/node": "22.14.1",
41
- "globals": "15.0.0",
42
- "jest": "29.7.0",
43
- "jest-environment-node": "29.7.0",
36
+ "@jest/globals": "30.1.2",
37
+ "@types/jest": "30.0.0",
38
+ "@types/node": "24.3.1",
39
+ "globals": "16.3.0",
40
+ "jest": "30.1.3",
41
+ "jest-environment-node": "30.1.2",
44
42
  "prettier": "3.5.3",
45
43
  "rimraf": "6.0.1",
46
- "ts-jest": "29.3.2",
47
- "typescript": "5.8.3"
44
+ "ts-jest": "29.4.1",
45
+ "typescript": "5.9.2"
48
46
  },
49
47
  "scripts": {
50
48
  "build": "tsc",
51
49
  "ci": "pnpm install --frozen-lockfile",
52
50
  "dev": "tsc --watch",
53
- "inspect": "HOST=0.0.0.0 pnpx @modelcontextprotocol/inspector node build/index.js",
51
+ "inspect": "HOST=0.0.0.0 pnpx @modelcontextprotocol/inspector --config ./inspector.json --server drawio",
54
52
  "lint": "tsc --noEmit",
55
53
  "format": "prettier --write \"src/**/*.ts\"",
56
54
  "format:check": "prettier --check \"src/**/*.ts\"",