npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

metro-mcp-plugin-lifecycle

v0.1.1

Published

metro-mcp plugin: own the Metro dev-server process — start/stop/restart with --reset-cache, bundle smoke check, duplicate-React diagnostics.

Readme

metro-mcp-plugin-lifecycle

A plugin for metro-mcp that lets an MCP-aware AI agent own the Metro dev-server process for a React Native / Expo project — start it, stop it, restart it with --reset-cache, and run static checks against it.

metro-mcp itself is an attach-only debugger: once Metro is running, it gives the agent access to the app's console, network, Redux state, component tree, and profiler over Chrome DevTools Protocol. It does not, however, start or stop Metro — that has to happen out-of-band, in a terminal the agent can't see. This plugin closes that gap so the agent can drive the whole loop end-to-end.

  • metro_start — spawn yarn start (or npm / pnpm / bun), wait for initialize_done, return ownership info.
  • metro_stop — terminate only the Metro process this plugin spawned (PID + start-time + recorded command + cwd validation, process group SIGTERM → SIGINT → SIGKILL with per-step revalidation).
  • metro_restart — sequential stop then start, optional --reset-cache.
  • metro_bundle_smoke — request the bundle URL for one or more platform/dev combinations and return parsed Metro errors as structured JSON (bundle bodies are never returned).
  • metro_diagnose_duplicate_react — pure-Node realpath walk across node_modules, workspaces, and file: / link: deps to find duplicate React installations (a common cause of Cannot read property 'useRef' of null).
  • metro_get_start_log — bounded stdout/stderr ring buffer from the spawned Metro process. Critical when Metro fails before its /events WebSocket comes up (e.g. bad metro.config.js).

Why a separate plugin

| metro-mcp covers | This plugin covers | |---|---| | Running app: console, network, errors, Redux state, components, profiler | Metro process: start, stop, restart, reset-cache | | Read-only /status + /events consumption | Process spawn, signal management, ownership locks | | symbolicate, reload_app (Hermes-level) | bundle_smoke, duplicate-React static diagnostics |

The two are complementary, not competitive.

Install

npm install --save-dev metro-mcp-plugin-lifecycle
# or
yarn add -D metro-mcp-plugin-lifecycle

Then register it with metro-mcp in metro-mcp.config.ts:

import { defineConfig } from 'metro-mcp';

export default defineConfig({
  plugins: ['metro-mcp-plugin-lifecycle'],
});

Or via CLI flag:

bunx metro-mcp --plugin metro-mcp-plugin-lifecycle

Or environment variable (colon-separated for multiple plugins):

METRO_MCP_PLUGINS=metro-mcp-plugin-lifecycle bunx metro-mcp

Usage

Once the plugin is registered, start metro-mcp the way you normally do (CLI / config / your MCP client's mcpServers entry). The agent can then call:

metro_start({ cwd: '/path/to/your/rn-or-expo-project' })

cwd defaults to the metro-mcp host process's working directory; pass it explicitly if your MCP client launches metro-mcp from elsewhere. The project must have a start script that runs Metro and forwards CLI args (the default React Native / Expo template already does — react-native start "$@" or expo start "$@").

Tools

metro_start

metro_start({
  resetCache?: boolean,       // default false
  port?: number,              // default 8081
  watchFolders?: string[],    // extra folders passed via --watchFolders
  packageManager?: 'yarn' | 'npm' | 'pnpm' | 'bun',  // default auto-detect from lockfile
  waitForReady?: boolean,     // default true
  timeoutMs?: number,         // default 60000
  cwd?: string,               // default process.cwd()
}) → {
  pid, port, ready,
  ownership: 'plugin' | 'external',
  alreadyRunning: boolean,
  cwd, command, statusUrl, lockPath, reason
}

Idempotent: if Metro is already responding on the requested port and the lock matches, returns the existing state instead of spawning a duplicate.

Refuses to start in several unsafe / conflicting cases:

  • A Metro this plugin owns is already running for this project on a different port (would orphan the existing process).
  • The port hosts an external Metro for a different project (would conflict).
  • The port hosts a non-Metro process (would never respond to /status).

If waitForReady is true and Metro fails to become ready (timeout, or another project's Metro takes the port mid-spawn), the spawned process group is killed and the lock is removed before throwing — no orphans, no stale lock.

metro_stop

metro_stop({
  force?: boolean,    // default false; skip SIGTERM/SIGINT and go straight to SIGKILL
  timeoutMs?: number, // default 5000; per-step budget
  cwd?: string,
}) → { stopped, pid, port, reason, finalSignal, steps }

Annotations: destructiveHint: true.

Validates identity before sending any signal — PID alive, start time matches the lock, and the recorded command's executable still matches the running command (catches PID reuse + lock hijack). /status is not required, so a Metro that's still booting or hung can still be stopped.

Sends SIGTERM to the process group, then re-checks the leader PID; if it's gone, probes /status to catch the "leader exited but workers still own the port" case. Escalates through SIGINT to SIGKILL only as long as something on either side is still alive.

Only Metros this plugin started can be stopped — externally started Metros are detected (so we won't start a duplicate on the same port) but never signalled.

If identity validation fails (PID dead or claimed by something unrelated), cleans up the lock without sending signals.

metro_restart

metro_restart({
  resetCache?: boolean,
  port?: number,
  watchFolders?: string[],
  packageManager?: 'yarn' | 'npm' | 'pnpm' | 'bun',
  timeoutMs?: number,
  cwd?: string,
}) → { stopped, started, pid, port, ready, reason }

Sequential metro_stop then metro_start via shared primitives — same ownership rules and ready detection as the standalone tools.

metro_bundle_smoke

metro_bundle_smoke({
  preset?: 'quick' | 'release' | 'full',  // shortcuts
  entries?: string[],     // default ['index']
  platforms?: ('ios' | 'android' | 'web')[],  // default ['ios']
  dev?: boolean[],        // default [false]
  minify?: boolean,
}) → {
  resolved, totalMs, okCount, failCount,
  results: Array<{
    entry, platform, dev, minify,
    ok, timingMs, httpStatus, sizeBytes,
    errors?: Array<{ type, message, file?, line?, column? }>
  }>
}

Presets:

  • quick — single combo (ios + dev=true); fastest
  • releaseios + android × dev=false + minify=true
  • fullios + android × dev=true and dev=false

Bundle bodies are not returned (can be many MB). Only metadata + parsed errors (errors[] shape from Metro's formatBundlingError).

metro_diagnose_duplicate_react

metro_diagnose_duplicate_react({
  includePackageManagerWhy?: boolean,  // optional `yarn why react` / `npm ls react` evidence
  watchFolders?: string[],             // extra roots (relative paths resolve against cwd)
  cwd?: string,
}) → {
  severity: 'ok' | 'warning' | 'error',
  summary,
  occurrences: Array<{ version, realpath, packageRoot, importPath }>,
  searchedRoots,
  evidence?: { tool, output }
}

Searches:

  • <cwd>/node_modules
  • watchFolders/*/node_modules
  • Workspace roots declared in root package.json
  • file: / link: dependency targets (and their nested node_modules)

Severity:

  • ok — single realpath
  • warning — single version, multiple realpaths (e.g. symlinks to the same physical install, or Metro bundling two copies)
  • error — multiple versions, or no React at all

metro_get_start_log

metro_get_start_log({
  lines?: number,            // default 200, max 500
  stream?: 'stdout' | 'stderr' | 'both',  // default 'both'
  cwd?: string,
}) → { stdout, stderr, truncated: { stdout, stderr }, available, reason }

Bounded ring buffer (capacity 500 lines per stream) of recent stdout/stderr from the Metro process spawned by metro_start. Useful when Metro fails to fully start (bad config, port conflict before /status, dependency error during initialize_started).

Buffer lives in the metro-mcp host process's memory: only Metros started in the current session are observable. A Metro spawned by a previous metro-mcp run, even if it's still owned by this plugin via the lock, has no log buffer.

Architecture notes

  • Ownership lock: ~/.cache/metro-mcp-plugin-lifecycle/<project-hash>.json records { pid, pgid, port, cwdRealpath, command, pidStartTime, startedAt, statusFingerprint }. metro_stop requires every field to still match the live system before sending signals — a stale or hijacked lock cannot accidentally kill an unrelated process.
  • Process spawning: direct child_process.spawn with detached: true (process group), not ctx.exec (one-shot, returns a string — unsuitable for long-running processes).
  • Host cleanup: the plugin installs a one-time SIGINT / SIGTERM / exit handler on the metro-mcp host process so spawned Metros are killed if the host exits cleanly or is interrupted. A hard crash or SIGKILL of the host bypasses this — recovery in that case relies on the next metro_start detecting and refusing the stale state.
  • Ready signal: poll http://<host>:<port>/status until it returns packager-status:running AND the X-React-Native-Project-Root header matches our realpath. Fails fast if a different project's Metro takes the port.
  • Port state resolver: a 9-state classifier (ours_running, ours_other_port, external_metro_same_project, external_metro_other_project, external_metro_unknown_project, port_taken_not_metro, stale_lock_port_free, stale_lock_external_metro, free) so every caller deals with the awkward cases explicitly instead of silently overwriting.

Known limitations (v0.1.0)

  • Not safe under concurrent metro_start calls. Lock acquisition is read-decide-write — two simultaneous starts can both see free and race. In practice MCP agents serialise tool calls within a session, so this is rarely hit. A true file-lock will land in a later version.
  • Workspace globs (packages/*) are not expanded by metro_diagnose_duplicate_react. Literal workspace paths and file:/link: deps are followed; globs are skipped. Pass the workspace roots explicitly via watchFolders if you need them searched.

Requirements

  • Node.js ≥ 20
  • metro-mcp ≥ 0.11 (pre-1.0; minor versions may introduce breaking changes)
  • A React Native or Expo project with Metro
  • macOS or Linux. Windows is untested — process identity uses ps -o lstart= and negative-PGID signalling, both POSIX-only.

License

MIT