mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 12:19:20 +00:00
Add initial MCP server (#261898)
This commit is contained in:
committed by
GitHub
parent
e98a20f554
commit
4a653ec816
9
.vscode/mcp.json
vendored
Normal file
9
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"servers": {
|
||||
"vscode-playwright-mcp": {
|
||||
"url": "http://localhost:33418/mcp",
|
||||
"type": "http"
|
||||
}
|
||||
},
|
||||
"inputs": []
|
||||
}
|
||||
25
.vscode/tasks.json
vendored
25
.vscode/tasks.json
vendored
@@ -198,7 +198,13 @@
|
||||
"windows": {
|
||||
"command": ".\\scripts\\code-server.bat"
|
||||
},
|
||||
"args": ["--no-launch", "--connection-token", "dev-token", "--port", "8080"],
|
||||
"args": [
|
||||
"--no-launch",
|
||||
"--connection-token",
|
||||
"dev-token",
|
||||
"--port",
|
||||
"8080"
|
||||
],
|
||||
"label": "Run code server",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
@@ -220,7 +226,12 @@
|
||||
"windows": {
|
||||
"command": ".\\scripts\\code-web.bat"
|
||||
},
|
||||
"args": ["--port", "8080", "--browser", "none"],
|
||||
"args": [
|
||||
"--port",
|
||||
"8080",
|
||||
"--browser",
|
||||
"none"
|
||||
],
|
||||
"label": "Run code web",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
@@ -268,7 +279,6 @@
|
||||
"detail": "node_modules/tsec/bin/tsec -p src/tsconfig.json --noEmit"
|
||||
},
|
||||
{
|
||||
// Used for monaco editor playground launch config
|
||||
"label": "Launch Http Server",
|
||||
"type": "shell",
|
||||
"command": "node_modules/.bin/ts-node -T ./scripts/playground-server",
|
||||
@@ -286,6 +296,15 @@
|
||||
"dependsOn": [
|
||||
"Core - Build"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Launch MCP Server",
|
||||
"type": "shell",
|
||||
"command": "cd test/mcp && npm run compile && npm start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1303,6 +1303,18 @@ export default tseslint.config(
|
||||
'@playwright/*',
|
||||
'*' // node modules
|
||||
]
|
||||
},
|
||||
{
|
||||
'target': 'test/mcp/**',
|
||||
'restrictions': [
|
||||
'test/automation',
|
||||
'test/mcp/**',
|
||||
'@vscode/*',
|
||||
'@parcel/*',
|
||||
'@playwright/*',
|
||||
'@modelcontextprotocol/sdk/**/*',
|
||||
'*' // node modules
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ export class PlaywrightDriver {
|
||||
) {
|
||||
}
|
||||
|
||||
get browserContext(): playwright.BrowserContext {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
async startTracing(name: string): Promise<void> {
|
||||
if (!this.options.tracing) {
|
||||
return; // tracing disabled
|
||||
|
||||
9
test/mcp/.gitignore
vendored
Normal file
9
test/mcp/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
Thumbs.db
|
||||
node_modules/
|
||||
out/
|
||||
keybindings.*.json
|
||||
test_data/
|
||||
src/vscode/driver.d.ts
|
||||
vscode-server*/
|
||||
137
test/mcp/README.md
Normal file
137
test/mcp/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Code - OSS Development MCP Server
|
||||
|
||||
This directory contains a Model Context Protocol (MCP) server that provides Playwright browser automation capabilities for Code - OSS development and testing. The MCP server exposes Code - OSS's Playwright testing infrastructure through a standardized interface, allowing AI assistants and other tools to interact with browsers programmatically.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI assistants to securely connect to external data sources and tools. This MCP server specifically provides browser automation capabilities using Playwright, making it possible for AI assistants to:
|
||||
|
||||
- Navigate web pages
|
||||
- Interact with UI elements (click, type, hover, etc.)
|
||||
- Take screenshots and capture page content
|
||||
- Evaluate JavaScript in browser contexts
|
||||
- Handle file uploads and downloads
|
||||
- Manage browser tabs and windows
|
||||
|
||||
## Quick Start
|
||||
|
||||
Getting started with the MCP server is simple - just run the pre-configured Code - OSS task:
|
||||
|
||||
### 1. Launch the MCP Server
|
||||
|
||||
In Code - OSS, open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and run:
|
||||
|
||||
```
|
||||
Tasks: Run Task → Launch MCP Server
|
||||
```
|
||||
|
||||
### 2. Start the MCP Server
|
||||
|
||||
Open the Command Palette and run:
|
||||
```
|
||||
MCP: List Servers → vscode-playwright-mcp → Start Server
|
||||
```
|
||||
or open [mcp.json](../../.vscode/mcp.json) and start it from there.
|
||||
|
||||
That's it! Your AI assistant can now use browser automation capabilities through MCP.
|
||||
|
||||
## What the Server Provides
|
||||
|
||||
The MCP server exposes a comprehensive set of browser automation tools through the MCP protocol:
|
||||
|
||||
### Navigation & Page Management
|
||||
- Navigate to URLs
|
||||
- Go back/forward in browser history
|
||||
- Manage browser tabs (open, close, switch)
|
||||
- Resize browser windows
|
||||
|
||||
### Element Interaction
|
||||
- Click on elements (single, double, right-click)
|
||||
- Type text into input fields
|
||||
- Hover over elements
|
||||
- Drag and drop between elements
|
||||
- Select options in dropdowns
|
||||
|
||||
### Content Capture & Analysis
|
||||
- Take screenshots (full page or specific elements)
|
||||
- Capture accessibility snapshots for better element targeting
|
||||
- Get page console messages
|
||||
- Monitor network requests
|
||||
|
||||
### Advanced Features
|
||||
- Evaluate JavaScript code in browser contexts
|
||||
- Handle file uploads
|
||||
- Wait for specific content or time delays
|
||||
- Handle browser dialogs and alerts
|
||||
|
||||
## Development
|
||||
|
||||
### Manual Setup (Advanced)
|
||||
|
||||
If you prefer to run the server manually:
|
||||
|
||||
```bash
|
||||
# Navigate to the MCP directory
|
||||
cd test/mcp
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Compile TypeScript
|
||||
npm run compile # or watch
|
||||
|
||||
# Start the server
|
||||
npm start
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
test/mcp/
|
||||
├── src/
|
||||
│ ├── main.ts # Express server and MCP endpoint handlers
|
||||
│ ├── playwright.ts # Code - OSS Playwright integration
|
||||
│ ├── inMemoryEventStore.ts # Session management for resumability
|
||||
│ └── utils.ts # Utility functions
|
||||
├── package.json # Dependencies and scripts
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Session Management**: Supports multiple concurrent MCP sessions with proper cleanup
|
||||
- **Resumability**: Built-in event store for connection resumption
|
||||
- **Code - OSS Integration**: Uses Code - OSS's existing Playwright test infrastructure
|
||||
- **CORS Support**: Configured for cross-origin requests
|
||||
- **Error Handling**: Comprehensive error handling and logging
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server Won't Start
|
||||
- Ensure Code - OSS's Core and Extension builds are running (they should start automatically)
|
||||
- Check that port 33418 is not already in use
|
||||
- Verify all dependencies are installed with `npm install`
|
||||
|
||||
### Connection Issues
|
||||
- Confirm the server is running on `http://localhost:33418/mcp`
|
||||
- Check your AI assistant's MCP configuration
|
||||
- Look for CORS-related errors in browser console
|
||||
|
||||
### Browser Automation Issues
|
||||
- Ensure Code - OSS has been built and run at least once
|
||||
- Check the server logs for Playwright-related errors
|
||||
- Verify the test repository is properly cloned
|
||||
|
||||
## Contributing
|
||||
|
||||
This MCP server is part of the Code - OSS development infrastructure. When making changes:
|
||||
|
||||
1. Follow the existing TypeScript and coding conventions
|
||||
2. Test with multiple MCP clients if possible
|
||||
3. Update this README if adding new capabilities
|
||||
4. Ensure proper error handling and logging
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the top-level project's license file for details.
|
||||
3220
test/mcp/package-lock.json
generated
Normal file
3220
test/mcp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
test/mcp/package.json
Normal file
31
test/mcp/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "code-oss-dev-mcp",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"main": "./out/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"compile": "cd ../automation && npm run compile && cd ../mcp && node ../../node_modules/typescript/bin/tsc",
|
||||
"watch-automation": "cd ../automation && npm run watch",
|
||||
"watch-mcp": "node ../../node_modules/typescript/bin/tsc --watch --preserveWatchOutput",
|
||||
"watch": "npm-run-all -lp watch-automation watch-mcp",
|
||||
"start": "node ./out/main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.17.3",
|
||||
"@playwright/mcp": "^0.0.33",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"ncp": "^2.0.0",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/ncp": "2.0.1",
|
||||
"@types/node": "22.x",
|
||||
"@types/node-fetch": "^2.5.10",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
81
test/mcp/src/inMemoryEventStore.ts
Normal file
81
test/mcp/src/inMemoryEventStore.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { EventStore } from '@modelcontextprotocol/sdk/server/streamableHttp';
|
||||
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types';
|
||||
|
||||
/**
|
||||
* Simple in-memory implementation of the EventStore interface for resumability
|
||||
* This is primarily intended for examples and testing, not for production use
|
||||
* where a persistent storage solution would be more appropriate.
|
||||
*/
|
||||
export class InMemoryEventStore implements EventStore {
|
||||
private events: Map<string, { streamId: string; message: JSONRPCMessage }> = new Map();
|
||||
|
||||
/**
|
||||
* Generates a unique event ID for a given stream ID
|
||||
*/
|
||||
private generateEventId(streamId: string): string {
|
||||
return `${streamId}_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the stream ID from an event ID
|
||||
*/
|
||||
private getStreamIdFromEventId(eventId: string): string {
|
||||
const parts = eventId.split('_');
|
||||
return parts.length > 0 ? parts[0] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores an event with a generated event ID
|
||||
* Implements EventStore.storeEvent
|
||||
*/
|
||||
async storeEvent(streamId: string, message: JSONRPCMessage): Promise<string> {
|
||||
const eventId = this.generateEventId(streamId);
|
||||
this.events.set(eventId, { streamId, message });
|
||||
return eventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays events that occurred after a specific event ID
|
||||
* Implements EventStore.replayEventsAfter
|
||||
*/
|
||||
async replayEventsAfter(lastEventId: string,
|
||||
{ send }: { send: (eventId: string, message: JSONRPCMessage) => Promise<void> }
|
||||
): Promise<string> {
|
||||
if (!lastEventId || !this.events.has(lastEventId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Extract the stream ID from the event ID
|
||||
const streamId = this.getStreamIdFromEventId(lastEventId);
|
||||
if (!streamId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let foundLastEvent = false;
|
||||
|
||||
// Sort events by eventId for chronological ordering
|
||||
const sortedEvents = [...this.events.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||||
|
||||
for (const [eventId, { streamId: eventStreamId, message }] of sortedEvents) {
|
||||
// Only include events from the same stream
|
||||
if (eventStreamId !== streamId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start sending events after we find the lastEventId
|
||||
if (eventId === lastEventId) {
|
||||
foundLastEvent = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (foundLastEvent) {
|
||||
await send(eventId, message);
|
||||
}
|
||||
}
|
||||
return streamId;
|
||||
}
|
||||
}
|
||||
186
test/mcp/src/main.ts
Normal file
186
test/mcp/src/main.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as express from 'express';
|
||||
import { Request, Response } from 'express';
|
||||
import * as cors from 'cors';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { InMemoryEventStore } from './inMemoryEventStore';
|
||||
import { getServer } from './playwright';
|
||||
|
||||
const MCP_PORT = 33418;
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Allow CORS all domains, expose the Mcp-Session-Id header
|
||||
app.use(cors({
|
||||
origin: '*', // Allow all origins
|
||||
exposedHeaders: ['Mcp-Session-Id']
|
||||
}));
|
||||
|
||||
// Map to store transports by session ID
|
||||
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
|
||||
// MCP POST endpoint with optional auth
|
||||
const mcpPostHandler = async (req: Request, res: Response) => {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
if (sessionId) {
|
||||
console.log(`Received MCP request for session: ${sessionId}`);
|
||||
} else {
|
||||
console.log('Request body:', req.body);
|
||||
}
|
||||
|
||||
try {
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
if (sessionId && transports[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = transports[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request
|
||||
const eventStore = new InMemoryEventStore();
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
eventStore, // Enable resumability
|
||||
onsessioninitialized: (sessionId: any) => {
|
||||
// Store the transport by session ID when session is initialized
|
||||
// This avoids race conditions where requests might come in before the session is stored
|
||||
console.log(`Session initialized with ID: ${sessionId}`);
|
||||
transports[sessionId] = transport;
|
||||
}
|
||||
});
|
||||
|
||||
// Set up onclose handler to clean up transport when closed
|
||||
transport.onclose = () => {
|
||||
const sid = transport.sessionId;
|
||||
if (sid && transports[sid]) {
|
||||
console.log(`Transport closed for session ${sid}, removing from transports map`);
|
||||
delete transports[sid];
|
||||
}
|
||||
};
|
||||
|
||||
// Connect the transport to the MCP server BEFORE handling the request
|
||||
// so responses can flow back through the same transport
|
||||
// NOTE: the shape of this is very similar to a Server but not quite
|
||||
// I think it's because it's electron while playwright expects browser.
|
||||
// TODO: Fix that.
|
||||
const server = await getServer() as any;
|
||||
server.onclose = () => {
|
||||
const sid = transport.sessionId;
|
||||
if (sid && transports[sid]) {
|
||||
console.log(`Transport closed for session ${sid}, removing from transports map`);
|
||||
delete transports[sid];
|
||||
}
|
||||
};
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
return; // Already handled
|
||||
} else {
|
||||
// Invalid request - no session ID or not initialization request
|
||||
res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Bad Request: No valid session ID provided',
|
||||
},
|
||||
id: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the request with existing transport - no need to reconnect
|
||||
// The existing transport is already connected to the server
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error('Error handling MCP request:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error',
|
||||
},
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set up routes with conditional auth middleware
|
||||
app.post('/mcp', mcpPostHandler);
|
||||
|
||||
// Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
|
||||
const mcpGetHandler = async (req: Request, res: Response) => {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
if (!sessionId || !transports[sessionId]) {
|
||||
res.status(400).send('Invalid or missing session ID');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Last-Event-ID header for resumability
|
||||
const lastEventId = req.headers['last-event-id'] as string | undefined;
|
||||
if (lastEventId) {
|
||||
console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
|
||||
} else {
|
||||
console.log(`Establishing new SSE stream for session ${sessionId}`);
|
||||
}
|
||||
|
||||
const transport = transports[sessionId];
|
||||
await transport.handleRequest(req, res);
|
||||
};
|
||||
|
||||
// Set up GET route with conditional auth middleware
|
||||
app.get('/mcp', mcpGetHandler);
|
||||
|
||||
// Handle DELETE requests for session termination (according to MCP spec)
|
||||
const mcpDeleteHandler = async (req: Request, res: Response) => {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
if (!sessionId || !transports[sessionId]) {
|
||||
res.status(400).send('Invalid or missing session ID');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Received session termination request for session ${sessionId}`);
|
||||
|
||||
try {
|
||||
const transport = transports[sessionId];
|
||||
await transport.handleRequest(req, res);
|
||||
} catch (error) {
|
||||
console.error('Error handling session termination:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send('Error processing session termination');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
app.delete('/mcp', mcpDeleteHandler);
|
||||
|
||||
app.listen(MCP_PORT, (error: any) => {
|
||||
if (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`MCP available at http://localhost:${MCP_PORT}/mcp`);
|
||||
});
|
||||
|
||||
// Handle server shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Shutting down server...');
|
||||
|
||||
// Close all active transports to properly clean up resources
|
||||
for (const sessionId in transports) {
|
||||
try {
|
||||
console.log(`Closing transport for session ${sessionId}`);
|
||||
await transports[sessionId].close();
|
||||
delete transports[sessionId];
|
||||
} catch (error) {
|
||||
console.error(`Error closing transport for session ${sessionId}:`, error);
|
||||
}
|
||||
}
|
||||
console.log('Server shutdown complete');
|
||||
process.exit(0);
|
||||
});
|
||||
169
test/mcp/src/playwright.ts
Normal file
169
test/mcp/src/playwright.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createConnection } from '@playwright/mcp';
|
||||
import { getDevElectronPath, Quality, ConsoleLogger, FileLogger, Logger, MultiLogger } from '../../automation';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as cp from 'child_process';
|
||||
import * as os from 'os';
|
||||
import { createApp, parseVersion } from './utils';
|
||||
import * as minimist from 'minimist';
|
||||
|
||||
const rootPath = path.join(__dirname, '..', '..', '..');
|
||||
|
||||
const [, , ...args] = process.argv;
|
||||
const opts = minimist(args, {
|
||||
string: [
|
||||
'browser',
|
||||
'build',
|
||||
'stable-build',
|
||||
'wait-time',
|
||||
'test-repo',
|
||||
'electronArgs'
|
||||
],
|
||||
boolean: [
|
||||
'verbose',
|
||||
'remote',
|
||||
'web',
|
||||
'headless',
|
||||
'tracing'
|
||||
],
|
||||
default: {
|
||||
verbose: false
|
||||
}
|
||||
}) as {
|
||||
verbose?: boolean;
|
||||
remote?: boolean;
|
||||
headless?: boolean;
|
||||
web?: boolean;
|
||||
tracing?: boolean;
|
||||
build?: string;
|
||||
'stable-build'?: string;
|
||||
browser?: 'chromium' | 'webkit' | 'firefox' | 'chromium-msedge' | 'chromium-chrome' | undefined;
|
||||
electronArgs?: string;
|
||||
};
|
||||
|
||||
const testDataPath = path.join(os.tmpdir(), 'vscsmoke');
|
||||
if (fs.existsSync(testDataPath)) {
|
||||
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 });
|
||||
}
|
||||
fs.mkdirSync(testDataPath, { recursive: true });
|
||||
process.once('exit', () => {
|
||||
try {
|
||||
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 });
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
});
|
||||
|
||||
const testRepoUrl = 'https://github.com/microsoft/vscode-smoketest-express';
|
||||
const workspacePath = path.join(testDataPath, 'vscode-smoketest-express');
|
||||
const extensionsPath = path.join(testDataPath, 'extensions-dir');
|
||||
fs.mkdirSync(extensionsPath, { recursive: true });
|
||||
|
||||
const logsRootPath = (() => {
|
||||
const logsParentPath = path.join(rootPath, '.build', 'logs');
|
||||
|
||||
let logsName: string;
|
||||
if (opts.web) {
|
||||
logsName = 'mcp-browser';
|
||||
} else if (opts.remote) {
|
||||
logsName = 'mcp-remote';
|
||||
} else {
|
||||
logsName = 'mcp-electron';
|
||||
}
|
||||
|
||||
return path.join(logsParentPath, logsName);
|
||||
})();
|
||||
|
||||
const crashesRootPath = (() => {
|
||||
const crashesParentPath = path.join(rootPath, '.build', 'crashes');
|
||||
|
||||
let crashesName: string;
|
||||
if (opts.web) {
|
||||
crashesName = 'mcp-browser';
|
||||
} else if (opts.remote) {
|
||||
crashesName = 'mcp-remote';
|
||||
} else {
|
||||
crashesName = 'mcp-electron';
|
||||
}
|
||||
|
||||
return path.join(crashesParentPath, crashesName);
|
||||
})();
|
||||
|
||||
const logger = createLogger();
|
||||
|
||||
function createLogger(): Logger {
|
||||
const loggers: Logger[] = [];
|
||||
|
||||
// Log to console if verbose
|
||||
if (opts.verbose) {
|
||||
loggers.push(new ConsoleLogger());
|
||||
}
|
||||
|
||||
// Prepare logs rot path
|
||||
fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 3 });
|
||||
fs.mkdirSync(logsRootPath, { recursive: true });
|
||||
|
||||
// Always log to log file
|
||||
loggers.push(new FileLogger(path.join(logsRootPath, 'smoke-test-runner.log')));
|
||||
|
||||
return new MultiLogger(loggers);
|
||||
}
|
||||
|
||||
async function setupRepository(): Promise<void> {
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
logger.log('Cloning test project repository...');
|
||||
const res = cp.spawnSync('git', ['clone', testRepoUrl, workspacePath], { stdio: 'inherit' });
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
throw new Error(`Clone operation failed: ${res.stderr.toString()}`);
|
||||
}
|
||||
} else {
|
||||
logger.log('Cleaning test project repository...');
|
||||
cp.spawnSync('git', ['fetch'], { cwd: workspacePath, stdio: 'inherit' });
|
||||
cp.spawnSync('git', ['reset', '--hard', 'FETCH_HEAD'], { cwd: workspacePath, stdio: 'inherit' });
|
||||
cp.spawnSync('git', ['clean', '-xdf'], { cwd: workspacePath, stdio: 'inherit' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getServer() {
|
||||
const testCodePath = getDevElectronPath();
|
||||
const electronPath = testCodePath;
|
||||
if (!fs.existsSync(electronPath || '')) {
|
||||
throw new Error(`Cannot find VSCode at ${electronPath}. Please run VSCode once first (scripts/code.sh, scripts\\code.bat) and try again.`);
|
||||
}
|
||||
process.env.VSCODE_REPOSITORY = rootPath;
|
||||
process.env.VSCODE_DEV = '1';
|
||||
process.env.VSCODE_CLI = '1';
|
||||
const quality = Quality.Dev;
|
||||
const userDataDir = path.join(testDataPath, 'd');
|
||||
|
||||
await setupRepository();
|
||||
const application = createApp({
|
||||
quality,
|
||||
version: parseVersion('0.0.0'),
|
||||
codePath: opts.build,
|
||||
workspacePath,
|
||||
userDataDir,
|
||||
extensionsPath,
|
||||
logger,
|
||||
logsPath: path.join(logsRootPath, 'suite_unknown'),
|
||||
crashesPath: path.join(crashesRootPath, 'suite_unknown'),
|
||||
verbose: opts.verbose,
|
||||
remote: opts.remote,
|
||||
web: opts.web,
|
||||
tracing: opts.tracing,
|
||||
headless: opts.headless,
|
||||
browser: opts.browser,
|
||||
extraArgs: (opts.electronArgs || '').split(' ').map(arg => arg.trim()).filter(arg => !!arg),
|
||||
}, opts => ({ ...opts, userDataDir: path.join(opts.userDataDir, 'ø') }));
|
||||
await application.start();
|
||||
const connection = await createConnection(undefined, () => Promise.resolve(application.code.driver.browserContext as any));
|
||||
application.code.driver.browserContext.on('close', () => {
|
||||
connection.close();
|
||||
});
|
||||
return connection;
|
||||
}
|
||||
46
test/mcp/src/utils.ts
Normal file
46
test/mcp/src/utils.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { dirname, join } from 'path';
|
||||
import { Application, ApplicationOptions } from '../../automation';
|
||||
|
||||
let logsCounter = 1;
|
||||
let crashCounter = 1;
|
||||
|
||||
export function parseVersion(version: string): { major: number; minor: number; patch: number } {
|
||||
const [, major, minor, patch] = /^(\d+)\.(\d+)\.(\d+)/.exec(version)!;
|
||||
return { major: parseInt(major), minor: parseInt(minor), patch: parseInt(patch) };
|
||||
}
|
||||
|
||||
export function suiteLogsPath(options: ApplicationOptions, suiteName: string): string {
|
||||
return join(dirname(options.logsPath), `${logsCounter++}_suite_${suiteName.replace(/[^a-z0-9\-]/ig, '_')}`);
|
||||
}
|
||||
|
||||
export function suiteCrashPath(options: ApplicationOptions, suiteName: string): string {
|
||||
return join(dirname(options.crashesPath), `${crashCounter++}_suite_${suiteName.replace(/[^a-z0-9\-]/ig, '_')}`);
|
||||
}
|
||||
|
||||
export function getRandomUserDataDir(options: ApplicationOptions): string {
|
||||
|
||||
// Pick a random user data dir suffix that is not
|
||||
// too long to not run into max path length issues
|
||||
// https://github.com/microsoft/vscode/issues/34988
|
||||
const userDataPathSuffix = [...Array(8)].map(() => Math.random().toString(36)[3]).join('');
|
||||
|
||||
return options.userDataDir.concat(`-${userDataPathSuffix}`);
|
||||
}
|
||||
|
||||
export function createApp(options: ApplicationOptions, optionsTransform?: (opts: ApplicationOptions) => ApplicationOptions): Application {
|
||||
if (optionsTransform) {
|
||||
options = optionsTransform({ ...options });
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
...options,
|
||||
userDataDir: getRandomUserDataDir(options)
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
22
test/mcp/tsconfig.json
Normal file
22
test/mcp/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": false,
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"target": "es2024",
|
||||
"strict": true,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"outDir": "out",
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"esnext", // for #201187
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user