joulescope-mcp
# 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
Scored across 10 tools
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.
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.
10 tools cover the essential operations for a JouleScope device: discovery, configuration, measurement, data recording, and low-level access. No unnecessary tools.
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.