Skip to content

feat: add jumpstarter-mcp package for AI agent integration#365

Merged
mangelajo merged 4 commits into
mainfrom
jmp-mcp
Mar 24, 2026
Merged

feat: add jumpstarter-mcp package for AI agent integration#365
mangelajo merged 4 commits into
mainfrom
jmp-mcp

Conversation

@mangelajo
Copy link
Copy Markdown
Member

@mangelajo mangelajo commented Mar 20, 2026

Summary

  • Adds jumpstarter-mcp, a new package providing an MCP (Model Context Protocol) server that exposes Jumpstarter's lease management, device connections, and command execution as structured tools for AI coding agents
  • Supports Cursor, Claude Code, Claude Desktop, and any MCP-compatible host via jmp mcp serve
  • Includes comprehensive documentation guide with setup instructions, usage examples, workflow diagrams, and prompt examples for AI-assisted Python development against live hardware
  • Adds 24 unit tests covering CLI introspection, driver method inspection, connection management, and command execution

What's included

New package: jumpstarter-mcp

  • MCP server (server.py): Registers tools for lease/exporter management, device connections, CLI command execution, and driver introspection
  • Connection manager (connections.py): Manages persistent device connections with graceful error handling for client disconnects
  • Introspection (introspect.py): Walk Click CLI trees and inspect Python driver method signatures/docstrings
  • Tools: Modular tool implementations for leases, connections, and commands
  • CLI entry point (cli.py): Adds jmp mcp serve subcommand

Documentation

  • New guide: ai-agent-integration.md with setup for Cursor, Claude Code, and Claude Desktop
  • Cross-reference from integration-patterns.md
  • Package reference symlink in reference/package-apis/mcp.md
  • Updated README with prompt examples for AI-assisted hardware development

Other changes

  • CompositeClient.__getattr__ now raises AttributeError instead of KeyError for missing attributes (fixes introspection compatibility)
  • jumpstarter-cli now optionally loads the MCP subcommand

Test plan

  • make lint-fix passes clean (0 errors)
  • make pkg-test-jumpstarter-mcp passes (24/24 tests)
  • Verify jmp mcp serve starts and tools are accessible from Cursor
  • Verify claude mcp add jumpstarter -- jmp mcp serve works in Claude Code
  • Verify docs build without warnings

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 20, 2026

Deploy Preview for jumpstarter-docs ready!

Name Link
🔨 Latest commit e943f1a
🔍 Latest deploy log https://app.netlify.com/projects/jumpstarter-docs/deploys/69c2af494c11900008c073d2
😎 Deploy Preview https://deploy-preview-365--jumpstarter-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

Adds a new stdio-based MCP server package (jumpstarter-mcp) with connection management, command execution, CLI/driver introspection, MCP CLI integration, docs, tests, and a small CompositeClient attribute-fallback fix. All changes are documentation, packaging, CLI wiring, and new MCP server/tooling code.

Changes

Cohort / File(s) Summary
Documentation: Guides & Reference
python/docs/source/getting-started/guides/ai-agent-integration.md, python/docs/source/getting-started/guides/index.md, python/docs/source/getting-started/guides/integration-patterns.md, python/docs/source/reference/package-apis/index.md, python/docs/source/reference/package-apis/mcp.md
Add AI Agent Integration guide and references; include MCP Server in guides and API reference toctrees.
MCP Package: Packaging & README
python/packages/jumpstarter-mcp/pyproject.toml, python/packages/jumpstarter-mcp/README.md
New package manifest and README describing the MCP server, tools, workflows, and logs.
MCP Package: CLI & Server
python/packages/jumpstarter-mcp/jumpstarter_mcp/cli.py, python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py
Add jmp mcp serve CLI and stdio-based MCP server with token handling, tool registration, logging, and lifecycle management.
MCP Package: Connection Manager
python/packages/jumpstarter-mcp/jumpstarter_mcp/connections.py
New Connection dataclass and ConnectionManager to manage background unix-socket exporter connections, log forwarding, lifecycle, and inspection helpers.
MCP Package: Tools (commands / leases / connections)
python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/commands.py, .../tools/leases.py, .../tools/connections.py
Add MCP-facing tool implementations: run commands with timeout, env export, CLI exploration, list/create/delete leases, connect/disconnect/list connections, and driver introspection.
MCP Package: Introspection
python/packages/jumpstarter-mcp/jumpstarter_mcp/introspect.py
Add CLI and driver introspection utilities: walk_click_tree, list_drivers, get_driver_methods, and method inspection helpers.
MCP Package: Tests
python/packages/jumpstarter-mcp/jumpstarter_mcp/server_test.py
Add pytest suite covering introspection, ConnectionManager, subprocess command execution/timeouts, and token refresh behavior.
CLI Integration & Workspace
python/packages/jumpstarter-cli/jumpstarter_cli/jmp.py, python/packages/jumpstarter-cli/pyproject.toml, python/pyproject.toml
Conditionally register mcp subcommand in jmp if jumpstarter_mcp is available; add jumpstarter-mcp dependency and workspace source.
Driver Composite Fix
python/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/client.py
Convert missing-child KeyError into AttributeError in CompositeClient.__getattr__ for correct attribute semantics.

Sequence Diagram(s)

sequenceDiagram
    participant IDE as IDE / AI Assistant
    participant MCP as MCP Server (jmp mcp serve)
    participant ConnMgr as ConnectionManager
    participant Exporter as Exporter / Device

    IDE->>MCP: jmp_create_lease(exporter, duration)
    MCP->>ConnMgr: connect(config, lease_name, exporter)
    ConnMgr->>Exporter: acquire lease & serve_unix_async()
    Exporter-->>ConnMgr: unix socket path + driver client
    ConnMgr-->>MCP: connection_id, socket_path

    IDE->>MCP: jmp_explore(connection_id)
    MCP->>ConnMgr: get connection -> client.cli
    ConnMgr-->>MCP: CLI tree / metadata
    MCP-->>IDE: CLI metadata

    IDE->>MCP: jmp_run(connection_id, ["..."], timeout)
    MCP->>Exporter: spawn subprocess with env (socket path)
    Exporter-->>MCP: stdout, exit code
    MCP-->>IDE: command result

    IDE->>MCP: jmp_disconnect(connection_id)
    MCP->>ConnMgr: disconnect(connection_id)
    ConnMgr->>Exporter: end lease / cleanup
    ConnMgr-->>MCP: disconnected
    MCP-->>IDE: status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

enhancement, python

Suggested reviewers

  • NickCao
  • kirkbrauer
  • raballew

Poem

🐇 I hopped to a socket, soft and neat,
MCP and agents now greet and meet,
Leases and drivers hum in a row,
Commands skip along where circuits glow,
A rabbit cheers as connections grow. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding a new jumpstarter-mcp package that enables AI agent integration through an MCP server.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, detailing the new MCP package, documentation additions, tests, and related changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jmp-mcp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
python/packages/jumpstarter-mcp/PLAN.md (1)

73-104: Add a language to the fenced code block.

Line 73 opens a fenced block without a language tag, which triggers markdownlint MD040. Use ```text (or another explicit language) for that block.

Suggested doc fix
-```
+```text
 Jumpstarter provides remote access to physical hardware devices through a
 controller that manages leases and exporters.
 ...
 - Use jmp_driver_methods to get exact method signatures for Python code
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @python/packages/jumpstarter-mcp/PLAN.md around lines 73 - 104, The fenced
code block showing the Jumpstarter workflow in PLAN.md is missing a language tag
(causing markdownlint MD040); update the opening fence from totext (or
another explicit language like "markdown" or "md") for the block that begins
with "Jumpstarter provides remote access..." so the linter recognizes the code
block language.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @python/packages/jumpstarter-mcp/PLAN.md:

  • Line 110: Update the package path references in PLAN.md to match the
    repository layout by replacing occurrences of "packages/jumpstarter-mcp/" with
    "python/packages/jumpstarter-mcp/" (including the text that describes creating
    the package and any entry-point notes like "jmp mcp serve"); ensure all other
    mentions (e.g., at line 332) are normalized as well so the doc consistently
    points to the actual repo location.

Nitpick comments:
In @python/packages/jumpstarter-mcp/PLAN.md:

  • Around line 73-104: The fenced code block showing the Jumpstarter workflow in
    PLAN.md is missing a language tag (causing markdownlint MD040); update the
    opening fence from totext (or another explicit language like "markdown"
    or "md") for the block that begins with "Jumpstarter provides remote access..."
    so the linter recognizes the code block language.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `62860ac2-ffcb-4f27-809a-11de3a0f6c8c`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0cf0d61d6188281bffd3737c9202d1d40f49efd6 and 8c178e29eb83798a057c5d9fa76fbf80751260ac.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `python/uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `python/packages/jumpstarter-mcp/PLAN.md`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread python/packages/jumpstarter-mcp/PLAN.md Outdated
@mangelajo mangelajo changed the title mcp plan feat: add jumpstarter-mcp package for AI agent integration Mar 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/packages/jumpstarter-cli/jumpstarter_cli/jmp.py`:
- Around line 41-46: The try/except block around "from jumpstarter_mcp.cli
import mcp" swallows real import errors and hides broken installs; replace it by
importing mcp unconditionally so failures surface (remove the try/except and
keep "from jumpstarter_mcp.cli import mcp" and "jmp.add_command(mcp)"), or if
you intentionally want to only handle missing package installs, narrow the
handler to ModuleNotFoundError and verify the exception.name equals
"jumpstarter_mcp" before suppressing; update the code around the import and
jmp.add_command(mcp) accordingly.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/connections.py`:
- Around line 228-246: The connection entry is left in self._connections after
the background tasks finish; update the teardown in the async with
anyio.create_task_group() block (around notify_tg.cancel_scope.cancel(), after
await notify_send.aclose() and after the socket/client teardown) to remove the
dead connection from the manager state by doing a safe removal like
self._connections.pop(connection_id, None) (or equivalent) so that
get_connection(), list_connections(), get_env(), and run_command() no longer see
stale entries; ensure removal is done unconditionally when that background
teardown completes and do it in the same scope as where
_forward_lease_notifications/_watch_lease_transfer and notify_send.aclose() are
handled to avoid races.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/introspect.py`:
- Around line 103-120: list_drivers() is producing dot-prefixed paths like
"client.power" that are incompatible with
get_driver_methods()/jmp_driver_methods which expect child-key lists like
["power"]; change list_drivers to emit child-relative paths (no leading
"client") and avoid producing empty segments for the root driver by: start with
an empty prefix (prefix="" default) or when composing child paths use
f"{prefix}.{name}".lstrip(".") / conditional join so the root entry becomes ""
or the driver name (not "client" or "client.."), and ensure any example or
caller that expects jmp_driver_methods gets the split path by doing
path.split(".") while skipping empty segments; update references to
list_drivers, get_driver_methods, and jmp_driver_methods accordingly.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py`:
- Around line 360-378: _wrap the file-logging setup in _setup_logging with a
broad try/cept so failures in Path.home(), log_dir.mkdir(), or
logging.FileHandler(...) cannot raise and stop the server; catch Exception
around the block that computes log_dir, creates the directory, creates the
FileHandler, and calls root.addHandler, and on failure skip adding the file
handler (optionally call logging.getLogger(__name__).warning or print a short
message) so the stdio server always starts even if file logging is unavailable.
Use the existing _setup_logging function and the symbols Path.home,
log_dir.mkdir, logging.FileHandler, and root.addHandler to locate where to wrap
the try/except.
- Around line 82-103: The current _load_config() creates a
ClientConfigV1Alpha1() instance that can be empty, so change the logic to detect
an unusable config and fall back to the saved user config: after constructing or
attempting to construct ClientConfigV1Alpha1, check its required fields (e.g.,
config.endpoint and config.token) for truthy values; if either is
missing/None/empty, call
UserConfigV1Alpha1.load_or_create().config.current_client and use that instead
(instead of relying solely on catching ValidationError). Ensure this check
references ClientConfigV1Alpha1 and
UserConfigV1Alpha1.load_or_create().config.current_client and returns a valid
config or raises the existing RuntimeError if still None.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/commands.py`:
- Around line 44-45: The logs currently print the full CLI argv which can leak
secrets; create a small sanitizer (e.g., redact_argv or redact_args) that
returns only the subcommand name (command[0]) or a redacted representation (mask
all subsequent tokens as "<redacted>" or replace with count) and use it in every
logger call that currently interpolates " ".join(command) (the Running
command/info, success, timeout, and failure logs) and in any exception handlers
in the same module so no positional or flag values are logged verbatim.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9909b028-dbfb-4526-9e1f-bbc1141a21bf

📥 Commits

Reviewing files that changed from the base of the PR and between 8c178e2 and d76e3a5.

⛔ Files ignored due to path filters (1)
  • python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • python/docs/source/getting-started/guides/ai-agent-integration.md
  • python/docs/source/getting-started/guides/index.md
  • python/docs/source/getting-started/guides/integration-patterns.md
  • python/docs/source/reference/package-apis/index.md
  • python/docs/source/reference/package-apis/mcp.md
  • python/packages/jumpstarter-cli/jumpstarter_cli/jmp.py
  • python/packages/jumpstarter-cli/pyproject.toml
  • python/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/client.py
  • python/packages/jumpstarter-mcp/README.md
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/__init__.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/cli.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/connections.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/introspect.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/server_test.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/__init__.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/commands.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/connections.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/leases.py
  • python/packages/jumpstarter-mcp/pyproject.toml
  • python/pyproject.toml
✅ Files skipped from review due to trivial changes (8)
  • python/docs/source/reference/package-apis/index.md
  • python/docs/source/getting-started/guides/index.md
  • python/docs/source/reference/package-apis/mcp.md
  • python/docs/source/getting-started/guides/integration-patterns.md
  • python/packages/jumpstarter-mcp/README.md
  • python/docs/source/getting-started/guides/ai-agent-integration.md
  • python/packages/jumpstarter-mcp/pyproject.toml
  • python/packages/jumpstarter-cli/pyproject.toml

Comment thread python/packages/jumpstarter-cli/jumpstarter_cli/jmp.py Outdated
Comment thread python/packages/jumpstarter-mcp/jumpstarter_mcp/connections.py
Comment thread python/packages/jumpstarter-mcp/jumpstarter_mcp/introspect.py Outdated
Comment on lines +82 to +103
def _load_config() -> ClientConfigV1Alpha1:
"""Load client config using the same resolution as jmp CLI."""
from pydantic import ValidationError

from jumpstarter.config.user import UserConfigV1Alpha1

config = None
try:
config = ClientConfigV1Alpha1()
except ValidationError:
pass

if config is None:
config = UserConfigV1Alpha1.load_or_create().config.current_client

if config is None:
raise RuntimeError(
"No jumpstarter client config found. "
"Run 'jmp config set-client <name>' or set JUMPSTARTER_* environment variables."
)

return config
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 20, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C3 'def _load_config|ClientConfigV1Alpha1\(|endpoint: str \| None|token: str \| None' \
  python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py \
  python/packages/jumpstarter/jumpstarter/config/client.py

Repository: jumpstarter-dev/jumpstarter

Length of output: 2859


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the full definition of ClientConfigV1Alpha1
fd -t f "client.py" python/packages/jumpstarter/jumpstarter/config/ --exec cat -n {} \; | \
  head -200

Repository: jumpstarter-dev/jumpstarter

Length of output: 7832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there are any validators or model_config that enforce required fields in ClientConfigV1Alpha1
rg -A 30 "class ClientConfigV1Alpha1" python/packages/jumpstarter/jumpstarter/config/client.py | head -50

Repository: jumpstarter-dev/jumpstarter

Length of output: 1691


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how _load_config() return value is actually used in the MCP server
rg -n "_load_config\(\)|endpoint|token" python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py | head -30

Repository: jumpstarter-dev/jumpstarter

Length of output: 297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the actual usage context where _load_config() is called in server.py
rg -B 3 -A 10 "_load_config\(\)" python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py | head -60

Repository: jumpstarter-dev/jumpstarter

Length of output: 1931


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there are any validators in ClientConfigV1Alpha1 that might validate endpoint/token
rg -A 50 "class ClientConfigV1Alpha1\(BaseSettings\)" python/packages/jumpstarter/jumpstarter/config/client.py | grep -A 50 "endpoint\|token\|validator"

Repository: jumpstarter-dev/jumpstarter

Length of output: 1585


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify that lease_tools functions use config.channel()
rg -n "def list_exporters|def list_leases|def create_lease" \
  python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/*.py | head -20

Repository: jumpstarter-dev/jumpstarter

Length of output: 344


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what lease_tools.list_exporters does with the config
rg -A 10 "async def list_exporters|config.channel\(\)" \
  python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/*.py | head -30

Repository: jumpstarter-dev/jumpstarter

Length of output: 1141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Final check: verify config.list_exporters eventually calls config.channel()
rg -A 5 "async def list_exporters" python/packages/jumpstarter/jumpstarter/config/client.py

Repository: jumpstarter-dev/jumpstarter

Length of output: 263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the full list_exporters implementation
rg -A 20 "async def list_exporters" python/packages/jumpstarter/jumpstarter/config/client.py | head -30

Repository: jumpstarter-dev/jumpstarter

Length of output: 779


_load_config() never reaches the saved-user-config fallback when no environment variables are set.

ClientConfigV1Alpha1() is constructible with default None values for endpoint and token, so the ValidationError catch never triggers. The function returns an unusable config instead of falling back to UserConfigV1Alpha1.load_or_create().config.current_client. This breaks the standard workflow where users run jmp config set-client <name> and expect it to work. At runtime, any MCP tool invocation fails with ConfigurationError when config.channel() is called.

Suggested fix
 def _load_config() -> ClientConfigV1Alpha1:
     """Load client config using the same resolution as jmp CLI."""
     from pydantic import ValidationError

     from jumpstarter.config.user import UserConfigV1Alpha1

-    config = None
-    try:
-        config = ClientConfigV1Alpha1()
-    except ValidationError:
-        pass
-
-    if config is None:
-        config = UserConfigV1Alpha1.load_or_create().config.current_client
+    try:
+        env_config = ClientConfigV1Alpha1()
+    except ValidationError:
+        env_config = None
+
+    config = env_config
+    if config is None or config.endpoint is None or config.token is None:
+        config = UserConfigV1Alpha1.load_or_create().config.current_client
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 82 -
103, The current _load_config() creates a ClientConfigV1Alpha1() instance that
can be empty, so change the logic to detect an unusable config and fall back to
the saved user config: after constructing or attempting to construct
ClientConfigV1Alpha1, check its required fields (e.g., config.endpoint and
config.token) for truthy values; if either is missing/None/empty, call
UserConfigV1Alpha1.load_or_create().config.current_client and use that instead
(instead of relying solely on catching ValidationError). Ensure this check
references ClientConfigV1Alpha1 and
UserConfigV1Alpha1.load_or_create().config.current_client and returns a valid
config or raises the existing RuntimeError if still None.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's incorrect, this is actually working.

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.

🧩 Analysis chain

Let me verify once more whether there are any validators in ClientConfigV1Alpha1 that I may have missed.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the complete ClientConfigV1Alpha1 definition including validators
cat -n python/packages/jumpstarter/jumpstarter/config/client.py | head -120

echo "---"
# Also check if there are model_validators or field_validators
rg -n "model_validator|field_validator|@validator" python/packages/jumpstarter/jumpstarter/config/client.py

Length of output: 4449


Let me check a few more lines to see the full ClientConfigV1Alpha1 field and validator definitions.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get lines 120-250 of client.py to see endpoint/token fields and any validators
sed -n '120,260p' python/packages/jumpstarter/jumpstarter/config/client.py | cat -n

Length of output: 6168


@mangelajo, thanks for the clarification. I can see it works correctly when JMP_ENDPOINT/JMP_TOKEN environment variables are set — in that case ClientConfigV1Alpha1() reads them via pydantic-settings and returns a fully configured instance.

My concern is specifically about users who configure Jumpstarter only via jmp config set-client <name> without any JMP_* env vars. In that scenario:

  1. ClientConfigV1Alpha1() succeeds without raising ValidationError (since both endpoint and token are str | None = Field(default=None) — no validators on the class).
  2. config is therefore never None, so the UserConfigV1Alpha1.load_or_create().config.current_client fallback is never reached.
  3. The returned config has endpoint=None/token=None, and every subsequent MCP tool call fails at channel() with ConfigurationError("endpoint or token not set in client config").

If you've verified that the named-client config path (jmp config set-client) also works correctly in your testing, I'm happy to accept that and note it as a learning. Could you clarify whether that path was tested, or whether the current design intentionally targets only the env-var-based configuration?


🧠 Learnings used
Learnt from: bennyz
Repo: jumpstarter-dev/jumpstarter PR: 320
File: python/packages/jumpstarter-cli/jumpstarter_cli/shell.py:308-341
Timestamp: 2026-03-14T11:11:29.265Z
Learning: In `python/packages/jumpstarter-cli/jumpstarter_cli/shell.py`, the `--tls-grpc` path (`_shell_direct_async`) intentionally does NOT use `ClientConfigV1Alpha1`. It constructs a synthetic config with `allow=[]`, `unsafe=True`, and `use_profiles=False` because this is a direct, controller-free connection mode. Do not flag the absence of a real client config here as an issue.

Learnt from: mangelajo
Repo: jumpstarter-dev/jumpstarter PR: 365
File: python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/commands.py:44-45
Timestamp: 2026-03-23T10:38:49.265Z
Learning: In the `jumpstarter-mcp` package (`python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/commands.py`), the `j` CLI binary does not generally accept sensitive arguments such as tokens or passwords. Logging the full `j` command argv (via `" ".join(command)`) is acceptable because the arguments are not sensitive and the MCP server log is local (`~/.jumpstarter/logs/mcp-server.log`). Do not flag this as a secret-leakage issue in future reviews.

Comment on lines +360 to +378
def _setup_logging() -> None:
"""Configure logging with both stderr and file output.

Adds a file handler to the root logger regardless of whether
other handlers (e.g. from the mcp library) are already present.
"""
from pathlib import Path

log_dir = Path.home() / ".jumpstarter" / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "mcp-server.log"

fmt = "%(asctime)s %(levelname)s %(name)s: %(message)s"
file_handler = logging.FileHandler(log_file, mode="a")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(fmt))

root = logging.getLogger()
root.addHandler(file_handler)
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.

⚠️ Potential issue | 🟠 Major

File logging shouldn’t be able to prevent the MCP server from starting.

Right now Path.home(), mkdir(), or logging.FileHandler(...) can raise before the stdio server comes up. In restricted environments with a read-only or missing home directory, jmp mcp serve will fail even though file logging is only a convenience.

Suggested fix
 def _setup_logging() -> None:
@@
-    log_dir = Path.home() / ".jumpstarter" / "logs"
-    log_dir.mkdir(parents=True, exist_ok=True)
-    log_file = log_dir / "mcp-server.log"
-
     fmt = "%(asctime)s %(levelname)s %(name)s: %(message)s"
-    file_handler = logging.FileHandler(log_file, mode="a")
-    file_handler.setLevel(logging.DEBUG)
-    file_handler.setFormatter(logging.Formatter(fmt))
-
     root = logging.getLogger()
-    root.addHandler(file_handler)
+    try:
+        log_dir = Path.home() / ".jumpstarter" / "logs"
+        log_dir.mkdir(parents=True, exist_ok=True)
+        file_handler = logging.FileHandler(log_dir / "mcp-server.log", mode="a")
+    except (OSError, RuntimeError):
+        logger.warning("Disabling MCP file logging", exc_info=True)
+    else:
+        file_handler.setLevel(logging.DEBUG)
+        file_handler.setFormatter(logging.Formatter(fmt))
+        root.addHandler(file_handler)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 360 -
378, _wrap the file-logging setup in _setup_logging with a broad try/cept so
failures in Path.home(), log_dir.mkdir(), or logging.FileHandler(...) cannot
raise and stop the server; catch Exception around the block that computes
log_dir, creates the directory, creates the FileHandler, and calls
root.addHandler, and on failure skip adding the file handler (optionally call
logging.getLogger(__name__).warning or print a short message) so the stdio
server always starts even if file logging is unavailable. Use the existing
_setup_logging function and the symbols Path.home, log_dir.mkdir,
logging.FileHandler, and root.addHandler to locate where to wrap the try/except.

Comment thread python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/commands.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/docs/source/getting-started/guides/ai-agent-integration.md`:
- Around line 33-34: The sentence mentioning the MCP server package is a run-on
and has a formatting typo (missing space after inline code): split into two
clear sentences and insert a space after `jumpstarter-mcp`, e.g. state that the
MCP server package is normally provided via a full install and then separately
note that the `jumpstarter-mcp` package provides the `jmp mcp serve` CLI
subcommand; update the phrase containing `jumpstarter-mcp` and `jmp mcp serve`
to ensure there is a space after the inline code and the wording is concise and
readable.
- Around line 175-199: Add a language identifier to the fenced code block that
begins with "$ claude" (the example console transcript) to satisfy markdownlint
MD040 by changing the opening fence from ``` to something like ```text (or
```console/```bash if more appropriate), and also remove any stray diff-markup
hyphens inside the block (e.g., the leading "-" before "The board is running
Fedora...") so the block contains only the transcript text.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bb8638f3-4385-4aab-9102-c7a22e970c37

📥 Commits

Reviewing files that changed from the base of the PR and between d76e3a5 and 91877ea.

📒 Files selected for processing (1)
  • python/docs/source/getting-started/guides/ai-agent-integration.md

Comment thread python/docs/source/getting-started/guides/ai-agent-integration.md Outdated
Comment thread python/docs/source/getting-started/guides/ai-agent-integration.md
Add an MCP (Model Context Protocol) server that exposes Jumpstarter's
lease management, device connections, and command execution as structured
tools accessible by AI coding agents (Cursor, Claude Code, Claude Desktop,
or any MCP-compatible host).

The server enables natural-language-driven hardware interaction from IDEs:
- Lease & exporter management (list, create, delete)
- Persistent device connections with CLI command execution
- Driver introspection (discover methods, signatures, docstrings)
- Environment export for Python scripting with the env() helper

Also includes:
- Documentation guide with setup instructions for Cursor, Claude Code,
  and Claude Desktop, plus usage examples and workflow diagrams
- Symlink in docs reference section for package API docs
- Unit tests (24 tests covering introspection, connections, commands)
- CompositeClient.__getattr__ fix to raise AttributeError instead of KeyError
- Remove connection from manager state in _run_connection finally block
  so expired/transferred leases don't leave stale entries
- Add driver_path field to list_drivers output (e.g. ["power"]) so
  jmp_drivers results can be passed directly to jmp_driver_methods
- Fix docs cross-reference to use doc-tree path instead of filesystem path

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (5)
python/docs/source/getting-started/guides/ai-agent-integration.md (2)

175-199: ⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced code block.

The code block at line 175 is missing a language identifier, which triggers markdownlint MD040.

Suggested fix
-```
+```text
 $ claude
 
 > /mcp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/docs/source/getting-started/guides/ai-agent-integration.md` around
lines 175 - 199, The fenced code block that begins with the shell interaction
starting with "$ claude" is missing a language identifier (triggering
markdownlint MD040); update the opening fence to include a language tag (e.g.,
```text) so the block becomes ```text ... ```; locate the block that contains
the lines starting with "$ claude" and the MCP interaction (commands like
"/mcp", "Connected MCP servers:", and the subsequent output) and add the
language identifier to the opening triple backticks.

33-34: ⚠️ Potential issue | 🟡 Minor

Fix the run-on sentence in prerequisites.

The sentence is awkward and has redundant phrasing ("which...which").

Suggested fix
-The MCP server package, which is normally provided when you perform a full install
-through the `jumpstarter-mcp` package which provides the `jmp mcp serve` subcommand on the CLI.
+The MCP server is provided by the `jumpstarter-mcp` package, which adds the
+`jmp mcp serve` subcommand. This package is included in a full Jumpstarter installation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/docs/source/getting-started/guides/ai-agent-integration.md` around
lines 33 - 34, Rewrite the run-on prerequisites sentence to remove the redundant
"which...which" and improve flow; replace the current line that references "The
MCP server package, which is normally provided when you perform a full install
through the `jumpstarter-mcp` package which provides the `jmp mcp serve`
subcommand on the CLI." with a concise sentence that names the MCP server
package, states it is installed by the `jumpstarter-mcp` package, and mentions
the `jmp mcp serve` subcommand once (e.g., "The MCP server package is installed
by the `jumpstarter-mcp` package and provides the `jmp mcp serve` CLI
subcommand."). Ensure the phrase "MCP server package", "`jumpstarter-mcp`", and
"`jmp mcp serve`" remain present for clarity.
python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py (2)

82-103: ⚠️ Potential issue | 🟠 Major

_load_config() fallback logic is still incomplete.

ClientConfigV1Alpha1() can be constructed with default None values for endpoint and token without raising ValidationError. The function returns this unusable config instead of falling back to the user config. This breaks the standard workflow where users run jmp config set-client <name>.

Suggested fix
 def _load_config() -> ClientConfigV1Alpha1:
     """Load client config using the same resolution as jmp CLI."""
     from pydantic import ValidationError

     from jumpstarter.config.user import UserConfigV1Alpha1

     config = None
     try:
         config = ClientConfigV1Alpha1()
     except ValidationError:
         pass

-    if config is None:
+    if config is None or config.endpoint is None or config.token is None:
         config = UserConfigV1Alpha1.load_or_create().config.current_client

     if config is None:
         raise RuntimeError(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 82 -
103, _load_config currently accepts a ClientConfigV1Alpha1 instance even when
required fields like endpoint or token are None because ClientConfigV1Alpha1()
doesn't raise ValidationError; update _load_config to detect unusable defaults
by checking the created config's essential fields (e.g., config.endpoint and
config.token) and if either is missing, treat it as None and fall back to
UserConfigV1Alpha1.load_or_create().config.current_client; after falling back,
re-check that the final config has both endpoint and token and raise the
RuntimeError if still missing so callers of _load_config get a valid usable
client config.

360-386: ⚠️ Potential issue | 🟠 Major

File logging setup can prevent the MCP server from starting.

Path.home(), mkdir(), or logging.FileHandler() can raise exceptions in restricted environments (read-only or missing home directory). This will crash the server before it comes up, even though file logging is only a convenience feature.

Suggested fix
 def _setup_logging() -> None:
     """Configure logging with both stderr and file output."""
     from pathlib import Path

+    fmt = "%(asctime)s %(levelname)s %(name)s: %(message)s"
+    root = logging.getLogger()
+
+    try:
-    log_dir = Path.home() / ".jumpstarter" / "logs"
-    log_dir.mkdir(parents=True, exist_ok=True)
-    log_file = log_dir / "mcp-server.log"
-
-    fmt = "%(asctime)s %(levelname)s %(name)s: %(message)s"
-    file_handler = logging.FileHandler(log_file, mode="a")
-    file_handler.setLevel(logging.DEBUG)
-    file_handler.setFormatter(logging.Formatter(fmt))
-
-    root = logging.getLogger()
-    root.addHandler(file_handler)
+        log_dir = Path.home() / ".jumpstarter" / "logs"
+        log_dir.mkdir(parents=True, exist_ok=True)
+        file_handler = logging.FileHandler(log_dir / "mcp-server.log", mode="a")
+        file_handler.setLevel(logging.DEBUG)
+        file_handler.setFormatter(logging.Formatter(fmt))
+        root.addHandler(file_handler)
+    except (OSError, RuntimeError):
+        logger.warning("File logging disabled (cannot create log file)")

     logging.getLogger("jumpstarter_mcp").setLevel(logging.DEBUG)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 360 -
386, The _setup_logging function currently calls Path.home(),
log_dir.mkdir(...), and logging.FileHandler(...) without guarding against IO/OS
errors which can abort startup; wrap the file-log creation and addition in a
try/except that catches OSError/IOError (and optionally PermissionError) around
Path.home(), mkdir, and logging.FileHandler so failures fall back to continuing
without a file handler, emit a warning to the existing stderr StreamHandler via
logging.getLogger().warning or print if no handlers exist, and only call
root.addHandler(file_handler) if the handler was successfully created; reference
_setup_logging, Path.home, mkdir, logging.FileHandler, root.addHandler, and the
file_handler variable when making the change.
python/packages/jumpstarter-mcp/jumpstarter_mcp/introspect.py (1)

151-167: ⚠️ Potential issue | 🟡 Minor

Malformed call_example for root-level drivers.

When driver_path is empty (root client), attr_path becomes an empty string, producing call examples like client..method(...) with a double dot.

Suggested fix
     attr_path = ".".join(driver_path)
     call_args = ", ".join(f"{p['name']}=..." for p in params)
-    method_call = f"client.{attr_path}.{name}({call_args})"
+    target = f"client.{attr_path}" if attr_path else "client"
+    method_call = f"{target}.{name}({call_args})"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/introspect.py` around lines
151 - 167, The call_example generation can produce a double-dot when driver_path
is empty because attr_path is joined into an empty string; update the
construction of method_call used in call_example so it conditionally includes
attr_path only when driver_path is non-empty (i.e., if driver_path: method_call
= f"client.{attr_path}.{name}({call_args})" else: method_call =
f"client.{name}({call_args})"), and keep attr_path and call_args logic as-is;
ensure call_example uses this corrected method_call variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@python/docs/source/getting-started/guides/ai-agent-integration.md`:
- Around line 175-199: The fenced code block that begins with the shell
interaction starting with "$ claude" is missing a language identifier
(triggering markdownlint MD040); update the opening fence to include a language
tag (e.g., ```text) so the block becomes ```text ... ```; locate the block that
contains the lines starting with "$ claude" and the MCP interaction (commands
like "/mcp", "Connected MCP servers:", and the subsequent output) and add the
language identifier to the opening triple backticks.
- Around line 33-34: Rewrite the run-on prerequisites sentence to remove the
redundant "which...which" and improve flow; replace the current line that
references "The MCP server package, which is normally provided when you perform
a full install through the `jumpstarter-mcp` package which provides the `jmp mcp
serve` subcommand on the CLI." with a concise sentence that names the MCP server
package, states it is installed by the `jumpstarter-mcp` package, and mentions
the `jmp mcp serve` subcommand once (e.g., "The MCP server package is installed
by the `jumpstarter-mcp` package and provides the `jmp mcp serve` CLI
subcommand."). Ensure the phrase "MCP server package", "`jumpstarter-mcp`", and
"`jmp mcp serve`" remain present for clarity.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/introspect.py`:
- Around line 151-167: The call_example generation can produce a double-dot when
driver_path is empty because attr_path is joined into an empty string; update
the construction of method_call used in call_example so it conditionally
includes attr_path only when driver_path is non-empty (i.e., if driver_path:
method_call = f"client.{attr_path}.{name}({call_args})" else: method_call =
f"client.{name}({call_args})"), and keep attr_path and call_args logic as-is;
ensure call_example uses this corrected method_call variable.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py`:
- Around line 82-103: _load_config currently accepts a ClientConfigV1Alpha1
instance even when required fields like endpoint or token are None because
ClientConfigV1Alpha1() doesn't raise ValidationError; update _load_config to
detect unusable defaults by checking the created config's essential fields
(e.g., config.endpoint and config.token) and if either is missing, treat it as
None and fall back to UserConfigV1Alpha1.load_or_create().config.current_client;
after falling back, re-check that the final config has both endpoint and token
and raise the RuntimeError if still missing so callers of _load_config get a
valid usable client config.
- Around line 360-386: The _setup_logging function currently calls Path.home(),
log_dir.mkdir(...), and logging.FileHandler(...) without guarding against IO/OS
errors which can abort startup; wrap the file-log creation and addition in a
try/except that catches OSError/IOError (and optionally PermissionError) around
Path.home(), mkdir, and logging.FileHandler so failures fall back to continuing
without a file handler, emit a warning to the existing stderr StreamHandler via
logging.getLogger().warning or print if no handlers exist, and only call
root.addHandler(file_handler) if the handler was successfully created; reference
_setup_logging, Path.home, mkdir, logging.FileHandler, root.addHandler, and the
file_handler variable when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef90b2ae-1a60-449c-aa12-dc45ccf5535b

📥 Commits

Reviewing files that changed from the base of the PR and between 91877ea and fe61941.

⛔ Files ignored due to path filters (1)
  • python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • python/docs/source/getting-started/guides/ai-agent-integration.md
  • python/docs/source/getting-started/guides/index.md
  • python/docs/source/getting-started/guides/integration-patterns.md
  • python/docs/source/reference/package-apis/index.md
  • python/docs/source/reference/package-apis/mcp.md
  • python/packages/jumpstarter-cli/jumpstarter_cli/jmp.py
  • python/packages/jumpstarter-cli/pyproject.toml
  • python/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/client.py
  • python/packages/jumpstarter-mcp/README.md
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/__init__.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/cli.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/connections.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/introspect.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/server_test.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/__init__.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/commands.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/connections.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/leases.py
  • python/packages/jumpstarter-mcp/pyproject.toml
  • python/pyproject.toml
✅ Files skipped from review due to trivial changes (6)
  • python/docs/source/reference/package-apis/mcp.md
  • python/docs/source/reference/package-apis/index.md
  • python/packages/jumpstarter-cli/pyproject.toml
  • python/docs/source/getting-started/guides/integration-patterns.md
  • python/packages/jumpstarter-mcp/README.md
  • python/packages/jumpstarter-mcp/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (6)
  • python/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/client.py
  • python/docs/source/getting-started/guides/index.md
  • python/pyproject.toml
  • python/packages/jumpstarter-cli/jumpstarter_cli/jmp.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/tools/connections.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/connections.py

When the access token is expired or within 30s of expiry, the MCP server
now automatically attempts a refresh using the stored refresh_token via
OIDC before making gRPC calls. This avoids forcing users to manually
re-authenticate with `jmp login` during long MCP sessions.

Made-with: Cursor
@mangelajo mangelajo requested a review from raballew March 23, 2026 12:07
@mangelajo
Copy link
Copy Markdown
Member Author

I have more ideas to improve it, like opening and streaming serial ports/stream ports in the background, or creating TCP/UDP redirections in the background. But I will do if we agree to merge this one, don't want to make this huge for review.

@bennyz bennyz self-requested a review March 24, 2026 08:44
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py (2)

428-438: ⚠️ Potential issue | 🟠 Major

File logging failures should not prevent MCP startup.

Lines [428]-[438] can raise in constrained environments (home dir/mkdir/file handler), which would abort jmp mcp serve before stdio is available.

Proposed fix
 def _setup_logging() -> None:
@@
-    log_dir = Path.home() / ".jumpstarter" / "logs"
-    log_dir.mkdir(parents=True, exist_ok=True)
-    log_file = log_dir / "mcp-server.log"
-
     fmt = "%(asctime)s %(levelname)s %(name)s: %(message)s"
-    file_handler = logging.FileHandler(log_file, mode="a")
-    file_handler.setLevel(logging.DEBUG)
-    file_handler.setFormatter(logging.Formatter(fmt))
-
     root = logging.getLogger()
-    root.addHandler(file_handler)
+    try:
+        log_dir = Path.home() / ".jumpstarter" / "logs"
+        log_dir.mkdir(parents=True, exist_ok=True)
+        file_handler = logging.FileHandler(log_dir / "mcp-server.log", mode="a")
+    except Exception:
+        logger.warning("MCP file logging disabled", exc_info=True)
+    else:
+        file_handler.setLevel(logging.DEBUG)
+        file_handler.setFormatter(logging.Formatter(fmt))
+        root.addHandler(file_handler)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 428 -
438, Wrap the file-logging setup (the log_dir, log_file, file_handler creation
and root.addHandler call) in a try/except that catches Exception and silently
falls back to not using a FileHandler so startup cannot be aborted by IO errors;
specifically, enclose the existing statements that create log_dir, call mkdir,
create logging.FileHandler, setLevel/setFormatter and
root.addHandler(file_handler) in a try block and in the except block do not
re-raise (optionally call logging.getLogger().debug or pass to record the
failure) so jmp mcp serve continues even when file-handler creation fails.

90-99: ⚠️ Potential issue | 🔴 Critical

Fallback logic should also handle incomplete env config.

Line [92] can produce a config object with missing runtime essentials (e.g., endpoint/token), so Line [96] never falls back to saved current_client, and downstream controller calls fail.

Proposed fix
 def _load_config() -> ClientConfigV1Alpha1:
@@
-    config = None
+    config: ClientConfigV1Alpha1 | None = None
     try:
         config = ClientConfigV1Alpha1()
     except ValidationError:
         pass

-    if config is None:
+    if config is None or not config.endpoint or not config.token:
         config = UserConfigV1Alpha1.load_or_create().config.current_client
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 90 -
99, The code initializing `config` with `ClientConfigV1Alpha1()` may create an
incomplete object missing critical runtime details; therefore, after the
try-except block in the server initialization, add validation logic to check if
required fields like endpoint and token are present in `config`. If the config
is incomplete or missing these essentials, fallback to loading the saved
`current_client` from `UserConfigV1Alpha1.load_or_create()`. This ensures that
fallback logic correctly handles partial environment configs to avoid downstream
failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server_test.py`:
- Around line 529-538: The test currently checks that ClientConfigV1Alpha1.save
wasn't called but doesn't assert the OIDC refresh path was skipped; update
test_token_without_exp_claim_skips_refresh to patch/mock the actual refresh
helper used by _ensure_fresh_token (e.g., the module-level refresh function
called inside _ensure_fresh_token) and assert that this mock was not called,
while keeping the existing assertions on result and ClientConfigV1Alpha1.save;
reference the test name test_token_without_exp_claim_skips_refresh, the function
under test _ensure_fresh_token, and the ClientConfigV1Alpha1 class so the patch
targets the correct symbol.

---

Duplicate comments:
In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py`:
- Around line 428-438: Wrap the file-logging setup (the log_dir, log_file,
file_handler creation and root.addHandler call) in a try/except that catches
Exception and silently falls back to not using a FileHandler so startup cannot
be aborted by IO errors; specifically, enclose the existing statements that
create log_dir, call mkdir, create logging.FileHandler, setLevel/setFormatter
and root.addHandler(file_handler) in a try block and in the except block do not
re-raise (optionally call logging.getLogger().debug or pass to record the
failure) so jmp mcp serve continues even when file-handler creation fails.
- Around line 90-99: The code initializing `config` with
`ClientConfigV1Alpha1()` may create an incomplete object missing critical
runtime details; therefore, after the try-except block in the server
initialization, add validation logic to check if required fields like endpoint
and token are present in `config`. If the config is incomplete or missing these
essentials, fallback to loading the saved `current_client` from
`UserConfigV1Alpha1.load_or_create()`. This ensures that fallback logic
correctly handles partial environment configs to avoid downstream failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89aca7ac-8b2d-4c06-8aa2-a7385beb5940

📥 Commits

Reviewing files that changed from the base of the PR and between fe61941 and 4a50f7b.

📒 Files selected for processing (2)
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/server_test.py

Comment on lines +529 to +538
async def test_token_without_exp_claim_skips_refresh(self):
config = MagicMock()
config.token = _make_jwt_payload(exp=None)
config.refresh_token = "some-refresh"

with patch("jumpstarter_mcp.server.ClientConfigV1Alpha1") as mock_cls:
result = await _ensure_fresh_token(config)

assert result is config
mock_cls.save.assert_not_called()
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.

⚠️ Potential issue | 🟡 Minor

test_token_without_exp_claim_skips_refresh does not verify “skip refresh”.

Line [529] names a skip-path test, but Lines [534]-[538] never assert that OIDC refresh was not invoked; the test can still pass if refresh is attempted and fails.

Proposed test tightening
     `@pytest.mark.asyncio`
     async def test_token_without_exp_claim_skips_refresh(self):
         config = MagicMock()
         config.token = _make_jwt_payload(exp=None)
         config.refresh_token = "some-refresh"

-        with patch("jumpstarter_mcp.server.ClientConfigV1Alpha1") as mock_cls:
+        with (
+            patch("jumpstarter_mcp.server.ClientConfigV1Alpha1") as mock_cls,
+            patch("jumpstarter_cli_common.oidc.Config") as mock_oidc_cls,
+        ):
             result = await _ensure_fresh_token(config)

         assert result is config
         mock_cls.save.assert_not_called()
+        mock_oidc_cls.assert_not_called()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server_test.py` around lines
529 - 538, The test currently checks that ClientConfigV1Alpha1.save wasn't
called but doesn't assert the OIDC refresh path was skipped; update
test_token_without_exp_claim_skips_refresh to patch/mock the actual refresh
helper used by _ensure_fresh_token (e.g., the module-level refresh function
called inside _ensure_fresh_token) and assert that this mock was not called,
while keeping the existing assertions on result and ClientConfigV1Alpha1.save;
reference the test name test_token_without_exp_claim_skips_refresh, the function
under test _ensure_fresh_token, and the ClientConfigV1Alpha1 class so the patch
targets the correct symbol.

Comment thread python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py Outdated
@mangelajo mangelajo enabled auto-merge March 24, 2026 15:36
@mangelajo
Copy link
Copy Markdown
Member Author

thank you @bennyz ! :)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py (2)

429-439: ⚠️ Potential issue | 🟠 Major

File logging setup can prevent the MCP server from starting.

Lines 429-435 can raise in restricted environments (e.g., unwritable home dir), which aborts jmp mcp serve before stdio is up. Logging should be best-effort here.

Proposed fix
 def _setup_logging() -> None:
@@
-    log_dir = Path.home() / ".jumpstarter" / "logs"
-    log_dir.mkdir(parents=True, exist_ok=True)
-    log_file = log_dir / "mcp-server.log"
-
     fmt = "%(asctime)s %(levelname)s %(name)s: %(message)s"
-    file_handler = logging.FileHandler(log_file, mode="a")
-    file_handler.setLevel(logging.DEBUG)
-    file_handler.setFormatter(logging.Formatter(fmt))
-
     root = logging.getLogger()
-    root.addHandler(file_handler)
+    try:
+        log_dir = Path.home() / ".jumpstarter" / "logs"
+        log_dir.mkdir(parents=True, exist_ok=True)
+        file_handler = logging.FileHandler(log_dir / "mcp-server.log", mode="a")
+        file_handler.setLevel(logging.DEBUG)
+        file_handler.setFormatter(logging.Formatter(fmt))
+        root.addHandler(file_handler)
+    except Exception:
+        logger.warning("MCP file logging disabled; continuing without file handler", exc_info=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 429 -
439, The file-based logging setup using Path.home(), log_dir.mkdir(...), and
logging.FileHandler(log_file) can raise exceptions in restricted environments
and abort startup; wrap the entire block that creates log_dir, creates log_file,
instantiates file_handler and calls root.addHandler(file_handler) in a broad
try/except that logs a warning to stderr (or the existing console/stdio logger)
and silently continues on failure so startup does not abort; ensure variables
referenced are log_dir, log_file, file_handler and root, and do not remove or
change the existing stdio/console handlers—only add the file handler when the
operations succeed.

91-99: ⚠️ Potential issue | 🔴 Critical

_load_config() can return an unusable config and skip the intended fallback.

On Line 93, ClientConfigV1Alpha1() may succeed with empty defaults, so the fallback on Line 98 is never reached. In that case, tools fail later when a real endpoint/token is required.

Proposed fix
 def _load_config() -> ClientConfigV1Alpha1:
@@
-    config = None
+    config: ClientConfigV1Alpha1 | None = None
     try:
         config = ClientConfigV1Alpha1()
     except ValidationError:
         pass
 
-    if config is None:
+    if config is None or not config.endpoint or not config.token:
         config = UserConfigV1Alpha1.load_or_create().config.current_client
 
-    if config is None:
+    if config is None or not config.endpoint or not config.token:
         raise RuntimeError(
             "No jumpstarter client config found. "
             "Run 'jmp config client use <name>' or set JUMPSTARTER_* environment variables."
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 91 -
99, ClientConfigV1Alpha1() can succeed with empty defaults and thus prevent the
intended fallback; change _load_config() so after constructing
ClientConfigV1Alpha1 (and catching ValidationError) you validate that required
fields (e.g. endpoint/token or whatever fields ClientConfigV1Alpha1 requires)
are present and non-empty, and if they are missing then load the fallback using
UserConfigV1Alpha1.load_or_create().config.current_client; reference
ClientConfigV1Alpha1, ValidationError, and
UserConfigV1Alpha1.load_or_create().config.current_client to locate where to add
the presence check and fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py`:
- Around line 243-257: The current logic prevents rebinding a log callback
(check of manager._log_callback) causing the one-shot _log closure to capture a
stale session; remove the early return and instead register a stable callback
via manager.set_log_callback that, on each call, calls mcp.get_context() to
fetch the current request_context/session and then forwards the message via
session.send_log_message (handling exceptions), so logs always go to the active
session; update references around manager.set_log_callback,
manager._log_callback, the _log closure and use
mcp.get_context()/request_context.session for each invocation.

---

Duplicate comments:
In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py`:
- Around line 429-439: The file-based logging setup using Path.home(),
log_dir.mkdir(...), and logging.FileHandler(log_file) can raise exceptions in
restricted environments and abort startup; wrap the entire block that creates
log_dir, creates log_file, instantiates file_handler and calls
root.addHandler(file_handler) in a broad try/except that logs a warning to
stderr (or the existing console/stdio logger) and silently continues on failure
so startup does not abort; ensure variables referenced are log_dir, log_file,
file_handler and root, and do not remove or change the existing stdio/console
handlers—only add the file handler when the operations succeed.
- Around line 91-99: ClientConfigV1Alpha1() can succeed with empty defaults and
thus prevent the intended fallback; change _load_config() so after constructing
ClientConfigV1Alpha1 (and catching ValidationError) you validate that required
fields (e.g. endpoint/token or whatever fields ClientConfigV1Alpha1 requires)
are present and non-empty, and if they are missing then load the fallback using
UserConfigV1Alpha1.load_or_create().config.current_client; reference
ClientConfigV1Alpha1, ValidationError, and
UserConfigV1Alpha1.load_or_create().config.current_client to locate where to add
the presence check and fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ffa8803-692e-4624-aa2b-baac829b18ed

📥 Commits

Reviewing files that changed from the base of the PR and between 4a50f7b and e943f1a.

📒 Files selected for processing (1)
  • python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py

Comment on lines +243 to +257
if manager._log_callback is not None:
return
try:
ctx = mcp.get_context()
if ctx.request_context:
session = ctx.request_context.session

async def _log(level: str, message: str) -> None:
try:
await session.send_log_message(level=level, data=message, logger="jumpstarter")
except Exception:
pass

manager.set_log_callback(_log)
except (LookupError, AttributeError):
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.

⚠️ Potential issue | 🟠 Major

Notification callback capture is one-shot and can go stale.

Line 243 prevents rebinding once a callback exists, but this manager stores only one callback and replaces it on set_log_callback(). That means later sessions may never receive logs.

Proposed fix
 def _capture_session_for_notifications(mcp: FastMCP, manager: ConnectionManager) -> None:
     """Try to capture the MCP session so background tasks can send log notifications."""
-    if manager._log_callback is not None:
-        return
     try:
         ctx = mcp.get_context()
         if ctx.request_context:
             session = ctx.request_context.session
@@
             manager.set_log_callback(_log)
     except (LookupError, AttributeError):
         pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-mcp/jumpstarter_mcp/server.py` around lines 243 -
257, The current logic prevents rebinding a log callback (check of
manager._log_callback) causing the one-shot _log closure to capture a stale
session; remove the early return and instead register a stable callback via
manager.set_log_callback that, on each call, calls mcp.get_context() to fetch
the current request_context/session and then forwards the message via
session.send_log_message (handling exceptions), so logs always go to the active
session; update references around manager.set_log_callback,
manager._log_callback, the _log closure and use
mcp.get_context()/request_context.session for each invocation.

@mangelajo mangelajo merged commit feef832 into main Mar 24, 2026
31 checks passed
@raballew raballew deleted the jmp-mcp branch June 5, 2026 11:37
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