Skip to content

Conversation

@elasticdotventures
Copy link
Member

Port of upstream PR 6067 into PromptExecution/pm2-mcp. Adds semantic state enrichment, log analysis tool, and privacy-safe describe as implemented in feature/mcp-semantic-state.

elasticdotventures and others added 5 commits December 3, 2025 12:12
Add MCP server support to PM2 for process management through MCP-compatible clients.

Features:
- New pm2-mcp binary that exposes PM2 process management via MCP
- 12 MCP tools for process lifecycle, logging, and monitoring:
  - pm2_list_processes, pm2_describe_process
  - pm2_start_process, pm2_restart_process, pm2_reload_process
  - pm2_stop_process, pm2_delete_process
  - pm2_flush_logs, pm2_reload_logs, pm2_tail_logs
  - pm2_dump, pm2_kill_daemon
- 2 MCP resources for real-time process information:
  - pm2://processes (list)
  - pm2://process/{id} (detail)
- Automatic sandbox environment detection and adaptation
- Support for stdio and HTTP (Streamable) transports
- Client notifications for sandbox status and recommendations
- Compatible with Claude Code, Codex, and other MCP clients

Implementation:
- New lib/mcp/server.js with full MCP server implementation
- Uses @modelcontextprotocol/sdk for MCP protocol
- Sandbox detection checks home directory writability and environment
- Auto-selects writable PM2_HOME in sandboxed environments
- No-daemon mode by default for MCP client compatibility
- Comprehensive environment variable configuration

Documentation:
- README with MCP server quickstart and setup commands
- Environment variables table (PM2_MCP_*, PM2_HOME, etc.)
- Sandbox detection explanation
- Tool and resource documentation
- Justfile recipes for easy registration with MCP clients

Related:
- Enables pkgx packaging: pkgxdev/pantry#11219
- Development fork: https://github.com/PromptExecution/pm2-mcp
- MCP Specification: https://modelcontextprotocol.io/

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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 Model Context Protocol (MCP) server capabilities to PM2, enabling AI assistants and MCP clients to manage PM2 processes through a standardized interface. It ports upstream PR 6067 and includes semantic state enrichment, log analysis tools, and privacy-safe describe functionality.

Key Changes:

  • Adds a new MCP server implementation that exposes PM2 process management through MCP tools and resources
  • Implements semantic state analysis with log pattern detection and health heuristics
  • Upgrades Node.js requirement from 16.0.0 to 22.0.0

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
package.json Updates Node version requirement to 22.0.0, adds MCP SDK and Zod dependencies, adds pm2-mcp binary and npm script
lib/mcp/server.js New MCP server implementation with 15 tools for process management, semantic state analysis, log parsing, and privacy-safe environment filtering
bin/pm2-mcp New CLI entry point for starting the MCP server with support for stdio and HTTP transports
README.md Documents the new MCP server feature, setup instructions, environment variables, and available tools/resources
Justfile Adds development recipes for registering and testing the MCP server with Claude Code and Codex

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +243 to +250
#### Codex (stdio)
```bash
# Add pm2-mcp to Codex
codex mcp add pm2-mcp -- pm2-mcp

# Verify registration
codex mcp list
```
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Inconsistent terminology: The text uses "Codex" to refer to a CLI tool but doesn't clarify what Codex is. Since this appears alongside Claude Code, consider adding a brief explanation or ensuring readers understand what Codex refers to in this context.

Copilot uses AI. Check for mistakes.
Comment on lines +353 to +373
async function tailFile(filePath, lineCount) {
const fh = await fs.promises.open(filePath, 'r');
try {
const stats = await fh.stat();
let position = stats.size;
const chunkSize = 8192;
let buffer = '';

while (position > 0 && buffer.split(/\r?\n/).length <= lineCount + 1) {
const readSize = Math.min(chunkSize, position);
position -= readSize;
const result = await fh.read({ buffer: Buffer.alloc(readSize), position });
buffer = result.buffer.slice(0, result.bytesRead).toString('utf8') + buffer;
}

const lines = buffer.trimEnd().split(/\r?\n/);
return lines.slice(-lineCount);
} finally {
await fh.close();
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation: The lines parameter in tailFile should be validated to ensure it's a positive integer. If a negative or zero value is passed, the logic in line 361 (buffer.split(/\r?\n/).length <= lineCount + 1) could behave unexpectedly. Add validation: if (lineCount <= 0) throw new Error('lineCount must be positive');

Copilot uses AI. Check for mistakes.
#!/usr/bin/env bash
set -euo pipefail
echo "Testing normal environment:"
node -e "const {detectSandbox} = require('./lib/mcp/server.js'); console.log(detectSandbox ? 'Available' : 'Not exported');" || echo "Normal detection test"
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test-sandbox recipe attempts to require a function that isn't exported from the module. Line 59 tries to require detectSandbox from ./lib/mcp/server.js, but this function is not exported in the module.exports at the end of that file. Either export the function or update this test to use a different approach.

Copilot uses AI. Check for mistakes.
Comment on lines +377 to +384
function redactValue(value) {
if (typeof value !== 'string') return value;
if (value.length === 0) return value;

const upper = value.toUpperCase();
const looksSecret =
SECRET_KEYS.some(key => upper.includes(key)) ||
/(?:sk-[A-Za-z0-9]{32,}|gh[pousr]_[A-Za-z0-9]{36,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,48}|[A-Za-z0-9]{40,})/.test(value);
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secret detection logic checks if the value string contains keywords like "KEY", "TOKEN", etc., which will cause false positives. For example, a value like "This is the KEY to success" would be redacted. The keyword check should only apply to the environment variable name (key), not the value. Consider refactoring to: function redactValue(value, key) { ... } and check the key name for keywords while only checking the value for secret patterns.

Copilot uses AI. Check for mistakes.
Comment on lines +338 to +340
httpServer.timeout = 120000; // 2 minutes
httpServer.headersTimeout = 60000; // 1 minute
httpServer.requestTimeout = 120000; // 2 minutes
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The HTTP server timeout values (120000ms, 60000ms) are hardcoded magic numbers. Extract them as named constants:

const HTTP_SERVER_TIMEOUT_MS = 120000; // 2 minutes
const HTTP_HEADERS_TIMEOUT_MS = 60000; // 1 minute
const HTTP_REQUEST_TIMEOUT_MS = 120000; // 2 minutes

This improves maintainability and makes it clear these are configurable values.

Suggested change
httpServer.timeout = 120000; // 2 minutes
httpServer.headersTimeout = 60000; // 1 minute
httpServer.requestTimeout = 120000; // 2 minutes
httpServer.timeout = HTTP_SERVER_TIMEOUT_MS;
httpServer.headersTimeout = HTTP_HEADERS_TIMEOUT_MS;
httpServer.requestTimeout = HTTP_REQUEST_TIMEOUT_MS;

Copilot uses AI. Check for mistakes.
console.error('[pm2-mcp][debug] failed to send sandbox notification', err);
}
});
}, 100);
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The timeout value of 100ms on line 1167 is a magic number with unclear reasoning. This should be documented or extracted to a named constant like SANDBOX_NOTIFICATION_DELAY_MS = 100 with a comment explaining why this delay is necessary (e.g., "Allow client connection handshake to complete").

Copilot uses AI. Check for mistakes.
}

const restartCount = env.restart_time || 0;
if (baseStatus === 'online' && restartCount >= 3 && confidence < 0.85) {
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The restart count threshold of 3 on line 537 is a magic number. Extract it as a named constant like DEGRADED_RESTART_THRESHOLD = 3 to make it clear this is a configurable heuristic for detecting degraded processes.

Copilot uses AI. Check for mistakes.
Comment on lines +514 to +561
function buildSemanticStateFromHeuristics(opts) {
const { env = {}, monit = {}, logAnalysis, logInfo } = opts;
const baseStatus = env.status || 'unknown';

let status = baseStatus === 'online' ? 'online' : baseStatus;
let context;
let inferredFrom = 'status';
let confidence = 0.4;
let progress;

if (logAnalysis?.topPattern) {
status = logAnalysis.topPattern.semanticStatus || status;
context = logAnalysis.topPattern.sample;
inferredFrom = 'log_pattern_match';
confidence = 0.9;
}

if (logAnalysis?.progressIndicators?.length) {
const latest = logAnalysis.progressIndicators[logAnalysis.progressIndicators.length - 1];
progress = latest.percent ?? latest.current;
}

const restartCount = env.restart_time || 0;
if (baseStatus === 'online' && restartCount >= 3 && confidence < 0.85) {
status = 'degraded';
context = `Restarted ${restartCount} times`;
inferredFrom = 'restart_count';
confidence = Math.max(confidence, 0.65);
}

const cpu = typeof monit.cpu === 'number' ? monit.cpu : null;
const now = Date.now();
const logAgeMs = logInfo?.lastModified ? now - logInfo.lastModified : null;
const uptimeMs = env.pm_uptime || null;

if (
baseStatus === 'online' &&
logAgeMs !== null &&
uptimeMs &&
uptimeMs > 2 * 60 * 1000 &&
logAgeMs > 5 * 60 * 1000 &&
cpu !== null &&
cpu < 1
) {
status = 'stuck';
context = `No logs for ${Math.round(logAgeMs / 60000)}m, cpu ${cpu}%`;
inferredFrom = 'log_silence';
confidence = Math.max(confidence, 0.7);
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The confidence values (0.4, 0.9, 0.85, 0.65, 0.7) throughout the semantic state building logic are magic numbers without clear explanation. Consider:

  1. Extracting them as named constants with documentation explaining the confidence scale
  2. Adding a comment explaining how these values were determined
  3. Documenting what each confidence level means (e.g., 0.4 = low confidence from status alone, 0.9 = high confidence from log pattern match)

Copilot uses AI. Check for mistakes.
elasticdotventures and others added 2 commits December 4, 2025 10:37
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Brian Horakh <35611074+elasticdotventures@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Brian Horakh <35611074+elasticdotventures@users.noreply.github.com>
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