Skip to main content
Glama
juanqui

joulescope-mcp

by juanqui
README.md
# JouleScope JS220 MCP Server

`joulescope-mcp` is a Model Context Protocol (MCP) server for the JouleScope JS220 precision energy analyzer. It exposes agent-friendly tools for measuring current, voltage, power, charge, and energy, plus lower-level access to the JouleScope driver PubSub topic tree.

The primary tool is `measure_energy`: provide a duration and accumulation interval, and it returns total charge and energy plus one sample per interval. For example, `duration_s=15` and `interval_s=0.5` returns 30 interval samples along with totals such as `total_charge_mAh`.

## Quick Start

Install directly from GitHub with `uvx`:

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/juanqui/joulescope-mcp",
        "joulescope-mcp"
      ]
    }
  }
}
```

If you prefer SSH, use the same Git install shape with the SSH URL:

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "command": "uvx",
      "args": [
        "--from",
        "git+ssh://git@github.com/juanqui/joulescope-mcp.git",
        "joulescope-mcp"
      ]
    }
  }
}
```

Then ask your MCP client:

```text
Measure JouleScope power for 15 seconds with 500 ms intervals. Include voltage.
```

Expected result shape, with compact sample arrays shortened for display:

```json
{
  "total_charge_mAh": 0.0051,
  "total_energy_mWh": 0.019,
  "average_current_mA": 1.23,
  "average_voltage_v": 3.70,
  "interval_count": 30,
  "sample_charge_mAh": [0.00015, 0.00015, 0.00015]
}
```

## Requirements

- Python 3.11 or newer
- JouleScope JS220 connected over USB
- [`uv`](https://docs.astral.sh/uv/getting-started/installation/) for the recommended `uvx` install path
- An MCP client that can run stdio servers

The Python package installs `pyjoulescope_driver>=2.1.0` and `pyjls>=0.17`. On Linux, configure JouleScope udev rules as documented by JouleScope before running the server.

`uvx` is an alias for `uv tool run`; it runs Python command-line tools in an isolated environment without a permanent install.

## Install Options

### Option 1: GitHub with `uvx`

```bash
uvx --from git+https://github.com/juanqui/joulescope-mcp joulescope-mcp
```

MCP JSON:

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/juanqui/joulescope-mcp",
        "joulescope-mcp"
      ]
    }
  }
}
```

Use this for normal installs.

### Option 2: GitHub over SSH with `uvx`

Use this if you prefer SSH or need GitHub SSH authentication:

```bash
uvx --from git+ssh://git@github.com/juanqui/joulescope-mcp.git joulescope-mcp
```

### Option 3: Local Checkout

Use this while developing or when you want to pin the MCP server to a local clone:

```bash
git clone git@github.com:juanqui/joulescope-mcp.git
cd joulescope-mcp
uv sync --extra dev
uv run joulescope-mcp
```

Verify that the driver can see the JS220:

```bash
uv run python -m pyjoulescope_driver scan
uv run python -m pyjoulescope_driver statistics --frequency 2 --duration 1
```

Local checkout MCP JSON:

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/joulescope-mcp",
        "run",
        "joulescope-mcp"
      ]
    }
  }
}
```

Use an absolute path. Several MCP clients launch servers with a limited `PATH`; if `uv` or `uvx` is not found, replace `"command": "uvx"` or `"command": "uv"` with the full executable path from `which uvx` or `which uv`.

## Client Configuration

Most MCP clients use one of two JSON shapes:

- `mcpServers`: Claude Desktop, Claude Code project config, Cursor, Windsurf, Cline, and many other clients
- `servers`: VS Code / GitHub Copilot MCP config

Choose one install command:

| Current situation | Use this command in client configs |
| --- | --- |
| Normal GitHub install | `uvx --from git+https://github.com/juanqui/joulescope-mcp joulescope-mcp` |
| GitHub over SSH | `uvx --from git+ssh://git@github.com/juanqui/joulescope-mcp.git joulescope-mcp` |
| Local development checkout | `uv --directory /absolute/path/to/joulescope-mcp run joulescope-mcp` |

The snippets below show the normal GitHub install path.

GitHub replacement:

```json
{
  "command": "uvx",
  "args": [
    "--from",
    "git+https://github.com/juanqui/joulescope-mcp",
    "joulescope-mcp"
  ]
}
```

Local checkout replacement:

```json
{
  "command": "uv",
  "args": [
    "--directory",
    "/absolute/path/to/joulescope-mcp",
    "run",
    "joulescope-mcp"
  ]
}
```

Configure only one always-on client for a physical JS220. Many desktop clients auto-start configured MCP servers, and the JS220 should not be shared between multiple MCP server processes.

## Timeout Configuration

`measure_energy` is a blocking hardware measurement. A 15 second measurement takes at least 15 seconds, plus JS220 startup and cleanup time. Configure MCP clients for a 5 minute tool timeout when they expose a timeout setting.

Recommended values:

- Tool call timeout: 300 seconds / 300,000 ms
- Server startup timeout: 60 seconds / 60,000 ms

If a client does not document a timeout setting, keep synchronous measurements short enough for that client or use a client with configurable MCP tool timeouts. A future async measurement API can avoid long single tool calls, but the current `measure_energy` call is synchronous by design.

### Claude Code

Recommended global install:

```bash
claude mcp add joulescope-js220 -- uvx --from git+https://github.com/juanqui/joulescope-mcp joulescope-mcp
claude mcp list
```

Launch Claude Code with 5 minute MCP tool calls and a 60 second server startup timeout:

```bash
MCP_TIMEOUT=60000 MCP_TOOL_TIMEOUT=300000 claude
```

Project `.mcp.json`:

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "type": "stdio",
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/juanqui/joulescope-mcp",
        "joulescope-mcp"
      ]
    }
  }
}
```

Inside Claude Code, run `/mcp` to inspect server status. `MCP_TIMEOUT` and `MCP_TOOL_TIMEOUT` are Claude Code process environment variables, not per-server `env` values.

### Claude Desktop

Edit:

- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/juanqui/joulescope-mcp",
        "joulescope-mcp"
      ]
    }
  }
}
```

Restart Claude Desktop after editing the file. Claude Desktop does not document a portable per-server timeout field in `claude_desktop_config.json`; if your launch environment supports MCP timeout environment variables, set `MCP_TOOL_TIMEOUT=300000` before starting Claude Desktop.

### Codex

Recommended global install:

```bash
codex mcp add joulescope-js220 -- uvx --from git+https://github.com/juanqui/joulescope-mcp joulescope-mcp
codex mcp list
```

Direct `~/.codex/config.toml` entry:

```toml
[mcp_servers.joulescope-js220]
command = "uvx"
args = ["--from", "git+https://github.com/juanqui/joulescope-mcp", "joulescope-mcp"]
startup_timeout_sec = 60
tool_timeout_sec = 300
```

The `codex mcp add` command creates the server entry. Edit `~/.codex/config.toml` afterward to add `startup_timeout_sec` and `tool_timeout_sec`.

### Cursor

Global config:

- macOS/Linux: `~/.cursor/mcp.json`
- Windows: `%USERPROFILE%\.cursor\mcp.json`

Project config:

- `.cursor/mcp.json`

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "type": "stdio",
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/juanqui/joulescope-mcp",
        "joulescope-mcp"
      ]
    }
  }
}
```

Cursor also supports adding MCP servers from Settings. After changing the config, restart Cursor or refresh MCP tools from the MCP settings panel. Cursor does not currently document a portable `mcp.json` timeout field; if your Cursor build exposes a request/tool timeout in Settings, set it to 300 seconds.

### VS Code / GitHub Copilot Agent Mode

Workspace config:

- `.vscode/mcp.json`

User config:

- Run `MCP: Open User Configuration` from the Command Palette.

```json
{
  "servers": {
    "joulescope-js220": {
      "type": "stdio",
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/juanqui/joulescope-mcp",
        "joulescope-mcp"
      ]
    }
  }
}
```

Command-line install:

```bash
code --add-mcp '{"name":"joulescope-js220","type":"stdio","command":"uvx","args":["--from","git+https://github.com/juanqui/joulescope-mcp","joulescope-mcp"]}'
```

VS Code's MCP configuration reference does not document a per-server tool timeout field. Do not add unsupported timeout keys to `.vscode/mcp.json`; use shorter synchronous measurements if your Copilot host times out long calls.

### Windsurf

Edit:

- macOS/Linux: `~/.codeium/windsurf/mcp_config.json`
- Windows: `%USERPROFILE%\.codeium\windsurf\mcp_config.json`

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/juanqui/joulescope-mcp",
        "joulescope-mcp"
      ]
    }
  }
}
```

Windsurf Cascade has a total enabled-tool limit. If you use many MCP servers, disable tools you do not need. Windsurf's public MCP docs do not document a portable timeout field in `mcp_config.json`; if your Windsurf build exposes a request/tool timeout setting, set it to 300 seconds.

### Cline

CLI install:

```bash
cline mcp add joulescope-js220 -- uvx --from git+https://github.com/juanqui/joulescope-mcp joulescope-mcp
```

Manual config:

- CLI default: `~/.cline/data/settings/cline_mcp_settings.json`
- VS Code extension: open the MCP Servers panel, then choose **Configure MCP Servers**

```json
{
  "mcpServers": {
    "joulescope-js220": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/juanqui/joulescope-mcp",
        "joulescope-mcp"
      ],
      "timeout": 300,
      "disabled": false,
      "alwaysAllow": []
    }
  }
}
```

## Run as HTTP

Most local MCP clients should use stdio. If you need streamable HTTP:

```bash
uvx --from git+https://github.com/juanqui/joulescope-mcp joulescope-mcp --transport streamable-http --mount-path /mcp
```

## Troubleshooting

- **Client cannot find `uvx`**: use the absolute path from `which uvx`.
- **No JouleScope found**: run your configured server command directly in a terminal to check startup errors, then run `uvx --from pyjoulescope-driver pyjoulescope_driver scan` to verify the official driver can see the JS220.
- **Permission denied on Linux**: install JouleScope udev rules and reconnect the JS220.
- **Multiple clients fight over the device**: run only one MCP client/server process against a JS220 at a time.
- **GitHub install fails**: verify `git clone https://github.com/juanqui/joulescope-mcp` works first.
- **Tools changed but client still shows old tools**: restart the client or reset/reload MCP tools.

Quick local health check:

```bash
uv run python scripts/hardware_smoke.py --duration-s 2 --interval-s 0.5
```

## Agent Workflow

For firmware or application power optimization, keep the measurement setup stable:

1. Run a baseline measurement with `measure_energy`.
2. Apply one firmware or software change.
3. Run the same `measure_energy` duration and interval again.
4. Compare `total_charge_mAh`, `total_energy_mWh`, `average_current_mA`, and the interval samples.
5. Repeat the measurement when differences are close to normal run-to-run variance.

Example request:

```json
{
  "duration_s": 15,
  "interval_s": 0.5,
  "compact": true
}
```

The response includes:

- `total_charge_mAh`: total charge over the measurement window
- `total_energy_mWh`: total energy over the measurement window
- `average_current_mA`: average current over the actual captured duration
- `average_power_mW`: average power over the actual captured duration
- `actual_interval_s`: average actual interval captured by the JS220
- `sample_charge_mAh`: compact per-interval charge list when `compact=true`
- `sample_energy_mWh`: compact per-interval energy list when `compact=true`
- `sample_voltage_avg_v`, `sample_voltage_min_v`, `sample_voltage_max_v`: compact per-interval voltage lists when `compact=true` and `include_voltage=true`
- `samples`: full per-interval statistics when `compact=false`

If `duration_s` is not an exact multiple of `interval_s`, the server rounds up to the next full interval and reports both `requested_duration_s` and `actual_duration_s`.

## Tools

### `list_devices`

Lists connected JouleScope devices. For JS220 devices, it attempts to include hardware, firmware, and FPGA versions.

### `device_info`

Returns retained driver topic values for a selected device. Set `include_metadata=true` to include topic metadata returned by the driver.

### `measure_energy`

Measures charge and energy using JS220 sensor-side statistics.

Parameters:

- `duration_s`: requested measurement duration in seconds
- `interval_s`: accumulation interval in seconds
- `device_path`: optional explicit device path, such as `u/js220/005920`
- `configure_auto_range`: defaults to true; configures current and voltage range modes to `auto`
- `compact`: returns compact charge and energy arrays and omits full samples
- `include_voltage`: when used with `compact`, also returns per-interval voltage arrays

Implementation detail: the JS220 publishes per-interval `current.integral` in coulombs and `power.integral` in joules. The server sums those integrals, then also converts charge to mAh and energy to mWh.

### `capture_statistics`

Frequency-based wrapper around `measure_energy`. Use when you want `frequency_hz` instead of `interval_s`.

### `configure_frontend`

Sets current and voltage range modes and optional range selections. Use `auto` for normal measurements.

### `target_power_status`

Reports whether the JS220 target/DUT power path is connected. For JS220, DUT power is controlled through `s/i/range/mode`: `off` disconnects Current+ from Current-, while `auto` or `manual` connects the target path for measurement.

### `set_target_power`

Connects or disconnects power to the DUT through the JS220 current path.

Parameters:

- `power_on`: true to connect target power, false to disconnect it
- `on_mode`: `auto` by default, or `manual`
- `settle_ms`: optional wait after changing state

### `cycle_target_power`

Power-cycles the DUT by setting target power off, waiting, then restoring target power.

Parameters:

- `off_ms`: hold-off time in milliseconds
- `on_mode`: `auto` by default, or `manual`
- `settle_ms`: optional wait after restoring power

### `record_jls`

Records raw samples to a JLS v2 file using the JouleScope driver's `Record` API. This is useful for later waveform analysis in the JouleScope UI or JLS tooling. Existing files are rejected unless `overwrite=true`.

### `read_gpi`

Reads JS220 general-purpose input state and returns a 32-bit value plus decoded pins.

### `list_topics`

Lists retained driver topics, values, and optional metadata. This is the discovery tool for advanced JS220 capabilities.

### `query_topic`

Queries one driver topic. Relative topics are resolved under the selected device, so `c/fw/version` becomes `u/js220/<serial>/c/fw/version`.

### `publish_topic`

Publishes a value to a driver topic. This exposes advanced JS220 features and can change device behavior. Prefer typed tools when available.

## Resources and Prompts

Resources:

- `joulescope://devices`: JSON device list
- `joulescope://driver`: server and driver version information

Prompt:

- `power_optimization_session`: template for measurement-driven power optimization loops

## Safety and Limits

The server opens a short-lived JouleScope driver connection per tool call and serializes device access inside one server process. Blocking measurements have guardrails:

- Minimum interval: 0.5 ms
- Maximum duration: 3600 seconds
- Maximum returned intervals: 10,000
- Statistics collection times out if the JS220 does not publish the expected samples

`set_target_power`, `cycle_target_power`, `record_jls`, and `publish_topic` are marked as write/destructive-capable MCP tools. `cycle_target_power` intentionally interrupts the DUT. Agents should use it only when an interruption is part of the requested test.

## Development

Set up a local checkout:

```bash
uv sync --extra dev
```

Run tests:

```bash
uv run python -m pytest
```

Run linting:

```bash
uv run python -m ruff check .
```

Build the package:

```bash
uv run python -m build
```

Hardware smoke test:

```bash
uv run python -m pyjoulescope_driver scan
uv run python -m pyjoulescope_driver statistics --frequency 2 --duration 1
uv run python - <<'PY'
from joulescope_mcp.service import Js220Service
r = Js220Service().measure_energy(duration_s=2, interval_s=0.5)
print(r["total_charge_mAh"], r["average_current_mA"], [s["charge_mAh"] for s in r["samples"]])
PY
```

Repeatable hardware smoke script:

```bash
uv run python scripts/hardware_smoke.py --duration-s 2 --interval-s 0.5
```

## Design

See [docs/design.md](docs/design.md) for the MCP design, measurement semantics, tool rationale, and verification strategy.
See [docs/testing.md](docs/testing.md) for repeatable software, MCP, and hardware checks.
See [docs/adversarial-reviews.md](docs/adversarial-reviews.md) for the implementation and README review logs.

## References

- JouleScope downloads and documentation: <https://www.joulescope.com/pages/downloads>
- JouleScope driver documentation: <https://joulescope-driver.readthedocs.io/>
- JouleScope driver source: <https://github.com/jetperch/joulescope_driver>
- JouleScope `dut_power.py` example: <https://github.com/jetperch/pyjoulescope_examples/blob/main/bin/dut_power.py>
- MCP Python SDK: <https://github.com/modelcontextprotocol/python-sdk>
- `uvx` tool execution: <https://docs.astral.sh/uv/concepts/tools/>

Client configuration references used for the examples above:

| Client | References |
| --- | --- |
| Claude Code | [Claude Code MCP docs](https://code.claude.com/docs/en/mcp), [Claude Code support FAQ](https://support.claude.com/en/articles/14554922-claude-code-user-faq) |
| Claude Desktop | [MCP local server quickstart](https://modelcontextprotocol.io/docs/develop/connect-local-servers), [Anthropic custom connectors note](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp) |
| Codex | [OpenAI Docs MCP Codex quickstart](https://developers.openai.com/learn/docs-mcp), [Codex MCP interface notes](https://github.com/openai/codex/blob/main/codex-rs/docs/codex_mcp_interface.md), plus local verification with `codex mcp add --help` and `codex mcp list` |
| Cursor | [Cursor MCP configuration docs](https://docs.cursor.com/advanced/model-context-protocol), [Cursor CLI MCP docs](https://docs.cursor.com/cli/mcp) |
| VS Code / GitHub Copilot | [Add and manage MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers), [VS Code MCP configuration reference](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration) |
| Windsurf | [Windsurf MCP integration docs](https://docs.windsurf.com/windsurf/cascade/mcp), [Windsurf MCP client guide from Grafana](https://grafana.com/docs/grafana/latest/developer-resources/mcp/clients/windsurf/) |
| Cline | [Cline CLI configuration](https://docs.cline.bot/getting-started/config), [Cline MCP overview](https://docs.cline.bot/mcp/mcp-overview) |

Popular MCP setup examples reviewed:

- GitHub MCP Server installation guides: <https://github.com/github/github-mcp-server/tree/main/docs/installation-guides>
- Context7 MCP installation guide: <https://context7.mintlify.dev/docs/installation>
- MCP local server quickstart: <https://modelcontextprotocol.io/docs/develop/connect-local-servers>
- OpenAI Docs MCP client examples: <https://developers.openai.com/learn/docs-mcp>

## License

Apache License 2.0. See [LICENSE](LICENSE).

TDQS

A3.7/5.0

Scored across 10 tools

Disambiguation5/5

Each tool has a distinct purpose: listing, configuring, measuring, reading GPI, recording, and low-level topic operations. The two measurement tools are clearly differentiated by their descriptions.

Naming Consistency4/5

All tools use snake_case, with most following a verb_noun pattern (e.g., list_devices, measure_energy). The exception is 'device_info', which is noun_noun, but the meaning is clear.

Tool Count5/5

10 tools cover the essential operations for a JouleScope device: discovery, configuration, measurement, data recording, and low-level access. No unnecessary tools.

Completeness5/5

The tool set provides a full lifecycle: listing devices, getting info, measuring energy (two methods), configuring frontend, reading GPI, recording raw data, and arbitrary topic query/publish. No obvious gaps.

Maintenance

ActivityInactive
ResponsivenessUnresponsive