Skip to content

Commit cc82731

Browse files
authored
feat: log channels, avoid polluting stdout (#25)
* docs, readme cli options for logging * lint, flag console methods to avoid stdio pollution * logger, use node diagnostics_channel for logs * options, context driven session id and log channel name * server.logger, mcp specific log wrappers * server.helpers, freeze and merge object helpers * testing unit, move local env flag over to jest setup * testing e2e, allow mcp testing client to accept mcp server logs * typings, refactor for readonly session values
1 parent e31c15b commit cc82731

30 files changed

+1769
-254
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,32 @@ Returned content format:
8383
- For each entry in urlList, the server loads its content, prefixes it with a header like: `# Documentation from <resolved-path-or-url>` and joins multiple entries using a separator: `\n\n---\n\n`.
8484
- If an entry fails to load, an inline error message is included for that entry.
8585

86+
## Logging
87+
88+
The server uses a `diagnostics_channel`–based logger that keeps STDIO stdout pure by default. No terminal output occurs unless you enable a sink.
89+
90+
- Defaults: `level='info'`, `stderr=false`, `protocol=false`
91+
- Sinks (opt‑in): `--log-stderr`, `--log-protocol` (forwards to MCP clients; requires advertising `capabilities.logging`)
92+
- Transport tag: `transport: 'stdio' | 'http'` (no I/O side effects)
93+
- Environment variables: not used for logging in this version
94+
- Process scope: logger is process‑global; recommend one server per process
95+
96+
CLI examples:
97+
98+
```bash
99+
patternfly-mcp # default (no terminal output)
100+
patternfly-mcp --verbose # level=debug (still no stderr)
101+
patternfly-mcp --log-stderr # enable stderr sink
102+
patternfly-mcp --log-level warn --log-stderr
103+
patternfly-mcp --log-protocol --log-level info # forward logs to MCP clients
104+
```
105+
106+
Programmatic:
107+
108+
```ts
109+
await start({ logging: { level: 'info', stderr: false, protocol: false } });
110+
```
111+
86112
### Tool: usePatternFlyDocs
87113

88114
Use this to fetch high-level index content (for example, a local README.md that contains relevant links, or llms.txt files in docs-host mode). From that content, you can select specific URLs to pass to fetchDocs.

eslint.config.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ export default [
5959
varsIgnorePattern: '^_'
6060
}],
6161
'n/no-process-exit': 0,
62-
'no-console': 0
62+
// Disallow console.log/info in runtime to protect STDIO; allow warn/error
63+
'no-console': ['error', { allow: ['warn', 'error'] }]
6364
}
6465
},
6566

@@ -73,7 +74,11 @@ export default [
7374
rules: {
7475
'@typescript-eslint/no-explicit-any': 0,
7576
'@typescript-eslint/ban-ts-comment': 1,
76-
'no-sparse-arrays': 0
77+
'no-sparse-arrays': 0,
78+
// Allow console usage in tests (spies, debug)
79+
'no-console': 0,
80+
// Relax stylistic padding in tests to reduce churn
81+
'@stylistic/padding-line-between-statements': 0
7782
}
7883
}
7984
];

jest.setupTests.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
// Shared helpers for Jest unit tests
22

3+
/**
4+
* Set NODE_ENV to 'local' for local testing.
5+
*/
6+
process.env.NODE_ENV = 'local';
7+
38
/**
49
* Note: Mock @patternfly/patternfly-component-schemas/json to avoid top-level await issues in Jest
510
* - Individual tests can override mock

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
"build:clean": "rm -rf dist",
2424
"build:watch": "npm run build -- --watch",
2525
"release": "changelog --non-cc --link-url https://github.com/patternfly/patternfly-mcp.git",
26-
"start": "node dist/index.js",
26+
"start": "node dist/index.js --verbose --log-stderr",
2727
"start:dev": "tsx watch src/index.ts",
28-
"test": "export NODE_ENV=local; npm run test:lint && npm run test:types && jest --roots=src/",
28+
"test": "npm run test:lint && npm run test:types && jest --roots=src/",
2929
"test:dev": "npm test -- --watchAll",
3030
"test:integration": "npm run build && jest --roots=tests/",
3131
"test:integration-dev": "npm run test:integration -- --watchAll",

src/__tests__/__snapshots__/index.test.ts.snap

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ exports[`main should merge default, cli and programmatic options, merge programm
66
[
77
{
88
"docsHost": true,
9+
"logging": {
10+
"level": "info",
11+
"protocol": false,
12+
"stderr": false,
13+
},
914
},
1015
],
1116
],
@@ -24,6 +29,11 @@ exports[`main should merge default, cli and programmatic options, merge programm
2429
[
2530
{
2631
"docsHost": true,
32+
"logging": {
33+
"level": "info",
34+
"protocol": false,
35+
"stderr": false,
36+
},
2737
},
2838
],
2939
],
@@ -42,6 +52,11 @@ exports[`main should merge default, cli and programmatic options, with empty pro
4252
[
4353
{
4454
"docsHost": true,
55+
"logging": {
56+
"level": "info",
57+
"protocol": false,
58+
"stderr": false,
59+
},
4560
},
4661
],
4762
],
@@ -60,6 +75,11 @@ exports[`main should merge default, cli and programmatic options, with undefined
6075
[
6176
{
6277
"docsHost": false,
78+
"logging": {
79+
"level": "info",
80+
"protocol": false,
81+
"stderr": false,
82+
},
6383
},
6484
],
6585
],
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`createLogger should activate stderr subscriber writes only at or above level: stderr 1`] = `
4+
[
5+
[
6+
"[INFO]: lorem ipsum :123 {"a":1}
7+
",
8+
],
9+
]
10+
`;
11+
12+
exports[`createLogger should attempt to subscribe and unsubscribe from a channel, with no logging options: subscribe 1`] = `
13+
{
14+
"subscribe": [],
15+
"unsubscribe": [],
16+
}
17+
`;
18+
19+
exports[`createLogger should attempt to subscribe and unsubscribe from a channel, with stderr, and emulated channel to pass checks: subscribe 1`] = `
20+
{
21+
"subscribe": [
22+
[
23+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
24+
[Function],
25+
],
26+
],
27+
"unsubscribe": [
28+
[
29+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
30+
[Function],
31+
],
32+
],
33+
}
34+
`;
35+
36+
exports[`logSeverity should return log severity, debug 1`] = `0`;
37+
38+
exports[`logSeverity should return log severity, default 1`] = `-1`;
39+
40+
exports[`logSeverity should return log severity, error 1`] = `3`;
41+
42+
exports[`logSeverity should return log severity, info 1`] = `1`;
43+
44+
exports[`logSeverity should return log severity, warn 1`] = `2`;
45+
46+
exports[`publish should attempt to create a log entry, args 1`] = `
47+
{
48+
"channel": [
49+
[
50+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
51+
],
52+
],
53+
"publish": [
54+
[
55+
{
56+
"args": [
57+
"dolor",
58+
"sit",
59+
"amet",
60+
],
61+
"level": "info",
62+
"msg": "lorem ipsum, info",
63+
"time": 1761955200000,
64+
"transport": "stdio",
65+
},
66+
],
67+
],
68+
}
69+
`;
70+
71+
exports[`publish should attempt to create a log entry, channel name 1`] = `
72+
{
73+
"channel": [
74+
[
75+
"custom-channel",
76+
],
77+
],
78+
"publish": [
79+
[
80+
{
81+
"args": [
82+
"dolor",
83+
"sit",
84+
"amet",
85+
],
86+
"level": "info",
87+
"msg": "lorem ipsum, info",
88+
"time": 1761955200000,
89+
"transport": undefined,
90+
},
91+
],
92+
],
93+
}
94+
`;
95+
96+
exports[`publish should attempt to create a log entry, default 1`] = `
97+
{
98+
"channel": [
99+
[
100+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
101+
],
102+
],
103+
"publish": [
104+
[
105+
{
106+
"level": undefined,
107+
"time": 1761955200000,
108+
"transport": "stdio",
109+
},
110+
],
111+
],
112+
}
113+
`;
114+
115+
exports[`publish should attempt to create a log entry, level 1`] = `
116+
{
117+
"channel": [
118+
[
119+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
120+
],
121+
],
122+
"publish": [
123+
[
124+
{
125+
"level": "info",
126+
"time": 1761955200000,
127+
"transport": "stdio",
128+
},
129+
],
130+
],
131+
}
132+
`;
133+
134+
exports[`publish should attempt to create a log entry, msg 1`] = `
135+
{
136+
"channel": [
137+
[
138+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
139+
],
140+
],
141+
"publish": [
142+
[
143+
{
144+
"level": "info",
145+
"msg": "lorem ipsum, info",
146+
"time": 1761955200000,
147+
"transport": "stdio",
148+
},
149+
],
150+
],
151+
}
152+
`;
153+
154+
exports[`registerStderrSubscriber should activate stderr subscriber writes only at or above level: stderr 1`] = `
155+
[
156+
[
157+
"[INFO]: lorem ipsum :123 {"a":1}
158+
",
159+
],
160+
]
161+
`;
162+
163+
exports[`registerStderrSubscriber should attempt to subscribe and unsubscribe from a channel: subscribe 1`] = `
164+
{
165+
"subscribe": [
166+
[
167+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
168+
[Function],
169+
],
170+
],
171+
"unsubscribe": [
172+
[
173+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
174+
[Function],
175+
],
176+
],
177+
}
178+
`;
179+
180+
exports[`subscribeToChannel should attempt to subscribe and unsubscribe from a channel: subscribe 1`] = `
181+
{
182+
"subscribe": [
183+
[
184+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
185+
[Function],
186+
],
187+
],
188+
"unsubscribe": [
189+
[
190+
"pf-mcp:log:1234d567-1ce9-123d-1413-a1234e56c789",
191+
[Function],
192+
],
193+
],
194+
}
195+
`;
196+
197+
exports[`subscribeToChannel should throw an error attempting to subscribe and unsubscribe from a channel: missing channel name 1`] = `"subscribeToChannel called without a configured logging channelName"`;

0 commit comments

Comments
 (0)