@novorb/mcp-core 2.1.2 → 3.0.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.
Files changed (240) hide show
  1. package/README.md +77 -163
  2. package/bin/mcp-core-jest.mjs +112 -0
  3. package/dist/auth.d.ts +57 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +74 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/bootstrap.d.ts +129 -0
  8. package/dist/bootstrap.d.ts.map +1 -0
  9. package/dist/bootstrap.js +206 -0
  10. package/dist/bootstrap.js.map +1 -0
  11. package/dist/client-input.d.ts +72 -0
  12. package/dist/client-input.d.ts.map +1 -0
  13. package/dist/client-input.js +109 -0
  14. package/dist/client-input.js.map +1 -0
  15. package/dist/credential-checks.d.ts +80 -0
  16. package/dist/credential-checks.d.ts.map +1 -0
  17. package/dist/credential-checks.js +189 -0
  18. package/dist/credential-checks.js.map +1 -0
  19. package/dist/credential-health.d.ts +12 -0
  20. package/dist/credential-health.d.ts.map +1 -0
  21. package/dist/credential-health.js +12 -0
  22. package/dist/credential-health.js.map +1 -0
  23. package/dist/credential-report.d.ts +46 -0
  24. package/dist/credential-report.d.ts.map +1 -0
  25. package/dist/credential-report.js +135 -0
  26. package/dist/credential-report.js.map +1 -0
  27. package/dist/credential-status-tool.d.ts +20 -0
  28. package/dist/credential-status-tool.d.ts.map +1 -0
  29. package/dist/credential-status-tool.js +33 -0
  30. package/dist/credential-status-tool.js.map +1 -0
  31. package/dist/credentials.d.ts +288 -0
  32. package/dist/credentials.d.ts.map +1 -0
  33. package/dist/credentials.js +652 -0
  34. package/dist/credentials.js.map +1 -0
  35. package/dist/express-peers.d.ts +29 -0
  36. package/dist/express-peers.d.ts.map +1 -0
  37. package/dist/express-peers.js +36 -0
  38. package/dist/express-peers.js.map +1 -0
  39. package/dist/express.d.ts +74 -0
  40. package/dist/express.d.ts.map +1 -0
  41. package/dist/express.js +164 -0
  42. package/dist/express.js.map +1 -0
  43. package/dist/http-client.d.ts +277 -42
  44. package/dist/http-client.d.ts.map +1 -1
  45. package/dist/http-client.js +620 -180
  46. package/dist/http-client.js.map +1 -1
  47. package/dist/index.d.ts +27 -6
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +47 -8
  50. package/dist/index.js.map +1 -1
  51. package/dist/limits.d.ts +28 -0
  52. package/dist/limits.d.ts.map +1 -0
  53. package/dist/limits.js +37 -0
  54. package/dist/limits.js.map +1 -0
  55. package/dist/list-pagination.d.ts +41 -0
  56. package/dist/list-pagination.d.ts.map +1 -0
  57. package/dist/list-pagination.js +104 -0
  58. package/dist/list-pagination.js.map +1 -0
  59. package/dist/net.d.ts +112 -0
  60. package/dist/net.d.ts.map +1 -0
  61. package/dist/net.js +490 -0
  62. package/dist/net.js.map +1 -0
  63. package/dist/notifications.d.ts +78 -0
  64. package/dist/notifications.d.ts.map +1 -0
  65. package/dist/notifications.js +173 -0
  66. package/dist/notifications.js.map +1 -0
  67. package/dist/pagination.d.ts +236 -5
  68. package/dist/pagination.d.ts.map +1 -1
  69. package/dist/pagination.js +393 -6
  70. package/dist/pagination.js.map +1 -1
  71. package/dist/prompts.d.ts +70 -0
  72. package/dist/prompts.d.ts.map +1 -0
  73. package/dist/prompts.js +109 -0
  74. package/dist/prompts.js.map +1 -0
  75. package/dist/protocol-errors.d.ts +15 -0
  76. package/dist/protocol-errors.d.ts.map +1 -0
  77. package/dist/protocol-errors.js +28 -0
  78. package/dist/protocol-errors.js.map +1 -0
  79. package/dist/protocol-logging.d.ts +37 -0
  80. package/dist/protocol-logging.d.ts.map +1 -0
  81. package/dist/protocol-logging.js +77 -0
  82. package/dist/protocol-logging.js.map +1 -0
  83. package/dist/rate-limiter.d.ts +36 -0
  84. package/dist/rate-limiter.d.ts.map +1 -0
  85. package/dist/rate-limiter.js +79 -0
  86. package/dist/rate-limiter.js.map +1 -0
  87. package/dist/request-state.d.ts +74 -0
  88. package/dist/request-state.d.ts.map +1 -0
  89. package/dist/request-state.js +190 -0
  90. package/dist/request-state.js.map +1 -0
  91. package/dist/resources.d.ts +181 -0
  92. package/dist/resources.d.ts.map +1 -0
  93. package/dist/resources.js +302 -0
  94. package/dist/resources.js.map +1 -0
  95. package/dist/result-limits.d.ts +80 -0
  96. package/dist/result-limits.d.ts.map +1 -0
  97. package/dist/result-limits.js +248 -0
  98. package/dist/result-limits.js.map +1 -0
  99. package/dist/retry.d.ts +79 -23
  100. package/dist/retry.d.ts.map +1 -1
  101. package/dist/retry.js +170 -57
  102. package/dist/retry.js.map +1 -1
  103. package/dist/sdk/client-stdio.d.ts +6 -0
  104. package/dist/sdk/client-stdio.d.ts.map +1 -0
  105. package/dist/sdk/client-stdio.js +6 -0
  106. package/dist/sdk/client-stdio.js.map +1 -0
  107. package/dist/sdk/client.d.ts +6 -0
  108. package/dist/sdk/client.d.ts.map +1 -0
  109. package/dist/sdk/client.js +6 -0
  110. package/dist/sdk/client.js.map +1 -0
  111. package/dist/sdk/express.d.ts +3 -0
  112. package/dist/sdk/express.d.ts.map +1 -0
  113. package/dist/sdk/express.js +11 -0
  114. package/dist/sdk/express.js.map +1 -0
  115. package/dist/sdk/node.d.ts +6 -0
  116. package/dist/sdk/node.d.ts.map +1 -0
  117. package/dist/sdk/node.js +6 -0
  118. package/dist/sdk/node.js.map +1 -0
  119. package/dist/sdk/server-stdio.d.ts +6 -0
  120. package/dist/sdk/server-stdio.d.ts.map +1 -0
  121. package/dist/sdk/server-stdio.js +6 -0
  122. package/dist/sdk/server-stdio.js.map +1 -0
  123. package/dist/sdk/server.d.ts +6 -0
  124. package/dist/sdk/server.d.ts.map +1 -0
  125. package/dist/sdk/server.js +6 -0
  126. package/dist/sdk/server.js.map +1 -0
  127. package/dist/server.d.ts +88 -35
  128. package/dist/server.d.ts.map +1 -1
  129. package/dist/server.js +98 -44
  130. package/dist/server.js.map +1 -1
  131. package/dist/task-request.d.ts +20 -0
  132. package/dist/task-request.d.ts.map +1 -0
  133. package/dist/task-request.js +43 -0
  134. package/dist/task-request.js.map +1 -0
  135. package/dist/task-store.d.ts +230 -0
  136. package/dist/task-store.d.ts.map +1 -0
  137. package/dist/task-store.js +459 -0
  138. package/dist/task-store.js.map +1 -0
  139. package/dist/task-tools.d.ts +115 -0
  140. package/dist/task-tools.d.ts.map +1 -0
  141. package/dist/task-tools.js +397 -0
  142. package/dist/task-tools.js.map +1 -0
  143. package/dist/testing/client.d.ts +25 -0
  144. package/dist/testing/client.d.ts.map +1 -0
  145. package/dist/testing/client.js +11 -0
  146. package/dist/testing/client.js.map +1 -0
  147. package/dist/testing/in-memory.d.ts +26 -0
  148. package/dist/testing/in-memory.d.ts.map +1 -0
  149. package/dist/testing/in-memory.js +41 -0
  150. package/dist/testing/in-memory.js.map +1 -0
  151. package/dist/testing/index.d.ts +20 -0
  152. package/dist/testing/index.d.ts.map +1 -0
  153. package/dist/testing/index.js +20 -0
  154. package/dist/testing/index.js.map +1 -0
  155. package/dist/testing/isolated-dirs.d.ts +27 -0
  156. package/dist/testing/isolated-dirs.d.ts.map +1 -0
  157. package/dist/testing/isolated-dirs.js +67 -0
  158. package/dist/testing/isolated-dirs.js.map +1 -0
  159. package/dist/testing/logs.d.ts +61 -0
  160. package/dist/testing/logs.d.ts.map +1 -0
  161. package/dist/testing/logs.js +113 -0
  162. package/dist/testing/logs.js.map +1 -0
  163. package/dist/testing/raw-http.d.ts +25 -0
  164. package/dist/testing/raw-http.d.ts.map +1 -0
  165. package/dist/testing/raw-http.js +41 -0
  166. package/dist/testing/raw-http.js.map +1 -0
  167. package/dist/testing/raw-peer.d.ts +43 -0
  168. package/dist/testing/raw-peer.d.ts.map +1 -0
  169. package/dist/testing/raw-peer.js +101 -0
  170. package/dist/testing/raw-peer.js.map +1 -0
  171. package/dist/testing/recording-server.d.ts +38 -0
  172. package/dist/testing/recording-server.d.ts.map +1 -0
  173. package/dist/testing/recording-server.js +38 -0
  174. package/dist/testing/recording-server.js.map +1 -0
  175. package/dist/testing/server-process.d.ts +76 -0
  176. package/dist/testing/server-process.d.ts.map +1 -0
  177. package/dist/testing/server-process.js +241 -0
  178. package/dist/testing/server-process.js.map +1 -0
  179. package/dist/testing/tools.d.ts +53 -0
  180. package/dist/testing/tools.d.ts.map +1 -0
  181. package/dist/testing/tools.js +98 -0
  182. package/dist/testing/tools.js.map +1 -0
  183. package/dist/testing/unit.d.ts +52 -0
  184. package/dist/testing/unit.d.ts.map +1 -0
  185. package/dist/testing/unit.js +95 -0
  186. package/dist/testing/unit.js.map +1 -0
  187. package/dist/tools.d.ts +205 -0
  188. package/dist/tools.d.ts.map +1 -0
  189. package/dist/tools.js +370 -0
  190. package/dist/tools.js.map +1 -0
  191. package/dist/transport.d.ts +143 -52
  192. package/dist/transport.d.ts.map +1 -1
  193. package/dist/transport.js +292 -123
  194. package/dist/transport.js.map +1 -1
  195. package/dist/url-path.d.ts +49 -0
  196. package/dist/url-path.d.ts.map +1 -0
  197. package/dist/url-path.js +165 -0
  198. package/dist/url-path.js.map +1 -0
  199. package/dist/utils/dataStore.d.ts +143 -0
  200. package/dist/utils/dataStore.d.ts.map +1 -0
  201. package/dist/utils/dataStore.js +429 -0
  202. package/dist/utils/dataStore.js.map +1 -0
  203. package/dist/utils/envLoader.d.ts +129 -0
  204. package/dist/utils/envLoader.d.ts.map +1 -0
  205. package/dist/utils/envLoader.js +399 -0
  206. package/dist/utils/envLoader.js.map +1 -0
  207. package/dist/utils/logger.d.ts +181 -0
  208. package/dist/utils/logger.d.ts.map +1 -0
  209. package/dist/utils/logger.js +664 -0
  210. package/dist/utils/logger.js.map +1 -0
  211. package/dist/utils/redact.d.ts +50 -0
  212. package/dist/utils/redact.d.ts.map +1 -0
  213. package/dist/utils/redact.js +318 -0
  214. package/dist/utils/redact.js.map +1 -0
  215. package/docs/api-reference.md +29 -0
  216. package/docs/building-a-server.md +63 -0
  217. package/docs/credentials.md +131 -0
  218. package/docs/data-store.md +37 -0
  219. package/docs/env.md +38 -0
  220. package/docs/features.md +28 -0
  221. package/docs/http-client.md +116 -0
  222. package/docs/logger.md +100 -0
  223. package/docs/migrating-to-v3.md +45 -0
  224. package/docs/outbound-urls.md +104 -0
  225. package/docs/pagination.md +45 -0
  226. package/docs/protocol-features.md +62 -0
  227. package/docs/resources-prompts-tasks.md +95 -0
  228. package/docs/sdk.md +33 -0
  229. package/docs/server.md +68 -0
  230. package/docs/testing-harness.md +139 -0
  231. package/docs/testing.md +151 -0
  232. package/docs/tools.md +88 -0
  233. package/docs/transport.md +85 -0
  234. package/examples/testing/tools.integration.test.example.ts +312 -0
  235. package/examples/testing/tools.unit.test.example.ts +149 -0
  236. package/package.json +130 -17
  237. package/dist/logger.d.ts +0 -80
  238. package/dist/logger.d.ts.map +0 -1
  239. package/dist/logger.js +0 -237
  240. package/dist/logger.js.map +0 -1
@@ -0,0 +1,664 @@
1
+ /**
2
+ * @novorb/mcp-core - Logger
3
+ *
4
+ * One logging utility for every MCP server. Sinks:
5
+ * - console: process.stderr only (never stdout, the stdio protocol channel)
6
+ * - <name>.log: the main log, every entry at or above the level
7
+ * - exceptions.log: ERROR entries only
8
+ * - debug.log: DEBUG entries only (so only when the level is DEBUG)
9
+ *
10
+ * Log directory: `<rootDir>/.logs` by default (rootDir = the consumer's package root, see
11
+ * `packageRootFrom`). LOG_PATH overrides it (absolute, or relative to rootDir). Without rootDir and
12
+ * without an absolute LOG_PATH the file sinks stay off (one console warning from initLogger); nothing
13
+ * is ever written relative to cwd.
14
+ *
15
+ * Switches (explicit config overrides env, env overrides the default):
16
+ * LOG_LEVEL DEBUG | INFO | WARN | ERROR default INFO
17
+ * LOG_FORMAT text | json default text
18
+ * LOG_CONSOLE true | false default true
19
+ * LOG_FILE_MAIN true | false default true (needs a directory)
20
+ * LOG_FILE_EXCEPTIONS true | false default true (needs a directory)
21
+ * LOG_FILE_DEBUG true | false default true (needs a directory)
22
+ * LOG_PATH log directory default <rootDir>/.logs
23
+ * LOG_FILE main file name (absolute = as-is) default <name>.log
24
+ *
25
+ * Text line: 2026-03-22T13:27:58.513Z | INFO | Server | Starting... | {"meta":"json"}
26
+ * JSON line: {"level":"INFO","timestamp":"...","component":"Server","message":"Starting...",...meta}
27
+ *
28
+ * Every entry is sanitised: sensitive-keyed meta fields become [REDACTED], secret query parameters
29
+ * in strings become name=[REDACTED], and PII in the message and every meta string is masked
30
+ * ([EMAIL] [PHONE] [CARD] [IBAN] [JWT] [TOKEN] [IP]). Text mode also strips control characters.
31
+ *
32
+ * `initLogger` reconfigures the singleton IN PLACE, so a logger obtained with `getLogger()` at import
33
+ * time (before init) writes to the configured sinks after init.
34
+ */
35
+ import * as fs from 'node:fs';
36
+ import * as path from 'node:path';
37
+ import { fileURLToPath } from 'node:url';
38
+ import { redactSecrets, sanitizeString } from './redact.js';
39
+ export { maskPii, redactSecrets } from './redact.js';
40
+ export var LogLevel;
41
+ (function (LogLevel) {
42
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
43
+ LogLevel[LogLevel["INFO"] = 1] = "INFO";
44
+ LogLevel[LogLevel["WARN"] = 2] = "WARN";
45
+ LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
46
+ })(LogLevel || (LogLevel = {}));
47
+ const DEFAULTS = {
48
+ level: LogLevel.INFO,
49
+ format: 'text',
50
+ console: true,
51
+ fileMain: true,
52
+ fileExceptions: true,
53
+ fileDebug: true,
54
+ maxLogSize: 10 * 1024 * 1024,
55
+ maxLogFiles: 5,
56
+ captureProcessErrors: false,
57
+ };
58
+ function parseLogLevel(value) {
59
+ if (!value)
60
+ return undefined;
61
+ const upper = value.toUpperCase();
62
+ const byName = LogLevel;
63
+ return upper in LogLevel && typeof byName[upper] === 'number' ? byName[upper] : undefined;
64
+ }
65
+ function parseFormat(value) {
66
+ const v = value?.toLowerCase();
67
+ return v === 'text' || v === 'json' ? v : undefined;
68
+ }
69
+ function parseBool(value) {
70
+ const v = value?.trim().toLowerCase();
71
+ if (v === 'true' || v === '1' || v === 'yes' || v === 'on')
72
+ return true;
73
+ if (v === 'false' || v === '0' || v === 'no' || v === 'off')
74
+ return false;
75
+ return undefined;
76
+ }
77
+ function defined(obj) {
78
+ const out = {};
79
+ if (!obj)
80
+ return out;
81
+ for (const [k, v] of Object.entries(obj)) {
82
+ if (v !== undefined)
83
+ out[k] = v;
84
+ }
85
+ return out;
86
+ }
87
+ /** Defaults, then env, then explicit options. */
88
+ function buildConfig(options) {
89
+ const env = process.env;
90
+ const fromEnv = defined({
91
+ level: parseLogLevel(env.LOG_LEVEL),
92
+ format: parseFormat(env.LOG_FORMAT),
93
+ console: parseBool(env.LOG_CONSOLE),
94
+ fileMain: parseBool(env.LOG_FILE_MAIN),
95
+ fileExceptions: parseBool(env.LOG_FILE_EXCEPTIONS),
96
+ fileDebug: parseBool(env.LOG_FILE_DEBUG),
97
+ logDir: env.LOG_PATH || undefined,
98
+ logFile: env.LOG_FILE || undefined,
99
+ });
100
+ return { ...DEFAULTS, ...fromEnv, ...defined(options) };
101
+ }
102
+ function sanitizeName(name) {
103
+ const safe = name?.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\.+/, '');
104
+ return safe ? safe : undefined;
105
+ }
106
+ /**
107
+ * Windows drive-relative (`C:logs`) and root-relative (`\logs`) paths resolve against a current
108
+ * directory, which would break the "never cwd" rule, so they are refused.
109
+ */
110
+ function isCwdDependent(p) {
111
+ if (/^[A-Za-z]:(?![\\/])/.test(p))
112
+ return true;
113
+ return process.platform === 'win32' && /^[\\/](?![\\/])/.test(p);
114
+ }
115
+ function resolveSinks(c) {
116
+ let dir;
117
+ let dirProblem;
118
+ if (c.logDir) {
119
+ if (isCwdDependent(c.logDir))
120
+ dirProblem = `LOG_PATH "${c.logDir}" is drive- or root-relative`;
121
+ else if (path.isAbsolute(c.logDir))
122
+ dir = c.logDir;
123
+ else if (c.rootDir)
124
+ dir = path.resolve(c.rootDir, c.logDir);
125
+ else
126
+ dirProblem = `LOG_PATH "${c.logDir}" is relative and no rootDir was given`;
127
+ }
128
+ else if (c.rootDir) {
129
+ dir = path.join(path.resolve(c.rootDir), '.logs');
130
+ }
131
+ else {
132
+ dirProblem = 'no rootDir and no LOG_PATH';
133
+ }
134
+ const sinks = { console: c.console };
135
+ const problems = [];
136
+ let missingName = false;
137
+ const badFile = c.logFile !== undefined && isCwdDependent(c.logFile);
138
+ const absoluteMain = !badFile && c.logFile !== undefined && path.isAbsolute(c.logFile);
139
+ if (c.fileMain) {
140
+ if (badFile) {
141
+ problems.push(`main log disabled: LOG_FILE "${c.logFile}" is drive- or root-relative`);
142
+ }
143
+ else if (absoluteMain) {
144
+ sinks.main = c.logFile;
145
+ }
146
+ else if (dir) {
147
+ const file = c.logFile ?? (sanitizeName(c.name) ? `${sanitizeName(c.name)}.log` : undefined);
148
+ if (file)
149
+ sinks.main = path.join(dir, file);
150
+ else
151
+ missingName = true;
152
+ }
153
+ }
154
+ if (c.fileExceptions && dir)
155
+ sinks.exceptions = path.join(dir, 'exceptions.log');
156
+ if (c.fileDebug && dir)
157
+ sinks.debug = path.join(dir, 'debug.log');
158
+ if (!dir && dirProblem) {
159
+ const mainNeedsDir = c.fileMain && !absoluteMain && !badFile;
160
+ const others = c.fileExceptions || c.fileDebug;
161
+ if (mainNeedsDir)
162
+ problems.unshift(`File logging disabled: ${dirProblem}`);
163
+ else if (others)
164
+ problems.unshift(`exceptions.log and debug.log disabled: ${dirProblem}`);
165
+ }
166
+ const hint = !dir && dirProblem ? ' (pass initLogger({ rootDir }) or set an absolute LOG_PATH)' : '';
167
+ return { sinks, warning: problems.length ? `${problems.join('; ')}${hint}` : undefined, missingName };
168
+ }
169
+ function isBrokenPipe(err) {
170
+ const code = typeof err === 'object' && err !== null ? err.code : undefined;
171
+ return code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED' || code === 'ERR_STREAM_WRITE_AFTER_END';
172
+ }
173
+ function describeError(err) {
174
+ return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
175
+ }
176
+ /** Collapse C0 control chars (incl. CR/LF) + DEL to spaces so a logged value can't forge log lines. */
177
+ function stripControlChars(s) {
178
+ let out = '';
179
+ for (let i = 0; i < s.length; i++) {
180
+ const code = s.charCodeAt(i);
181
+ out += code < 0x20 || code === 0x7f ? ' ' : s[i];
182
+ }
183
+ return out;
184
+ }
185
+ /** Registered secret values shorter than this are ignored (they would mask ordinary words). */
186
+ export const MIN_SECRET_LENGTH = 6;
187
+ const SECRET_MASK = '[SECRET]';
188
+ const secretValues = new Set();
189
+ /**
190
+ * Register secret VALUES (API tokens, passwords) that are masked verbatim as `[SECRET]` wherever they
191
+ * appear in any log entry, in both line formats (raw and JSON-escaped). The env loader calls this for
192
+ * every set `secret: true` variable. Process-global; values shorter than MIN_SECRET_LENGTH are ignored.
193
+ */
194
+ export function registerSecretValues(values) {
195
+ for (const value of values) {
196
+ if (typeof value !== 'string' || value.length < MIN_SECRET_LENGTH)
197
+ continue;
198
+ secretValues.add(value);
199
+ const escaped = JSON.stringify(value).slice(1, -1);
200
+ if (escaped !== value)
201
+ secretValues.add(escaped);
202
+ }
203
+ }
204
+ /** Forget every registered secret value (tests). */
205
+ export function clearSecretValues() {
206
+ secretValues.clear();
207
+ }
208
+ /** Replace every registered secret value in `s` with `[SECRET]` (longest first). */
209
+ export function maskSecretValues(s) {
210
+ if (secretValues.size === 0)
211
+ return s;
212
+ let out = s;
213
+ for (const secret of [...secretValues].sort((a, b) => b.length - a.length)) {
214
+ if (out.includes(secret))
215
+ out = out.split(secret).join(SECRET_MASK);
216
+ }
217
+ return out;
218
+ }
219
+ const logListeners = new Set();
220
+ /**
221
+ * Subscribe to accepted log entries (process-global, survives `initLogger` reconfiguration; cleared by
222
+ * `resetLogger`). Returns the unsubscribe function. A throwing listener is ignored, and a listener that
223
+ * logs from inside its own callback is not re-entered.
224
+ */
225
+ export function addLogListener(listener) {
226
+ logListeners.add(listener);
227
+ return () => {
228
+ logListeners.delete(listener);
229
+ };
230
+ }
231
+ let notifyingListeners = false;
232
+ function sanitizedMeta(meta) {
233
+ if (meta === undefined)
234
+ return undefined;
235
+ const json = JSON.stringify(redactSecrets(meta));
236
+ return json === undefined ? undefined : JSON.parse(maskSecretValues(json));
237
+ }
238
+ function notifyListeners(level, name, message, context, meta) {
239
+ if (logListeners.size === 0 || notifyingListeners)
240
+ return;
241
+ notifyingListeners = true;
242
+ try {
243
+ let entry;
244
+ try {
245
+ entry = {
246
+ level,
247
+ ...(name !== undefined ? { logger: name } : {}),
248
+ context: maskSecretValues(sanitizeString(context ?? 'General')),
249
+ message: maskSecretValues(sanitizeString(maskSecretValues(message))),
250
+ meta: sanitizedMeta(meta),
251
+ };
252
+ }
253
+ catch {
254
+ entry = { level, context: 'Logger', message: '[unformattable entry]' };
255
+ }
256
+ for (const listener of logListeners) {
257
+ try {
258
+ listener(entry);
259
+ }
260
+ catch {
261
+ // A listener never breaks logging.
262
+ }
263
+ }
264
+ }
265
+ finally {
266
+ notifyingListeners = false;
267
+ }
268
+ }
269
+ const LEVEL_NAMES = {
270
+ [LogLevel.DEBUG]: 'DEBUG',
271
+ [LogLevel.INFO]: 'INFO ',
272
+ [LogLevel.WARN]: 'WARN ',
273
+ [LogLevel.ERROR]: 'ERROR',
274
+ };
275
+ /** A rendered line longer than this is cut (text) or loses its meta (json). */
276
+ const MAX_LINE_CHARS = 256 * 1024;
277
+ const RESERVED_JSON_FIELDS = new Set(['level', 'timestamp', 'component', 'message', 'meta']);
278
+ // stderr and the process are global, so the broken-pipe state, the stderr guard and the fatal
279
+ // re-entrancy flag are too.
280
+ let stderrBroken = false;
281
+ let stderrGuard = null;
282
+ const consoleLoggers = new Set();
283
+ let dirWarningShown = false;
284
+ let fatalInProgress = false;
285
+ /**
286
+ * Keep one stderr `error` listener while any logger has the console on. An EPIPE marks stderr broken
287
+ * (the console sink stops); any other error is re-thrown when no one else listens, exactly as Node
288
+ * would have done without this listener.
289
+ */
290
+ function updateStderrGuard(logger, consoleOn) {
291
+ if (consoleOn)
292
+ consoleLoggers.add(logger);
293
+ else
294
+ consoleLoggers.delete(logger);
295
+ if (consoleLoggers.size > 0 && !stderrGuard) {
296
+ stderrGuard = (err) => {
297
+ if (isBrokenPipe(err)) {
298
+ stderrBroken = true;
299
+ return;
300
+ }
301
+ if (process.stderr.listenerCount('error') === 1)
302
+ throw err;
303
+ };
304
+ process.stderr.on('error', stderrGuard);
305
+ }
306
+ else if (consoleLoggers.size === 0 && stderrGuard) {
307
+ process.stderr.removeListener('error', stderrGuard);
308
+ stderrGuard = null;
309
+ }
310
+ }
311
+ export class Logger {
312
+ config = DEFAULTS;
313
+ resolved = { console: true };
314
+ fatalHandlers = null;
315
+ // Known size per file, so rotation needs one stat per file per configuration, not one per line.
316
+ sizes = new Map();
317
+ constructor(options) {
318
+ this.configure(options);
319
+ }
320
+ /**
321
+ * Reconfigure in place: recompute every setting from defaults + env + `options` (exactly as the
322
+ * constructor does), create the log directories, and install or remove the process-error handlers.
323
+ */
324
+ configure(options) {
325
+ this.config = buildConfig(options);
326
+ this.resolved = resolveSinks(this.config).sinks;
327
+ this.sizes.clear();
328
+ for (const file of [this.resolved.main, this.resolved.exceptions, this.resolved.debug]) {
329
+ if (!file)
330
+ continue;
331
+ try {
332
+ fs.mkdirSync(path.dirname(file), { recursive: true });
333
+ }
334
+ catch {
335
+ // Silent: a file sink may fail without affecting the others.
336
+ }
337
+ }
338
+ updateStderrGuard(this, this.resolved.console);
339
+ if (this.config.captureProcessErrors)
340
+ this.attachProcessHandlers();
341
+ else
342
+ this.detachProcessHandlers();
343
+ }
344
+ /** The active sinks (console flag and absolute file paths). */
345
+ sinks() {
346
+ return { ...this.resolved };
347
+ }
348
+ /** Remove the process-error handlers and this logger's claim on the stderr guard. */
349
+ detach() {
350
+ this.detachProcessHandlers();
351
+ updateStderrGuard(this, false);
352
+ }
353
+ /** Remove the process-error handlers, if installed. */
354
+ detachProcessHandlers() {
355
+ if (!this.fatalHandlers)
356
+ return;
357
+ process.removeListener('uncaughtException', this.fatalHandlers.exception);
358
+ process.removeListener('unhandledRejection', this.fatalHandlers.rejection);
359
+ this.fatalHandlers = null;
360
+ }
361
+ attachProcessHandlers() {
362
+ if (this.fatalHandlers)
363
+ return;
364
+ this.fatalHandlers = {
365
+ exception: (err) => this.onFatal('Uncaught exception', err),
366
+ rejection: (reason) => this.onFatal('Unhandled rejection', reason),
367
+ };
368
+ process.on('uncaughtException', this.fatalHandlers.exception);
369
+ process.on('unhandledRejection', this.fatalHandlers.rejection);
370
+ }
371
+ onFatal(kind, err) {
372
+ // Process-wide: a second capturing logger, or a fault raised while handling one, does nothing.
373
+ if (fatalInProgress)
374
+ return;
375
+ fatalInProgress = true;
376
+ try {
377
+ // An EPIPE means stderr is gone: logging it back to stderr is exactly the runaway loop.
378
+ if (isBrokenPipe(err))
379
+ stderrBroken = true;
380
+ const stack = err instanceof Error && err.stack ? { stack: err.stack } : undefined;
381
+ this.write(LogLevel.ERROR, `${kind}: ${describeError(err)}`, 'Process', stack);
382
+ }
383
+ finally {
384
+ process.exit(1);
385
+ }
386
+ }
387
+ /** Write one WARN line to the console sink only (used for the directory warning). */
388
+ consoleWarn(message, context) {
389
+ if (this.resolved.console)
390
+ this.writeConsole(this.format(LogLevel.WARN, message, context));
391
+ }
392
+ formatText(level, message, context, meta) {
393
+ const timestamp = new Date().toISOString();
394
+ const levelStr = LEVEL_NAMES[level] ?? LogLevel[level].padEnd(5);
395
+ const contextStr = stripControlChars(sanitizeString(context ?? 'General'));
396
+ const safeMessage = stripControlChars(sanitizeString(message));
397
+ const metaStr = meta !== undefined ? ` | ${JSON.stringify(redactSecrets(meta))}` : '';
398
+ let line = `${timestamp} | ${levelStr} | ${contextStr} | ${safeMessage}${metaStr}`;
399
+ if (line.length > MAX_LINE_CHARS) {
400
+ line = `${line.slice(0, MAX_LINE_CHARS)}...[TRUNCATED ${line.length - MAX_LINE_CHARS} chars]`;
401
+ }
402
+ return `${line}\n`;
403
+ }
404
+ formatJson(level, message, context, meta) {
405
+ const entry = {
406
+ level: LogLevel[level],
407
+ timestamp: new Date().toISOString(),
408
+ component: sanitizeString(context ?? 'General'),
409
+ message: sanitizeString(message),
410
+ };
411
+ if (meta !== undefined) {
412
+ const safeMeta = redactSecrets(meta);
413
+ if (typeof safeMeta === 'object' && safeMeta !== null && !Array.isArray(safeMeta)) {
414
+ // Meta never overwrites the entry's own fields: colliding keys go under `meta`.
415
+ const collisions = {};
416
+ for (const [k, v] of Object.entries(safeMeta)) {
417
+ if (RESERVED_JSON_FIELDS.has(k))
418
+ collisions[k] = v;
419
+ else
420
+ entry[k] = v;
421
+ }
422
+ if (Object.keys(collisions).length > 0)
423
+ entry.meta = collisions;
424
+ }
425
+ else {
426
+ entry.meta = safeMeta;
427
+ }
428
+ }
429
+ let line = JSON.stringify(entry);
430
+ if (line.length > MAX_LINE_CHARS) {
431
+ line = JSON.stringify({
432
+ level: entry.level,
433
+ timestamp: entry.timestamp,
434
+ component: entry.component,
435
+ message: String(entry.message).slice(0, MAX_LINE_CHARS / 2),
436
+ meta: `[TRUNCATED: meta too large (${line.length} chars)]`,
437
+ });
438
+ }
439
+ return `${line}\n`;
440
+ }
441
+ format(level, message, context, meta) {
442
+ try {
443
+ // Registered secrets are masked in the raw message first (before truncation could split one),
444
+ // then in the whole rendered line (context and every meta string included).
445
+ const safeMessage = maskSecretValues(message);
446
+ const line = this.config.format === 'json'
447
+ ? this.formatJson(level, safeMessage, context, meta)
448
+ : this.formatText(level, safeMessage, context, meta);
449
+ return maskSecretValues(line);
450
+ }
451
+ catch {
452
+ // Logging must never throw into the caller: drop the meta and keep the line.
453
+ if (meta === undefined)
454
+ return `${new Date().toISOString()} | ${LEVEL_NAMES[level]} | Logger | [unformattable entry]\n`;
455
+ return this.format(level, message, context, '[unserialisable meta]');
456
+ }
457
+ }
458
+ write(level, message, context, meta) {
459
+ if (level < this.config.level)
460
+ return;
461
+ const line = this.format(level, message, context, meta);
462
+ const { main, exceptions, debug } = this.resolved;
463
+ if (main)
464
+ this.writeFile(main, line);
465
+ if (exceptions && level === LogLevel.ERROR)
466
+ this.writeFile(exceptions, line);
467
+ if (debug && level === LogLevel.DEBUG)
468
+ this.writeFile(debug, line);
469
+ if (this.resolved.console)
470
+ this.writeConsole(line);
471
+ notifyListeners(level, this.config.name, message, context, meta);
472
+ }
473
+ writeConsole(line) {
474
+ if (stderrBroken)
475
+ return;
476
+ try {
477
+ process.stderr.write(line);
478
+ }
479
+ catch (err) {
480
+ if (isBrokenPipe(err))
481
+ stderrBroken = true;
482
+ }
483
+ }
484
+ knownSize(file) {
485
+ let size = this.sizes.get(file);
486
+ if (size === undefined) {
487
+ try {
488
+ size = fs.statSync(file).size;
489
+ }
490
+ catch {
491
+ size = 0;
492
+ }
493
+ this.sizes.set(file, size);
494
+ }
495
+ return size;
496
+ }
497
+ writeFile(file, line) {
498
+ try {
499
+ if (this.knownSize(file) > this.config.maxLogSize)
500
+ this.rotate(file);
501
+ try {
502
+ fs.appendFileSync(file, line, 'utf8');
503
+ }
504
+ catch (err) {
505
+ // The directory was removed after configuration: recreate it once and retry.
506
+ if (err.code !== 'ENOENT')
507
+ throw err;
508
+ fs.mkdirSync(path.dirname(file), { recursive: true });
509
+ fs.appendFileSync(file, line, 'utf8');
510
+ }
511
+ this.sizes.set(file, this.knownSize(file) + Buffer.byteLength(line, 'utf8'));
512
+ }
513
+ catch {
514
+ // Silent: a file sink may fail without affecting the others.
515
+ }
516
+ }
517
+ /** maxLogFiles counts the active file: N keeps the active file plus N-1 rotated ones (minimum 1). */
518
+ rotate(file) {
519
+ try {
520
+ const keep = Math.max(1, Math.floor(this.config.maxLogFiles));
521
+ if (keep === 1) {
522
+ fs.unlinkSync(file);
523
+ }
524
+ else {
525
+ if (fs.existsSync(`${file}.${keep - 1}`))
526
+ fs.unlinkSync(`${file}.${keep - 1}`);
527
+ for (let i = keep - 2; i > 0; i--) {
528
+ if (fs.existsSync(`${file}.${i}`))
529
+ fs.renameSync(`${file}.${i}`, `${file}.${i + 1}`);
530
+ }
531
+ fs.renameSync(file, `${file}.1`);
532
+ }
533
+ }
534
+ catch {
535
+ // Silent
536
+ }
537
+ this.sizes.set(file, 0);
538
+ }
539
+ debug(message, context, meta) {
540
+ this.write(LogLevel.DEBUG, message, context, meta);
541
+ }
542
+ info(message, context, meta) {
543
+ this.write(LogLevel.INFO, message, context, meta);
544
+ }
545
+ warn(message, context, meta) {
546
+ this.write(LogLevel.WARN, message, context, meta);
547
+ }
548
+ error(message, context, meta) {
549
+ this.write(LogLevel.ERROR, message, context, meta);
550
+ }
551
+ setLevel(level) {
552
+ this.config = { ...this.config, level };
553
+ }
554
+ }
555
+ let loggerInstance = null;
556
+ /**
557
+ * Initialise (or reconfigure in place) the logger singleton. Call once at startup with the server
558
+ * `name` and its package `rootDir`. Throws when the main file is on, a directory exists, and neither
559
+ * `name` nor LOG_FILE is given. Warns once on the console when file sinks are on but there is no
560
+ * usable directory.
561
+ */
562
+ export function initLogger(options) {
563
+ const resolution = resolveSinks(buildConfig(options));
564
+ if (resolution.missingName) {
565
+ throw new Error('initLogger: `name` is required for the main log file (or set LOG_FILE, or fileMain: false)');
566
+ }
567
+ if (loggerInstance)
568
+ loggerInstance.configure(options);
569
+ else
570
+ loggerInstance = new Logger(options);
571
+ if (resolution.warning && !dirWarningShown) {
572
+ dirWarningShown = true;
573
+ loggerInstance.consoleWarn(resolution.warning, 'Logger');
574
+ }
575
+ return loggerInstance;
576
+ }
577
+ /** Get the singleton, creating a default (console-only unless LOG_PATH is absolute) one if needed. */
578
+ export function getLogger(options) {
579
+ if (!loggerInstance)
580
+ loggerInstance = new Logger(options);
581
+ return loggerInstance;
582
+ }
583
+ /** Drop the singleton, its process handlers and the process-global console state (tests). */
584
+ export function resetLogger() {
585
+ loggerInstance?.detach();
586
+ loggerInstance = null;
587
+ consoleLoggers.clear();
588
+ if (stderrGuard)
589
+ process.stderr.removeListener('error', stderrGuard);
590
+ stderrGuard = null;
591
+ stderrBroken = false;
592
+ dirWarningShown = false;
593
+ fatalInProgress = false;
594
+ secretValues.clear();
595
+ logListeners.clear();
596
+ notifyingListeners = false;
597
+ }
598
+ /**
599
+ * The package root of the module at `importMetaUrl` (pass `import.meta.url`): the nearest ancestor
600
+ * directory holding a package.json. Works from `src/` and `dist/` alike and is Windows-safe
601
+ * (`fileURLToPath`, never `URL.pathname`). Also accepts a plain file path.
602
+ */
603
+ export function packageRootFrom(importMetaUrl) {
604
+ const asString = importMetaUrl instanceof URL ? importMetaUrl.href : importMetaUrl;
605
+ const file = asString.startsWith('file:') ? fileURLToPath(asString) : path.resolve(asString);
606
+ let dir = path.dirname(file);
607
+ for (;;) {
608
+ if (fs.existsSync(path.join(dir, 'package.json')))
609
+ return dir;
610
+ const parent = path.dirname(dir);
611
+ if (parent === dir)
612
+ throw new Error(`packageRootFrom: no package.json above ${file}`);
613
+ dir = parent;
614
+ }
615
+ }
616
+ const MAX_NESTED_KEYS = 20;
617
+ const TOOL_CONTEXT = 'Tools';
618
+ function describeShape(value) {
619
+ if (value === null)
620
+ return 'null';
621
+ if (Array.isArray(value))
622
+ return `array(${value.length})`;
623
+ if (typeof value === 'string')
624
+ return `string(${value.length})`;
625
+ if (typeof value === 'object') {
626
+ const keys = Object.keys(value);
627
+ const shown = keys.slice(0, MAX_NESTED_KEYS).join(',');
628
+ const more = keys.length > MAX_NESTED_KEYS ? `,...+${keys.length - MAX_NESTED_KEYS}` : '';
629
+ return `object{${shown}${more}}`;
630
+ }
631
+ return typeof value;
632
+ }
633
+ /**
634
+ * Summarise tool arguments by shape only, never by value. Per top-level key: `string(<len>)`,
635
+ * `array(<len>)`, `object{<key names, one level, max 20>}`, `number`, `boolean`, `null`, `undefined`,
636
+ * `bigint`, `function`. Non-object args are summarised under `_args`; null/undefined give `{}`.
637
+ */
638
+ export function summarizeArgs(args) {
639
+ if (args === null || args === undefined)
640
+ return {};
641
+ if (typeof args !== 'object' || Array.isArray(args))
642
+ return { _args: describeShape(args) };
643
+ const out = {};
644
+ for (const [k, v] of Object.entries(args))
645
+ out[k] = describeShape(v);
646
+ return out;
647
+ }
648
+ /** INFO `Tool call: <name>` with meta `{ tool, args: summarizeArgs(args) }`. Never the argument values. */
649
+ export function logToolCall(name, args, context) {
650
+ getLogger().info(`Tool call: ${name}`, context ?? TOOL_CONTEXT, { tool: name, args: summarizeArgs(args) });
651
+ }
652
+ /**
653
+ * `Tool result: <name>` with meta `{ tool, isError, durationMs }` (durationMs rounded). INFO on
654
+ * success, WARN when isError. Never the result content.
655
+ */
656
+ export function logToolResult(name, outcome, context) {
657
+ const meta = { tool: name, isError: outcome.isError, durationMs: Math.round(outcome.durationMs) };
658
+ if (outcome.isError)
659
+ getLogger().warn(`Tool result: ${name}`, context ?? TOOL_CONTEXT, meta);
660
+ else
661
+ getLogger().info(`Tool result: ${name}`, context ?? TOOL_CONTEXT, meta);
662
+ }
663
+ export default getLogger;
664
+ //# sourceMappingURL=logger.js.map