Skip to content

Commit 7bd9cd0

Browse files
committed
Merge branch 'master' into release
2 parents 30117a0 + f6a94a0 commit 7bd9cd0

File tree

16 files changed

+519
-332
lines changed

16 files changed

+519
-332
lines changed

apps/intellij/src/main/kotlin/dev/nx/console/ProjectPostStartup.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package dev.nx.console
22

3-
import com.intellij.ide.plugins.PluginManagerCore
43
import com.intellij.openapi.project.DumbService
54
import com.intellij.openapi.project.Project
65
import com.intellij.openapi.startup.ProjectActivity
76
import dev.nx.console.ai.PeriodicAiCheckService
87
import dev.nx.console.cloud.CIPEMonitoringService
98
import dev.nx.console.ide.ProjectGraphErrorProblemProvider
10-
import dev.nx.console.mcp.McpServerService
119
import dev.nx.console.nxls.NxlsService
1210
import dev.nx.console.settings.NxConsoleSettingsProvider
1311
import dev.nx.console.telemetry.TelemetryEvent
@@ -18,7 +16,6 @@ import dev.nx.console.utils.nxBasePath
1816
import dev.nx.console.utils.sync_services.NxProjectJsonToProjectMap
1917
import dev.nx.console.utils.sync_services.NxVersionUtil
2018
import java.io.File
21-
import kotlinx.coroutines.delay
2219
import kotlinx.coroutines.launch
2320

2421
internal class ProjectPostStartup : ProjectActivity {
@@ -57,19 +54,6 @@ internal class ProjectPostStartup : ProjectActivity {
5754

5855
// Initialize periodic AI configuration check
5956
PeriodicAiCheckService.getInstance(project).initialize()
60-
61-
val aiAssistantPlugin =
62-
PluginManagerCore.plugins.find { it.pluginId.idString == "com.intellij.ml.llm" }
63-
if (aiAssistantPlugin != null && aiAssistantPlugin.isEnabled) {
64-
// Wait for indexing to complete
65-
66-
delay(10000)
67-
68-
val mcpService = McpServerService.getInstance(project)
69-
if (!mcpService.isMcpServerSetup()) {
70-
Notifier.notifyMcpServerInstall(project)
71-
}
72-
}
7357
}
7458

7559
TelemetryService.getInstance(project)

apps/intellij/src/main/kotlin/dev/nx/console/ai/PeriodicAiCheckService.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import com.intellij.openapi.actionSystem.AnActionEvent
1111
import com.intellij.openapi.application.EDT
1212
import com.intellij.openapi.components.Service
1313
import com.intellij.openapi.project.Project
14+
import dev.nx.console.mcp.McpServerService
1415
import dev.nx.console.telemetry.TelemetryEvent
1516
import dev.nx.console.telemetry.TelemetryService
1617
import dev.nx.console.utils.NxLatestVersionGeneralCommandLine
1718
import dev.nx.console.utils.NxProvenance
19+
import java.io.File
1820
import kotlinx.coroutines.*
1921

2022
@Service(Service.Level.PROJECT)
@@ -75,6 +77,12 @@ class PeriodicAiCheckService(private val project: Project, private val cs: Corou
7577

7678
try {
7779
val workspaceRoot = project.basePath ?: "."
80+
81+
// Only run AI checks in Nx workspaces
82+
if (!File(workspaceRoot, "nx.json").exists()) {
83+
return
84+
}
85+
7886
val (hasProvenance, _) =
7987
withContext(Dispatchers.IO) { NxProvenance.nxLatestProvenanceCheck(workspaceRoot) }
8088

@@ -205,6 +213,7 @@ class PeriodicAiCheckService(private val project: Project, private val cs: Corou
205213
mapOf("source" to "notification"),
206214
)
207215
ConfigureAiAgentsService.getInstance(project).runConfigureCommand()
216+
McpServerService.getInstance(project).setupMcpServer()
208217
}
209218
}
210219

apps/intellij/src/main/kotlin/dev/nx/console/utils/Notifier.kt

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -203,42 +203,6 @@ class Notifier {
203203
getGroup().createNotification(message, type).setTitle("Nx Console").notify(project)
204204
}
205205

206-
fun notifyMcpServerInstall(project: Project) {
207-
val hideNotificationPropertyKey = "nx.console.mcp.server.install.notification.hide"
208-
209-
if (
210-
PropertiesComponent.getInstance(project)
211-
.getBoolean(hideNotificationPropertyKey, false)
212-
) {
213-
return
214-
}
215-
216-
val notification =
217-
getGroup()
218-
.createNotification(
219-
"Install the Nx MCP Server to enhance AI assistant with Nx-specific knowledge?",
220-
NotificationType.INFORMATION,
221-
)
222-
.setTitle("Nx Console")
223-
224-
notification.addActions(
225-
setOf(
226-
NotificationAction.createSimpleExpiring("Install") {
227-
notification.expire()
228-
val mcpService = dev.nx.console.mcp.McpServerService.getInstance(project)
229-
mcpService.setupMcpServer()
230-
},
231-
NotificationAction.createSimpleExpiring("Don't ask again") {
232-
notification.expire()
233-
PropertiesComponent.getInstance(project)
234-
.setValue(hideNotificationPropertyKey, true)
235-
},
236-
)
237-
)
238-
239-
notification.notify(project)
240-
}
241-
242206
fun notifyAiAssistantPluginRequired(project: Project) {
243207
getGroup()
244208
.createNotification(
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import {
2+
cleanupNxWorkspace,
3+
defaultVersion,
4+
e2eCwd,
5+
newWorkspace,
6+
simpleReactWorkspaceOptions,
7+
uniq,
8+
TestMCPClient,
9+
} from '@nx-console/shared-e2e-utils';
10+
import { spawn, ChildProcess } from 'node:child_process';
11+
import { rmSync } from 'node:fs';
12+
import { join } from 'node:path';
13+
import { workspaceRoot } from 'nx/src/devkit-exports';
14+
15+
describe('HTTP Multi-Client', () => {
16+
let serverProcess: ChildProcess;
17+
const serverPort = 9922;
18+
const workspaceName = uniq('nx-mcp-http-test');
19+
const testWorkspacePath = join(e2eCwd, workspaceName);
20+
const serverPath = join(workspaceRoot, 'dist', 'apps', 'nx-mcp', 'main.js');
21+
22+
// Helper to wait for HTTP server to be ready by polling
23+
async function waitForServerReady(
24+
port: number,
25+
timeoutMs = 60000,
26+
): Promise<void> {
27+
const startTime = Date.now();
28+
29+
while (Date.now() - startTime < timeoutMs) {
30+
try {
31+
// Try to connect to the server
32+
// this is not an official supported thing in the spec but it works for checkign aliveness
33+
const response = await fetch(`http://localhost:${port}/mcp`, {
34+
method: 'OPTIONS',
35+
});
36+
37+
// If we get any response (even an error), the server is up
38+
console.log(`Server is ready on port ${port}`);
39+
return;
40+
} catch (error) {
41+
// Server not ready yet, wait and retry
42+
await new Promise((resolve) => setTimeout(resolve, 500));
43+
}
44+
}
45+
46+
throw new Error(`Timeout waiting for server to be ready on port ${port}`);
47+
}
48+
49+
beforeAll(async () => {
50+
// Create workspace
51+
newWorkspace({
52+
name: workspaceName,
53+
options: simpleReactWorkspaceOptions,
54+
});
55+
56+
// Start HTTP MCP server without workspace path
57+
// The workspace will be determined per-session based on requests
58+
serverProcess = spawn(
59+
'node',
60+
[serverPath, '--transport=http', `--port=${serverPort}`],
61+
{
62+
stdio: 'pipe',
63+
env: {
64+
...process.env,
65+
NX_NO_CLOUD: 'true',
66+
MCP_AUTO_OPEN_ENABLED: 'false',
67+
},
68+
cwd: testWorkspacePath, // Set working directory to workspace
69+
},
70+
);
71+
72+
// Log server output for debugging
73+
serverProcess.stdout?.on('data', (data) => {
74+
if (process.env['NX_VERBOSE_LOGGING']) {
75+
console.log(`[MCP Server] ${data.toString()}`);
76+
}
77+
});
78+
79+
serverProcess.stderr?.on('data', (data) => {
80+
if (process.env['NX_VERBOSE_LOGGING']) {
81+
console.error(`[MCP Server Error] ${data.toString()}`);
82+
}
83+
});
84+
85+
// Wait for server to be ready by polling the endpoint
86+
await waitForServerReady(serverPort);
87+
88+
console.log(`MCP HTTP server confirmed ready on port ${serverPort}`);
89+
});
90+
91+
afterAll(async () => {
92+
// Kill server
93+
if (serverProcess) {
94+
serverProcess.kill('SIGTERM');
95+
// Wait a bit for graceful shutdown
96+
await new Promise((resolve) => setTimeout(resolve, 1000));
97+
if (!serverProcess.killed) {
98+
serverProcess.kill('SIGKILL');
99+
}
100+
}
101+
102+
// Clean up workspace
103+
await cleanupNxWorkspace(testWorkspacePath, defaultVersion);
104+
rmSync(testWorkspacePath, { recursive: true, force: true });
105+
});
106+
107+
it('should handle two simultaneous clients listing tools', async () => {
108+
const serverUrl = `http://localhost:${serverPort}/mcp`;
109+
110+
// Create two test clients
111+
const client1 = new TestMCPClient(serverUrl, 'test-client-1');
112+
const client2 = new TestMCPClient(serverUrl, 'test-client-2');
113+
114+
// Connect both clients in parallel
115+
await Promise.all([client1.connect(), client2.connect()]);
116+
117+
// Make two simultaneous requests
118+
const [tools1, tools2] = await Promise.all([
119+
client1.listTools(),
120+
client2.listTools(),
121+
]);
122+
123+
// Verify both clients got valid responses
124+
expect(tools1).toBeDefined();
125+
expect(tools2).toBeDefined();
126+
127+
const toolNames1 = tools1.map((tool: any) => tool.name);
128+
const toolNames2 = tools2.map((tool: any) => tool.name);
129+
130+
// Both should have the same set of tools
131+
const expectedTools = [
132+
'nx_docs',
133+
'nx_available_plugins',
134+
'nx_workspace',
135+
'nx_workspace_path',
136+
'nx_project_details',
137+
'nx_generators',
138+
'nx_generator_schema',
139+
];
140+
141+
expect(toolNames1).toEqual(expectedTools);
142+
expect(toolNames2).toEqual(expectedTools);
143+
144+
console.log('Both clients successfully connected and retrieved tools');
145+
146+
// Disconnect both clients
147+
await Promise.all([client1.disconnect(), client2.disconnect()]);
148+
});
149+
150+
it('should allow both clients to invoke tools and get results', async () => {
151+
const serverUrl = `http://localhost:${serverPort}/mcp`;
152+
153+
// Create two test clients
154+
const client1 = new TestMCPClient(serverUrl, 'test-client-3');
155+
const client2 = new TestMCPClient(serverUrl, 'test-client-4');
156+
157+
// Connect both clients in parallel
158+
await Promise.all([client1.connect(), client2.connect()]);
159+
160+
// Make two simultaneous tool calls
161+
const [result1, result2] = await Promise.all([
162+
client1.callTool('nx_workspace_path', {}),
163+
client2.callTool('nx_workspace_path', {}),
164+
]);
165+
166+
// Verify both clients got valid tool results
167+
expect(result1.content).toBeDefined();
168+
expect(result2.content).toBeDefined();
169+
170+
// The nx_workspace_path tool returns the workspace path as text
171+
expect(result1.content[0].type).toBe('text');
172+
expect(result1.content[0].text).toContain(testWorkspacePath);
173+
174+
expect(result2.content[0].type).toBe('text');
175+
expect(result2.content[0].text).toContain(testWorkspacePath);
176+
177+
console.log(
178+
'Both clients successfully invoked tools and received correct results',
179+
);
180+
181+
// Disconnect both clients
182+
await Promise.all([client1.disconnect(), client2.disconnect()]);
183+
});
184+
});

apps/vscode/package.json

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,6 @@
206206
"when": "view == nxCloudRecentCIPE && viewItem == run",
207207
"group": "inline@1"
208208
},
209-
{
210-
"command": "nxCloud.helpMeFixCipeError",
211-
"when": "view == nxCloudRecentCIPE && viewItem == failedTask && isInCursor",
212-
"group": "inline@1"
213-
},
214209
{
215210
"command": "nxConsole.showProblems",
216211
"when": "view == nxProjects && viewItem == projectGraphError",
@@ -310,10 +305,6 @@
310305
"command": "nxCloud.showRunInApp",
311306
"when": "false"
312307
},
313-
{
314-
"command": "nxCloud.helpMeFixCipeError",
315-
"when": "false"
316-
},
317308
{
318309
"command": "nxCloud.applyAiFix",
319310
"when": "false"
@@ -904,14 +895,6 @@
904895
"command": "nxCloud.openApp",
905896
"icon": "$(cloud)"
906897
},
907-
{
908-
"title": "Help me fix the latest CIPE error",
909-
"command": "nxCloud.helpMeFixCipeError",
910-
"icon": {
911-
"dark": "assets/cursor-dark.svg",
912-
"light": "assets/cursor-light.svg"
913-
}
914-
},
915898
{
916899
"title": "Apply AI Fix",
917900
"command": "nxCloud.applyAiFix"

apps/vscode/src/assets/cursor-dark.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/vscode/src/assets/cursor-light.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

libs/shared/e2e-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './lib/utils';
2+
export * from './lib/mcp-test-client';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3+
4+
export class TestMCPClient {
5+
private client: Client;
6+
private transport: StreamableHTTPClientTransport | null = null;
7+
8+
constructor(
9+
private serverUrl: string,
10+
clientName: string,
11+
) {
12+
this.client = new Client({
13+
name: clientName,
14+
version: '1.0.0',
15+
});
16+
}
17+
18+
async connect(): Promise<void> {
19+
this.transport = new StreamableHTTPClientTransport(new URL(this.serverUrl));
20+
await this.client.connect(this.transport);
21+
}
22+
23+
async listTools(): Promise<any[]> {
24+
const result = await this.client.listTools();
25+
return result.tools;
26+
}
27+
28+
async callTool(name: string, args: Record<string, any> = {}): Promise<any> {
29+
return await this.client.callTool({
30+
name,
31+
arguments: args,
32+
});
33+
}
34+
35+
async disconnect(): Promise<void> {
36+
await this.client.close();
37+
}
38+
}

0 commit comments

Comments
 (0)