Skip to content

Commit da4a56e

Browse files
fix(context): keep MCP protocol output clean
1 parent 79246ff commit da4a56e

4 files changed

Lines changed: 36 additions & 18 deletions

File tree

‎.claude-plugin/marketplace.json‎

Lines changed: 0 additions & 15 deletions
This file was deleted.

‎packages/context/src/mcp.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,7 @@ const createContextMcpServer = (
751751
inputSchema: testSnapshotInput,
752752
annotations: {
753753
readOnlyHint: false,
754-
destructiveHint: false,
754+
destructiveHint: true,
755755
openWorldHint: true,
756756
},
757757
},

‎packages/rstack/src/mcp.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// cspell:ignore modelcontextprotocol
22
import path from 'node:path';
3+
import { Writable } from 'node:stream';
34
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
45
import {
56
captureLintSnapshot,
@@ -11,6 +12,25 @@ import { withRstackConfigTarget } from './config.ts';
1112

1213
declare const RSTACK_VERSION: string;
1314

15+
const reserveStandardOutputForProtocol = (): Writable => {
16+
const standardOutput = process.stdout;
17+
const writeProtocol = standardOutput.write.bind(standardOutput);
18+
19+
Object.defineProperty(standardOutput, 'write', {
20+
configurable: true,
21+
value: process.stderr.write.bind(process.stderr),
22+
writable: true,
23+
});
24+
25+
return new Writable({
26+
decodeStrings: false,
27+
write(chunk, encoding, callback) {
28+
if (writeProtocol(String(chunk), encoding)) callback();
29+
else standardOutput.once('drain', callback);
30+
},
31+
});
32+
};
33+
1434
const runContextMcpServer = async (startPath: string): Promise<void> => {
1535
const { workspaceRoot } = await resolveContextWorkspace(startPath);
1636
const server = createContextMcpServer(workspaceRoot, {
@@ -28,7 +48,7 @@ const runContextMcpServer = async (startPath: string): Promise<void> => {
2848
}),
2949
});
3050

31-
await server.connect(new StdioServerTransport());
51+
await server.connect(new StdioServerTransport(process.stdin, reserveStandardOutputForProtocol()));
3252
};
3353

3454
export { runContextMcpServer };

‎packages/rstack/tests/context/mcp.test.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ test('registers the exact ordered fifteen-tool catalog with accurate annotations
210210
});
211211
expect(tools[12]?.annotations).toMatchObject({
212212
readOnlyHint: false,
213-
destructiveHint: false,
213+
destructiveHint: true,
214214
openWorldHint: true,
215215
});
216216
expect(tools[13]).toMatchObject({
@@ -812,6 +812,7 @@ registerHooks({
812812
name: 'rstack-built-process-test',
813813
version: '1.0.0',
814814
});
815+
transport.stderr?.on('data', () => undefined);
815816

816817
try {
817818
await client.connect(transport);
@@ -1691,6 +1692,7 @@ test('loads distinct package lint and test configs through the built root MCP',
16911692
await writeFile(
16921693
path.join(workspaceRoot, fixture.configPath),
16931694
`import { define } from ${JSON.stringify(rstackEntry)};
1695+
console.log('loaded ${fixture.root} config');
16941696
define.lint([{ files: ['src/**/*.ts'], rules: { 'no-debugger': '${fixture.lintRule}' } }]);
16951697
define.test({ include: ['./tests/*.never.ts'], passWithNoTests: ${fixture.testStatus === 'pass'} });
16961698
`,
@@ -1708,6 +1710,14 @@ define.test({ include: ['./tests/*.never.ts'], passWithNoTests: ${fixture.testSt
17081710
name: 'rstack-package-config-test',
17091711
version: '1.0.0',
17101712
});
1713+
let serverStderr = '';
1714+
transport.stderr?.on('data', (chunk: Buffer) => {
1715+
serverStderr += String(chunk);
1716+
});
1717+
const transportErrors: Error[] = [];
1718+
transport.onerror = (error) => {
1719+
transportErrors.push(error);
1720+
};
17111721

17121722
try {
17131723
await client.connect(transport);
@@ -1743,6 +1753,9 @@ define.test({ include: ['./tests/*.never.ts'], passWithNoTests: ${fixture.testSt
17431753
for (const result of [...lintResults, ...testResults]) {
17441754
expect(result.isError).not.toBe(true);
17451755
}
1756+
expect(transportErrors).toEqual([]);
1757+
expect(serverStderr).toContain('loaded packages/app config');
1758+
expect(serverStderr).toContain('loaded packages/library config');
17461759

17471760
expect(lintResults.map((result) => result.structuredContent)).toEqual([
17481761
expect.objectContaining({

0 commit comments

Comments
 (0)