-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathmcp.ts
More file actions
168 lines (144 loc) · 5.38 KB
/
Copy pathmcp.ts
File metadata and controls
168 lines (144 loc) · 5.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { AuthCommand } from './authCommand'
import { Flags } from '@oclif/core'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as os from 'node:os'
import { spawn } from 'node:child_process'
import { EXAMPLE_SPEC_TS, CHECKLY_CONFIG_TS, TSCONFIG_JSON, PACKAGE_JSON } from '../templates/packed-files'
// Default playwright.config.ts since it's not in packed-files
const PLAYWRIGHT_CONFIG_TS = `import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});`
export default class McpCommand extends AuthCommand {
static coreCommand = true
static hidden = false
static description = 'Run MCP server tests with Playwright on Checkly.'
static state = 'beta'
static flags = {
verbose: Flags.boolean({
char: 'v',
description: 'Show all logs, errors, and stdout from processes',
default: false,
}),
}
async run(): Promise<void> {
const { flags } = await this.parse(McpCommand)
const { verbose } = flags
let tempDir: string | null = null
let pinggyUrl: string | null = null
// Output initial "in progress" message
this.log('starting mcp server in progress (this can take a while)..')
this.log('it will run for 20 minutes and then exit automatically')
try {
// Create temporary directory
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'checkly-mcp-'))
// Write all files to temp directory
await fs.writeFile(path.join(tempDir, 'example.spec.ts'), EXAMPLE_SPEC_TS)
await fs.writeFile(path.join(tempDir, 'checkly.config.ts'), CHECKLY_CONFIG_TS)
await fs.writeFile(path.join(tempDir, 'tsconfig.json'), TSCONFIG_JSON)
await fs.writeFile(path.join(tempDir, 'package.json'), PACKAGE_JSON)
await fs.writeFile(path.join(tempDir, 'playwright.config.ts'), PLAYWRIGHT_CONFIG_TS)
// Run npm install in the temp directory
const { execa } = await import('execa')
if (verbose) {
this.log('Installing dependencies...')
}
await execa('npm', ['install'], {
cwd: tempDir,
stdio: verbose ? 'inherit' : 'pipe' // Show output in verbose mode
})
// Save current directory
const originalCwd = process.cwd()
// Change to temp directory
process.chdir(tempDir)
try {
// Run pw-test command with stream-logs enabled using spawn to capture output
// Use the same node and checkly command that was used to run this command
const nodeExecutable = process.argv[0]
const checklyExecutable = process.argv[1]
const args = [checklyExecutable, 'pw-test', '--stream-logs']
const pwTestProcess = spawn(nodeExecutable, args, {
cwd: tempDir,
env: { ...process.env },
stdio: ['inherit', 'pipe', 'pipe']
})
// Capture stdout
pwTestProcess.stdout?.on('data', (data) => {
const output = data.toString()
// In verbose mode, show all output
if (verbose) {
process.stdout.write(output)
}
// Parse for Pinggy URL
const urlMatch = output.match(/🌐 PINGGY PUBLIC URL:\s*(https:\/\/[a-z0-9-]+\.a\.free\.pinggy\.link)/)
if (urlMatch && !pinggyUrl) {
pinggyUrl = urlMatch[1]
// Output JSON immediately when URL is found
this.log(JSON.stringify({ serverUrl: pinggyUrl }))
}
})
// Capture stderr as well
pwTestProcess.stderr?.on('data', (data) => {
const output = data.toString()
// In verbose mode, show all error output
if (verbose) {
process.stderr.write(output)
}
// Also check stderr for URLs (pinggy might output there)
const urlMatch = output.match(/🌐 PINGGY PUBLIC URL:\s*(https:\/\/[a-z0-9-]+\.a\.free\.pinggy\.link)/)
if (urlMatch && !pinggyUrl) {
pinggyUrl = urlMatch[1]
// Output JSON immediately when URL is found
this.log(JSON.stringify({ serverUrl: pinggyUrl }))
}
})
// Wait for process to complete
await new Promise<void>((resolve, reject) => {
pwTestProcess.on('close', (code) => {
if (code === 0) {
resolve()
} else {
reject(new Error(`Process exited with code ${code}`))
}
})
pwTestProcess.on('error', (error) => {
reject(error)
})
})
} finally {
// Always restore original directory
process.chdir(originalCwd)
}
} catch (error) {
// Show errors in verbose mode
if (verbose && error instanceof Error) {
this.error(error.message)
}
process.exit(1)
} finally {
// Clean up temp directory
if (tempDir) {
try {
await fs.rm(tempDir, { recursive: true, force: true })
} catch (cleanupError) {
// Suppress cleanup errors
}
}
}
}
}