site-calc-investment 1.2.2__tar.gz → 1.2.3__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.
Files changed (53) hide show
  1. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/CHANGELOG.md +19 -0
  2. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/PKG-INFO +55 -1
  3. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/README.md +54 -0
  4. site_calc_investment-1.2.3/docs/MCP_SERVER_SPEC.md +413 -0
  5. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/pyproject.toml +1 -1
  6. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/__init__.py +1 -1
  7. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/mcp/config.py +6 -0
  8. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/mcp/data_loaders.py +70 -0
  9. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/mcp/server.py +62 -8
  10. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_mcp/test_data_loaders.py +107 -1
  11. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_mcp/test_integration.py +23 -1
  12. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_mcp/test_tools.py +49 -1
  13. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/uv.lock +193 -194
  14. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/.github/workflows/ci.yml +0 -0
  15. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/.github/workflows/publish.yml +0 -0
  16. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/.gitignore +0 -0
  17. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/CONTRIBUTING.md +0 -0
  18. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/LICENSE +0 -0
  19. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/MIGRATION_GUIDE.md +0 -0
  20. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/QUICK_START.md +0 -0
  21. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/READY_TO_PUBLISH.md +0 -0
  22. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/docs/INVESTMENT_CLIENT_SPEC.md +0 -0
  23. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/examples/01_basic_capacity_planning.py +0 -0
  24. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/examples/02_scenario_comparison.py +0 -0
  25. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/examples/03_financial_analysis.py +0 -0
  26. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/publish_to_github.bat +0 -0
  27. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/publish_to_github.sh +0 -0
  28. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/analysis/__init__.py +0 -0
  29. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/analysis/comparison.py +0 -0
  30. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/analysis/financial.py +0 -0
  31. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/api/__init__.py +0 -0
  32. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/api/client.py +0 -0
  33. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/exceptions.py +0 -0
  34. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/mcp/__init__.py +0 -0
  35. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/mcp/scenario.py +0 -0
  36. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/models/__init__.py +0 -0
  37. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/models/common.py +0 -0
  38. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/models/devices.py +0 -0
  39. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/models/requests.py +0 -0
  40. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/site_calc_investment/models/responses.py +0 -0
  41. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/conftest.py +0 -0
  42. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_api_client.py +0 -0
  43. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_common_models.py +0 -0
  44. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_device_models.py +0 -0
  45. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_financial_analysis.py +0 -0
  46. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_mcp/__init__.py +0 -0
  47. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_mcp/conftest.py +0 -0
  48. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_mcp/test_mcp_production.py +0 -0
  49. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_mcp/test_scenario.py +0 -0
  50. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_production.py +0 -0
  51. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_request_models.py +0 -0
  52. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/tests/test_scenario_comparison.py +0 -0
  53. {site_calc_investment-1.2.2 → site_calc_investment-1.2.3}/verify_ready.py +0 -0
@@ -5,6 +5,25 @@ All notable changes to the Site-Calc Investment Client will be documented in thi
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.3] - 2026-02-04
9
+
10
+ ### Added
11
+ - **`save_data_file` MCP tool**: New tool (#15) that writes generated data (price arrays, demand
12
+ profiles) to CSV files on the local filesystem. Solves the problem where the LLM cannot write
13
+ files directly but the MCP server can.
14
+ - Supports named columns with automatic `.csv` extension
15
+ - Relative paths resolve against `INVESTMENT_DATA_DIR` environment variable
16
+ - Returned file path can be used directly in `add_device` via `{"file": "...", "column": "..."}`
17
+ - **`INVESTMENT_DATA_DIR` environment variable**: Optional config for `save_data_file` base directory
18
+ - **MCP Server specification**: Full docs at `docs/MCP_SERVER_SPEC.md`
19
+
20
+ ### Changed
21
+ - MCP server instructions updated to inform the LLM about `save_data_file` capability
22
+ - MCP tool count: 14 -> 15
23
+
24
+ ---
25
+
26
+
8
27
  ## [1.2.2] - 2026-02-04
9
28
 
10
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: site-calc-investment
3
- Version: 1.2.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
@@ -172,6 +172,60 @@ comparison = compare_scenarios(
172
172
  print(comparison) # DataFrame with NPV, IRR, costs, revenues
173
173
  ```
174
174
 
175
+ ## MCP Server (Claude Desktop Integration)
176
+
177
+ The package includes an MCP server for use with Claude Desktop and other LLM tools.
178
+
179
+ ### Installation
180
+
181
+ ```bash
182
+ pip install site-calc-investment[mcp]
183
+ ```
184
+
185
+ ### Claude Desktop Configuration
186
+
187
+ Add to `claude_desktop_config.json`:
188
+
189
+ ```json
190
+ {
191
+ "mcpServers": {
192
+ "site-calc-investment": {
193
+ "command": "uv",
194
+ "args": ["run", "--directory", "/path/to/client-investment", "site-calc-investment-mcp"],
195
+ "env": {
196
+ "INVESTMENT_API_URL": "http://your-api-url",
197
+ "INVESTMENT_API_KEY": "inv_your_key_here",
198
+ "INVESTMENT_DATA_DIR": "/path/to/data/directory"
199
+ }
200
+ }
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### Tools (15)
206
+
207
+ | Tool | Description |
208
+ |------|-------------|
209
+ | `create_scenario` | Create a new draft scenario |
210
+ | `add_device` | Add a device (battery, CHP, PV, etc.) |
211
+ | `set_timespan` | Set optimization time horizon |
212
+ | `set_investment_params` | Set financial parameters (NPV, IRR) |
213
+ | `review_scenario` | Review scenario before submission |
214
+ | `remove_device` | Remove a device |
215
+ | `delete_scenario` | Delete a scenario |
216
+ | `list_scenarios` | List all draft scenarios |
217
+ | `submit_scenario` | Submit for optimization |
218
+ | `get_job_status` | Check job progress |
219
+ | `get_job_result` | Get optimization results |
220
+ | `cancel_job` | Cancel a job |
221
+ | `list_jobs` | List all jobs |
222
+ | `get_device_schema` | Get device property schema |
223
+ | `save_data_file` | Save generated data as CSV |
224
+
225
+ `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.
226
+
227
+ See [docs/MCP_SERVER_SPEC.md](docs/MCP_SERVER_SPEC.md) for full specification.
228
+
175
229
  ## Documentation
176
230
 
177
231
  Full documentation available at: https://github.com/stranma/site-calc-investment#readme
@@ -0,0 +1,413 @@
1
+ # MCP Server Specification
2
+
3
+ **Package:** `site-calc-investment[mcp]`
4
+ **Server name:** `site-calc-investment`
5
+ **Protocol:** MCP (Model Context Protocol) via FastMCP
6
+ **Tools:** 15
7
+
8
+ ---
9
+
10
+ ## 1. Overview
11
+
12
+ The MCP server exposes the Site-Calc investment planning API as tools that LLMs (e.g., Claude Desktop) can call interactively. Users describe what they want to optimize in natural language, and the LLM assembles scenarios, submits jobs, and retrieves results through these tools.
13
+
14
+ ### 1.1 Architecture
15
+
16
+ ```
17
+ Claude Desktop (LLM)
18
+ |
19
+ | MCP protocol (stdio)
20
+ v
21
+ site-calc-investment-mcp (local process)
22
+ |
23
+ | HTTPS / REST
24
+ v
25
+ Site-Calc API (remote server)
26
+ ```
27
+
28
+ The MCP server runs **locally** on the user's machine. It has full filesystem access (for CSV data files) and network access (for the optimization API).
29
+
30
+ ### 1.2 Key Capabilities
31
+
32
+ | Feature | Value |
33
+ |---------|-------|
34
+ | Tools | 15 |
35
+ | Device types | 10 |
36
+ | Max horizon | 100,000 intervals (~11 years) |
37
+ | Resolution | 1-hour |
38
+ | Local filesystem | Read + Write (CSV data files) |
39
+ | API connection | HTTPS to Site-Calc server |
40
+
41
+ ---
42
+
43
+ ## 2. Configuration
44
+
45
+ ### 2.1 Environment Variables
46
+
47
+ | Variable | Required | Description |
48
+ |----------|----------|-------------|
49
+ | `INVESTMENT_API_URL` | Yes | Site-Calc API base URL |
50
+ | `INVESTMENT_API_KEY` | Yes | API key (starts with `inv_`) |
51
+ | `INVESTMENT_DATA_DIR` | No | Default directory for `save_data_file` relative paths |
52
+
53
+ `INVESTMENT_API_URL` and `INVESTMENT_API_KEY` are required for job submission tools (`submit_scenario`, `get_job_status`, `get_job_result`, `cancel_job`). Scenario assembly tools work without them.
54
+
55
+ `INVESTMENT_DATA_DIR` sets the base directory for resolving relative paths in `save_data_file`. If not set, relative paths resolve against the current working directory.
56
+
57
+ ### 2.2 Claude Desktop Configuration
58
+
59
+ Add to `claude_desktop_config.json`:
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "site-calc-investment": {
65
+ "command": "C:\\Users\\Admin\\.local\\bin\\uv.exe",
66
+ "args": [
67
+ "run",
68
+ "--directory", "C:\\my_source\\site-calc\\client-investment",
69
+ "site-calc-investment-mcp"
70
+ ],
71
+ "env": {
72
+ "INVESTMENT_API_URL": "https://api.site-calc.example.com",
73
+ "INVESTMENT_API_KEY": "inv_your_api_key_here",
74
+ "INVESTMENT_DATA_DIR": "C:\\my_source\\BESS_Optimization_Tool"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### 2.3 Installation
82
+
83
+ ```bash
84
+ pip install site-calc-investment[mcp]
85
+ ```
86
+
87
+ Or with uv (recommended for development):
88
+
89
+ ```bash
90
+ cd client-investment
91
+ uv sync --group dev
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 3. Tool Reference
97
+
98
+ ### 3.1 Scenario Assembly Tools
99
+
100
+ #### `create_scenario`
101
+
102
+ Create a new draft optimization scenario.
103
+
104
+ | Parameter | Type | Required | Description |
105
+ |-----------|------|----------|-------------|
106
+ | `name` | string | Yes | Human-readable name |
107
+ | `description` | string | No | Longer description |
108
+
109
+ **Returns:** `{"scenario_id": "sc_...", "name": "..."}`
110
+
111
+ ---
112
+
113
+ #### `add_device`
114
+
115
+ Add a device to a draft scenario.
116
+
117
+ | Parameter | Type | Required | Description |
118
+ |-----------|------|----------|-------------|
119
+ | `scenario_id` | string | Yes | Target scenario |
120
+ | `device_type` | string | Yes | One of 10 device types (see Section 4) |
121
+ | `name` | string | Yes | Unique device name within scenario |
122
+ | `properties` | object | Yes | Device-specific properties |
123
+ | `schedule` | object | No | Runtime constraints |
124
+
125
+ Properties support data shorthand:
126
+ - A number (e.g., `50.0`) -- expanded to constant array matching the timespan
127
+ - A list (e.g., `[30, 40, 80, 50]`) -- used directly
128
+ - A file reference (e.g., `{"file": "prices.csv", "column": "price_eur"}`) -- loaded from local CSV
129
+
130
+ **Returns:** Summary string.
131
+
132
+ ---
133
+
134
+ #### `set_timespan`
135
+
136
+ Set the optimization time horizon.
137
+
138
+ | Parameter | Type | Required | Default | Description |
139
+ |-----------|------|----------|---------|-------------|
140
+ | `scenario_id` | string | Yes | | Target scenario |
141
+ | `start_year` | int | Yes | | Start year (e.g., 2025) |
142
+ | `years` | int | No | 1 | Number of years |
143
+
144
+ One year = 8,760 intervals. Maximum ~11 years (100,000 intervals).
145
+
146
+ **Returns:** Confirmation string with interval count.
147
+
148
+ ---
149
+
150
+ #### `set_investment_params`
151
+
152
+ Set financial parameters for ROI calculation (NPV, IRR, payback).
153
+
154
+ | Parameter | Type | Required | Default | Description |
155
+ |-----------|------|----------|---------|-------------|
156
+ | `scenario_id` | string | Yes | | Target scenario |
157
+ | `discount_rate` | float | No | 0.05 | Annual discount rate (0-0.5) |
158
+ | `project_lifetime_years` | int | No | timespan years | Project lifetime |
159
+ | `device_capital_costs` | object | No | | CAPEX by device name (EUR) |
160
+ | `device_annual_opex` | object | No | | Annual O&M by device name (EUR) |
161
+
162
+ **Returns:** Confirmation string.
163
+
164
+ ---
165
+
166
+ #### `review_scenario`
167
+
168
+ Show a summary of the draft scenario before submitting.
169
+
170
+ | Parameter | Type | Required | Description |
171
+ |-----------|------|----------|-------------|
172
+ | `scenario_id` | string | Yes | Scenario to review |
173
+
174
+ **Returns:** Dict with `name`, `devices`, `timespan`, `investment_params`, `validation`.
175
+
176
+ ---
177
+
178
+ #### `remove_device`
179
+
180
+ Remove a device from a draft scenario.
181
+
182
+ | Parameter | Type | Required | Description |
183
+ |-----------|------|----------|-------------|
184
+ | `scenario_id` | string | Yes | Target scenario |
185
+ | `device_name` | string | Yes | Device to remove |
186
+
187
+ **Returns:** Confirmation string.
188
+
189
+ ---
190
+
191
+ #### `delete_scenario`
192
+
193
+ Delete a draft scenario entirely.
194
+
195
+ | Parameter | Type | Required | Description |
196
+ |-----------|------|----------|-------------|
197
+ | `scenario_id` | string | Yes | Scenario to delete |
198
+
199
+ **Returns:** Confirmation string.
200
+
201
+ ---
202
+
203
+ #### `list_scenarios`
204
+
205
+ List all active draft scenarios.
206
+
207
+ **Returns:** List of `{"id", "name", "device_count", "has_timespan", "job_count"}`.
208
+
209
+ ---
210
+
211
+ ### 3.2 Job Submission and Management Tools
212
+
213
+ #### `submit_scenario`
214
+
215
+ Submit a draft scenario for server-side optimization.
216
+
217
+ | Parameter | Type | Required | Default | Description |
218
+ |-----------|------|----------|---------|-------------|
219
+ | `scenario_id` | string | Yes | | Scenario to submit |
220
+ | `objective` | string | No | `maximize_profit` | Optimization objective |
221
+ | `solver_timeout` | int | No | 300 | Time limit in seconds (max 900) |
222
+
223
+ Objectives: `maximize_profit`, `minimize_cost`, `maximize_self_consumption`.
224
+
225
+ **Returns:** `{"job_id": "...", "status": "pending"}`
226
+
227
+ ---
228
+
229
+ #### `get_job_status`
230
+
231
+ Check job status and progress.
232
+
233
+ | Parameter | Type | Required | Description |
234
+ |-----------|------|----------|-------------|
235
+ | `job_id` | string | Yes | Job identifier |
236
+
237
+ **Returns:** Dict with `job_id`, `status`, and optionally `progress`, `message`, `estimated_completion_seconds`, `solver_time_seconds`, `error`.
238
+
239
+ ---
240
+
241
+ #### `get_job_result`
242
+
243
+ Retrieve completed optimization results.
244
+
245
+ | Parameter | Type | Required | Default | Description |
246
+ |-----------|------|----------|---------|-------------|
247
+ | `job_id` | string | Yes | | Job identifier |
248
+ | `detail_level` | string | No | `summary` | `summary`, `monthly`, or `full` |
249
+
250
+ Detail levels:
251
+ - **summary** -- Aggregated totals (profit, cost, solve time, investment metrics). Compact.
252
+ - **monthly** -- Summary + per-device monthly breakdowns.
253
+ - **full** -- All data including hourly schedules. Can be very large.
254
+
255
+ **Returns:** Result dict at requested detail level.
256
+
257
+ ---
258
+
259
+ #### `cancel_job`
260
+
261
+ Cancel a pending or running job.
262
+
263
+ | Parameter | Type | Required | Description |
264
+ |-----------|------|----------|-------------|
265
+ | `job_id` | string | Yes | Job to cancel |
266
+
267
+ **Returns:** `{"job_id": "...", "status": "cancelled"}`
268
+
269
+ ---
270
+
271
+ #### `list_jobs`
272
+
273
+ List all scenarios and their associated jobs.
274
+
275
+ **Returns:** List of `{"scenario_id", "scenario_name", "job_ids", "job_count"}`.
276
+
277
+ ---
278
+
279
+ ### 3.3 Data File Tools
280
+
281
+ #### `save_data_file`
282
+
283
+ Save generated data to a CSV file on the local filesystem.
284
+
285
+ This tool exists because the LLM cannot write files directly, but this MCP server runs locally and can. Use it to persist generated data arrays (prices, demand profiles, etc.) so they can be referenced in `add_device` via `{"file": "<path>", "column": "<name>"}`.
286
+
287
+ | Parameter | Type | Required | Default | Description |
288
+ |-----------|------|----------|---------|-------------|
289
+ | `file_path` | string | Yes | | Filename or path (e.g., `"prices_2025.csv"`) |
290
+ | `columns` | object | Yes | | Named columns: `{"col_name": [float, ...]}` |
291
+ | `overwrite` | bool | No | false | Allow overwriting existing files |
292
+
293
+ Path resolution:
294
+ - Absolute paths are used as-is
295
+ - Relative paths resolve against `INVESTMENT_DATA_DIR` (or cwd if not set)
296
+ - `.csv` extension is appended if missing
297
+ - Non-`.csv` extensions are rejected
298
+
299
+ **Returns:**
300
+ ```json
301
+ {
302
+ "file_path": "C:\\Users\\Admin\\data\\prices.csv",
303
+ "columns": ["hour", "price_eur_mwh"],
304
+ "rows": 8760,
305
+ "message": "Saved 8760 rows to C:\\Users\\Admin\\data\\prices.csv"
306
+ }
307
+ ```
308
+
309
+ **Typical workflow:**
310
+ ```
311
+ 1. LLM generates price array (8760 values)
312
+ 2. LLM calls save_data_file(file_path="prices_2025.csv", columns={"price_eur": [...]})
313
+ 3. LLM calls add_device(properties={"price": {"file": "C:/.../prices_2025.csv", "column": "price_eur"}, ...})
314
+ ```
315
+
316
+ ---
317
+
318
+ ### 3.4 Helper Tools
319
+
320
+ #### `get_device_schema`
321
+
322
+ Get the properties schema for a device type.
323
+
324
+ | Parameter | Type | Required | Description |
325
+ |-----------|------|----------|-------------|
326
+ | `device_type` | string | Yes | Device type name |
327
+
328
+ **Returns:** Schema dict with `properties`, `supports_schedule`, `example`.
329
+
330
+ ---
331
+
332
+ ## 4. Supported Device Types
333
+
334
+ | Device Type | Description | Key Properties |
335
+ |-------------|-------------|----------------|
336
+ | `battery` | Battery energy storage | capacity, max_power, efficiency |
337
+ | `chp` | Combined heat and power | gas_input, el_output, heat_output |
338
+ | `heat_accumulator` | Thermal storage | capacity, max_power, efficiency |
339
+ | `photovoltaic` | Solar PV | peak_power_mw, location, tilt, azimuth |
340
+ | `electricity_import` | Buy from grid | price, max_import |
341
+ | `electricity_export` | Sell to grid | price, max_export |
342
+ | `gas_import` | Gas supply | price, max_import |
343
+ | `heat_export` | Sell heat | price, max_export |
344
+ | `electricity_demand` | Electricity load | max_demand_profile |
345
+ | `heat_demand` | Heat load | max_demand_profile |
346
+
347
+ Use `get_device_schema(device_type)` for full property documentation.
348
+
349
+ ---
350
+
351
+ ## 5. End-to-End Example
352
+
353
+ A typical session for battery arbitrage analysis:
354
+
355
+ ```
356
+ User: "Evaluate a 10 MWh battery with 2025 German electricity prices"
357
+
358
+ LLM actions:
359
+ 1. Generate 8760 hourly prices for 2025
360
+ 2. save_data_file(file_path="de_prices_2025.csv",
361
+ columns={"hour": [0..8759], "price_eur_mwh": [32.1, 28.5, ...]})
362
+ 3. create_scenario(name="10 MWh Battery - DE 2025")
363
+ 4. set_timespan(scenario_id=sid, start_year=2025)
364
+ 5. add_device(scenario_id=sid, device_type="battery", name="BESS",
365
+ properties={"capacity": 10.0, "max_power": 5.0, "efficiency": 0.90})
366
+ 6. add_device(scenario_id=sid, device_type="electricity_import", name="GridBuy",
367
+ properties={"price": {"file": ".../de_prices_2025.csv", "column": "price_eur_mwh"},
368
+ "max_import": 5.0})
369
+ 7. add_device(scenario_id=sid, device_type="electricity_export", name="GridSell",
370
+ properties={"price": {"file": ".../de_prices_2025.csv", "column": "price_eur_mwh"},
371
+ "max_export": 5.0})
372
+ 8. set_investment_params(scenario_id=sid, discount_rate=0.05,
373
+ device_capital_costs={"BESS": 500000})
374
+ 9. review_scenario(scenario_id=sid)
375
+ 10. submit_scenario(scenario_id=sid)
376
+ 11. get_job_status(job_id=jid) -- poll until complete
377
+ 12. get_job_result(job_id=jid, detail_level="summary")
378
+ ```
379
+
380
+ LLM then presents the results: profit, NPV, IRR, payback period.
381
+
382
+ ---
383
+
384
+ ## 6. Error Handling
385
+
386
+ Tools raise standard Python exceptions that FastMCP translates to MCP error responses:
387
+
388
+ | Exception | Cause |
389
+ |-----------|-------|
390
+ | `ValueError` | Invalid parameters (wrong device type, missing properties, bad column data) |
391
+ | `FileNotFoundError` | Referenced CSV/JSON file does not exist |
392
+ | `FileExistsError` | `save_data_file` with `overwrite=False` on existing file |
393
+ | `KeyError` | Nonexistent scenario_id or device_name |
394
+
395
+ The LLM receives error messages and can retry with corrected parameters.
396
+
397
+ ---
398
+
399
+ ## 7. Testing
400
+
401
+ ```bash
402
+ # Run MCP server tests
403
+ cd client-investment && uv run pytest tests/test_mcp/ -v
404
+
405
+ # Run full test suite
406
+ cd client-investment && uv run pytest tests/ -v
407
+ ```
408
+
409
+ Test coverage includes:
410
+ - 11 tests for `save_csv` data layer
411
+ - 2 tests for `save_data_file` tool integration
412
+ - 7 MCP protocol integration tests (via FastMCP Client)
413
+ - 77+ tests for scenario assembly and job management tools
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "site-calc-investment"
3
- version = "1.2.2"
3
+ version = "1.2.3"
4
4
  description = "Python client for Site-Calc investment planning (capacity sizing, ROI analysis)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@
3
3
  Python client for long-term capacity planning and investment ROI analysis.
4
4
  """
5
5
 
6
- __version__ = "1.2.2"
6
+ __version__ = "1.2.3"
7
7
 
8
8
  from site_calc_investment.analysis import (
9
9
  aggregate_annual,
@@ -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