drawio-mcp-server 1.1.3 → 1.2.1

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,9 @@ 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 as create_console_logger } from "./mcp_console_logger.js";
13
+ import { create_logger as create_server_logger, validLogLevels, } from "./mcp_server_logger.js";
13
14
  const PORT = 3333;
14
- const log = create_logger();
15
15
  async function checkPortAvailable(port) {
16
16
  return new Promise((resolve) => {
17
17
  const server = createServer();
@@ -75,15 +75,30 @@ async function start_websocket_server() {
75
75
  });
76
76
  return app;
77
77
  }
78
+ const logger_type = process.env.LOGGER_TYPE;
79
+ let capabilities = {
80
+ resources: {},
81
+ tools: {},
82
+ };
83
+ if (logger_type === "mcp_server") {
84
+ capabilities = {
85
+ ...capabilities,
86
+ logging: {
87
+ setLevels: true,
88
+ levels: validLogLevels,
89
+ },
90
+ };
91
+ }
78
92
  // Create server instance
79
93
  const server = new McpServer({
80
94
  name: "drawio-mcp-server",
81
- version: "1.0.3",
82
- capabilities: {
83
- resources: {},
84
- tools: {},
85
- },
95
+ version: "1.2.1",
96
+ }, {
97
+ capabilities,
86
98
  });
99
+ const log = logger_type === "mcp_server"
100
+ ? create_server_logger(server)
101
+ : create_console_logger();
87
102
  const bus = create_bus(log)(emitter);
88
103
  const id_generator = nanoid_id_generator();
89
104
  const context = {
@@ -0,0 +1,116 @@
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
+ });
108
+ return {
109
+ log: (level, message, ...data) => {
110
+ log3(level, ".", { message, data });
111
+ },
112
+ debug: (message, ...data) => {
113
+ log3("debug", ".", { message, data });
114
+ },
115
+ };
116
+ }
@@ -0,0 +1,196 @@
1
+ import { jest } from "@jest/globals";
2
+ import { create_logger } from "./mcp_server_logger.js";
3
+ describe("create_logger", () => {
4
+ let mock_send_logging_message;
5
+ let mock_set_request_handler;
6
+ let mock_server;
7
+ beforeEach(() => {
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
+ };
16
+ });
17
+ afterEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+ it("should return a Logger object with log and debug methods", () => {
21
+ const logger = create_logger(mock_server);
22
+ expect(logger).toBeDefined();
23
+ expect(typeof logger.log).toBe("function");
24
+ expect(typeof logger.debug).toBe("function");
25
+ });
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";
82
+ const data1 = { key1: "value1" };
83
+ const data2 = { key2: "value2" };
84
+ const data3 = "string data";
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
+ });
195
+ });
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.1",
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\"",