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.
- package/CHANGELOG.md +19 -5
- package/README.md +13 -0
- package/build/resources/CHANGELOG.md +20 -4
- package/build/resources/README.md +13 -0
- package/build/resources/config.md +19 -4
- package/build/resources/response-format.md +12 -8
- package/package.json +13 -2
- package/build/api/client.js +0 -119
- package/build/config/index.js +0 -259
- package/build/main.js +0 -44
- package/build/server/http-server.js +0 -330
- package/build/server/mcp-server.js +0 -210
- package/build/server/resources.js +0 -73
- package/build/server/tool-input-zod.js +0 -86
- package/build/tools/base.js +0 -145
- package/build/tools/household/definitions.js +0 -161
- package/build/tools/household/handlers.js +0 -109
- package/build/tools/household/index.js +0 -25
- package/build/tools/index.js +0 -2
- package/build/tools/inventory/definitions.js +0 -416
- package/build/tools/inventory/handlers.js +0 -443
- package/build/tools/inventory/index.js +0 -32
- package/build/tools/module-loader.js +0 -154
- package/build/tools/recipes/definitions.js +0 -278
- package/build/tools/recipes/handlers.js +0 -477
- package/build/tools/recipes/index.js +0 -33
- package/build/tools/recipes/validations.js +0 -29
- package/build/tools/shopping/definitions.js +0 -73
- package/build/tools/shopping/handlers.js +0 -43
- package/build/tools/shopping/index.js +0 -14
- package/build/tools/system/definitions.js +0 -89
- package/build/tools/system/handlers.js +0 -156
- package/build/tools/system/index.js +0 -16
- package/build/tools/types.js +0 -1
- package/build/tools/validation-helpers.js +0 -39
- package/build/types/index.js +0 -64
- package/build/utils/errors.js +0 -143
- package/build/utils/logger.js +0 -142
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
# [2.
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
|
27
|
-
| `ENABLE_HTTP_SERVER`
|
|
28
|
-
| `HTTP_SERVER_PORT`
|
|
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
|
|
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": "
|
|
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.
|
|
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": "
|
|
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
|
}
|
package/build/api/client.js
DELETED
|
@@ -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 };
|
package/build/config/index.js
DELETED
|
@@ -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
|
-
});
|