Skip to content

Cross-platform support, init/install subcommands, setup.sh bootstrap#2

Merged
zhgchgli0718 merged 9 commits into
mainfrom
claude/medium-reader-mcp-JMyan
May 5, 2026
Merged

Cross-platform support, init/install subcommands, setup.sh bootstrap#2
zhgchgli0718 merged 9 commits into
mainfrom
claude/medium-reader-mcp-JMyan

Conversation

@zhgchgli0718

Copy link
Copy Markdown
Member

Summary

Follow-up to the initial merge (#1). Drops the macOS-only restriction and lands one-shot onboarding.

What's new since #1

Cross-platform support (macOS / Linux / Windows)

Each platform uses its native secret store via a hand-written CLI wrapper. src/credentials.ts dispatches based on process.platform; override with MCP_MEDIUM_READER_BACKEND={keychain|secret-tool|dpapi|file}.

Platform Backend Tool
macOS Keychain security CLI
Linux Secret Service (libsecret — GNOME Keyring / KWallet) secret-tool CLI
Windows DPAPI per-user encryption (same primitive that backs Credential Manager) powershell.exe
Any (fallback) Plain JSON file (0600 on POSIX, %APPDATA% per-user ACL on Windows) none

On Linux the dispatcher probes for secret-tool and falls back to the file backend with a stderr notice if libsecret-tools isn't installed.

cross-spawn for proper Windows .bat shim handling around Node's CVE-2024-27980 mitigation. src/platform.ts probes ZMediumToMarkdown.bat / .cmd / bare on Windows, bare on POSIX.

package.json no longer declares "os": ["darwin"]. assertDarwin() removed from server entry.

Auto-setup: mcp-medium-reader init

One-shot orchestrator: gem version check → credentials walkthrough → install for all four supported MCP clients. Each step is also exposed as its own subcommand (setup, install, doctor).

Client Config file (per OS)
Claude Desktop macOS: ~/Library/Application Support/Claude/claude_desktop_config.json · Win: %APPDATA%\Claude\claude_desktop_config.json · Linux: ~/.config/Claude/claude_desktop_config.json
Claude Code ~/.claude.json
OpenAI Codex ~/.codex/config.toml (TOML via smol-toml)
Gemini CLI ~/.gemini/settings.json

Limit clients with --clients=claude-desktop,gemini. Existing config keys are preserved; reports added / updated / unchanged / error per target.

setup.sh — single-command bootstrap

bash <(curl -fsSL https://raw.githubusercontent.com/ZhgChgLi/mcp-medium-reader/main/setup.sh)

Probes Ruby ≥ 3.2 and Node.js ≥ 18 with platform-aware install hints (brew / apt / dnf / RubyInstaller / nodejs.org), installs the gem and the npm package, verifies both binaries land on PATH, then execs mcp-medium-reader init. Flags: --no-init / --no-gem / --no-npm / --help. Detects curl-pipe-bash (no TTY) and skips interactive init with a manual-run hint instead of hanging on a readline prompt.

Commits

  • a96ee2a Cross-platform credential storage; drop macOS-only restriction
  • f8e4abc Three native-backend wrappers (security / secret-tool / DPAPI) + file fallback + dispatcher
  • 6be0abe install / init subcommands; multi-OS CI matrix (ubuntu × macos × windows × Node 20 / 22)
  • a9aed85 README and CHANGELOG rewrite for cross-platform + new subcommands
  • 1469383 setup.sh one-shot bootstrap

Test plan

  • test/credentials.test.ts — file backend (forced via env): get/set/delete/listPresence/readAll, 0600 perms on POSIX. Backend selection: env override, platform default, unknown value falls through.
  • test/install.test.ts — JSON/TOML config writes; existing-key preservation; added/updated/unchanged/error outcomes; defaultTargets shape stable.
  • CI: .github/workflows/ci.yml runs npm ci && npm run build && npm test on ubuntu-latest / macos-latest / windows-latest × Node 20 & 22.
  • Manual on each OS:
    • bash setup.sh end-to-end on macOS / Linux / Git Bash / WSL.
    • mcp-medium-reader doctor reports the right backend per OS (macOS keychain / Linux secret-tool / Windows dpapi).
    • On Linux without libsecret, doctor falls back to file and prints the stderr notice.
    • Native Windows manual: gem install ZMediumToMarkdown && npm install -g mcp-medium-reader && mcp-medium-reader init.
    • install --clients=claude-desktop only touches Claude Desktop config.

Setup guide: https://github.com/ZhgChgLi/ZMediumToMarkdown/wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy


Generated by Claude Code

This is a behavior break that gives up macOS Keychain in exchange for
a working Windows / Linux story. Two coupled changes:

1. Credential storage: per-user JSON file at OS-appropriate config dir
     macOS / Linux: $XDG_CONFIG_HOME/mcp-medium-reader/credentials.json
                    (defaulting to ~/.config/...)
     Windows:       %APPDATA%/mcp-medium-reader/credentials.json
   File mode is 0600 on POSIX (atomic write via tmp+rename); Windows
   relies on the default per-user ACL under %APPDATA%. The previous
   `security` CLI wrappers are gone. New module src/credentials.ts
   keeps the same external API (getSecret / setSecret / deleteSecret /
   listPresence / readAll) so callers don't change.

   Override $MCP_MEDIUM_READER_CONFIG_DIR repurposed for tests and
   non-default layouts.

2. Drop "os": ["darwin"] from package.json and remove assertDarwin()
   from src/index.ts. Server now runs on Linux, macOS, and Windows.

3. Cross-platform spawn: switch zmedium.ts from node:child_process
   spawn to cross-spawn, since Node's CVE-2024-27980 mitigation
   refuses to spawn .bat / .cmd files directly and the Ruby gem
   installs as a .bat shim on Windows. cross-spawn handles both
   POSIX shell scripts and Windows .bat shims with proper arg
   quoting (no shell:true, no injection risk).

   src/platform.ts probes ZMediumToMarkdown.bat / .cmd / bare on
   Windows; bare on POSIX. Version probe uses execFile + shell:true
   on Windows (fixed args, no user input).

Other touch-ups:
- errors.ts: KeychainError -> CredentialsError
- warnings.ts: KeychainSnapshot type -> CredentialsSnapshot
- doctor.ts: prints config dir + creds file path instead of "service:
  mcp-medium-reader"
- setup.ts: prints credentials file path; final hint points users at
  the new `install` / `init` subcommands.

Tests: keychain.test.ts is replaced by credentials.test.ts which
uses MCP_MEDIUM_READER_CONFIG_DIR + a fresh tmp dir per test, asserts
the file lives at credentials.json under the config dir, and verifies
0600 perms on POSIX.
…allback

Three hand-written CLI wrappers, one per native secret store, plus a
plain-file fallback. The dispatcher in src/credentials.ts picks one
based on `process.platform` (or env override) and caches it.

  src/credentials/types.ts
    Backend interface + Account types + SERVICE_NAME constant.

  src/credentials/keychain.ts (macOS)
    `security` CLI wrapper. find/add/delete-generic-password with
    -s mcp-medium-reader. Exit-44 -> null/no-op. ENOENT carries a
    setup-friendly error pointing at MCP_MEDIUM_READER_BACKEND=file.

  src/credentials/secrettool.ts (Linux)
    `secret-tool` CLI (libsecret) wrapper. lookup / store / clear,
    keyed on `service mcp-medium-reader account <name>`. `store`
    reads the value from stdin so it never appears on argv. probe()
    via `secret-tool --version` so the dispatcher can fall back to
    the file backend on headless boxes without libsecret + DBus.

  src/credentials/dpapi.ts (Windows)
    powershell.exe + DPAPI (the same per-user encryption that backs
    Credential Manager / Chrome's saved passwords). Each value is
    encrypted via ConvertTo/ConvertFrom-SecureString and stored as a
    base64 blob inside %APPDATA%\\mcp-medium-reader\\credentials.json.
    PowerShell scripts passed via -EncodedCommand (UTF-16LE base64)
    so argv quoting is irrelevant; values are piped over stdin.
    listPresence skips decryption — empty-string check on the file
    is enough to answer "is it set?".

  src/credentials/file.ts
    Plain JSON fallback (atomic tmp+rename + chmod 0600 on POSIX,
    default ACL on Windows). Honors MCP_MEDIUM_READER_CONFIG_DIR.

  src/credentials.ts (dispatcher)
    Picks backend at first use:
      MCP_MEDIUM_READER_BACKEND=keychain|secret-tool|dpapi|file
      else: darwin -> keychain, win32 -> dpapi,
            linux -> secret-tool (probed; falls through to file with
            an actionable stderr notice if libsecret is missing),
            other -> file
    Re-exports public API (getSecret/setSecret/deleteSecret/readAll/
    listPresence) plus getBackendName / getCredentialsLocation for
    doctor / setup display.

  src/setup.ts, src/doctor.ts
    Display the resolved backend name and storage location instead
    of the previous hard-coded "service: mcp-medium-reader" string.

  test/credentials.test.ts
    Forces backend=file via env so the suite is platform-agnostic;
    keeps the existing get/set/delete/listPresence/readAll cases
    plus new "backend selection honors override / matches platform
    default / tolerates unknown value" cases.
Two new subcommands close the gap between "npm install -g" and a
working MCP client.

  src/install.ts
    Knows the four supported MCP clients and where their configs
    live on each OS:
      claude-desktop -> ~/Library/.../Claude/claude_desktop_config.json
                        / %APPDATA%/Claude/...   / ~/.config/Claude/...
      claude-code    -> ~/.claude.json
      codex          -> ~/.codex/config.toml          (TOML via smol-toml)
      gemini         -> ~/.gemini/settings.json
    installToClient loads the existing config (creating an empty one
    if missing), sets <jsonPath> to {command:"mcp-medium-reader"},
    and writes back. Reports added / updated / unchanged / error per
    target. runInstall(--clients=...) lets users opt into a subset.
    SERVER_KEY = "medium-reader" is the registered name shown in the
    LLM tool-call UI.

  src/init.ts
    One-shot orchestrator: gem version check -> credentials backend
    probe -> runSetup() -> runInstall(). Mirrors what a packaged
    installer would do.

  src/cli.ts
    Adds `init` and `install` subcommands; updates --help.

  test/install.test.ts
    Covers added / updated / unchanged outcomes for both JSON and
    TOML formats; preservation of existing keys; malformed JSON
    surfaces as a non-fatal error result; defaultTargets shape is
    stable.

  .github/workflows/ci.yml
    Now ubuntu/macos/windows x Node 20/22 matrix. The "os": ["darwin"]
    constraint was already removed from package.json; with cross-spawn
    handling Windows .bat shims and the credential backend dispatcher
    selecting per-platform stores, the test suite (forced to the file
    backend via env) runs identically on all three OSes.
README rewrites the framing from "macOS-only" to "cross-platform" and
documents:
  * the per-platform credentials backend table (Keychain / secret-tool
    / DPAPI / file) and the MCP_MEDIUM_READER_BACKEND override
  * `mcp-medium-reader init` as the recommended onboarding flow (deps +
    credentials + MCP client install in one shot)
  * `mcp-medium-reader install --clients=...` and the four supported
    client config paths per OS
  * SERVER_KEY = "medium-reader" so users know what name appears in
    their LLM tool-call UI
  * troubleshooting rows for libsecret missing on Linux and powershell
    missing on Windows (both point at the file fallback)

CHANGELOG follows Keep-a-Changelog: Added section enumerates the
backends, install/init subcommands, four-client default install, and
the multi-OS CI matrix; Changed section flags the removal of the
darwin-only constraint and the keychain.ts -> credentials.ts rename.
Single-command onboarding for macOS / Linux / Git Bash / WSL:

  bash <(curl -fsSL .../setup.sh)

The script:
  1. Detects platform (Darwin / Linux / MINGW|MSYS|CYGWIN).
  2. Probes Ruby >= 3.2 and Node.js >= 18 with platform-aware install
     hints (brew / apt / dnf / RubyInstaller / nodejs.org).
  3. Installs (or upgrades) the ZMediumToMarkdown gem.
  4. Installs (or upgrades) mcp-medium-reader globally via npm.
  5. Verifies both binaries actually land on PATH and surfaces a
     clear hint if not (Homebrew Ruby PATH gotcha, npm prefix on
     Windows, etc.).
  6. exec's `mcp-medium-reader init` for the interactive credentials
     + 4-client MCP install pass.

UX details:
  - --no-init / --no-gem / --no-npm flags for partial runs.
  - Color output gated on `[ -t 1 ]` (no escape codes when piped).
  - When stdin isn't a TTY (curl-pipe-bash), skips the interactive
    init and prints "open a terminal and run mcp-medium-reader init"
    instead of hanging on a readline prompt that can't receive input.
  - `set -euo pipefail` for strict failure semantics.
  - --help dumps the leading comment block.

README rewrites the install section to lead with the curl-piped
setup.sh + a Windows-native fallback path.
…tform

Two CI failures, fixed together.

1. `npm ci` failed in 6-15 seconds across all 12 jobs because no
   `package-lock.json` is committed. Switch to `npm install
   --no-audit --no-fund`. The semver ranges in package.json are tight
   enough for reproducible builds; lockfile can be added later in a
   separate PR after a clean local install.

2. test/fsutil.test.ts asserted `resolveOutputDir('/var/foo') ===
   '/var/foo'`. That's true on POSIX but on Windows path.resolve
   prepends the current drive (`C:\var\foo`). Compare against
   `path.resolve('/var/foo')` on both sides so the assertion shape
   is identical on every platform.

Verified locally: `npm install && npm run build && npm test` →
56 tests pass on Node 22 / Ubuntu.
src/keychain.ts is left over from the initial PR #1's macOS-only
implementation. Its only export was the `security` CLI wrapper which
now lives at src/credentials/keychain.ts (one of four backends behind
the src/credentials.ts dispatcher).

It also imports `KeychainError` which was renamed to
`CredentialsError` in errors.ts during the cross-platform refactor —
so the file actually fails `tsc` on its own. Removing it unbreaks
the build.
This test was tied to the old src/keychain.ts module which is gone in
the cross-platform refactor. It imports `KeychainError` and `SERVICE`
from a path / symbols that no longer exist, so vitest can't even
collect the file — that's the symptom CI was hitting.

Coverage for the security CLI wrapper now lives indirectly in
test/credentials.test.ts (which forces backend=file via env so it
runs the same on all three OSes). The macOS-only security argv shape
still lives in src/credentials/keychain.ts; we deliberately don't
unit-test it because mocking node:child_process.execFile portably is
brittle and the real wrapper is exercised end-to-end via
`mcp-medium-reader doctor` on macOS.
actions/setup-node@v4's `cache: 'npm'` option fails before npm runs
when it can't find package-lock.json / npm-shrinkwrap.json / yarn.lock
to compute the cache key. We don't ship a lockfile yet, so the cache
config errored out every job in setup before reaching the install
step. Remove the `cache:` line; revisit when we commit a lockfile.
@zhgchgli0718 zhgchgli0718 merged commit d5a5e7c into main May 5, 2026
12 checks passed
zhgchgli0718 added a commit that referenced this pull request May 5, 2026
When system Ruby is below the gem's required 3.2, defer to rbenv if it's
available rather than just bailing:

  1. command -v rbenv -> if missing, fall back to the original "upgrade
     Ruby and re-run" hint with a pointer to rbenv.
  2. Probe rbenv versions --bare for an already-installed >= 3.2; if
     found, switch via RBENV_VERSION and prepend $(rbenv root)/shims to
     PATH so subsequent `ruby`, `gem`, `ZMediumToMarkdown` calls hit it.
  3. If nothing >= 3.2 is installed, pick the latest stable 3.x from
     rbenv install --list and rbenv install it (TTY confirmation prompt;
     curl-pipe-bash skips the prompt and proceeds).
  4. After install, re-validate by reading RUBY_VERSION from the now-
     active rbenv ruby; abort if still below 3.2.

Adds --no-rbenv flag for users who'd rather fail fast than have the
script touch their Ruby toolchain. The gem-install step now also runs
`rbenv rehash` so the freshly installed ZMediumToMarkdown shim shows
up immediately on PATH without a shell restart.

(Pushed direct to main: rbenv changes landed on the working branch
after PR #2 was already merged, so they missed the merge.)
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