-
Notifications
You must be signed in to change notification settings - Fork 0
Add MCP MCP test harness #2
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
Conversation
Signed-off-by: Brian Horakh <35611074+elasticdotventures@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds an MCP (Model Context Protocol) server implementation for PM2, enabling AI tools to interact with PM2 processes through a standardized protocol. It also updates the minimum Node.js version requirement from 16.x to 22.0.0 across all packaging and documentation.
- Implements a complete MCP server exposing PM2 operations as tools and resources
- Adds comprehensive test harness using in-memory transport for validation
- Updates Node.js version requirements across package.json, packagers, and documentation
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/mcp/server.js | New MCP server implementation with PM2 tool/resource handlers |
| bin/pm2-mcp | New entry point binary for the MCP server |
| test/mcp/server.mocha.js | Test suite validating MCP server tools and resources |
| test/unit.sh | Integrates MCP tests into the unit test suite |
| package.json | Updates Node.js engine requirement to >=22.0.0 |
| package-lock.json | Updates Node.js engine requirement to >=22.0.0 |
| packager/debian/control | Updates Node.js dependency to >=22.0.0 |
| packager/build-deb-rpm.sh | Updates RPM Node.js dependency to >=22.0.0 |
| packager/alpine/pm2/APKBUILD | Updates Alpine Node.js dependency to >=22 |
| README.md | Updates documentation to reflect Node.js 22.x requirement |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * Exposes the core PM2 controls and state as Model Context Protocol tools/resources. | ||
| */ | ||
| const fs = require('fs'); | ||
| const z = require('zod'); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The zod package is used but not listed in package.json dependencies. This will cause the MCP server to fail at runtime with a module not found error. Add zod to the dependencies in package.json.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
| */ | ||
| const fs = require('fs'); | ||
| const z = require('zod'); | ||
| const { McpServer, ResourceTemplate } = require('@modelcontextprotocol/sdk/server/mcp.js'); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The @modelcontextprotocol/sdk package is used but not listed in package.json dependencies. This will cause imports to fail at runtime. Add @modelcontextprotocol/sdk to the dependencies in package.json.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
| const { Client } = require('@modelcontextprotocol/sdk/client'); | ||
| const { InMemoryTransport } = require('@modelcontextprotocol/sdk/inMemory.js'); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The @modelcontextprotocol/sdk package is used but not listed in package.json dependencies. This will cause imports to fail at runtime. Add @modelcontextprotocol/sdk to the dependencies (or devDependencies for test-only usage).
| }) | ||
| ]); | ||
| } finally { | ||
| clearTimeout(timer); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The clearTimeout(timer) will be called even if timer is undefined (when the Promise.race rejects from the first promise). While this is safe in JavaScript, it would be clearer to initialize timer to null or only clear if defined: if (timer) clearTimeout(timer).
| server.registerTool( | ||
| 'pm2_restart_process', | ||
| { | ||
| title: 'Restart a PM2 process', | ||
| description: 'Restart a process by id, name, or "all".', | ||
| inputSchema: restartSchema | ||
| }, | ||
| async ({ process, updateEnv }) => { | ||
| try { | ||
| await ensureConnected(); | ||
| await pm2Restart(process, { updateEnv }); | ||
| const processes = (await pm2List()).map(formatProcess); | ||
| const summary = { action: 'restart', process, updateEnv, processes }; | ||
| return { | ||
| content: textContent(summary), | ||
| structuredContent: summary | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| server.registerTool( | ||
| 'pm2_reload_process', | ||
| { | ||
| title: 'Reload a PM2 process', | ||
| description: 'Perform a zero-downtime reload (cluster mode only).', | ||
| inputSchema: reloadSchema | ||
| }, | ||
| async ({ process, updateEnv }) => { | ||
| try { | ||
| await ensureConnected(); | ||
| await pm2Reload(process, { updateEnv }); | ||
| const processes = (await pm2List()).map(formatProcess); | ||
| const summary = { action: 'reload', process, updateEnv, processes }; | ||
| return { | ||
| content: textContent(summary), | ||
| structuredContent: summary | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| server.registerTool( | ||
| 'pm2_stop_process', | ||
| { | ||
| title: 'Stop a PM2 process', | ||
| description: 'Stop a process by id, name, or "all".', | ||
| inputSchema: stopSchema | ||
| }, | ||
| async ({ process }) => { | ||
| try { | ||
| await ensureConnected(); | ||
| await pm2Stop(process); | ||
| const processes = (await pm2List()).map(formatProcess); | ||
| const summary = { action: 'stop', process, processes }; | ||
| return { | ||
| content: textContent(summary), | ||
| structuredContent: summary | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| server.registerTool( | ||
| 'pm2_delete_process', | ||
| { | ||
| title: 'Delete a PM2 process', | ||
| description: 'Delete a process by id, name, or "all".', | ||
| inputSchema: deleteSchema | ||
| }, | ||
| async ({ process }) => { | ||
| try { | ||
| await ensureConnected(); | ||
| await pm2Delete(process); | ||
| const processes = (await pm2List()).map(formatProcess); | ||
| const summary = { action: 'delete', process, processes }; | ||
| return { | ||
| content: textContent(summary), | ||
| structuredContent: summary | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| server.registerTool( | ||
| 'pm2_flush_logs', | ||
| { | ||
| title: 'Flush PM2 logs', | ||
| description: 'Flush log files for a process id, name, or "all".', | ||
| inputSchema: flushSchema | ||
| }, | ||
| async ({ process }) => { | ||
| try { | ||
| await ensureConnected(); | ||
| await pm2Flush(process); | ||
| return { | ||
| content: textContent({ action: 'flush', process }), | ||
| structuredContent: { action: 'flush', process } | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| server.registerTool( | ||
| 'pm2_reload_logs', | ||
| { | ||
| title: 'Reload PM2 logs', | ||
| description: 'Rotate and reopen log files (pm2 reloadLogs).' | ||
| }, | ||
| async () => { | ||
| try { | ||
| await ensureConnected(); | ||
| await pm2ReloadLogs(); | ||
| return { | ||
| content: textContent({ action: 'reloadLogs' }), | ||
| structuredContent: { action: 'reloadLogs' } | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| server.registerTool( | ||
| 'pm2_dump', | ||
| { | ||
| title: 'Dump PM2 process list', | ||
| description: 'Persist the current PM2 process list to the dump file.' | ||
| }, | ||
| async () => { | ||
| try { | ||
| await ensureConnected(); | ||
| await pm2Dump(); | ||
| return { | ||
| content: textContent({ action: 'dump' }), | ||
| structuredContent: { action: 'dump' } | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| server.registerTool( | ||
| 'pm2_tail_logs', | ||
| { | ||
| title: 'Tail PM2 logs', | ||
| description: 'Read the last N lines from a process log file.', | ||
| inputSchema: logsSchema | ||
| }, | ||
| async ({ process, type, lines }) => { | ||
| try { | ||
| await ensureConnected(); | ||
| const description = await pm2Describe(process); | ||
| if (!description || description.length === 0) { | ||
| throw new Error(`No process found for "${process}"`); | ||
| } | ||
| const env = description[0].pm2_env || {}; | ||
| const logPath = | ||
| type === 'combined' | ||
| ? env.pm_log_path || env.pm_out_log_path || env.pm_err_log_path | ||
| : type === 'out' | ||
| ? env.pm_out_log_path | ||
| : env.pm_err_log_path; | ||
|
|
||
| if (!logPath) throw new Error('No log path found for this process'); | ||
| const data = await tailFile(logPath, lines); | ||
| const payload = { process, type, logPath, lines: data }; | ||
| return { | ||
| content: textContent(`Last ${lines} lines from ${logPath}:\n${data.join('\n')}`), | ||
| structuredContent: payload | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| server.registerTool( | ||
| 'pm2_kill_daemon', | ||
| { | ||
| title: 'Kill PM2 daemon', | ||
| description: 'Stops the PM2 daemon and all managed processes.' | ||
| }, | ||
| async () => { | ||
| try { | ||
| await ensureConnected(); | ||
| await pm2KillDaemon(); | ||
| isConnected = false; | ||
| return { | ||
| content: textContent({ action: 'killDaemon' }), | ||
| structuredContent: { action: 'killDaemon' } | ||
| }; | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Several tools registered in the MCP server lack test coverage: pm2_restart_process, pm2_reload_process, pm2_flush_logs, pm2_reload_logs, and pm2_dump. Consider adding test cases for these tools to ensure they work correctly through the MCP interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
|
@elasticdotventures I've opened a new pull request, #3, to work on those changes. Once the pull request is ready, I'll request review from you. |
…ush_logs, pm2_reload_logs, and pm2_dump tools Co-authored-by: elasticdotventures <35611074+elasticdotventures@users.noreply.github.com>
[WIP] Address feedback on 'Add MCP MCP test harness' PR
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Brian Horakh <35611074+elasticdotventures@users.noreply.github.com>
|
@elasticdotventures I've opened a new pull request, #4, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@elasticdotventures I've opened a new pull request, #5, to work on those changes. Once the pull request is ready, I'll request review from you. |
[WIP] Update MCP test harness implementation based on feedback
Address feedback on MCP test harness implementation
Summary
Testing