mcp-grocy 2.5.0 → 2.6.0

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 (38) hide show
  1. package/CHANGELOG.md +19 -5
  2. package/README.md +13 -0
  3. package/build/resources/CHANGELOG.md +20 -4
  4. package/build/resources/README.md +13 -0
  5. package/build/resources/config.md +19 -4
  6. package/build/resources/response-format.md +12 -8
  7. package/package.json +13 -2
  8. package/build/api/client.js +0 -119
  9. package/build/config/index.js +0 -259
  10. package/build/main.js +0 -44
  11. package/build/server/http-server.js +0 -330
  12. package/build/server/mcp-server.js +0 -210
  13. package/build/server/resources.js +0 -73
  14. package/build/server/tool-input-zod.js +0 -86
  15. package/build/tools/base.js +0 -145
  16. package/build/tools/household/definitions.js +0 -161
  17. package/build/tools/household/handlers.js +0 -109
  18. package/build/tools/household/index.js +0 -25
  19. package/build/tools/index.js +0 -2
  20. package/build/tools/inventory/definitions.js +0 -416
  21. package/build/tools/inventory/handlers.js +0 -443
  22. package/build/tools/inventory/index.js +0 -32
  23. package/build/tools/module-loader.js +0 -154
  24. package/build/tools/recipes/definitions.js +0 -278
  25. package/build/tools/recipes/handlers.js +0 -477
  26. package/build/tools/recipes/index.js +0 -33
  27. package/build/tools/recipes/validations.js +0 -29
  28. package/build/tools/shopping/definitions.js +0 -73
  29. package/build/tools/shopping/handlers.js +0 -43
  30. package/build/tools/shopping/index.js +0 -14
  31. package/build/tools/system/definitions.js +0 -89
  32. package/build/tools/system/handlers.js +0 -156
  33. package/build/tools/system/index.js +0 -16
  34. package/build/tools/types.js +0 -1
  35. package/build/tools/validation-helpers.js +0 -39
  36. package/build/types/index.js +0 -64
  37. package/build/utils/errors.js +0 -143
  38. package/build/utils/logger.js +0 -142
package/CHANGELOG.md CHANGED
@@ -1,15 +1,29 @@
1
- # [2.5.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.4.4...v2.5.0) (2026-03-30)
2
-
1
+ # [2.6.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.5.0...v2.6.0) (2026-07-06)
3
2
 
4
3
  ### Bug Fixes
5
4
 
6
- * add read permissions for contents in release workflow to support nested publish-docker job ([5a055a6](https://github.com/miguelangel-nubla/mcp-grocy/commit/5a055a62cf07fa3f0f03d4319e485350c111804a))
7
- * improve test build isolation and commit-specific tagging ([b1875cc](https://github.com/miguelangel-nubla/mcp-grocy/commit/b1875ccc080111c6d56700aab9c493e418d155be))
5
+ - reap idle streamable HTTP sessions to prevent McpServer leak ([c55b2c4](https://github.com/miguelangel-nubla/mcp-grocy/commit/c55b2c4808dac4ac5f0c55a45936c8f995d5efc3))
6
+
7
+ ### Features
8
+
9
+ - add shopping_list_update_item tool to allow modifying list items ([dcbc974](https://github.com/miguelangel-nubla/mcp-grocy/commit/dcbc9746630667bbe89330bab7f7e9f84a3f060f))
10
+ - add shopping*lists*\* configurations to example yaml ([204f03d](https://github.com/miguelangel-nubla/mcp-grocy/commit/204f03dca8c9ca17f3cfc476274f0a5b844912c7))
11
+ - configure husky git hooks ([c8ac158](https://github.com/miguelangel-nubla/mcp-grocy/commit/c8ac1582f477c6dc074166b5a9753a06608c8007))
12
+ - enhance shopping list tools with manual item support and enriched metadata resolution ([a1a8ce3](https://github.com/miguelangel-nubla/mcp-grocy/commit/a1a8ce341b61e5b24a366ae74a95a2c92e93e07c))
13
+ - enrich shopping list tool responses with product and quantity unit details ([56bd553](https://github.com/miguelangel-nubla/mcp-grocy/commit/56bd553e17d913db1f2c61fc963a900f6477f45f))
14
+ - expand shopping module with get, update, and filtered list capabilities ([d7e0e5d](https://github.com/miguelangel-nubla/mcp-grocy/commit/d7e0e5d94962c1b4daf0835a91fc275c25188673))
15
+ - make productId optional in shopping list add tool ([a427e18](https://github.com/miguelangel-nubla/mcp-grocy/commit/a427e1832a9b70e579dae062c7665ec472656359))
16
+
17
+ # [2.5.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.4.4...v2.5.0) (2026-03-30)
18
+
19
+ ### Bug Fixes
8
20
 
21
+ - add read permissions for contents in release workflow to support nested publish-docker job ([5a055a6](https://github.com/miguelangel-nubla/mcp-grocy/commit/5a055a62cf07fa3f0f03d4319e485350c111804a))
22
+ - improve test build isolation and commit-specific tagging ([b1875cc](https://github.com/miguelangel-nubla/mcp-grocy/commit/b1875ccc080111c6d56700aab9c493e418d155be))
9
23
 
10
24
  ### Features
11
25
 
12
- * improve tool call error handling, and implement multi-server shutdown management ([4d19cfb](https://github.com/miguelangel-nubla/mcp-grocy/commit/4d19cfb35dc238a41a92d2fba505998b6a59e1ae))
26
+ - improve tool call error handling, and implement multi-server shutdown management ([4d19cfb](https://github.com/miguelangel-nubla/mcp-grocy/commit/4d19cfb35dc238a41a92d2fba505998b6a59e1ae))
13
27
 
14
28
  # Changelog
15
29
 
package/README.md CHANGED
@@ -228,6 +228,19 @@ npm start
228
228
  - Increase `REST_RESPONSE_SIZE_LIMIT` if you have many products/stock entries
229
229
  - Consider disabling unused tools in `mcp-grocy.yaml`
230
230
 
231
+ **High memory usage / RAM grows over time (HTTP mode)**
232
+
233
+ - In HTTP mode the server automatically reaps idle MCP sessions (by default, after
234
+ 5 minutes with no requests), so clients that disconnect without `DELETE /mcp` —
235
+ or that repeatedly reconnect with a new session — no longer leak per-session
236
+ server instances. No action is required.
237
+ - Tune via `MCP_SESSION_IDLE_TIMEOUT_MS` / `MCP_SESSION_SWEEP_INTERVAL_MS` (or the
238
+ `server.session_idle_timeout_ms` / `server.session_sweep_interval_ms` YAML keys):
239
+ lower the timeout on memory-constrained hosts, raise it if a client legitimately
240
+ idles for long periods between calls.
241
+ - For the lowest overhead, prefer an MCP client that reuses its `Mcp-Session-Id`
242
+ across requests and sends `DELETE /mcp` when it disconnects.
243
+
231
244
  #### Debug Mode
232
245
 
233
246
  Enable detailed logging and use the MCP inspector:
@@ -1,15 +1,31 @@
1
- # [2.5.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.4.4...v2.5.0) (2026-03-30)
1
+ # [2.6.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.5.0...v2.6.0) (2026-07-06)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * add read permissions for contents in release workflow to support nested publish-docker job ([5a055a6](https://github.com/miguelangel-nubla/mcp-grocy/commit/5a055a62cf07fa3f0f03d4319e485350c111804a))
7
- * improve test build isolation and commit-specific tagging ([b1875cc](https://github.com/miguelangel-nubla/mcp-grocy/commit/b1875ccc080111c6d56700aab9c493e418d155be))
6
+ * reap idle streamable HTTP sessions to prevent McpServer leak ([c55b2c4](https://github.com/miguelangel-nubla/mcp-grocy/commit/c55b2c4808dac4ac5f0c55a45936c8f995d5efc3))
7
+
8
+
9
+ ### Features
10
+
11
+ * add shopping_list_update_item tool to allow modifying list items ([dcbc974](https://github.com/miguelangel-nubla/mcp-grocy/commit/dcbc9746630667bbe89330bab7f7e9f84a3f060f))
12
+ * add shopping_lists_* configurations to example yaml ([204f03d](https://github.com/miguelangel-nubla/mcp-grocy/commit/204f03dca8c9ca17f3cfc476274f0a5b844912c7))
13
+ * configure husky git hooks ([c8ac158](https://github.com/miguelangel-nubla/mcp-grocy/commit/c8ac1582f477c6dc074166b5a9753a06608c8007))
14
+ * enhance shopping list tools with manual item support and enriched metadata resolution ([a1a8ce3](https://github.com/miguelangel-nubla/mcp-grocy/commit/a1a8ce341b61e5b24a366ae74a95a2c92e93e07c))
15
+ * enrich shopping list tool responses with product and quantity unit details ([56bd553](https://github.com/miguelangel-nubla/mcp-grocy/commit/56bd553e17d913db1f2c61fc963a900f6477f45f))
16
+ * expand shopping module with get, update, and filtered list capabilities ([d7e0e5d](https://github.com/miguelangel-nubla/mcp-grocy/commit/d7e0e5d94962c1b4daf0835a91fc275c25188673))
17
+ * make productId optional in shopping list add tool ([a427e18](https://github.com/miguelangel-nubla/mcp-grocy/commit/a427e1832a9b70e579dae062c7665ec472656359))
18
+
19
+ # [2.5.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.4.4...v2.5.0) (2026-03-30)
20
+
21
+ ### Bug Fixes
8
22
 
23
+ - add read permissions for contents in release workflow to support nested publish-docker job ([5a055a6](https://github.com/miguelangel-nubla/mcp-grocy/commit/5a055a62cf07fa3f0f03d4319e485350c111804a))
24
+ - improve test build isolation and commit-specific tagging ([b1875cc](https://github.com/miguelangel-nubla/mcp-grocy/commit/b1875ccc080111c6d56700aab9c493e418d155be))
9
25
 
10
26
  ### Features
11
27
 
12
- * improve tool call error handling, and implement multi-server shutdown management ([4d19cfb](https://github.com/miguelangel-nubla/mcp-grocy/commit/4d19cfb35dc238a41a92d2fba505998b6a59e1ae))
28
+ - improve tool call error handling, and implement multi-server shutdown management ([4d19cfb](https://github.com/miguelangel-nubla/mcp-grocy/commit/4d19cfb35dc238a41a92d2fba505998b6a59e1ae))
13
29
 
14
30
  # Changelog
15
31
 
@@ -228,6 +228,19 @@ npm start
228
228
  - Increase `REST_RESPONSE_SIZE_LIMIT` if you have many products/stock entries
229
229
  - Consider disabling unused tools in `mcp-grocy.yaml`
230
230
 
231
+ **High memory usage / RAM grows over time (HTTP mode)**
232
+
233
+ - In HTTP mode the server automatically reaps idle MCP sessions (by default, after
234
+ 5 minutes with no requests), so clients that disconnect without `DELETE /mcp` —
235
+ or that repeatedly reconnect with a new session — no longer leak per-session
236
+ server instances. No action is required.
237
+ - Tune via `MCP_SESSION_IDLE_TIMEOUT_MS` / `MCP_SESSION_SWEEP_INTERVAL_MS` (or the
238
+ `server.session_idle_timeout_ms` / `server.session_sweep_interval_ms` YAML keys):
239
+ lower the timeout on memory-constrained hosts, raise it if a client legitimately
240
+ idles for long periods between calls.
241
+ - For the lowest overhead, prefer an MCP client that reuses its `Mcp-Session-Id`
242
+ across requests and sends `DELETE /mcp` when it disconnects.
243
+
231
244
  #### Debug Mode
232
245
 
233
246
  Enable detailed logging and use the MCP inspector:
@@ -22,10 +22,14 @@ Advanced configuration reference for the **mcp-grocy** npm package (this MCP ser
22
22
 
23
23
  Enable HTTP/SSE transport for web-based access:
24
24
 
25
- | Variable | Description | Default | Example |
26
- | -------------------- | ------------------------- | ------- | ------- |
27
- | `ENABLE_HTTP_SERVER` | Enable HTTP/SSE transport | `false` | `true` |
28
- | `HTTP_SERVER_PORT` | HTTP server port | `8080` | `3000` |
25
+ | Variable | Description | Default | Example |
26
+ | ------------------------------- | ---------------------------------------------------- | -------- | -------- |
27
+ | `ENABLE_HTTP_SERVER` | Enable HTTP/SSE transport | `false` | `true` |
28
+ | `HTTP_SERVER_PORT` | HTTP server port | `8080` | `3000` |
29
+ | `MCP_SESSION_IDLE_TIMEOUT_MS` | Reap an idle MCP session after this many ms | `300000` | `600000` |
30
+ | `MCP_SESSION_SWEEP_INTERVAL_MS` | How often, in ms, to sweep for idle sessions to reap | `60000` | `30000` |
31
+
32
+ The matching YAML keys are `server.session_idle_timeout_ms` and `server.session_sweep_interval_ms`.
29
33
 
30
34
  ### Transport Modes
31
35
 
@@ -33,6 +37,17 @@ Enable HTTP/SSE transport for web-based access:
33
37
  - **HTTP** - Streamable HTTP for web applications (`POST /mcp`)
34
38
  - **SSE** - Server-Sent Events for real-time web clients (`GET /mcp/sse`)
35
39
 
40
+ ### Session Reaping (HTTP mode)
41
+
42
+ In HTTP mode the server keeps a transport and per-session MCP server instance for
43
+ each session. Clients that disconnect without sending `DELETE /mcp`, or that
44
+ reconnect with a new session id, would otherwise leave those instances in memory.
45
+ The server reaps any session with no requests for `MCP_SESSION_IDLE_TIMEOUT_MS`,
46
+ freeing its resources. A reaped client transparently re-initializes on its next
47
+ request, so the default of 5 minutes is safe for normal use; lower it on
48
+ memory-constrained hosts or raise it if a client legitimately idles for long
49
+ periods between calls.
50
+
36
51
  ## 🛠️ Tool Configuration
37
52
 
38
53
  Tools are configured using the YAML configuration file `mcp-grocy.yaml`.
@@ -102,7 +102,10 @@ These values can be set in your `.env` file for local development or in your pro
102
102
 
103
103
  ## Specialized Grocy Tools Response Format
104
104
 
105
- Tools like `inventory_stock_get_all`, `inventory_products_get`, `shopping_list_add_item`, etc., return the Grocy API response as MCP text content (usually stringified JSON).
105
+ Tools like `inventory_stock_get_all`, `inventory_products_get`, `shopping_list_add_item`, etc., return successful Grocy API responses as structured MCP tool output:
106
+
107
+ - `structuredContent.data`: the parsed Grocy response.
108
+ - `content`: short human-readable status text.
106
109
 
107
110
  Example shape for a product-related read (illustrative):
108
111
 
@@ -111,14 +114,15 @@ Example shape for a product-related read (illustrative):
111
114
  "content": [
112
115
  {
113
116
  "type": "text",
114
- "text": "{\n \"id\": \"1\",\n \"name\": \"Cookies\",\n \"description\": null,\n \"product_group_id\": \"1\",\n \"qu_id_purchase\": \"2\",\n \"qu_id_stock\": \"2\",\n \"qu_factor_purchase_to_stock\": \"1.0\",\n \"barcode\": null,\n \"min_stock_amount\": \"0\",\n \"default_best_before_days\": \"0\",\n \"default_best_before_days_after_open\": \"0\",\n \"default_best_before_days_after_freezing\": \"0\",\n \"default_best_before_days_after_thawing\": \"0\",\n \"picture_file_name\": null,\n \"allow_partial_units_in_stock\": \"0\",\n \"row_created_timestamp\": \"2023-01-01 10:00:00\",\n \"show_in_recipes_list\": \"1\",\n \"has_sub_products\": \"0\",\n \"active\": \"1\",\n \"calories\": null,\n \"cumulate_min_stock_amount_of_sub_products\": \"0\",\n \"due_type\": \"1\",\n \"quick_consume_amount\": \"1.0\",\n \"hide_on_stock_overview\": \"0\",\n \"default_stock_label_type\": \"0\",\n \"should_not_be_frozen\": \"0\",\n \"treat_opened_as_out_of_stock\": \"1\",
115
- \"no_own_stock\": \"0\",
116
- \"default_consume_location_id\": null,
117
- \"move_on_open\": \"0\",
118
- \"userfields\": null
119
- }"
117
+ "text": "Product retrieved successfully"
120
118
  }
121
- ]
119
+ ],
120
+ "structuredContent": {
121
+ "data": {
122
+ "id": "1",
123
+ "name": "Cookies"
124
+ }
125
+ }
122
126
  }
123
127
  ```
124
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-grocy",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Model Context Protocol (MCP) server for Grocy integration",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "prebuild": "node scripts/build.js",
22
22
  "build": "tsc",
23
- "prepare": "npm run build",
23
+ "prepare": "husky",
24
24
  "start": "node build/main.js",
25
25
  "dev": "npm run build && node build/main.js",
26
26
  "watch": "tsc --watch",
@@ -71,6 +71,8 @@
71
71
  "eslint": "^9.39.4",
72
72
  "eslint-config-prettier": "^10.1.8",
73
73
  "fs-extra": "^11.3.4",
74
+ "husky": "^9.1.7",
75
+ "lint-staged": "^17.0.8",
74
76
  "prettier": "^3.8.1",
75
77
  "semantic-release": "^25.0.3",
76
78
  "ts-node": "^10.9.1",
@@ -103,5 +105,14 @@
103
105
  },
104
106
  "config": {
105
107
  "supportedGrocyVersion": "4.5.0"
108
+ },
109
+ "lint-staged": {
110
+ "*.{ts,js,mjs,cjs}": [
111
+ "eslint --fix",
112
+ "prettier --write"
113
+ ],
114
+ "*.{json,yaml,yml,md}": [
115
+ "prettier --write"
116
+ ]
106
117
  }
107
118
  }
@@ -1,119 +0,0 @@
1
- /**
2
- * Simplified API client for Grocy
3
- */
4
- import axios from 'axios';
5
- import https from 'https';
6
- import { config } from '../config/index.js';
7
- import { logger } from '../utils/logger.js';
8
- import { ApiError, ErrorHandler } from '../utils/errors.js';
9
- export class GrocyApiClient {
10
- axiosInstance;
11
- constructor() {
12
- this.axiosInstance = this.createAxiosInstance();
13
- this.setupInterceptors();
14
- }
15
- createAxiosInstance() {
16
- const instance = axios.create({
17
- baseURL: config.grocy.base_url,
18
- validateStatus: () => true, // Handle all status codes manually
19
- timeout: 30000,
20
- maxContentLength: config.grocy.max_response_bytes,
21
- httpsAgent: config.grocy.enable_ssl_verify
22
- ? undefined
23
- : new https.Agent({
24
- rejectUnauthorized: false,
25
- }),
26
- });
27
- return instance;
28
- }
29
- setupInterceptors() {
30
- // Request logging
31
- this.axiosInstance.interceptors.request.use((config) => {
32
- logger.api(`${config.method?.toUpperCase()} ${config.url}`);
33
- return config;
34
- }, (error) => {
35
- logger.error('Request error', 'API', { error: error.message });
36
- return Promise.reject(error);
37
- });
38
- // Response logging
39
- this.axiosInstance.interceptors.response.use((response) => {
40
- if (response.status >= 400) {
41
- logger.warn(`HTTP ${response.status}`, 'API', {
42
- url: response.config?.url,
43
- status: response.status,
44
- });
45
- }
46
- return response;
47
- }, (error) => {
48
- logger.error('Response error', 'API', { error: error.message });
49
- return Promise.reject(error);
50
- });
51
- }
52
- normalizeEndpoint(endpoint) {
53
- if (endpoint.startsWith('/api/'))
54
- return endpoint;
55
- if (endpoint.startsWith('api/'))
56
- return `/${endpoint}`;
57
- if (endpoint.startsWith('/'))
58
- return `/api${endpoint}`;
59
- return `/api/${endpoint}`;
60
- }
61
- buildQueryString(params) {
62
- return Object.entries(params)
63
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
64
- .join('&');
65
- }
66
- async request(endpoint, options = {}) {
67
- const { method = 'GET', body = null, headers = {}, queryParams = {}, timeout } = options;
68
- return ErrorHandler.handleAsync(async () => {
69
- let url = this.normalizeEndpoint(endpoint);
70
- if (Object.keys(queryParams).length > 0) {
71
- url += `?${this.buildQueryString(queryParams)}`;
72
- }
73
- const requestConfig = {
74
- method,
75
- url,
76
- headers: {
77
- Accept: 'application/json',
78
- 'Content-Type': 'application/json',
79
- ...config.getCustomHeaders(),
80
- ...headers,
81
- },
82
- ...(timeout && { timeout }),
83
- };
84
- if (['POST', 'PUT', 'PATCH'].includes(method) && body !== null) {
85
- requestConfig.data = body;
86
- }
87
- const response = await this.axiosInstance.request(requestConfig);
88
- if (response.status >= 400) {
89
- throw new ApiError(response.data?.message || `HTTP ${response.status} error`, response.status, `${method} ${url}`, { responseData: response.data });
90
- }
91
- return {
92
- data: response.data,
93
- status: response.status,
94
- headers: response.headers,
95
- };
96
- }, `API ${method} ${endpoint}`);
97
- }
98
- // Convenience methods
99
- async get(endpoint, options = {}) {
100
- return this.request(endpoint, { ...options, method: 'GET' });
101
- }
102
- async post(endpoint, body, options = {}) {
103
- return this.request(endpoint, { ...options, method: 'POST', body });
104
- }
105
- async put(endpoint, body, options = {}) {
106
- return this.request(endpoint, { ...options, method: 'PUT', body });
107
- }
108
- async delete(endpoint, options = {}) {
109
- return this.request(endpoint, { ...options, method: 'DELETE' });
110
- }
111
- async patch(endpoint, body, options = {}) {
112
- return this.request(endpoint, { ...options, method: 'PATCH', body });
113
- }
114
- }
115
- // Export singleton instance
116
- export const apiClient = new GrocyApiClient();
117
- export default apiClient;
118
- // Re-export ApiError for convenience
119
- export { ApiError };
@@ -1,259 +0,0 @@
1
- /**
2
- * Unified configuration system
3
- * Combines environment variables and YAML configuration
4
- */
5
- import { z } from 'zod';
6
- import { readFileSync, existsSync } from 'fs';
7
- import { resolve, dirname } from 'path';
8
- import { fileURLToPath } from 'url';
9
- import YAML from 'yaml';
10
- import { logger } from '../utils/logger.js';
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- /** Default cap for Grocy HTTP response bodies (axios maxContentLength), in bytes */
13
- export const DEFAULT_MAX_RESPONSE_BYTES = 52_428_800; // 50 MiB
14
- // Environment schema
15
- const EnvironmentSchema = z.object({
16
- // Grocy Configuration
17
- GROCY_BASE_URL: z.string().url().optional(),
18
- GROCY_API_KEY: z.string().optional(),
19
- GROCY_ENABLE_SSL_VERIFY: z.enum(['true', 'false']).optional(),
20
- GROCY_MAX_RESPONSE_BYTES: z.string().regex(/^\d+$/).optional(),
21
- // Server Configuration
22
- REST_RESPONSE_SIZE_LIMIT: z.string().regex(/^\d+$/).optional(),
23
- ENABLE_HTTP_SERVER: z.enum(['true', 'false']).optional(),
24
- HTTP_SERVER_PORT: z.string().regex(/^\d+$/).optional(),
25
- HTTP_CORS_ORIGIN: z.string().optional(),
26
- MCP_HTTP_ACCESS_TOKEN: z.string().optional(),
27
- // Logging Configuration
28
- LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR']).optional(),
29
- LOG_CATEGORIES: z.string().optional(),
30
- // Build Configuration
31
- RELEASE_VERSION: z.string().optional(),
32
- NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
33
- });
34
- // YAML configuration schema
35
- const YamlConfigSchema = z
36
- .object({
37
- server: z
38
- .object({
39
- enable_http_server: z.boolean().default(false),
40
- http_server_port: z.number().min(1).max(65535).default(8080),
41
- /** CORS `Access-Control-Allow-Origin` for HTTP MCP endpoints (`*` or a single origin URL) */
42
- http_cors_origin: z.string().min(1).default('*'),
43
- /** When set, MCP HTTP/SSE routes require `Authorization: Bearer <token>`, `X-MCP-Access-Token`, or `access_token` query (GET only). */
44
- http_access_token: z.string().optional(),
45
- })
46
- .strict()
47
- .default({
48
- enable_http_server: false,
49
- http_server_port: 8080,
50
- http_cors_origin: '*',
51
- }),
52
- grocy: z
53
- .object({
54
- base_url: z.string().url().default('http://localhost:9283'),
55
- api_key: z.string().optional(),
56
- enable_ssl_verify: z.boolean().default(true),
57
- response_size_limit: z.number().positive().default(10000),
58
- /** Max Grocy API response body size in bytes (all tools); larger responses fail fast */
59
- max_response_bytes: z.number().positive().default(DEFAULT_MAX_RESPONSE_BYTES),
60
- })
61
- .strict()
62
- .default({
63
- base_url: 'http://localhost:9283',
64
- enable_ssl_verify: true,
65
- response_size_limit: 10000,
66
- max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
67
- }),
68
- tools: z
69
- .record(z.string(), z
70
- .object({
71
- enabled: z.boolean().default(false),
72
- ack_token: z.string().optional(),
73
- })
74
- .catchall(z.unknown()))
75
- .default({}),
76
- })
77
- .strict();
78
- export class ConfigManager {
79
- static instance;
80
- config;
81
- // Unified config properties - final resolved values
82
- grocy;
83
- server;
84
- tools;
85
- constructor(configPath) {
86
- this.config = this.loadConfig(configPath);
87
- // Expose final resolved values
88
- this.grocy = {
89
- base_url: this.config.yaml.grocy.base_url,
90
- ...(this.config.yaml.grocy.api_key !== undefined && {
91
- api_key: this.config.yaml.grocy.api_key,
92
- }),
93
- enable_ssl_verify: this.config.yaml.grocy.enable_ssl_verify,
94
- response_size_limit: this.config.yaml.grocy.response_size_limit,
95
- max_response_bytes: this.config.yaml.grocy.max_response_bytes,
96
- };
97
- this.server = {
98
- enable_http_server: this.config.yaml.server.enable_http_server,
99
- http_server_port: this.config.yaml.server.http_server_port,
100
- http_cors_origin: this.config.yaml.server.http_cors_origin,
101
- ...(this.config.yaml.server.http_access_token !== undefined &&
102
- this.config.yaml.server.http_access_token !== '' && {
103
- http_access_token: this.config.yaml.server.http_access_token,
104
- }),
105
- };
106
- this.tools = this.config.yaml.tools;
107
- }
108
- static getInstance() {
109
- if (!ConfigManager.instance) {
110
- ConfigManager.instance = new ConfigManager();
111
- }
112
- return ConfigManager.instance;
113
- }
114
- loadConfig(configPath) {
115
- // Load environment variables
116
- const env = this.loadEnvironment();
117
- // Load YAML configuration
118
- const yaml = this.loadYamlConfig(configPath);
119
- // Apply environment variable overrides
120
- this.applyEnvironmentOverrides(yaml, env);
121
- return { env, yaml };
122
- }
123
- loadEnvironment() {
124
- try {
125
- return EnvironmentSchema.parse(process.env);
126
- }
127
- catch (error) {
128
- if (error instanceof z.ZodError) {
129
- logger.error('Invalid environment variables', 'CONFIG');
130
- for (const issue of error.issues) {
131
- const path = issue.path?.length ? issue.path.join('.') : '(root)';
132
- logger.error(`${path}: ${issue.message}`, 'CONFIG');
133
- }
134
- process.exit(1);
135
- }
136
- throw error;
137
- }
138
- }
139
- loadYamlConfig(configPath) {
140
- const yamlPath = this.findConfigFile(configPath);
141
- try {
142
- let configData = {};
143
- if (existsSync(yamlPath)) {
144
- const yamlContent = readFileSync(yamlPath, 'utf8');
145
- configData = YAML.parse(yamlContent) || {};
146
- logger.config(`Loaded YAML config from: ${yamlPath}`);
147
- }
148
- else {
149
- logger.config('No YAML config found, using defaults');
150
- }
151
- return YamlConfigSchema.parse(configData);
152
- }
153
- catch (error) {
154
- if (error instanceof z.ZodError) {
155
- logger.error('Invalid YAML configuration', 'CONFIG');
156
- for (const issue of error.issues) {
157
- const path = issue.path?.length ? issue.path.join('.') : '(root)';
158
- logger.error(`${path}: ${issue.message}`, 'CONFIG');
159
- }
160
- process.exit(1);
161
- }
162
- throw error;
163
- }
164
- }
165
- findConfigFile(configPath) {
166
- if (configPath)
167
- return configPath;
168
- // Look for config files in the following order:
169
- // 1. Current working directory (for development)
170
- // 2. Project root (relative to the compiled main.js)
171
- const projectRoot = resolve(__dirname, '../..');
172
- const possiblePaths = [
173
- resolve(process.cwd(), 'mcp-grocy.yaml'),
174
- resolve(process.cwd(), 'mcp-grocy.yml'),
175
- resolve(projectRoot, 'mcp-grocy.yaml'),
176
- resolve(projectRoot, 'mcp-grocy.yml'),
177
- ];
178
- return possiblePaths.find((path) => existsSync(path)) ?? possiblePaths[0];
179
- }
180
- // Public getters
181
- getConfig() {
182
- return this.config;
183
- }
184
- getApiUrl() {
185
- return this.grocy.base_url.endsWith('/')
186
- ? `${this.grocy.base_url}api`
187
- : `${this.grocy.base_url}/api`;
188
- }
189
- getCustomHeaders() {
190
- const headers = {};
191
- if (this.grocy.api_key) {
192
- headers['GROCY-API-KEY'] = this.grocy.api_key;
193
- }
194
- return headers;
195
- }
196
- /**
197
- * Apply environment variable overrides to YAML configuration
198
- */
199
- applyEnvironmentOverrides(yaml, env) {
200
- // Grocy configuration overrides
201
- if (env.GROCY_BASE_URL) {
202
- yaml.grocy.base_url = env.GROCY_BASE_URL;
203
- }
204
- if (env.GROCY_API_KEY) {
205
- yaml.grocy.api_key = env.GROCY_API_KEY;
206
- }
207
- if (env.GROCY_ENABLE_SSL_VERIFY !== undefined) {
208
- yaml.grocy.enable_ssl_verify = env.GROCY_ENABLE_SSL_VERIFY === 'true';
209
- }
210
- if (env.REST_RESPONSE_SIZE_LIMIT !== undefined) {
211
- yaml.grocy.response_size_limit = parseInt(env.REST_RESPONSE_SIZE_LIMIT, 10);
212
- }
213
- if (env.GROCY_MAX_RESPONSE_BYTES !== undefined) {
214
- yaml.grocy.max_response_bytes = parseInt(env.GROCY_MAX_RESPONSE_BYTES, 10);
215
- }
216
- // Server configuration overrides
217
- if (env.ENABLE_HTTP_SERVER !== undefined) {
218
- yaml.server.enable_http_server = env.ENABLE_HTTP_SERVER === 'true';
219
- }
220
- if (env.HTTP_SERVER_PORT !== undefined) {
221
- yaml.server.http_server_port = parseInt(env.HTTP_SERVER_PORT, 10);
222
- }
223
- if (env.HTTP_CORS_ORIGIN !== undefined && env.HTTP_CORS_ORIGIN.length > 0) {
224
- yaml.server.http_cors_origin = env.HTTP_CORS_ORIGIN;
225
- }
226
- if (env.MCP_HTTP_ACCESS_TOKEN !== undefined) {
227
- yaml.server.http_access_token =
228
- env.MCP_HTTP_ACCESS_TOKEN.length > 0 ? env.MCP_HTTP_ACCESS_TOKEN : undefined;
229
- }
230
- }
231
- parseToolConfiguration() {
232
- const enabledTools = new Set();
233
- const toolSubConfigs = new Map();
234
- const toolAckTokens = new Map();
235
- for (const [toolName, toolConfig] of Object.entries(this.config.yaml.tools)) {
236
- if (toolConfig.enabled) {
237
- enabledTools.add(toolName);
238
- // Store ack_token separately if configured
239
- if (toolConfig.ack_token && typeof toolConfig.ack_token === 'string') {
240
- toolAckTokens.set(toolName, toolConfig.ack_token);
241
- }
242
- // Extract sub-configs (everything except standard fields)
243
- const subConfigs = new Map();
244
- for (const [key, value] of Object.entries(toolConfig)) {
245
- if (!['enabled', 'ack_token'].includes(key)) {
246
- subConfigs.set(key, value);
247
- }
248
- }
249
- if (subConfigs.size > 0) {
250
- toolSubConfigs.set(toolName, subConfigs);
251
- }
252
- }
253
- }
254
- return { enabledTools, toolSubConfigs, toolAckTokens };
255
- }
256
- }
257
- // Export singleton instance
258
- export const config = ConfigManager.getInstance();
259
- export default config;
package/build/main.js DELETED
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Simplified main entry point
4
- * Demonstrates the refactored architecture
5
- */
6
- // Load environment variables
7
- import 'dotenv/config';
8
- import { GrocyMcpServer } from './server/mcp-server.js';
9
- import { config } from './config/index.js';
10
- import { VERSION, PACKAGE_NAME as SERVER_NAME } from './version.js';
11
- import { logger } from './utils/logger.js';
12
- import { ErrorHandler } from './utils/errors.js';
13
- // Startup banner
14
- logger.info(`Starting ${SERVER_NAME} v${VERSION}`, 'SERVER');
15
- async function main() {
16
- return ErrorHandler.handleAsync(async () => {
17
- // Check API key
18
- if (!config.grocy.api_key) {
19
- logger.warn('No API key configured. Some operations may fail.', 'CONFIG');
20
- }
21
- // Log configuration summary
22
- logger.config(`Grocy URL: ${config.grocy.base_url}`);
23
- logger.config(`SSL Verify: ${config.grocy.enable_ssl_verify}`);
24
- logger.config(`HTTP Server: ${config.server.enable_http_server}`);
25
- // Create and start server
26
- const server = await GrocyMcpServer.create();
27
- await server.start();
28
- logger.info('Server started successfully', 'SERVER');
29
- }, 'server startup');
30
- }
31
- // Error handling
32
- process.on('uncaughtException', (error) => {
33
- logger.error('Uncaught exception', 'PROCESS', { error });
34
- process.exit(1);
35
- });
36
- process.on('unhandledRejection', (reason, promise) => {
37
- logger.error('Unhandled rejection', 'PROCESS', { reason, promise });
38
- process.exit(1);
39
- });
40
- // Start the application
41
- main().catch((error) => {
42
- logger.error('Failed to start server', 'SERVER', { error });
43
- process.exit(1);
44
- });