-
Notifications
You must be signed in to change notification settings - Fork 230
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
MaxKless
wants to merge
30
commits into
master
Choose a base branch
from
mcp-ide-comms-jsonrpc
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,822
−651
Open
Changes from 11 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 8653a1b
feat: create JSON-RPC client with content-length protocol compatibility
MaxKless 2152e54
refactor: remove NxIdeProvider in favor of IIdeJsonRpcClient
MaxKless 5f68371
add ide client and use it
MaxKless 679a306
refactor mcp to be more dynamic
MaxKless e4fee43
support both ways for now
MaxKless 1440624
talking between them works!!
MaxKless 4a3b287
successful communication between vscode & stdio mcp
MaxKless 7b7543c
ensure no stdio output
MaxKless 9319069
formatting
MaxKless f56f898
type cleanup
MaxKless d727ed7
add e2e test for ide comms
MaxKless 9c82258
self-review fixes
MaxKless dec9473
further fixes
MaxKless 89ad46c
fix sync
MaxKless abd9ee7
fix failure
MaxKless b8b844e
fix test order
MaxKless 513bc12
kill process.stdin & add logging
MaxKless 644dce8
fix assignment rule error
MaxKless 8d509c9
formatting
MaxKless 25ebd62
add logs
MaxKless 21f40fc
make logging crazier
MaxKless efb252b
w/ cleanup
MaxKless 27add75
formatting
MaxKless 69ec618
dont try to create named pipe directory
MaxKless 24d4b12
fix
MaxKless 25f5d15
add cleanup
MaxKless 228b0cb
remove overly complicated test & catch error
MaxKless 4530978
nx sync
MaxKless 70b6e5a
changes
MaxKless File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { IIdeJsonRpcClient } from '@nx-console/shared-types'; | ||
import { Logger, testIdeConnection } from '@nx-console/shared-utils'; | ||
import { IdeJsonRpcClient } from './json-rpc-client'; | ||
|
||
/** | ||
* Factory function to create and connect an IDE client | ||
*/ | ||
export async function createIdeClient( | ||
workspacePath: string, | ||
logger?: Logger, | ||
): Promise<{ client: IIdeJsonRpcClient | undefined; available: boolean }> { | ||
try { | ||
// Check if IDE is actually listening on the socket | ||
const ideListening = await testIdeConnection(workspacePath); | ||
|
||
if (!ideListening) { | ||
return { client: undefined, available: false }; | ||
} | ||
|
||
// Create and connect client | ||
const client = new IdeJsonRpcClient(workspacePath, 2000, 5, 5000, 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 }; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
import { | ||
ConnectionStatus, | ||
IDE_RPC_METHODS, | ||
OpenGenerateUiResponse, | ||
IIdeJsonRpcClient, | ||
} from '@nx-console/shared-types'; | ||
import { getNxConsoleSocketPath, Logger } from '@nx-console/shared-utils'; | ||
import { Socket } from 'net'; | ||
import { platform } from 'os'; | ||
import * as rpc from 'vscode-jsonrpc/node'; | ||
|
||
// Define typed request and notification types | ||
const focusProjectRequest = new rpc.NotificationType<{ projectName: string }>( | ||
IDE_RPC_METHODS.FOCUS_PROJECT, | ||
); | ||
const focusTaskRequest = new rpc.NotificationType<{ | ||
projectName: string; | ||
taskName: string; | ||
}>(IDE_RPC_METHODS.FOCUS_TASK); | ||
const showFullProjectGraphRequest = new rpc.NotificationType<void>( | ||
IDE_RPC_METHODS.SHOW_FULL_PROJECT_GRAPH, | ||
); | ||
const openGenerateUiRequest = new rpc.RequestType< | ||
{ generatorName: string; options: Record<string, unknown>; cwd?: string }, | ||
OpenGenerateUiResponse, | ||
void | ||
>(IDE_RPC_METHODS.OPEN_GENERATE_UI); | ||
|
||
/** | ||
* JSON-RPC client for communicating with the IDE using vscode-jsonrpc | ||
*/ | ||
export class IdeJsonRpcClient implements IIdeJsonRpcClient { | ||
private socket: Socket | null = null; | ||
private connection: rpc.MessageConnection | null = null; | ||
private status: ConnectionStatus = 'disconnected'; | ||
private reconnectAttempts = 0; | ||
private reconnectTimer: NodeJS.Timeout | null = null; | ||
private disconnectionHandler?: (client: IdeJsonRpcClient) => void; | ||
|
||
constructor( | ||
private workspacePath: string, | ||
private reconnectInterval?: number, | ||
private maxReconnectAttempts?: number, | ||
private requestTimeout?: number, | ||
private logger?: Logger, | ||
) {} | ||
|
||
/** | ||
* Set a handler to be called when the client disconnects | ||
*/ | ||
onDisconnection(handler: (client: IdeJsonRpcClient) => void): void { | ||
this.disconnectionHandler = handler; | ||
} | ||
|
||
/** | ||
* Connect to the IDE socket | ||
*/ | ||
async connect(): Promise<void> { | ||
if (this.status === 'connected' || this.status === 'connecting') { | ||
return; | ||
} | ||
|
||
this.status = 'connecting'; | ||
|
||
try { | ||
const socketPath = getNxConsoleSocketPath(this.workspacePath); | ||
|
||
// Create socket connection | ||
this.socket = new Socket(); | ||
|
||
// Wait for socket to connect | ||
await new Promise<void>((resolve, reject) => { | ||
this.socket!.on('connect', () => { | ||
this.status = 'connected'; | ||
this.reconnectAttempts = 0; | ||
this.logger?.log(`Connected to IDE at ${socketPath}`); | ||
resolve(); | ||
}); | ||
|
||
this.socket!.on('error', (error) => { | ||
this.logger?.log('Socket connection error:', error); | ||
reject(error); | ||
}); | ||
|
||
// Connect to socket | ||
if (platform() === 'win32') { | ||
// On Windows, connect to named pipe | ||
this.socket!.connect(socketPath); | ||
} else { | ||
// On Unix, connect to socket file | ||
this.socket!.connect(socketPath); | ||
} | ||
}); | ||
|
||
// Create vscode-jsonrpc message connection | ||
const reader = new rpc.SocketMessageReader(this.socket); | ||
const writer = new rpc.SocketMessageWriter(this.socket); | ||
this.connection = rpc.createMessageConnection(reader, writer); | ||
|
||
// Set up connection event handlers | ||
this.connection.onClose(() => { | ||
this.logger?.log('JSON-RPC connection closed'); | ||
this.handleDisconnection(); | ||
}); | ||
|
||
this.connection.onError((error) => { | ||
this.logger?.log('JSON-RPC connection error:', error); | ||
this.handleDisconnection(); | ||
}); | ||
|
||
// Start listening for messages | ||
this.connection.listen(); | ||
} catch (error) { | ||
this.logger?.log('Failed to connect to IDE:', error); | ||
this.handleDisconnection(); | ||
throw error; | ||
} | ||
} | ||
|
||
/** | ||
* Disconnect from the IDE | ||
*/ | ||
disconnect(): void { | ||
if (this.reconnectTimer) { | ||
clearTimeout(this.reconnectTimer); | ||
this.reconnectTimer = null; | ||
} | ||
|
||
if (this.connection) { | ||
this.connection.dispose(); | ||
this.connection = null; | ||
} | ||
|
||
if (this.socket) { | ||
this.socket.destroy(); | ||
this.socket = null; | ||
} | ||
|
||
this.status = 'disconnected'; | ||
} | ||
|
||
/** | ||
* Get current connection status | ||
*/ | ||
getStatus(): ConnectionStatus { | ||
return this.status; | ||
} | ||
|
||
/** | ||
* Handle disconnection and attempt reconnection | ||
*/ | ||
private handleDisconnection(): void { | ||
if (this.status === 'disconnected') { | ||
return; // Already handling disconnection | ||
} | ||
|
||
this.status = 'error'; | ||
|
||
if (this.connection) { | ||
this.connection.dispose(); | ||
this.connection = null; | ||
} | ||
|
||
if (this.socket) { | ||
this.socket.destroy(); | ||
this.socket = null; | ||
} | ||
|
||
// Attempt reconnection if configured | ||
const maxAttempts = this.maxReconnectAttempts || 5; | ||
const interval = this.reconnectInterval || 2000; | ||
|
||
if (this.reconnectAttempts < maxAttempts) { | ||
this.reconnectAttempts++; | ||
this.logger?.log( | ||
`Attempting to reconnect (${this.reconnectAttempts}/${maxAttempts})...`, | ||
); | ||
|
||
this.reconnectTimer = setTimeout(() => { | ||
this.connect().catch((error) => { | ||
this.logger?.log('Reconnection failed:', error); | ||
if (this.reconnectAttempts >= maxAttempts) { | ||
this.status = 'disconnected'; | ||
this.logger?.log('Max reconnection attempts reached. Giving up.'); | ||
} | ||
}); | ||
}, interval); | ||
} else { | ||
this.status = 'disconnected'; | ||
this.logger?.log('Max reconnection attempts reached. Connection lost.'); | ||
|
||
// Notify handler of permanent disconnection | ||
if (this.disconnectionHandler) { | ||
this.disconnectionHandler(this); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Focus on a specific project in the IDE | ||
*/ | ||
async focusProject(projectName: string): Promise<void> { | ||
if (!this.connection || this.status !== 'connected') { | ||
throw new Error('Not connected to IDE'); | ||
} | ||
|
||
await this.connection.sendNotification(focusProjectRequest, { | ||
projectName, | ||
}); | ||
} | ||
|
||
/** | ||
* Focus on a specific task in the IDE | ||
*/ | ||
async focusTask(projectName: string, taskName: string): Promise<void> { | ||
if (!this.connection || this.status !== 'connected') { | ||
throw new Error('Not connected to IDE'); | ||
} | ||
|
||
await this.connection.sendNotification(focusTaskRequest, { | ||
projectName, | ||
taskName, | ||
}); | ||
} | ||
|
||
/** | ||
* Show the full project graph in the IDE | ||
*/ | ||
async showFullProjectGraph(): Promise<void> { | ||
if (!this.connection || this.status !== 'connected') { | ||
throw new Error('Not connected to IDE'); | ||
} | ||
|
||
await this.connection.sendNotification(showFullProjectGraphRequest); | ||
} | ||
|
||
/** | ||
* Open the generator UI in the IDE | ||
*/ | ||
async openGenerateUi( | ||
generatorName: string, | ||
options: Record<string, unknown>, | ||
cwd?: string, | ||
): Promise<string> { | ||
if (!this.connection || this.status !== 'connected') { | ||
throw new Error('Not connected to IDE'); | ||
} | ||
|
||
const response = await this.connection.sendRequest(openGenerateUiRequest, { | ||
generatorName, | ||
options, | ||
cwd, | ||
}); | ||
return response.logFileName; | ||
} | ||
|
||
/** | ||
* Send a notification to the IDE (fire-and-forget) | ||
*/ | ||
async sendNotification(method: string, params?: unknown): Promise<void> { | ||
if (!this.connection || this.status !== 'connected') { | ||
throw new Error('Not connected to IDE'); | ||
} | ||
|
||
const notificationType = new rpc.NotificationType<unknown>(method); | ||
this.connection.sendNotification(notificationType, params); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.