cellpy-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ uv.lock
@@ -0,0 +1,9 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2026, Jan Petter Maehlen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: cellpy-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for cellpy — battery cell data, plots and the cellpy API, for agents and chat clients.
5
+ Project-URL: Homepage, https://github.com/cellpy/cellpy-mcp
6
+ Project-URL: Repository, https://github.com/cellpy/cellpy-mcp
7
+ Project-URL: Issues, https://github.com/cellpy/cellpy-mcp/issues
8
+ Project-URL: cellpy, https://github.com/jepegit/cellpy
9
+ Author: cellpy
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,battery,cellpy,electrochemistry,llm,mcp
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: cellpy>=2.1.3
22
+ Requires-Dist: mcp>=2.0
23
+ Requires-Dist: plotly>=5.22
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Requires-Dist: tables; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # cellpy-mcp
30
+
31
+ An [MCP](https://modelcontextprotocol.io) server for
32
+ [cellpy](https://github.com/jepegit/cellpy). It lets an agent — or a chat
33
+ window — load battery cells, collect them into frames, render figures, export
34
+ data, look up how any cellpy call works, and set up a batch project. Without
35
+ writing any Python.
36
+
37
+ ```bash
38
+ pip install cellpy-mcp
39
+ cellpy mcp install # register it with your chat client
40
+ ```
41
+
42
+ Then restart the client and ask it to plot something.
43
+
44
+ ## Who it is for
45
+
46
+ - **People building on cellpy** — a GUI, a script that automates cell handling.
47
+ The tools are the useful API, and `describe_api` gives real signatures from
48
+ the version you have installed.
49
+ - **People who just want to ask.** Open a chat window, point it at a file, ask
50
+ for capacity versus cycle. The prompts below are pickable starting points, so
51
+ you do not have to know what to ask for.
52
+ - **People who would rather not use a terminal.** `new_project` is `cellpy new`
53
+ without the command line.
54
+
55
+ Nothing is hosted. Under stdio your chat client starts the server itself, on
56
+ your machine, reading your files — there is no service to sign up for and
57
+ nothing leaves the machine except what you paste into the chat.
58
+
59
+ ## What it can do
60
+
61
+ Cells and figures:
62
+
63
+ | Tool | What it gives you |
64
+ |---|---|
65
+ | `list_instruments` | loaders, and whether each can actually run on this machine |
66
+ | `load_cell` | a handle, cycle count, mass, summary column names |
67
+ | `list_cells` | what is loaded |
68
+ | `describe_plot_families` | the 20 summary families, marked available or missing-columns |
69
+ | `collect` | a handle, row count, columns, `is_grouped`, direction counts |
70
+ | `preview_collection` | a few rows, capped at 20 |
71
+ | `render` | writes a figure; returns trace types and points plotted |
72
+ | `export_collection` | writes csv/parquet/json; returns rows and bytes |
73
+
74
+ The cellpy API — "how does this call work, and what are its arguments":
75
+
76
+ | Tool | What it gives you |
77
+ |---|---|
78
+ | `search_api` | calls matching a name or a docstring line |
79
+ | `describe_api` | signature, argument types and defaults, docstring, `undocumented_parameters`, optionally source |
80
+
81
+ Batch templating:
82
+
83
+ | Tool | What it gives you |
84
+ |---|---|
85
+ | `list_templates` | registered and local templates, and which is the default |
86
+ | `new_project` | a project from a template; returns the notebooks it made |
87
+
88
+ Prompts: `analyse_cell`, `start_batch_project`, `explain_call`.
89
+
90
+ ## Where it may read and write
91
+
92
+ Everything is confined to a set of roots, and both reads and writes are checked
93
+ — an unchecked write would make the read check decoration.
94
+
95
+ By default the roots are the directories cellpy already knows about
96
+ (`rawdatadir`, `cellpydatadir`, `outdatadir`, `notebookdir`), because you told
97
+ cellpy where your data is when you set it up. Override with `CELLPY_MCP_ROOT`,
98
+ which takes several directories separated the way `PATH` separates them:
99
+
100
+ ```bash
101
+ CELLPY_MCP_ROOT=/data/cells:/data/out cellpy mcp serve
102
+ ```
103
+
104
+ Roots that are not plain local directories are dropped: `rawdatadir` is often
105
+ `scp://host/…`, and containment here is `pathlib`-based and cannot express
106
+ "inside that remote share". If cellpy has no configured paths at all, the
107
+ single root is `~/cellpy_mcp` — never your whole filesystem.
108
+
109
+ ## Four things it does on purpose
110
+
111
+ **Handles, not data.** Only `preview_collection` returns rows. A tool result
112
+ goes into a model's context window, and one collected summary is ~29 kB of CSV
113
+ while a raw figure can be several MB.
114
+
115
+ **Results carry the traps.** `render` returns `trace_types` alongside
116
+ `points_plotted` and `rows_collected`, so an agent that asked for a density film
117
+ can see it got `histogram2d` rather than lines, and one that forgot
118
+ `direction="both"` can see it plotted 891 of 2328 rows. `describe_api` returns
119
+ `undocumented_parameters` for the same reason: cellpy documents about half its
120
+ arguments, and a model should know when the package never said.
121
+
122
+ **It follows the docstring's own cross-references.** `CellpyCell.get_cap` takes
123
+ 23 arguments, documents none, and points at
124
+ `cellpy.readers.capacity_curves.get_cap` — which documents 22 of 24. Following
125
+ that takes argument coverage across the documented API from 51% to 72%. Only a
126
+ docs site resolves those markers; nobody reading a docstring does.
127
+
128
+ **One client per process.** State is process-wide, deliberately: the MCP SDK
129
+ does not give a tool a stable session identity, and under stdio each client
130
+ spawns its own process anyway. Do not put this behind a shared HTTP endpoint as
131
+ written.
132
+
133
+ ## Limits worth knowing
134
+
135
+ - `load_cell` blocks, with no progress and no cancellation, so a slow load can
136
+ look like a hang to a client. This is the gap most worth closing.
137
+ - No quota and no eviction: an agent can fill the sandbox with figures, and
138
+ cells stay in memory until the process exits.
139
+ - `new_project` has to create the project directory itself, because `cellpy new`
140
+ prompts even with `no_input=True`
141
+ ([cellpy#990](https://github.com/jepegit/cellpy/issues/990)). It also
142
+ downloads a cookiecutter from GitHub on first use.
143
+
144
+ ## Running it without cellpy's shim
145
+
146
+ `cellpy mcp serve` needs cellpy 2.2 or newer. Otherwise:
147
+
148
+ ```bash
149
+ python -m cellpy_mcp serve
150
+ python -m cellpy_mcp install --dry-run
151
+ python -m cellpy_mcp status
152
+ ```
153
+
154
+ ## Background
155
+
156
+ The design, the measurements behind it, and a two-round prototype log are in
157
+ [cellpy#840](https://github.com/jepegit/cellpy/issues/840).
158
+
159
+ MIT licensed.
@@ -0,0 +1,131 @@
1
+ # cellpy-mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server for
4
+ [cellpy](https://github.com/jepegit/cellpy). It lets an agent — or a chat
5
+ window — load battery cells, collect them into frames, render figures, export
6
+ data, look up how any cellpy call works, and set up a batch project. Without
7
+ writing any Python.
8
+
9
+ ```bash
10
+ pip install cellpy-mcp
11
+ cellpy mcp install # register it with your chat client
12
+ ```
13
+
14
+ Then restart the client and ask it to plot something.
15
+
16
+ ## Who it is for
17
+
18
+ - **People building on cellpy** — a GUI, a script that automates cell handling.
19
+ The tools are the useful API, and `describe_api` gives real signatures from
20
+ the version you have installed.
21
+ - **People who just want to ask.** Open a chat window, point it at a file, ask
22
+ for capacity versus cycle. The prompts below are pickable starting points, so
23
+ you do not have to know what to ask for.
24
+ - **People who would rather not use a terminal.** `new_project` is `cellpy new`
25
+ without the command line.
26
+
27
+ Nothing is hosted. Under stdio your chat client starts the server itself, on
28
+ your machine, reading your files — there is no service to sign up for and
29
+ nothing leaves the machine except what you paste into the chat.
30
+
31
+ ## What it can do
32
+
33
+ Cells and figures:
34
+
35
+ | Tool | What it gives you |
36
+ |---|---|
37
+ | `list_instruments` | loaders, and whether each can actually run on this machine |
38
+ | `load_cell` | a handle, cycle count, mass, summary column names |
39
+ | `list_cells` | what is loaded |
40
+ | `describe_plot_families` | the 20 summary families, marked available or missing-columns |
41
+ | `collect` | a handle, row count, columns, `is_grouped`, direction counts |
42
+ | `preview_collection` | a few rows, capped at 20 |
43
+ | `render` | writes a figure; returns trace types and points plotted |
44
+ | `export_collection` | writes csv/parquet/json; returns rows and bytes |
45
+
46
+ The cellpy API — "how does this call work, and what are its arguments":
47
+
48
+ | Tool | What it gives you |
49
+ |---|---|
50
+ | `search_api` | calls matching a name or a docstring line |
51
+ | `describe_api` | signature, argument types and defaults, docstring, `undocumented_parameters`, optionally source |
52
+
53
+ Batch templating:
54
+
55
+ | Tool | What it gives you |
56
+ |---|---|
57
+ | `list_templates` | registered and local templates, and which is the default |
58
+ | `new_project` | a project from a template; returns the notebooks it made |
59
+
60
+ Prompts: `analyse_cell`, `start_batch_project`, `explain_call`.
61
+
62
+ ## Where it may read and write
63
+
64
+ Everything is confined to a set of roots, and both reads and writes are checked
65
+ — an unchecked write would make the read check decoration.
66
+
67
+ By default the roots are the directories cellpy already knows about
68
+ (`rawdatadir`, `cellpydatadir`, `outdatadir`, `notebookdir`), because you told
69
+ cellpy where your data is when you set it up. Override with `CELLPY_MCP_ROOT`,
70
+ which takes several directories separated the way `PATH` separates them:
71
+
72
+ ```bash
73
+ CELLPY_MCP_ROOT=/data/cells:/data/out cellpy mcp serve
74
+ ```
75
+
76
+ Roots that are not plain local directories are dropped: `rawdatadir` is often
77
+ `scp://host/…`, and containment here is `pathlib`-based and cannot express
78
+ "inside that remote share". If cellpy has no configured paths at all, the
79
+ single root is `~/cellpy_mcp` — never your whole filesystem.
80
+
81
+ ## Four things it does on purpose
82
+
83
+ **Handles, not data.** Only `preview_collection` returns rows. A tool result
84
+ goes into a model's context window, and one collected summary is ~29 kB of CSV
85
+ while a raw figure can be several MB.
86
+
87
+ **Results carry the traps.** `render` returns `trace_types` alongside
88
+ `points_plotted` and `rows_collected`, so an agent that asked for a density film
89
+ can see it got `histogram2d` rather than lines, and one that forgot
90
+ `direction="both"` can see it plotted 891 of 2328 rows. `describe_api` returns
91
+ `undocumented_parameters` for the same reason: cellpy documents about half its
92
+ arguments, and a model should know when the package never said.
93
+
94
+ **It follows the docstring's own cross-references.** `CellpyCell.get_cap` takes
95
+ 23 arguments, documents none, and points at
96
+ `cellpy.readers.capacity_curves.get_cap` — which documents 22 of 24. Following
97
+ that takes argument coverage across the documented API from 51% to 72%. Only a
98
+ docs site resolves those markers; nobody reading a docstring does.
99
+
100
+ **One client per process.** State is process-wide, deliberately: the MCP SDK
101
+ does not give a tool a stable session identity, and under stdio each client
102
+ spawns its own process anyway. Do not put this behind a shared HTTP endpoint as
103
+ written.
104
+
105
+ ## Limits worth knowing
106
+
107
+ - `load_cell` blocks, with no progress and no cancellation, so a slow load can
108
+ look like a hang to a client. This is the gap most worth closing.
109
+ - No quota and no eviction: an agent can fill the sandbox with figures, and
110
+ cells stay in memory until the process exits.
111
+ - `new_project` has to create the project directory itself, because `cellpy new`
112
+ prompts even with `no_input=True`
113
+ ([cellpy#990](https://github.com/jepegit/cellpy/issues/990)). It also
114
+ downloads a cookiecutter from GitHub on first use.
115
+
116
+ ## Running it without cellpy's shim
117
+
118
+ `cellpy mcp serve` needs cellpy 2.2 or newer. Otherwise:
119
+
120
+ ```bash
121
+ python -m cellpy_mcp serve
122
+ python -m cellpy_mcp install --dry-run
123
+ python -m cellpy_mcp status
124
+ ```
125
+
126
+ ## Background
127
+
128
+ The design, the measurements behind it, and a two-round prototype log are in
129
+ [cellpy#840](https://github.com/jepegit/cellpy/issues/840).
130
+
131
+ MIT licensed.
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "cellpy-mcp"
3
+ # Single-sourced from the package, so a release cannot ship a wheel whose
4
+ # metadata and __version__ disagree — see [tool.hatch.version] below.
5
+ dynamic = ["version"]
6
+ description = "MCP server for cellpy — battery cell data, plots and the cellpy API, for agents and chat clients."
7
+ readme = "README.md"
8
+ license = "MIT"
9
+ license-files = ["LICENSE"]
10
+ # cellpy itself requires >= 3.13, so this cannot be lower however much the MCP
11
+ # SDK would allow (it takes 3.10).
12
+ requires-python = ">=3.13"
13
+ authors = [{ name = "cellpy" }]
14
+ keywords = ["battery", "electrochemistry", "cellpy", "mcp", "agents", "llm"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Science/Research",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Scientific/Engineering",
22
+ "Topic :: Scientific/Engineering :: Chemistry",
23
+ ]
24
+
25
+ dependencies = [
26
+ "cellpy>=2.1.3",
27
+ "mcp>=2.0",
28
+ "plotly>=5.22",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/cellpy/cellpy-mcp"
33
+ Repository = "https://github.com/cellpy/cellpy-mcp"
34
+ Issues = "https://github.com/cellpy/cellpy-mcp/issues"
35
+ cellpy = "https://github.com/jepegit/cellpy"
36
+
37
+ [project.scripts]
38
+ # `cellpy mcp serve` is the discoverable spelling and goes through cellpy's
39
+ # shim. This is the same thing without it — useful in a client config, where
40
+ # naming the executable directly is one less layer to get wrong.
41
+ cellpy-mcp = "cellpy_mcp.__main__:main"
42
+
43
+ [project.optional-dependencies]
44
+ dev = ["pytest>=8", "tables"]
45
+
46
+ [build-system]
47
+ requires = ["hatchling"]
48
+ build-backend = "hatchling.build"
49
+
50
+ [tool.hatch.version]
51
+ path = "src/cellpy_mcp/__init__.py"
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/cellpy_mcp"]
55
+
56
+ [tool.hatch.build.targets.sdist]
57
+ include = ["src/", "tests/", "README.md", "LICENSE"]
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
61
+ addopts = "-q"
62
+ markers = [
63
+ "essential: critical-path tests run in GitHub Actions CI",
64
+ "network: needs the network (a cookiecutter clone); deselected in CI",
65
+ ]
@@ -0,0 +1,83 @@
1
+ """An MCP server for cellpy — battery cell data, plots, and the cellpy API.
2
+
3
+ Four things a caller can do without writing Python: load cells and collect them
4
+ into frames; render figures and export data; ask what any cellpy call takes and
5
+ what its arguments mean; and set up a batch project from a template.
6
+
7
+ **This module is the contract `cellpy mcp` depends on.** cellpy ships a thin
8
+ command group (`cellpy mcp serve | install | status`) that imports this package
9
+ and, when it is absent, says how to install it. cellpy deliberately does not
10
+ depend on the MCP SDK — it is young and moving, and a long-lived network-facing
11
+ process is a security surface a data library should not carry — so the four
12
+ names below are load-bearing across a repository boundary. Changing them
13
+ without changing cellpy breaks the command:
14
+
15
+ __version__
16
+ serve(root=None)
17
+ install(root=None, client=None, dry_run=False) -> str
18
+ describe() -> dict
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from pathlib import Path
24
+
25
+ __version__ = "0.1.0"
26
+
27
+ __all__ = ["__version__", "serve", "install", "describe", "build_server", "Sandbox"]
28
+
29
+
30
+ def build_server(sandbox=None, state=None):
31
+ """An `MCPServer` with every tool and prompt registered."""
32
+ from .server import build_server as _build
33
+
34
+ return _build(sandbox=sandbox, state=state)
35
+
36
+
37
+ def serve(root: str | Path | None = None) -> None:
38
+ """Run the server over stdio. Blocks until the client disconnects.
39
+
40
+ Prints nothing: stdout *is* the protocol channel, and a friendly banner on
41
+ it is a parse error at the other end.
42
+ """
43
+ from .sandbox import Sandbox
44
+
45
+ build_server(Sandbox.from_environment(root)).run(transport="stdio")
46
+
47
+
48
+ def install(
49
+ root: str | Path | None = None,
50
+ client: str | None = None,
51
+ dry_run: bool = False,
52
+ ) -> str:
53
+ """Register this server with a chat client; return the path written."""
54
+ from .clients import install as _install
55
+ from .sandbox import Sandbox
56
+
57
+ return _install(Sandbox.from_environment(root).roots, client=client, dry_run=dry_run)
58
+
59
+
60
+ def describe() -> dict:
61
+ """What `cellpy mcp status` reports beyond the two version numbers."""
62
+ from .clients import config_path
63
+ from .sandbox import Sandbox
64
+
65
+ sandbox = Sandbox.from_environment()
66
+ described = {"roots": ", ".join(str(root) for root in sandbox.roots)}
67
+ try:
68
+ target = config_path()
69
+ except ValueError: # pragma: no cover - only if CLIENTS shrinks
70
+ return described
71
+ described["client config"] = f"{target}{'' if target.exists() else ' (not present)'}"
72
+ return described
73
+
74
+
75
+ def __getattr__(name: str):
76
+ # `Sandbox` is re-exported for callers who want to build a server with an
77
+ # explicit set of roots, but importing it eagerly would drag cellpy's
78
+ # config in just because someone imported the package.
79
+ if name == "Sandbox":
80
+ from .sandbox import Sandbox
81
+
82
+ return Sandbox
83
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,61 @@
1
+ """`python -m cellpy_mcp` — what a chat client spawns.
2
+
3
+ `cellpy mcp serve` is the discoverable spelling and goes through cellpy's shim.
4
+ This is the same thing without it, and it is what `install` writes into a client
5
+ config: naming the interpreter and the module directly is one fewer layer to be
6
+ wrong about when a GUI launches it from nowhere in particular.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+
14
+
15
+ def main(argv: list[str] | None = None) -> int:
16
+ parser = argparse.ArgumentParser(
17
+ prog="cellpy-mcp", description="MCP server for cellpy."
18
+ )
19
+ subcommands = parser.add_subparsers(dest="command")
20
+
21
+ serve = subcommands.add_parser("serve", help="run over stdio (the default)")
22
+ serve.add_argument("--root", help="a directory the server may read and write")
23
+
24
+ install = subcommands.add_parser("install", help="register with a chat client")
25
+ install.add_argument("--root", help="a directory the server may read and write")
26
+ install.add_argument("--client", help="which chat client to register with")
27
+ install.add_argument(
28
+ "--dry-run", action="store_true", help="print the target instead of writing"
29
+ )
30
+
31
+ subcommands.add_parser("status", help="report the roots and the client config")
32
+
33
+ args = parser.parse_args(argv)
34
+
35
+ from . import describe, install as do_install, serve as do_serve
36
+
37
+ # No subcommand means serve: a client config that says `-m cellpy_mcp` and
38
+ # nothing else must start a server, not print usage to the protocol channel.
39
+ if args.command in (None, "serve"):
40
+ do_serve(root=getattr(args, "root", None))
41
+ return 0
42
+
43
+ if args.command == "install":
44
+ try:
45
+ target = do_install(root=args.root, client=args.client, dry_run=args.dry_run)
46
+ except ValueError as exc:
47
+ print(exc, file=sys.stderr)
48
+ return 1
49
+ verb = "would register" if args.dry_run else "registered"
50
+ print(f"{verb} 'cellpy' in {target}")
51
+ if not args.dry_run:
52
+ print("restart the client to pick it up.")
53
+ return 0
54
+
55
+ for key, value in describe().items():
56
+ print(f"{key}: {value}")
57
+ return 0
58
+
59
+
60
+ if __name__ == "__main__":
61
+ raise SystemExit(main())