Skip to content

Commit 98f4df9

Browse files
committed
feat: Add Pip Server Management MCP tools
Implement checkPythonEnvironment and startPipServer MCP tools: - checkPythonEnvironment: Checks if Python environment supports Deephaven pip server - startPipServer: Starts a managed Deephaven pip server Key changes: - Created MCP tools following established patterns - Added pipServerController dependency to McpServer - Wired dependency through McpController and ExtensionController - Full test coverage with 10 passing tests - Includes helpful hints for missing dependencies (#copilot-worktree-2026-01)
1 parent f1c280c commit 98f4df9

8 files changed

Lines changed: 381 additions & 6 deletions

File tree

src/controllers/ExtensionController.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,8 @@ export class ExtensionController implements IDisposable {
427427
this._extensionInfo.mcpVersion,
428428
this._serverManager,
429429
this._pythonDiagnostics,
430-
this._pythonWorkspace
430+
this._pythonWorkspace,
431+
this._pipServerController
431432
);
432433

433434
this._context.subscriptions.push(this._mcpController);

src/controllers/McpController.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import * as vscode from 'vscode';
22
import { ControllerBase } from './ControllerBase';
33
import { McpServer } from '../mcp';
44
import { McpServerDefinitionProvider } from '../providers';
5-
import type { IServerManager, IConfigService, McpVersion } from '../types';
5+
import type {
6+
IServerManager,
7+
IConfigService,
8+
McpVersion,
9+
PipServerController,
10+
} from '../types';
611
import type { FilteredWorkspace } from '../services';
712
import { isWindsurf, Logger } from '../util';
813
import {
@@ -24,6 +29,7 @@ export class McpController extends ControllerBase {
2429
private _serverManager: IServerManager;
2530
private _pythonDiagnostics: vscode.DiagnosticCollection;
2631
private _pythonWorkspace: FilteredWorkspace;
32+
private _pipServerController: PipServerController | null;
2733

2834
private _mcpServer: McpServer | null = null;
2935
private _mcpServerDefinitionProvider: McpServerDefinitionProvider | null =
@@ -36,7 +42,8 @@ export class McpController extends ControllerBase {
3642
mcpVersion: McpVersion,
3743
serverManager: IServerManager,
3844
pythonDiagnostics: vscode.DiagnosticCollection,
39-
pythonWorkspace: FilteredWorkspace
45+
pythonWorkspace: FilteredWorkspace,
46+
pipServerController: PipServerController | null
4047
) {
4148
super();
4249

@@ -46,6 +53,7 @@ export class McpController extends ControllerBase {
4653
this._serverManager = serverManager;
4754
this._pythonDiagnostics = pythonDiagnostics;
4855
this._pythonWorkspace = pythonWorkspace;
56+
this._pipServerController = pipServerController;
4957

5058
// Register copy MCP URL command
5159
this.registerCommand(COPY_MCP_URL_CMD, this.copyUrl, this);
@@ -101,7 +109,8 @@ export class McpController extends ControllerBase {
101109
this._mcpServer = new McpServer(
102110
this._pythonDiagnostics,
103111
this._pythonWorkspace,
104-
this._serverManager
112+
this._serverManager,
113+
this._pipServerController
105114
);
106115
this.disposables.push(this._mcpServer);
107116

src/mcp/McpServer.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ import * as vscode from 'vscode';
22
import { McpServer as SdkMcpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
33
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
44
import * as http from 'http';
5-
import type { IServerManager, McpTool, McpToolSpec } from '../types';
5+
import type {
6+
IServerManager,
7+
McpTool,
8+
McpToolSpec,
9+
PipServerController,
10+
} from '../types';
611
import { MCP_SERVER_NAME } from '../common';
712
import {
13+
createCheckPythonEnvTool,
814
createListConnectionsTool,
915
createListServersTool,
1016
createRunCodeFromUriTool,
1117
createRunCodeTool,
18+
createStartPipServerTool,
1219
} from './tools';
1320
import { withResolvers } from '../util';
1421
import { DisposableBase, type FilteredWorkspace } from '../services';
@@ -26,17 +33,20 @@ export class McpServer extends DisposableBase {
2633
readonly pythonDiagnostics: vscode.DiagnosticCollection;
2734
readonly pythonWorkspace: FilteredWorkspace;
2835
readonly serverManager: IServerManager;
36+
readonly pipServerController: PipServerController | null;
2937

3038
constructor(
3139
pythonDiagnostics: vscode.DiagnosticCollection,
3240
pythonWorkspace: FilteredWorkspace,
33-
serverManager: IServerManager
41+
serverManager: IServerManager,
42+
pipServerController: PipServerController | null
3443
) {
3544
super();
3645

3746
this.pythonDiagnostics = pythonDiagnostics;
3847
this.pythonWorkspace = pythonWorkspace;
3948
this.serverManager = serverManager;
49+
this.pipServerController = pipServerController;
4050

4151
// Create an MCP server
4252
this.server = new SdkMcpServer({
@@ -49,6 +59,8 @@ export class McpServer extends DisposableBase {
4959
this.registerTool(createRunCodeFromUriTool(this));
5060
this.registerTool(createListConnectionsTool(this));
5161
this.registerTool(createListServersTool(this));
62+
this.registerTool(createCheckPythonEnvTool(this));
63+
this.registerTool(createStartPipServerTool(this));
5264
}
5365

5466
private registerTool<Spec extends McpToolSpec>({
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { createCheckPythonEnvTool } from './checkPythonEnvironment';
3+
import type { PipServerController } from '../../types';
4+
import { McpToolResponse } from '../utils/mcpUtils';
5+
6+
vi.mock('vscode');
7+
8+
const MOCK_EXECUTION_TIME_MS = 100;
9+
10+
const EXPECTED_AVAILABLE = {
11+
success: true,
12+
message: 'Python environment is available',
13+
details: {
14+
isAvailable: true,
15+
interpreterPath: '/path/to/python',
16+
},
17+
executionTimeMs: MOCK_EXECUTION_TIME_MS,
18+
} as const;
19+
20+
const EXPECTED_NOT_AVAILABLE = {
21+
success: true,
22+
message: 'Python environment is not available',
23+
details: {
24+
isAvailable: false,
25+
},
26+
executionTimeMs: MOCK_EXECUTION_TIME_MS,
27+
} as const;
28+
29+
const EXPECTED_CONTROLLER_NOT_AVAILABLE = {
30+
success: false,
31+
message: 'PipServerController not available',
32+
executionTimeMs: MOCK_EXECUTION_TIME_MS,
33+
} as const;
34+
35+
const EXPECTED_CHECK_ERROR = {
36+
success: false,
37+
message: 'Failed to check Python environment: Check failed',
38+
executionTimeMs: MOCK_EXECUTION_TIME_MS,
39+
} as const;
40+
41+
describe('checkPythonEnvironment', () => {
42+
const mockPipServerController = {
43+
checkPipInstall: vi.fn(),
44+
} as unknown as PipServerController;
45+
46+
beforeEach(() => {
47+
vi.clearAllMocks();
48+
49+
vi.spyOn(McpToolResponse.prototype, 'getElapsedTimeMs').mockReturnValue(
50+
MOCK_EXECUTION_TIME_MS
51+
);
52+
});
53+
54+
it('should return correct tool spec', () => {
55+
const tool = createCheckPythonEnvTool({
56+
pipServerController: mockPipServerController,
57+
});
58+
59+
expect(tool.name).toBe('checkPythonEnvironment');
60+
expect(tool.spec.title).toBe('Check Python Environment');
61+
expect(tool.spec.description).toBe(
62+
'Check if the Python environment supports starting a Deephaven pip server.'
63+
);
64+
});
65+
66+
it('should return error when pipServerController is null', async () => {
67+
const tool = createCheckPythonEnvTool({ pipServerController: null });
68+
const result = await tool.handler({});
69+
70+
expect(result.structuredContent).toEqual(EXPECTED_CONTROLLER_NOT_AVAILABLE);
71+
});
72+
73+
it('should return success when Python environment is available', async () => {
74+
vi.mocked(mockPipServerController.checkPipInstall).mockResolvedValue({
75+
isAvailable: true,
76+
interpreterPath: '/path/to/python',
77+
});
78+
79+
const tool = createCheckPythonEnvTool({
80+
pipServerController: mockPipServerController,
81+
});
82+
const result = await tool.handler({});
83+
84+
expect(mockPipServerController.checkPipInstall).toHaveBeenCalledOnce();
85+
expect(result.structuredContent).toEqual(EXPECTED_AVAILABLE);
86+
});
87+
88+
it('should return success when Python environment is not available', async () => {
89+
vi.mocked(mockPipServerController.checkPipInstall).mockResolvedValue({
90+
isAvailable: false,
91+
});
92+
93+
const tool = createCheckPythonEnvTool({
94+
pipServerController: mockPipServerController,
95+
});
96+
const result = await tool.handler({});
97+
98+
expect(mockPipServerController.checkPipInstall).toHaveBeenCalledOnce();
99+
expect(result.structuredContent).toEqual(EXPECTED_NOT_AVAILABLE);
100+
});
101+
102+
it('should handle errors from checkPipInstall', async () => {
103+
const error = new Error('Check failed');
104+
vi.mocked(mockPipServerController.checkPipInstall).mockRejectedValue(error);
105+
106+
const tool = createCheckPythonEnvTool({
107+
pipServerController: mockPipServerController,
108+
});
109+
const result = await tool.handler({});
110+
111+
expect(result.structuredContent).toEqual(EXPECTED_CHECK_ERROR);
112+
});
113+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { z } from 'zod';
2+
import type {
3+
McpTool,
4+
McpToolHandlerResult,
5+
PipServerController,
6+
} from '../../types';
7+
import { McpToolResponse } from '../utils';
8+
9+
const spec = {
10+
title: 'Check Python Environment',
11+
description:
12+
'Check if the Python environment supports starting a Deephaven pip server.',
13+
inputSchema: {},
14+
outputSchema: {
15+
success: z.boolean(),
16+
message: z.string(),
17+
executionTimeMs: z.number().describe('Execution time in milliseconds'),
18+
details: z
19+
.object({
20+
isAvailable: z.boolean(),
21+
interpreterPath: z.string().optional(),
22+
})
23+
.optional(),
24+
},
25+
} as const;
26+
27+
type Spec = typeof spec;
28+
type HandlerResult = McpToolHandlerResult<Spec>;
29+
type CheckPythonEnvTool = McpTool<Spec>;
30+
31+
export function createCheckPythonEnvTool({
32+
pipServerController,
33+
}: {
34+
pipServerController: PipServerController | null;
35+
}): CheckPythonEnvTool {
36+
return {
37+
name: 'checkPythonEnvironment',
38+
spec,
39+
handler: async (): Promise<HandlerResult> => {
40+
const response = new McpToolResponse();
41+
42+
if (pipServerController == null) {
43+
return response.error('PipServerController not available');
44+
}
45+
46+
try {
47+
const result = await pipServerController.checkPipInstall();
48+
49+
if (result.isAvailable) {
50+
return response.success('Python environment is available', {
51+
isAvailable: true,
52+
interpreterPath: result.interpreterPath,
53+
});
54+
}
55+
56+
return response.success('Python environment is not available', {
57+
isAvailable: false,
58+
});
59+
} catch (error) {
60+
return response.error('Failed to check Python environment', error);
61+
}
62+
},
63+
};
64+
}

src/mcp/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
export * from './checkPythonEnvironment';
12
export * from './listConnections';
23
export * from './listServers';
34
export * from './runCode';
45
export * from './runCodeFromUri';
6+
export * from './startPipServer';

0 commit comments

Comments
 (0)