Skip to content

feat: enable mcp to communicate with IDE via JSON-RPC server #2640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
882af6b
refactor: move socket utilities to shared library and add detection
MaxKless Jul 23, 2025
8653a1b
feat: create JSON-RPC client with content-length protocol compatibility
MaxKless Jul 23, 2025
2152e54
refactor: remove NxIdeProvider in favor of IIdeJsonRpcClient
MaxKless Jul 23, 2025
5f68371
add ide client and use it
MaxKless Jul 24, 2025
679a306
refactor mcp to be more dynamic
MaxKless Jul 24, 2025
e4fee43
support both ways for now
MaxKless Jul 24, 2025
1440624
talking between them works!!
MaxKless Jul 24, 2025
4a3b287
successful communication between vscode & stdio mcp
MaxKless Jul 24, 2025
7b7543c
ensure no stdio output
MaxKless Jul 28, 2025
9319069
formatting
MaxKless Jul 28, 2025
f56f898
type cleanup
MaxKless Jul 28, 2025
d727ed7
add e2e test for ide comms
MaxKless Jul 28, 2025
9c82258
self-review fixes
MaxKless Jul 28, 2025
dec9473
further fixes
MaxKless Jul 28, 2025
89ad46c
fix sync
MaxKless Jul 28, 2025
abd9ee7
fix failure
MaxKless Jul 28, 2025
b8b844e
fix test order
MaxKless Jul 28, 2025
513bc12
kill process.stdin & add logging
MaxKless Jul 30, 2025
644dce8
fix assignment rule error
MaxKless Jul 30, 2025
8d509c9
formatting
MaxKless Jul 30, 2025
25ebd62
add logs
MaxKless Jul 30, 2025
21f40fc
make logging crazier
MaxKless Jul 30, 2025
efb252b
w/ cleanup
MaxKless Jul 31, 2025
27add75
formatting
MaxKless Jul 31, 2025
69ec618
dont try to create named pipe directory
MaxKless Jul 31, 2025
24d4b12
fix
MaxKless Jul 31, 2025
25f5d15
add cleanup
MaxKless Jul 31, 2025
228b0cb
remove overly complicated test & catch error
MaxKless Jul 31, 2025
4530978
nx sync
MaxKless Jul 31, 2025
70b6e5a
changes
MaxKless Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .nx/workflows/windows-distribution-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ distribute-on:
small-changeset: 3 windows-medium-js
medium-changeset: 4 windows-medium-js
large-changeset: 6 windows-medium-js

assignment-rules:
- project: intellij
target: build
runs-on:
- windows-medium-js
14 changes: 12 additions & 2 deletions apps/nx-mcp-e2e/src/tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {
cleanupNxWorkspace,
createInvokeMCPInspectorCLI,
defaultVersion,
e2eCwd,
logWindowsFileLocks,
newWorkspace,
simpleReactWorkspaceOptions,
uniq,
} from '@nx-console/shared-e2e-utils';
import { rmSync } from 'node:fs';
import { platform } from 'node:os';
import { join } from 'node:path';

describe('tools', () => {
Expand All @@ -26,7 +30,13 @@ describe('tools', () => {
);
});

afterAll(() => {
afterAll(async () => {
// Clean up Nx workspace processes before attempting to remove directory
await cleanupNxWorkspace(testWorkspacePath, defaultVersion);

if (platform() === 'win32') {
logWindowsFileLocks(testWorkspacePath);
}
rmSync(testWorkspacePath, { recursive: true, force: true });
});

Expand All @@ -39,8 +49,8 @@ describe('tools', () => {
expect(toolNames).toEqual([
'nx_docs',
'nx_available_plugins',
'nx_workspace_path',
'nx_workspace',
'nx_workspace_path',
'nx_project_details',
'nx_generators',
'nx_generator_schema',
Expand Down
10 changes: 9 additions & 1 deletion apps/nx-mcp-e2e/src/workspace_detection.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {
cleanupNxWorkspace,
createInvokeMCPInspectorCLI,
defaultVersion,
e2eCwd,
logWindowsFileLocks,
newWorkspace,
simpleReactWorkspaceOptions,
uniq,
} from '@nx-console/shared-e2e-utils';
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { platform } from 'node:os';
import { join } from 'node:path';

describe('workspace detection', () => {
Expand All @@ -28,7 +32,11 @@ describe('workspace detection', () => {
);
});

afterAll(() => {
afterAll(async () => {
if (platform() === 'win32') {
logWindowsFileLocks(testWorkspacePath);
}
await cleanupNxWorkspace(testWorkspacePath, defaultVersion);
rmSync(testWorkspacePath, { recursive: true, force: true });
});

Expand Down
12 changes: 11 additions & 1 deletion apps/nx-mcp-e2e/src/workspace_path.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {
cleanupNxWorkspace,
createInvokeMCPInspectorCLI,
defaultVersion,
e2eCwd,
logWindowsFileLocks,
newWorkspace,
simpleReactWorkspaceOptions,
uniq,
} from '@nx-console/shared-e2e-utils';
import { rmSync } from 'node:fs';
import { platform } from 'node:os';
import { join } from 'node:path';

describe('workspace path', () => {
Expand All @@ -26,7 +30,13 @@ describe('workspace path', () => {
);
});

afterAll(() => {
afterAll(async () => {
// Clean up Nx workspace processes before attempting to remove directory
await cleanupNxWorkspace(testWorkspacePath, defaultVersion);

if (platform() === 'win32') {
logWindowsFileLocks(testWorkspacePath);
}
rmSync(testWorkspacePath, { recursive: true, force: true });
});

Expand Down
3 changes: 3 additions & 0 deletions apps/nx-mcp-e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../../libs/shared/utils"
},
{
"path": "../../libs/shared/e2e-utils"
}
Expand Down
22 changes: 22 additions & 0 deletions apps/nx-mcp/src/ensureOnlyJsonRpcStdout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function ensureOnlyJsonRpcStdout() {
process.stdout.write = ((
chunk: any,
encodingOrCallback?: any,
callback?: any,
) => {
const message = chunk.toString();

if (
!message.startsWith('Content-Length:') &&
(!message.startsWith('{') || !message.includes('"jsonrpc":'))
) {
process.stderr.write(message);
return;
}

const originalWrite = process.stdout.constructor.prototype.write.bind(
process.stdout,
);
return originalWrite(chunk, encodingOrCallback, callback);
}) as typeof process.stdout.write;
}
32 changes: 32 additions & 0 deletions apps/nx-mcp/src/ide-client/create-ide-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { IIdeJsonRpcClient } from '@nx-console/shared-types';
import { Logger, testIdeConnection } from '@nx-console/shared-utils';
import { IdeJsonRpcClient } from './json-rpc-client';

export async function createIdeClient(
workspacePath: string,
logger?: Logger,
): Promise<{ client: IIdeJsonRpcClient | undefined; available: boolean }> {
try {
const ideListening = await testIdeConnection(workspacePath);

if (!ideListening) {
return { client: undefined, available: false };
}

const client = new IdeJsonRpcClient(workspacePath, logger);

await client.connect();

// Verify connection is actually working
const status = client.getStatus();
if (status !== 'connected') {
client.disconnect();
return { client: undefined, available: false };
}

return { client, available: true };
} catch (error) {
logger?.log('Failed to create IDE client:', error);
return { client: undefined, available: false };
}
}
Loading
Loading