Skip to content

fix(server): register the mcpm_up MCP tool so mcpm serve exposes it#64

Merged
m1ngshum merged 3 commits into
mainfrom
fix/register-mcpm-up-tool
Jun 8, 2026
Merged

fix(server): register the mcpm_up MCP tool so mcpm serve exposes it#64
m1ngshum merged 3 commits into
mainfrom
fix/register-mcpm-up-tool

Conversation

@m1ngshum

@m1ngshum m1ngshum commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Two commits:

  1. Register the mcpm_up MCP tool — it was fully built (schema UpInput, handler handleMcpUp, hardened in v0.7.1) but never registerTool'd, so mcpm serve exposed 8 tools while docs claimed 9. Now 9.
  2. Harden the MCP surface (multi-agent review follow-up) — exposing a batch-installer to untrusted MCP callers surfaced pre-existing weaknesses in handleMcpUp/handleUp; locked them down without changing CLI behavior.

Commit 1 — registration

server.registerTool("mcpm_up", { inputSchema: UpInput.shape, annotations: { destructiveHint: true } }, handler) + the two imports, matching the existing 8 tools. Handler/tool-def unchanged.

Commit 2 — security hardening

A multi-agent review (correctness / security / tests / conventions, each finding adversarially confirmed) returned REQUEST_CHANGES. The registration diff itself was clean; all findings were about the now-exposed handler. Addressed:

Finding Severity Fix
Silent failure: bare catch swallowed handleUp early throws → empty "success" high Capture + surface via error field; guarantee non-empty failed on throw; categorize via a typed recordResult sink (emoji-scraping couldn't tell blocked from failed)
Path-traversal guard string-only + dead if (stackFile !== undefined) high Unconditional resolved-path containment (path.resolve + cwd boundary) — closes Windows-backslash & sibling-prefix bypasses
Stack file harvests ambient process.env secrets high New allowProcessEnv option (default true = CLI); MCP surface passes false
URL servers bypass the registry trust gate high New allowUrlServers option (default true = CLI); MCP surface passes false → URL servers recorded as blocked, not installed
No test asserted the registered tool set (why the missing reg slipped through) high registerTools() extracted + test asserting every TOOL_DEFINITIONS name is registered
MCP-surface paths (dryRun / blocked / missing-env / URL) untested; UpInput defaults unasserted med Added handleMcpUp suite + defaults test

CLI behavior preserved: every new option/dep is gated on === false / optional and is set only by the MCP surface (handleMcpUp). registerUpCommand passes none of them.

Verification

  • tsc --noEmit clean · pnpm build clean
  • Full suite 1297 tests pass (+2 files / +10 tests). Two existing path-validation tests updated only for the new error message (same reject intent).
  • mcpm serve registers 9 tools.

Known minor behavior

For blocked-by-policy / URL-blocked outcomes, handleUp records the server in result.blocked and also throws its summary, so result.error is set and a summary line appears in result.failed alongside the real blocked entry — accurate (the op didn't fully succeed), if slightly redundant.

Related

Complements docs PR #63 (which documents the current 8-tool surface). After this merges, the serve-tool count becomes 9 — see the note there about reconciling.

🤖 Generated with Claude Code

m1ngshum added 2 commits June 8, 2026 17:37
mcpm_up was fully built as an MCP tool — schema (tools.ts: UpInput),
handler (handlers.ts: handleMcpUp, with path-traversal validation and
stubbed interactive callbacks), and hardened in v0.7.1 ('mcpm_up no
longer auto-confirms') — but the server.registerTool() call was never
added. So 'mcpm serve' advertised 9 tools (docs/CLAUDE) while exposing 8.

Wire it in, matching the existing tool registrations:
- import UpInput (tools.ts) and handleMcpUp (handlers.ts)
- register 'mcpm_up' with inputSchema UpInput.shape and
  annotations.destructiveHint = true, after mcpm_setup

MCP-safe: handleMcpUp stubs confirm/promptEnvVar and passes ci=true, so
no interactive prompt can hang the stdio server; it never calls
process.exit and returns structured output.

Verify: tsc --noEmit clean, tsup build clean, full suite 1287 tests pass
(no test asserts a tool count). serve now registers 9 tools.
The mcpm_up registration was clean, but exposing handleMcpUp to
untrusted MCP callers surfaced pre-existing weaknesses. Lock down the
MCP surface WITHOUT changing CLI behavior (new options default to
current CLI semantics; only the MCP surface opts into the strict path).

- silent failure: handleMcpUp's bare catch swallowed handleUp's early
  throws (no clients / lock failure / missing required env in CI),
  returning an empty success-looking result. Now surfaces the message
  via an `error` field and guarantees a non-empty `failed` on throw.
  Categorization uses a typed recordResult sink instead of scraping
  the success/block glyphs from output (which can't tell blocked from
  failed — policy reasons never contain 'blocked' and both use the
  same mark).
- path traversal: replace the dead string-only stackFile guard with
  unconditional resolved-path containment (path.resolve + cwd boundary),
  closing Windows-backslash and sibling-prefix bypasses.
- env harvesting: new allowProcessEnv option (default true = CLI); the
  MCP surface passes false so an attacker-controlled stack file can't
  read ambient process.env secrets into installed configs.
- URL servers: new allowUrlServers option (default true = CLI); the MCP
  surface passes false so URL servers (which bypass the registry trust
  gate) are recorded as blocked, not installed.
- tests: registration-completeness guard (asserts every TOOL_DEFINITIONS
  name is registered — the gap that hid the missing mcpm_up), plus
  handleMcpUp dryRun / blocked-by-policy / missing-env / URL-blocked
  and UpInput default-value assertions.

CLI preserved (registerUpCommand sets none of the new options/deps).
tsc --noEmit clean, build clean, full suite 1297 tests pass.
m1ngshum added a commit that referenced this pull request Jun 8, 2026
The earlier doc-drift pass set the MCP tool count to 8 to match main's
then-current code (mcpm_up built but unregistered). PR #64 registers
mcpm_up, so the correct count is 9. Restore mcpm_up to the tool lists in
README and CLAUDE.

Merge order: this should land with/after #64 (which registers the tool);
main's code exposes 9 only once #64 is merged.
m1ngshum added a commit that referenced this pull request Jun 8, 2026
* docs: add rendered architecture diagrams (Mermaid)

Add four GitHub-native Mermaid diagrams, each grounded in source and
render-validated with mermaid-cli:

- README 'How it works': system overview (local-first, only the public
  registry is remote; trust scan + atomic config writes + guard runtime)
- ARCHITECTURE 'Data Flow': install + trust-gate flow (replaces ASCII),
  with the min-trust and already-installed fail branches made explicit
- ARCHITECTURE: new 'Secrets resolution data flow' — shows plaintext
  secrets live only in the guarded child's in-memory env, never on disk
- ARCHITECTURE 'Guard data flow': stdio MITM relay as a sequence diagram
  (replaces ASCII) — bidirectional parent->child / child->parent
  inspection, schema-drift block path, and event-log sink

No code changes.

* docs: mirror guard relay sequence diagram into GUARD.md

Adds the same render-validated runtime message-flow sequence diagram to
GUARD.md's 'Mental model' section (as 'Full message flow'), so the
user-facing guard reference shows the bidirectional inspection pipeline
alongside the existing ASCII sketch + detection-layers table. Same
diagram already lives in ARCHITECTURE.md's 'Guard data flow'.

* docs: fix scanner trust model in README overview diagram

Audit against src/scanner/ found the overview diagram mislabeled the
trust pipeline. Corrected to the real 4-component model from
trust-score.ts:
- Health Check (0-30, health-check.ts: spawn + verify)
- Tier 1: Static Patterns (0-40, tier1.ts: secrets/injection/typosquat/exfil)
- Tier 2: External Scan (0-20, tier2.ts: optional MCP-Scan)
- Registry Metadata (0-10: publisher/age/downloads)

Previously Tier 1/Tier 2 labels were swapped and registry metadata was
folded into Tier 1; trust score 'max 80 (100 with external scan)' now
reflects the dynamic maxPossible. Install + secrets + guard-relay
diagrams audited and confirmed accurate (no change).

* docs: fix drift found auditing all docs against source

Full-repo doc audit (every implementation doc vs src/), each finding
independently confirmed with file:line evidence. Surgical fixes:

README.md
- add 'mcpm why' to the Commands table (was undocumented)
- expand 'mcpm publish' into scaffold / check / submit subcommands
- mcpm serve exposes 8 tools, not 9 (mcpm_up is not registered)

docs/ARCHITECTURE.md
- CI matrix is Node 22/24/26 (was 20/22/24)
- commands/ module: 24 commands, guard 12 subcommands, publish 3
- complete the Commands table (add outdated/why/secrets/publish)

docs/GUARD.md
- wrap transform includes --declared-env and --orig-hash flags
- policy-overrides actions are ignore/warn/block/log_only (+ paused_until)

docs/POLICY.md
- mark signature_overrides as optional (matches Zod schema)

docs/SIGNATURES.md
- inspection model also scans structuredContent + JSON-RPC errors
  (tool_response) and title + full inputSchema (tool_description)

CLAUDE.md
- version v0.8.0, Node >=22.0.0, last-updated 2026-06-03
- local storage: aliases.json (not scans.json, which doesn't exist)
- mcpm serve exposes 8 tools; complete the command list

src/commands/guard.ts
- header comment: all v0.5.0 guard subcommands are implemented
  (was 'land in subsequent build steps')

* docs: serve exposes 9 tools (reconcile with mcpm_up registration)

The earlier doc-drift pass set the MCP tool count to 8 to match main's
then-current code (mcpm_up built but unregistered). PR #64 registers
mcpm_up, so the correct count is 9. Restore mcpm_up to the tool lists in
README and CLAUDE.

Merge order: this should land with/after #64 (which registers the tool);
main's code exposes 9 only once #64 is merged.
The handleMcpUp tests are integration-style (real temp dirs) and the
first one pays a one-time dynamic import("../commands/up.js"). Under CI
coverage instrumentation on the slower Node 22 runner that first test
(dryRun) crossed the 5000ms default and timed out, while Node 24/26
passed. Logic is correct (all 7 pass; ~1s locally) — raise the
per-file testTimeout to 30s to absorb CI-runner variance.
@m1ngshum m1ngshum merged commit e6533a3 into main Jun 8, 2026
7 checks passed
@m1ngshum m1ngshum deleted the fix/register-mcpm-up-tool branch June 8, 2026 12:54
m1ngshum added a commit that referenced this pull request Jun 8, 2026
…ew (H1/M1/M3/L1) (#65)

* fix(security): close MCP-surface secret-leak gaps from post-ship review (H1/M1/M3/L1)

The #64 hardening blocked process.env on the MCP surface but a post-ship
security review surfaced gaps that survive it:

- H1 (HIGH): the working-directory `.env` was read ungated, so an attacker-
  controlled stack file could still siphon the host's `.env` into an installed
  server config even with allowProcessEnv:false. Add `allowEnvFile` (default
  true = CLI); the MCP surface passes false and skips reading `.env`.
- M3 (MED): containment used lexical path.resolve only, so an in-cwd symlink
  pointing outside cwd passed the check and the reader followed it. Add a
  realpath re-check (ENOENT-tolerant).
- M1 (MED): a whole-batch throw pushed its error *message* into `failed`, which
  is contracted to hold server names. Surface it via the `error` field only.
- L1 (LOW): env resolution used truthiness, dropping legitimate empty-string
  values. Compare against undefined.

Also corrects the misleading blocked-by-policy test (review H2): it mocked
deps.computeTrustScore, which handleMcpUp never calls (it builds its own), so the
gate was only exercised by coincidence. The test now drives the real trust
pipeline; new tests prove the `.env` is not harvested and a symlink is rejected.

tsc + tsup clean; full vitest suite green.

* fix(security): enforce a hard trust floor on the mcpm_up MCP path (M2)

The single-install MCP tool (handleInstall) clamps installs to a non-overridable
HARD_TRUST_FLOOR (issue #24), but the batch `mcpm_up` MCP path enforced only the
stack file's `policy` — which the untrusted caller controls and can omit or set to
`minTrustScore: 0`. A prompt-injected agent could therefore install a low-trust
server via `mcpm_up` that `mcpm_install` would reject.

Add a `minTrustFloor` option to handleUp (default undefined = no floor = CLI
behavior); processServer blocks any server scoring below it BEFORE the
caller-controlled policy check. handleMcpUp passes the same HARD_TRUST_FLOOR the
single-install tool uses, closing the bypass while leaving the CLI unchanged.

Test: a score-30 server with no stack policy is blocked by a floor of 50.

* fix(security): address code-review on #65 (M3 error-leak + M2 wiring test)

Multi-agent review follow-ups (no blockers found; these are the should-fix items):

- M3 (handlers.ts): the realpath containment catch only swallowed ENOENT, so an
  untrusted-caller stackFile that is a circular symlink (ELOOP) or has a file as a
  path component (ENOTDIR) escaped as a raw ErrnoException with an internal stack
  trace. Treat ENOENT/ELOOP/ENOTDIR alike (fall through); the containment Error has
  no `.code` so it still propagates.
- M2 wiring coverage: add handlers-up-wiring.test.ts — mocks handleUp and asserts
  handleMcpUp passes allowProcessEnv/allowEnvFile/allowUrlServers:false AND
  minTrustFloor:25. Deleting the floor from the call site now fails a test.
- Nits: missing-env test asserts `failed` contains the server NAME (names-only
  contract, symmetric with the no-clients test); corrected the H2 test comment
  (real score is 55/80 = 69%, not ~62%; the override set the unused
  deps.computeTrustScore); clarified the absolute-vs-percentage floor is intentional.

tsc + tsup clean; full suite green (1301).
@m1ngshum m1ngshum mentioned this pull request Jun 8, 2026
m1ngshum added a commit that referenced this pull request Jun 8, 2026
Bump version, banners (v0.8.0 -> v0.8.1), and CLAUDE.md; add the 0.8.1
CHANGELOG entry. Release content: mcpm_up MCP tool registration (#64), the
post-ship security review fixes (#65, #66), and the hono/semver dep bumps
(#62, #61). No source changes here beyond the version string.
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.

1 participant