Skip to content

feat: prepare npm publishing with doctor CLI - #9

Merged
yohnark merged 3 commits into
mainfrom
chore/npm-publish-0.1.0
Aug 4, 2026
Merged

yohnark merged 3 commits into
mainfrom
chore/npm-publish-0.1.0

Conversation

@yohnark

@yohnark yohnark commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • publish the package as unscoped mottainai with npm metadata, public access defaults, and release workflow
  • expose the existing upstream management commands through the packaged executable
  • add mottainai doctor with human-readable and JSON diagnostics
  • preserve argument-free startup as the MCP stdio server and add explicit mottainai serve
  • update npm, Claude Code, Codex, and diagnostic usage documentation

Doctor checks

  • Node.js version and configuration validity
  • workspace and .mottainai write access
  • ripgrep availability
  • enabled upstream commands, working directories, required environment variables, OAuth provider configuration, and capability declarations

The default diagnostic path remains static and does not start or connect to upstream servers.

Validation

  • pnpm run typecheck
  • pnpm test — 459 passed
  • pnpm run build
  • git diff --check
  • packed and installed mottainai@0.1.0 outside the checkout
  • ran packaged doctor, doctor --json, and list
  • connected to the packaged argument-free stdio server and verified 19 tools plus the disposable workspace root

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@yohnark, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d7821a2-8236-4260-a3d2-e7d07e6e0b87

📥 Commits

Reviewing files that changed from the base of the PR and between 8104649 and 60071fd.

📒 Files selected for processing (5)
  • .github/workflows/publish.yml
  • src/cli.ts
  • src/index.ts
  • src/mcp-cli.test.ts
  • src/server.ts
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added the mottainai command-line interface for managing upstreams and profiles.
    • Added mottainai doctor diagnostics with human-readable and JSON output.
    • Added server startup and configuration support through the CLI.
    • Simplified installation with npx -y mottainai.
    • Added commands for listing and inspecting configured upstreams.
  • Documentation

    • Updated quick-start, validation, runtime status, and development setup instructions.
  • Publishing

    • Automated package validation and npm publishing when a release is published.

Walkthrough

Changes

CLI and release flow

Layer / File(s) Summary
Doctor diagnostics and validation
src/commands/doctor.ts, src/commands/doctor.test.ts
Adds structured checks for runtime, configuration, workspace, upstreams, authentication, and capabilities.
CLI commands and dispatch
src/cli.ts, src/index.ts, scripts/mcp.ts, src/mcp-cli.test.ts
Adds upstream and profile management commands and routes public entry points through runCli.
Server initialization and connection
src/server.ts
Adds configuration loading, component initialization, proxy registration, and stdio connection.
Package distribution and release publishing
package.json, .github/workflows/publish.yml, README.md
Updates npm package metadata, adds release publishing checks, and documents direct usage and development setup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • yohn-jp/mottainai#5: Refactors the CLI into src/cli.ts and updates the related dispatch wrappers.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the npm publishing preparation and doctor CLI additions.
Description check ✅ Passed The description accurately covers the package publishing, CLI, doctor diagnostics, server behavior, documentation, and validation changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/npm-publish-0.1.0

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/cli.ts (1)

192-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

runCli rejects the serve command it documents.

USAGE at lines 13-14 advertises mottainai and mottainai serve, but runCli has no serve branch, so it falls through to fail(USAGE). scripts/mcp.ts forwards every argument to runCli, so scripts/mcp.ts serve prints usage and exits 1. Only src/index.ts intercepts serve. Either handle serve in runCli and let both entry points share one dispatch, or remove the server lines from USAGE.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 192 - 199, Update runCli to recognize the documented
"serve" command instead of falling through to fail(USAGE), ensuring
scripts/mcp.ts serve and the main entry point share the same dispatch behavior;
preserve the existing doctor and unknown-command handling.
src/index.ts (1)

7-20: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle runServer failures explicitly.

runServer can reject during config load or OAuth provider load. This entry point does not catch it, so Node prints an unhandled-rejection stack trace. This entry point runs the MCP stdio server, so a clean message on stderr plus process.exitCode = 1 gives a better client-side diagnostic.

♻️ Proposed structure
-if (args.length === 0) {
-  await runServer();
-} else if (args[0] === "serve") {
-  const configIndex = args.indexOf("--config", 1);
-  const configPath = configIndex === -1 ? undefined : args[configIndex + 1];
-  if (configIndex !== -1 && configPath === undefined) {
-    console.error("missing value for --config");
-    process.exitCode = 1;
-  } else {
-    await runServer(configPath);
-  }
-} else {
-  process.exitCode = await runCli(args);
-}
+if (args.length === 0 || args[0] === "serve") {
+  const configIndex = args.indexOf("--config", 1);
+  const configPath = configIndex === -1 ? undefined : args[configIndex + 1];
+  if (configIndex !== -1 && configPath === undefined) {
+    console.error("missing value for --config");
+    process.exitCode = 1;
+  } else {
+    try {
+      await runServer(configPath);
+    } catch (error) {
+      console.error(error instanceof Error ? error.message : String(error));
+      process.exitCode = 1;
+    }
+  }
+} else {
+  process.exitCode = await runCli(args);
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 7 - 20, Wrap the `runServer` calls in the
`src/index.ts` entry-point flow with explicit rejection handling, including both
the no-argument path and the `"serve"` path. On failure, print a concise
diagnostic to stderr and set `process.exitCode = 1`, preventing an
unhandled-rejection stack trace while preserving the existing config-argument
validation and CLI behavior.
src/server.ts (1)

28-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded server version can drift from the published package version.

The Server metadata sets version: "0.1.0" directly. This PR prepares package.json for npm publishing with a real version number. If the hardcoded string is not kept in sync with the actual published version, clients that read the MCP server's reported version (for compatibility checks or diagnostics) get an incorrect value after every release.

Read the version from package.json at build or runtime instead of hardcoding it.

♻️ Proposed fix
+import packageJson from "../package.json" with { type: "json" };
...
   const server = new Server(
-    { name: "mottainai", version: "0.1.0" },
+    { name: "mottainai", version: packageJson.version },
     { capabilities: { tools: {} } },
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` around lines 28 - 31, Update the Server metadata
initialization to source the version from package.json instead of the hardcoded
"0.1.0" value, ensuring the reported version remains synchronized with the
published package while preserving the existing server name and capabilities.
.github/workflows/publish.yml (1)

32-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pin the ad-hoc npm install to an exact version.

npm install --global npm@11 installs whatever the latest 11.x release is at run time, since only the major version is pinned. This is flagged by static analysis as an ad-hoc install outside the lockfile. Pin an exact version (for example npm@11.5.1 or later, the minimum required for OIDC trusted publishing) for reproducible releases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 32 - 33, Update the “Require an
OIDC-capable npm CLI” workflow step to install npm using an exact version, such
as npm@11.5.1 or a later supported release, instead of the floating npm@11
range.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mcp-cli.test.ts`:
- Around line 228-238: The test in this file depends on ripgrep being installed,
but the publish workflow does not include a step to install it while the CI
workflow does. Either add a ripgrep installation step to the publish workflow
file (matching the approach in the CI workflow) to ensure the dependency is
available when pnpm test runs, or refactor the test to remove its dependency on
the ripgrep command.

In `@src/server.ts`:
- Around line 11-48: The runServer function creates an UpstreamRegistry instance
in the upstreams variable but never closes it during shutdown, leaving stdio
child processes alive. Add an idempotent shutdown path in runServer that awaits
upstreams.close() before the process exits, triggered by transport closure or
process signals (SIGINT/SIGTERM). Ensure this cleanup runs after
server.connect(transport) completes and before the function returns.

---

Nitpick comments:
In @.github/workflows/publish.yml:
- Around line 32-33: Update the “Require an OIDC-capable npm CLI” workflow step
to install npm using an exact version, such as npm@11.5.1 or a later supported
release, instead of the floating npm@11 range.

In `@src/cli.ts`:
- Around line 192-199: Update runCli to recognize the documented "serve" command
instead of falling through to fail(USAGE), ensuring scripts/mcp.ts serve and the
main entry point share the same dispatch behavior; preserve the existing doctor
and unknown-command handling.

In `@src/index.ts`:
- Around line 7-20: Wrap the `runServer` calls in the `src/index.ts` entry-point
flow with explicit rejection handling, including both the no-argument path and
the `"serve"` path. On failure, print a concise diagnostic to stderr and set
`process.exitCode = 1`, preventing an unhandled-rejection stack trace while
preserving the existing config-argument validation and CLI behavior.

In `@src/server.ts`:
- Around line 28-31: Update the Server metadata initialization to source the
version from package.json instead of the hardcoded "0.1.0" value, ensuring the
reported version remains synchronized with the published package while
preserving the existing server name and capabilities.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8048bb1a-7715-493f-a19d-4ad51e8fff09

📥 Commits

Reviewing files that changed from the base of the PR and between c525e81 and 8104649.

📒 Files selected for processing (10)
  • .github/workflows/publish.yml
  • README.md
  • package.json
  • scripts/mcp.ts
  • src/cli.ts
  • src/commands/doctor.test.ts
  • src/commands/doctor.ts
  • src/index.ts
  • src/mcp-cli.test.ts
  • src/server.ts

Comment thread src/mcp-cli.test.ts
Comment thread src/server.ts
Comment on lines +11 to +48
export async function runServer(configPath?: string, cwd: string = process.cwd()): Promise<void> {
const snapshot = loadConfigSnapshot(configPath, cwd);
const oauthCredentialProvider = await loadOAuthCredentialProvider(
snapshot.gatewayConfig.oauthProviderModule,
path.dirname(snapshot.configPath),
);
const upstreams = new UpstreamRegistry(
Object.entries(snapshot.config.mcpServers).map(([name, upstream]) => ({ name, ...upstream })),
undefined,
oauthCredentialProvider,
);
const logger = createLogger();
const artifactStore = new InMemoryArtifactStore({
ttlMs: snapshot.gatewayConfig.resultTtlMs,
maxEntries: snapshot.gatewayConfig.resultMaxEntries,
});

const server = new Server(
{ name: "mottainai", version: "0.1.0" },
{ capabilities: { tools: {} } },
);
const activeProfileName = snapshot.config.gateway?.activeProfile;
const activeProfile = activeProfileName === undefined
? undefined
: snapshot.config.profiles?.[activeProfileName];
registerProxyHandlers(
server,
upstreams,
logger,
artifactStore,
snapshot.gatewayConfig,
{},
activeProfile,
);

const transport = new StdioServerTransport();
await server.connect(transport);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether upstream shutdown is wired to process signals or transport close events.
rg -n 'SIGINT|SIGTERM|\.close\(\)|onclose' src/proxy.ts src/server.ts src/index.ts src/upstream.ts

Repository: yohn-jp/mottainai

Length of output: 408


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- tracked files ---'
git ls-files | rg '(^|/)(src/(server|proxy|upstream|index)\.ts|package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|README\.md)$'

printf '%s\n' '--- src/server.ts ---'
cat -n src/server.ts

printf '%s\n' '--- src/proxy.ts outline and shutdown-related code ---'
ast-grep outline src/proxy.ts
rg -n -C 5 'registerProxyHandlers|close|onclose|SIGINT|SIGTERM|transport|server\.' src/proxy.ts

printf '%s\n' '--- src/upstream.ts relevant sections ---'
cat -n src/upstream.ts | sed -n '1,235p'

printf '%s\n' '--- package metadata and SDK references ---'
rg -n -C 3 'StdioServerTransport|`@modelcontextprotocol`|Server|transport|close|SIGINT|SIGTERM' package.json package-lock.json pnpm-lock.yaml yarn.lock README.md src 2>/dev/null || true

Repository: yohn-jp/mottainai

Length of output: 50376


🌐 Web query:

@modelcontextprotocol/sdk 1.29.0 StdioServerTransport SIGINT SIGTERM close implementation server connect

💡 Result:

In the @modelcontextprotocol/sdk, StdioServerTransport is designed for communication over standard input (stdin) and standard output (stdout) [1][2]. Regarding handling shutdown signals (SIGINT/SIGTERM) and pipe closure, here is the current technical status: 1. Stdin EOF Handling: Modern versions of the SDK (specifically addressing issues where servers became "zombie" processes) have implemented logic within StdioServerTransport to detect when the client closes the stdin pipe [3]. The transport now listens for 'end' and 'close' events on stdin, which triggers the transport's close method [3]. This is considered the primary, portable mechanism for graceful shutdown in the MCP stdio specification, as it occurs when the client terminates or disconnects [1]. 2. SIGINT/SIGTERM Handling: The SDK's transport implementation does not automatically intercept process-level signals like SIGINT or SIGTERM [4][3]. These signals are operating system-level events directed at the entire process. It is the responsibility of the server application developer to handle these signals explicitly (e.g., via Node.js process.on('SIGINT',...)) and call the server's or transport's close method to ensure graceful cleanup [4]. Examples in the SDK have been updated to include these explicit handlers [4]. 3. Close Implementation: The close method in StdioServerTransport is designed to be idempotent [3]. When invoked—whether via explicit application logic (e.g., signal handler), stdin closure, or direct calls—it performs the following: - Removes event listeners from stdin and stdout [5][6]. - Optionally pauses stdin if no other listeners remain [5][6]. - Clears internal read buffers [5][6]. - Fires the onclose callback [5][3]. In summary, while stdin EOF is now automatically handled to trigger transport closure, application developers must still implement their own process-level signal handlers (SIGINT/SIGTERM) to trigger this shutdown flow gracefully when the process receives these signals [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- src/index.ts ---'
cat -n src/index.ts

printf '%s\n' '--- package scripts ---'
cat -n package.json | sed -n '1,55p'

printf '%s\n' '--- focused proxy lifecycle references ---'
rg -n -C 8 'registerProxyHandlers|UpstreamRegistry|upstreams\.close|server\.on|transport\.on|onclose|close\(' src/proxy.ts src/index.ts src/server.ts src/*.test.ts

Repository: yohn-jp/mottainai

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -eu

base='https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm'
for file in server/stdio.js server/index.js shared/transport.js; do
  printf '\n--- %s ---\n' "$file"
  curl -fsSL "$base/$file" | rg -n -C 8 'class StdioServerTransport|class Server|on\("end"|on\("close"|onclose|async close|connect\(' || true
done

printf '\n--- exact installed SDK version from lockfile ---\n'
rg -n -C 2 \"'`@modelcontextprotocol/sdk`@|version: 1\\.29\\.0\" pnpm-lock.yaml

Repository: yohn-jp/mottainai

Length of output: 3448


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact SDK 1.29.0 stdio lifecycle ---'
curl -fsSL 'https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/server/stdio.js' \
  | sed -n '1,95p'

printf '%s\n' '--- exact SDK 1.29.0 server connect/close lifecycle ---'
curl -fsSL 'https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/shared/protocol.js' \
  | rg -n -C 10 'connect\(|close\(|onclose|transport\.on' || true
curl -fsSL 'https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/server/index.js' \
  | rg -n -C 10 'connect\(|close\(|onclose|transport\.on' || true

printf '%s\n' '--- exact lockfile entry ---'
rg -n -C 3 'modelcontextprotocol/sdk@1\.29\.0|version: 1\.29\.0' pnpm-lock.yaml

Repository: yohn-jp/mottainai

Length of output: 8649


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact SDK 1.29.0 upstream stdio client close ---'
curl -fsSL 'https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/client/stdio.js' \
  | sed -n '1,220p' \
  | rg -n -C 12 'class StdioClientTransport|async close|child|kill|AbortController|on\("exit"|on\("close"'

printf '%s\n' '--- repository shutdown wiring, excluding tests ---'
rg -n -C 4 'process\.on|once\(["'\"']SIG|SIGINT|SIGTERM|upstreams\.close|server\.close|transport\.close|onclose' src --glob '!*.test.ts' || true

Repository: yohn-jp/mottainai

Length of output: 4953


Wire UpstreamRegistry.close() into server shutdown.

registerProxyHandlers and SDK StdioServerTransport do not close upstreams on transport closure or SIGINT/SIGTERM. Add an idempotent shutdown path in runServer that awaits upstreams.close() before the process exits. This prevents stdio upstream child processes from remaining alive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` around lines 11 - 48, The runServer function creates an
UpstreamRegistry instance in the upstreams variable but never closes it during
shutdown, leaving stdio child processes alive. Add an idempotent shutdown path
in runServer that awaits upstreams.close() before the process exits, triggered
by transport closure or process signals (SIGINT/SIGTERM). Ensure this cleanup
runs after server.connect(transport) completes and before the function returns.

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