site-calc-investment 1.2.2__py3-none-any.whl → 1.2.3__py3-none-any.whl
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.
- site_calc_investment/__init__.py +1 -1
- site_calc_investment/mcp/config.py +6 -0
- site_calc_investment/mcp/data_loaders.py +70 -0
- site_calc_investment/mcp/server.py +62 -8
- {site_calc_investment-1.2.2.dist-info → site_calc_investment-1.2.3.dist-info}/METADATA +55 -1
- {site_calc_investment-1.2.2.dist-info → site_calc_investment-1.2.3.dist-info}/RECORD +9 -9
- {site_calc_investment-1.2.2.dist-info → site_calc_investment-1.2.3.dist-info}/WHEEL +0 -0
- {site_calc_investment-1.2.2.dist-info → site_calc_investment-1.2.3.dist-info}/entry_points.txt +0 -0
- {site_calc_investment-1.2.2.dist-info → site_calc_investment-1.2.3.dist-info}/licenses/LICENSE +0 -0
site_calc_investment/__init__.py
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_data_dir() -> Optional[str]:
|
|
9
|
+
"""Get the configured data directory from INVESTMENT_DATA_DIR, or None."""
|
|
10
|
+
return os.environ.get("INVESTMENT_DATA_DIR") or None
|
|
5
11
|
|
|
6
12
|
|
|
7
13
|
@dataclass(frozen=True)
|
|
@@ -169,3 +169,73 @@ def _find_first_numeric_column(headers: list[str], file_path: str) -> int:
|
|
|
169
169
|
if hint in h_lower:
|
|
170
170
|
return i
|
|
171
171
|
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _resolve_save_path(file_path: str, data_dir: Optional[str] = None) -> str:
|
|
175
|
+
"""Resolve a file path for saving, applying data_dir for relative paths.
|
|
176
|
+
|
|
177
|
+
:param file_path: Filename or path (relative or absolute).
|
|
178
|
+
:param data_dir: Base directory for relative paths (or None for cwd).
|
|
179
|
+
:returns: Absolute path string.
|
|
180
|
+
:raises ValueError: If the extension is present but not '.csv'.
|
|
181
|
+
"""
|
|
182
|
+
_, ext = os.path.splitext(file_path)
|
|
183
|
+
if ext and ext.lower() != ".csv":
|
|
184
|
+
raise ValueError(f"Only .csv files are supported, got '{ext}'. Use a .csv extension or omit the extension.")
|
|
185
|
+
if not ext:
|
|
186
|
+
file_path = file_path + ".csv"
|
|
187
|
+
|
|
188
|
+
if os.path.isabs(file_path):
|
|
189
|
+
return file_path
|
|
190
|
+
|
|
191
|
+
base = data_dir if data_dir else os.getcwd()
|
|
192
|
+
return os.path.abspath(os.path.join(base, file_path))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def save_csv(
|
|
196
|
+
file_path: str,
|
|
197
|
+
columns: dict[str, list[float]],
|
|
198
|
+
data_dir: Optional[str] = None,
|
|
199
|
+
overwrite: bool = False,
|
|
200
|
+
) -> str:
|
|
201
|
+
"""Save column data as a CSV file.
|
|
202
|
+
|
|
203
|
+
:param file_path: Filename or path. Relative paths resolve against data_dir (or cwd).
|
|
204
|
+
Extension '.csv' is appended if missing.
|
|
205
|
+
:param columns: Named columns of numeric data. All must have the same length.
|
|
206
|
+
:param data_dir: Base directory for relative paths.
|
|
207
|
+
:param overwrite: Allow overwriting an existing file (default: False).
|
|
208
|
+
:returns: Absolute path to the saved file.
|
|
209
|
+
:raises ValueError: If columns are empty, have no rows, or have mismatched lengths.
|
|
210
|
+
:raises FileExistsError: If file exists and overwrite is False.
|
|
211
|
+
"""
|
|
212
|
+
if not columns:
|
|
213
|
+
raise ValueError("columns must not be empty -- provide at least one named column.")
|
|
214
|
+
|
|
215
|
+
lengths = {name: len(vals) for name, vals in columns.items()}
|
|
216
|
+
unique_lengths = set(lengths.values())
|
|
217
|
+
|
|
218
|
+
if unique_lengths == {0}:
|
|
219
|
+
raise ValueError("All columns have 0 rows -- provide at least one row of data.")
|
|
220
|
+
if len(unique_lengths) > 1:
|
|
221
|
+
raise ValueError(f"All columns must have the same length, got: {lengths}")
|
|
222
|
+
|
|
223
|
+
resolved = _resolve_save_path(file_path, data_dir)
|
|
224
|
+
|
|
225
|
+
if not overwrite and os.path.exists(resolved):
|
|
226
|
+
raise FileExistsError(f"File already exists: {resolved}. Set overwrite=True to replace it.")
|
|
227
|
+
|
|
228
|
+
parent = os.path.dirname(resolved)
|
|
229
|
+
if parent:
|
|
230
|
+
os.makedirs(parent, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
col_names = list(columns.keys())
|
|
233
|
+
row_count = len(next(iter(columns.values())))
|
|
234
|
+
|
|
235
|
+
with open(resolved, "w", encoding="utf-8", newline="") as f:
|
|
236
|
+
writer = csv.writer(f)
|
|
237
|
+
writer.writerow(col_names)
|
|
238
|
+
for i in range(row_count):
|
|
239
|
+
writer.writerow([columns[name][i] for name in col_names])
|
|
240
|
+
|
|
241
|
+
return resolved
|
|
@@ -5,14 +5,21 @@ from typing import Any, Literal, Optional, cast
|
|
|
5
5
|
from fastmcp import FastMCP
|
|
6
6
|
|
|
7
7
|
from site_calc_investment.api.client import InvestmentClient
|
|
8
|
-
from site_calc_investment.mcp.config import Config
|
|
8
|
+
from site_calc_investment.mcp.config import Config, get_data_dir
|
|
9
|
+
from site_calc_investment.mcp.data_loaders import save_csv
|
|
9
10
|
from site_calc_investment.mcp.scenario import ScenarioStore
|
|
10
11
|
|
|
11
12
|
mcp = FastMCP(
|
|
12
13
|
"site-calc-investment",
|
|
13
|
-
instructions=
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
instructions=(
|
|
15
|
+
"Investment planning optimization tools for energy systems. "
|
|
16
|
+
"Build scenarios with batteries, CHP, PV, and market connections, "
|
|
17
|
+
"then submit for optimization to find optimal dispatch and ROI.\n\n"
|
|
18
|
+
"IMPORTANT: Use save_data_file to write generated data (price arrays, "
|
|
19
|
+
"demand profiles) to the local filesystem BEFORE referencing them in "
|
|
20
|
+
"add_device. This MCP server runs locally and has filesystem access -- "
|
|
21
|
+
"you do not need to ask the user to save files manually."
|
|
22
|
+
),
|
|
16
23
|
)
|
|
17
24
|
|
|
18
25
|
_store = ScenarioStore()
|
|
@@ -360,6 +367,52 @@ def list_jobs() -> list[dict[str, Any]]:
|
|
|
360
367
|
return result
|
|
361
368
|
|
|
362
369
|
|
|
370
|
+
# --- Data File Tools ---
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def save_data_file(
|
|
374
|
+
file_path: str,
|
|
375
|
+
columns: dict[str, list[float]],
|
|
376
|
+
overwrite: bool = False,
|
|
377
|
+
) -> dict[str, Any]:
|
|
378
|
+
"""Save generated data to a CSV file on the local filesystem.
|
|
379
|
+
|
|
380
|
+
This tool exists because you (the LLM) cannot write files directly --
|
|
381
|
+
but this MCP server runs on the user's local machine and CAN.
|
|
382
|
+
Use it to persist generated data arrays (prices, demand profiles, etc.)
|
|
383
|
+
so they can be referenced in add_device via {"file": "<path>", "column": "<name>"}.
|
|
384
|
+
|
|
385
|
+
Typical workflow:
|
|
386
|
+
1. Generate price/demand data as arrays
|
|
387
|
+
2. Call save_data_file to write them as CSV
|
|
388
|
+
3. Use the returned file_path in add_device properties
|
|
389
|
+
|
|
390
|
+
:param file_path: Filename or path (e.g., "prices_2025.csv").
|
|
391
|
+
Relative paths resolve against INVESTMENT_DATA_DIR env var (or cwd).
|
|
392
|
+
Extension '.csv' is appended if missing.
|
|
393
|
+
:param columns: Named columns of numeric data.
|
|
394
|
+
Example: {"hour": [0, 1, 2, ...], "price_eur_mwh": [30.5, 42.1, ...]}.
|
|
395
|
+
All columns must have the same length.
|
|
396
|
+
:param overwrite: Allow overwriting an existing file (default: False).
|
|
397
|
+
:returns: Dict with file_path (absolute), column names, and row count.
|
|
398
|
+
"""
|
|
399
|
+
data_dir = get_data_dir()
|
|
400
|
+
saved_path = save_csv(
|
|
401
|
+
file_path=file_path,
|
|
402
|
+
columns=columns,
|
|
403
|
+
data_dir=data_dir,
|
|
404
|
+
overwrite=overwrite,
|
|
405
|
+
)
|
|
406
|
+
col_names = list(columns.keys())
|
|
407
|
+
row_count = len(next(iter(columns.values())))
|
|
408
|
+
return {
|
|
409
|
+
"file_path": saved_path,
|
|
410
|
+
"columns": col_names,
|
|
411
|
+
"rows": row_count,
|
|
412
|
+
"message": f"Saved {row_count} rows to {saved_path}",
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
363
416
|
# --- Helper Tools ---
|
|
364
417
|
|
|
365
418
|
|
|
@@ -644,11 +697,11 @@ def get_device_schema(device_type: str) -> dict[str, Any]:
|
|
|
644
697
|
"description": "Maximum demand profile (MW, not MWh!)",
|
|
645
698
|
},
|
|
646
699
|
"min_demand_profile": {
|
|
647
|
-
"type": "float | list[float]",
|
|
700
|
+
"type": "float | list[float] | {file: str}",
|
|
648
701
|
"required": False,
|
|
649
702
|
"default": 0,
|
|
650
703
|
"unit": "MW",
|
|
651
|
-
"description": "Minimum demand profile or constant",
|
|
704
|
+
"description": "Minimum demand profile or constant. Supports file loading.",
|
|
652
705
|
},
|
|
653
706
|
},
|
|
654
707
|
"supports_schedule": False,
|
|
@@ -664,11 +717,11 @@ def get_device_schema(device_type: str) -> dict[str, Any]:
|
|
|
664
717
|
"description": "Maximum heat demand profile (MW)",
|
|
665
718
|
},
|
|
666
719
|
"min_demand_profile": {
|
|
667
|
-
"type": "float | list[float]",
|
|
720
|
+
"type": "float | list[float] | {file: str}",
|
|
668
721
|
"required": False,
|
|
669
722
|
"default": 0,
|
|
670
723
|
"unit": "MW",
|
|
671
|
-
"description": "Minimum heat demand profile or constant",
|
|
724
|
+
"description": "Minimum heat demand profile or constant. Supports file loading.",
|
|
672
725
|
},
|
|
673
726
|
},
|
|
674
727
|
"supports_schedule": False,
|
|
@@ -702,3 +755,4 @@ mcp.tool()(get_job_result)
|
|
|
702
755
|
mcp.tool()(cancel_job)
|
|
703
756
|
mcp.tool()(list_jobs)
|
|
704
757
|
mcp.tool()(get_device_schema)
|
|
758
|
+
mcp.tool()(save_data_file)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: site-calc-investment
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Python client for Site-Calc investment planning (capacity sizing, ROI analysis)
|
|
5
5
|
Project-URL: Homepage, https://github.com/stranma/site-calc-investment
|
|
6
6
|
Project-URL: Documentation, https://github.com/stranma/site-calc-investment#readme
|
|
@@ -212,6 +212,60 @@ comparison = compare_scenarios(
|
|
|
212
212
|
print(comparison) # DataFrame with NPV, IRR, costs, revenues
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
## MCP Server (Claude Desktop Integration)
|
|
216
|
+
|
|
217
|
+
The package includes an MCP server for use with Claude Desktop and other LLM tools.
|
|
218
|
+
|
|
219
|
+
### Installation
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
pip install site-calc-investment[mcp]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Claude Desktop Configuration
|
|
226
|
+
|
|
227
|
+
Add to `claude_desktop_config.json`:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"mcpServers": {
|
|
232
|
+
"site-calc-investment": {
|
|
233
|
+
"command": "uv",
|
|
234
|
+
"args": ["run", "--directory", "/path/to/client-investment", "site-calc-investment-mcp"],
|
|
235
|
+
"env": {
|
|
236
|
+
"INVESTMENT_API_URL": "http://your-api-url",
|
|
237
|
+
"INVESTMENT_API_KEY": "inv_your_key_here",
|
|
238
|
+
"INVESTMENT_DATA_DIR": "/path/to/data/directory"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Tools (15)
|
|
246
|
+
|
|
247
|
+
| Tool | Description |
|
|
248
|
+
|------|-------------|
|
|
249
|
+
| `create_scenario` | Create a new draft scenario |
|
|
250
|
+
| `add_device` | Add a device (battery, CHP, PV, etc.) |
|
|
251
|
+
| `set_timespan` | Set optimization time horizon |
|
|
252
|
+
| `set_investment_params` | Set financial parameters (NPV, IRR) |
|
|
253
|
+
| `review_scenario` | Review scenario before submission |
|
|
254
|
+
| `remove_device` | Remove a device |
|
|
255
|
+
| `delete_scenario` | Delete a scenario |
|
|
256
|
+
| `list_scenarios` | List all draft scenarios |
|
|
257
|
+
| `submit_scenario` | Submit for optimization |
|
|
258
|
+
| `get_job_status` | Check job progress |
|
|
259
|
+
| `get_job_result` | Get optimization results |
|
|
260
|
+
| `cancel_job` | Cancel a job |
|
|
261
|
+
| `list_jobs` | List all jobs |
|
|
262
|
+
| `get_device_schema` | Get device property schema |
|
|
263
|
+
| `save_data_file` | Save generated data as CSV |
|
|
264
|
+
|
|
265
|
+
`save_data_file` lets the LLM write generated data (price arrays, demand profiles) to local CSV files, which can then be referenced in `add_device` properties.
|
|
266
|
+
|
|
267
|
+
See [docs/MCP_SERVER_SPEC.md](docs/MCP_SERVER_SPEC.md) for full specification.
|
|
268
|
+
|
|
215
269
|
## Documentation
|
|
216
270
|
|
|
217
271
|
Full documentation available at: https://github.com/stranma/site-calc-investment#readme
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
site_calc_investment/__init__.py,sha256=
|
|
1
|
+
site_calc_investment/__init__.py,sha256=5pzvQMGWiyaDU8ncrdevsRNvGHutVboe1C78BKqTMqA,2084
|
|
2
2
|
site_calc_investment/exceptions.py,sha256=dz1wXM3Y4hHzK-PaQPY7sFPgLwUd6bZyWfJDVH9aBZk,2108
|
|
3
3
|
site_calc_investment/analysis/__init__.py,sha256=u6nwlhY3gQnxn-nkUGc68ynmoBU7NUt3uoUuM9AfQIs,414
|
|
4
4
|
site_calc_investment/analysis/comparison.py,sha256=3aqKpVcGy-2YfQZjS6qAzN12vuzvwDmE1JgZn6fuuIE,4487
|
|
@@ -6,17 +6,17 @@ site_calc_investment/analysis/financial.py,sha256=gzKYmylGDKZDqx1ZBefZwJMS9NuflH
|
|
|
6
6
|
site_calc_investment/api/__init__.py,sha256=LmrMw5jO24beExGtcau7B34NnMkJUmDsHngEdzuCans,140
|
|
7
7
|
site_calc_investment/api/client.py,sha256=Uh4yYPIC9oL1uiaaTe36xFeb5yfOsxkMetZojxRZoJs,15064
|
|
8
8
|
site_calc_investment/mcp/__init__.py,sha256=jxRR08m6sGun1lOLQ5Z9X9uB26UqVyT90is7vvvW49w,326
|
|
9
|
-
site_calc_investment/mcp/config.py,sha256=
|
|
10
|
-
site_calc_investment/mcp/data_loaders.py,sha256=
|
|
9
|
+
site_calc_investment/mcp/config.py,sha256=FuESohfC0yfk6g1R326DbAZoZN7V1YQ2Jbx7Vi6IN3M,1292
|
|
10
|
+
site_calc_investment/mcp/data_loaders.py,sha256=Y1Ilk2YSh-qd3bDcN_MokxLevljPcDO3VGRnPYYdvvk,8995
|
|
11
11
|
site_calc_investment/mcp/scenario.py,sha256=APLrYCXdl2VWMebk_kWfZu3Mvpbw9DIFHtnYGV9bH3Q,18774
|
|
12
|
-
site_calc_investment/mcp/server.py,sha256=
|
|
12
|
+
site_calc_investment/mcp/server.py,sha256=V1LFddWm6XPt1bdEZ4FZ_h7nYZxU7r_56JsitCki27M,27616
|
|
13
13
|
site_calc_investment/models/__init__.py,sha256=aJsKxZGYe8AwsB7ZVrjRJs961iCd6KNKN5fpeYe52FY,1779
|
|
14
14
|
site_calc_investment/models/common.py,sha256=r6Acg8uICJTETQ1FoVuBEdM8ThLQqwCVhclYO3kmCAY,5817
|
|
15
15
|
site_calc_investment/models/devices.py,sha256=RmlbcxjavC3nFRyCJ6n8aUp2ZosVTpszmMmJfqpgd9c,9895
|
|
16
16
|
site_calc_investment/models/requests.py,sha256=K3ESSgh3Qkq26jIL3TLTD6TRGmttVSiUUPTdbCmwRiw,5089
|
|
17
17
|
site_calc_investment/models/responses.py,sha256=pmFfhSCAlCUZTxRDh0IdBn5znJiiFjF7U3LHZON_yQY,5482
|
|
18
|
-
site_calc_investment-1.2.
|
|
19
|
-
site_calc_investment-1.2.
|
|
20
|
-
site_calc_investment-1.2.
|
|
21
|
-
site_calc_investment-1.2.
|
|
22
|
-
site_calc_investment-1.2.
|
|
18
|
+
site_calc_investment-1.2.3.dist-info/METADATA,sha256=ZO-8638LBrDp9whS0TxFN3g1LaooAmGlEY_FCbT_YgM,9189
|
|
19
|
+
site_calc_investment-1.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
20
|
+
site_calc_investment-1.2.3.dist-info/entry_points.txt,sha256=4q9JDR4ahtnx04H_wnMkl_td1Ki88099NbS7qRrOfg0,75
|
|
21
|
+
site_calc_investment-1.2.3.dist-info/licenses/LICENSE,sha256=tmWogc4edAjn_ccDCKSLvMGW7asakJnAlQWuNqgApIs,1071
|
|
22
|
+
site_calc_investment-1.2.3.dist-info/RECORD,,
|
|
File without changes
|
{site_calc_investment-1.2.2.dist-info → site_calc_investment-1.2.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{site_calc_investment-1.2.2.dist-info → site_calc_investment-1.2.3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|