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.
- cellpy_mcp-0.1.0/.gitignore +9 -0
- cellpy_mcp-0.1.0/LICENSE +9 -0
- cellpy_mcp-0.1.0/PKG-INFO +159 -0
- cellpy_mcp-0.1.0/README.md +131 -0
- cellpy_mcp-0.1.0/pyproject.toml +65 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/__init__.py +83 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/__main__.py +61 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/api.py +406 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/cells.py +325 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/clients.py +108 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/errors.py +30 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/projects.py +117 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/prompts.py +70 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/sandbox.py +162 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/server.py +56 -0
- cellpy_mcp-0.1.0/src/cellpy_mcp/state.py +61 -0
- cellpy_mcp-0.1.0/tests/conftest.py +79 -0
- cellpy_mcp-0.1.0/tests/test_api_tools.py +199 -0
- cellpy_mcp-0.1.0/tests/test_cell_tools.py +129 -0
- cellpy_mcp-0.1.0/tests/test_clients.py +99 -0
- cellpy_mcp-0.1.0/tests/test_contract.py +65 -0
- cellpy_mcp-0.1.0/tests/test_project_tools.py +77 -0
- cellpy_mcp-0.1.0/tests/test_sandbox.py +152 -0
cellpy_mcp-0.1.0/LICENSE
ADDED
|
@@ -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())
|