Skip to content

Conversation

@elasticdotventures
Copy link
Member

Summary

  • add pm2 MCP server implementation exportable start helper
  • add mocha harness using MCP in-memory transport to validate tools/resources and happy/error flows
  • wire MCP test into unit.sh

Testing

  • npx mocha test/mcp/server.mocha.js

Copy link

Copilot AI left a 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');
Copy link

Copilot AI Dec 1, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

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');
Copy link

Copilot AI Dec 1, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

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

Comment on lines +8 to +9
const { Client } = require('@modelcontextprotocol/sdk/client');
const { InMemoryTransport } = require('@modelcontextprotocol/sdk/inMemory.js');
Copy link

Copilot AI Dec 1, 2025

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).

Copilot uses AI. Check for mistakes.
})
]);
} finally {
clearTimeout(timer);
Copy link

Copilot AI Dec 1, 2025

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).

Copilot uses AI. Check for mistakes.
Comment on lines +339 to +546
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);
}
}
);
Copy link

Copilot AI Dec 1, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

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

Copy link

Copilot AI commented Dec 1, 2025

@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.

Copilot AI and others added 4 commits December 1, 2025 12:35
…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>
Copy link

Copilot AI commented Dec 1, 2025

@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.

Copy link

Copilot AI commented Dec 1, 2025

@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
@elasticdotventures elasticdotventures merged commit c51a51c into master Dec 1, 2025
3 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants