mcp-grocy 2.2.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 +45 -0
- package/README.md +125 -32
- package/build/resources/CHANGELOG.md +47 -0
- package/build/resources/DOCS.md +8 -3
- package/build/resources/README.md +125 -32
- package/build/resources/api-reference.md +88 -88
- package/build/resources/config.md +45 -17
- package/build/resources/examples.md +120 -221
- package/build/resources/installation.md +9 -14
- package/build/resources/response-format.md +29 -20
- package/build/version.js +2 -2
- package/package.json +50 -24
- package/build/api/client.js +0 -121
- package/build/config/index.js +0 -205
- package/build/main.js +0 -44
- package/build/server/http-server.js +0 -218
- package/build/server/mcp-server.js +0 -163
- package/build/server/resources.js +0 -60
- package/build/tools/base.js +0 -141
- package/build/tools/household/definitions.js +0 -157
- 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 -379
- package/build/tools/inventory/handlers.js +0 -430
- package/build/tools/inventory/index.js +0 -32
- package/build/tools/module-loader.js +0 -151
- package/build/tools/recipes/definitions.js +0 -261
- package/build/tools/recipes/handlers.js +0 -471
- package/build/tools/recipes/index.js +0 -33
- package/build/tools/recipes/validations.js +0 -23
- package/build/tools/shopping/definitions.js +0 -71
- package/build/tools/shopping/handlers.js +0 -43
- package/build/tools/shopping/index.js +0 -14
- package/build/tools/system/definitions.js +0 -86
- package/build/tools/system/handlers.js +0 -94
- package/build/tools/system/index.js +0 -16
- package/build/tools/types.js +0 -1
- package/build/tools/validation-helpers.js +0 -36
- package/build/types/index.js +0 -62
- package/build/utils/errors.js +0 -138
- package/build/utils/logger.js +0 -141
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Grocy API Response Format Documentation
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The dev tool **`system_dev_test_request`** returns a JSON payload with request details, response information, and validation results. Other tools (e.g. `inventory_stock_get_all`, `shopping_list_add_item`) return the Grocy API body as text content (typically stringified JSON).
|
|
4
4
|
|
|
5
|
-
## `
|
|
5
|
+
## `system_dev_test_request` tool response structure
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
@@ -29,7 +29,7 @@ The Grocy API testing tool (`test_request`) returns a comprehensive JSON respons
|
|
|
29
29
|
"id": "1",
|
|
30
30
|
"name": "Cookies",
|
|
31
31
|
"description": null,
|
|
32
|
-
"product_group_id": "1"
|
|
32
|
+
"product_group_id": "1"
|
|
33
33
|
// ... other product fields ...
|
|
34
34
|
}
|
|
35
35
|
},
|
|
@@ -72,9 +72,10 @@ GROCY_API_KEY=your-private-api-key
|
|
|
72
72
|
|
|
73
73
|
These values can be set in your `.env` file for local development or in your project configuration for production use.
|
|
74
74
|
|
|
75
|
-
## Response Fields for `
|
|
75
|
+
## Response Fields for `system_dev_test_request`
|
|
76
76
|
|
|
77
77
|
### Request Details (`request`)
|
|
78
|
+
|
|
78
79
|
- `url`: Full URL of the Grocy API endpoint called, including base URL and path.
|
|
79
80
|
- `method`: HTTP method used (e.g., GET, POST, PUT, DELETE).
|
|
80
81
|
- `headers`: Request headers sent to the Grocy API. Sensitive headers like `GROCY-API-KEY` will have their values redacted.
|
|
@@ -82,6 +83,7 @@ These values can be set in your `.env` file for local development or in your pro
|
|
|
82
83
|
- `authMethod`: Authentication method used. For Grocy, this will typically be `apikey` if `GROCY_API_KEY` is configured, or `none`.
|
|
83
84
|
|
|
84
85
|
### Response Details (`response`)
|
|
86
|
+
|
|
85
87
|
- `statusCode`: HTTP status code returned by the Grocy API (e.g., 200, 400, 401).
|
|
86
88
|
- `statusText`: HTTP status message (e.g., "OK", "Bad Request").
|
|
87
89
|
- `timing`: Duration of the API request in milliseconds.
|
|
@@ -89,6 +91,7 @@ These values can be set in your `.env` file for local development or in your pro
|
|
|
89
91
|
- `body`: Response body content from the Grocy API. This will be the JSON data returned by Grocy.
|
|
90
92
|
|
|
91
93
|
### Validation (`validation`)
|
|
94
|
+
|
|
92
95
|
- `isError`: Boolean, `true` if the HTTP status code is 400 or higher, indicating an error.
|
|
93
96
|
- `messages`: Array of messages, including success messages or error details.
|
|
94
97
|
- `truncated` (optional): If the response body exceeds `REST_RESPONSE_SIZE_LIMIT`, this object will contain details about the truncation.
|
|
@@ -99,26 +102,32 @@ These values can be set in your `.env` file for local development or in your pro
|
|
|
99
102
|
|
|
100
103
|
## Specialized Grocy Tools Response Format
|
|
101
104
|
|
|
102
|
-
Tools like `
|
|
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.
|
|
109
|
+
|
|
110
|
+
Example shape for a product-related read (illustrative):
|
|
103
111
|
|
|
104
|
-
Example for `get_product` (if it existed as a specialized tool for a single product):
|
|
105
112
|
```json
|
|
106
113
|
{
|
|
107
114
|
"content": [
|
|
108
115
|
{
|
|
109
116
|
"type": "text",
|
|
110
|
-
"text": "
|
|
111
|
-
\"no_own_stock\": \"0\",
|
|
112
|
-
\"default_consume_location_id\": null,
|
|
113
|
-
\"move_on_open\": \"0\",
|
|
114
|
-
\"userfields\": null
|
|
115
|
-
}"
|
|
117
|
+
"text": "Product retrieved successfully"
|
|
116
118
|
}
|
|
117
|
-
]
|
|
119
|
+
],
|
|
120
|
+
"structuredContent": {
|
|
121
|
+
"data": {
|
|
122
|
+
"id": "1",
|
|
123
|
+
"name": "Cookies"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
118
126
|
}
|
|
119
127
|
```
|
|
120
128
|
|
|
121
129
|
If an error occurs with a specialized tool, the response will typically look like:
|
|
130
|
+
|
|
122
131
|
```json
|
|
123
132
|
{
|
|
124
133
|
"content": [
|
|
@@ -131,9 +140,10 @@ If an error occurs with a specialized tool, the response will typically look lik
|
|
|
131
140
|
}
|
|
132
141
|
```
|
|
133
142
|
|
|
134
|
-
## Error
|
|
143
|
+
## Error response example for `system_dev_test_request`
|
|
144
|
+
|
|
145
|
+
If **`system_dev_test_request`** encounters an API error (e.g., authentication failure):
|
|
135
146
|
|
|
136
|
-
If the `test_request` tool encounters an API error (e.g., authentication failure):
|
|
137
147
|
```json
|
|
138
148
|
{
|
|
139
149
|
"request": {
|
|
@@ -149,17 +159,16 @@ If the `test_request` tool encounters an API error (e.g., authentication failure
|
|
|
149
159
|
"statusCode": 401,
|
|
150
160
|
"statusText": "Unauthorized",
|
|
151
161
|
"timing": "50ms",
|
|
152
|
-
"headers": {
|
|
162
|
+
"headers": {
|
|
163
|
+
/* ... headers ... */
|
|
164
|
+
},
|
|
153
165
|
"body": {
|
|
154
166
|
"error_message": "API key is missing or invalid."
|
|
155
167
|
}
|
|
156
168
|
},
|
|
157
169
|
"validation": {
|
|
158
170
|
"isError": true,
|
|
159
|
-
"messages": [
|
|
160
|
-
"Request failed with status 401",
|
|
161
|
-
"API key is missing or invalid."
|
|
162
|
-
]
|
|
171
|
+
"messages": ["Request failed with status 401", "API key is missing or invalid."]
|
|
163
172
|
}
|
|
164
173
|
}
|
|
165
174
|
```
|
package/build/version.js
CHANGED
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",
|
|
@@ -14,16 +14,28 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"build",
|
|
16
16
|
"README.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"prebuild": "node scripts/build.js",
|
|
21
22
|
"build": "tsc",
|
|
22
|
-
"prepare": "
|
|
23
|
+
"prepare": "husky",
|
|
24
|
+
"start": "node build/main.js",
|
|
25
|
+
"dev": "npm run build && node build/main.js",
|
|
23
26
|
"watch": "tsc --watch",
|
|
24
27
|
"inspector": "npx @modelcontextprotocol/inspector build/main.js",
|
|
25
28
|
"test": "vitest run",
|
|
29
|
+
"test:mcp-validator": "bash scripts/run-mcp-validator.sh",
|
|
30
|
+
"dev:mcp-tef": "bash scripts/run-mcp-tef.sh",
|
|
31
|
+
"report:mcp-tef": "bash scripts/mcp-tef-report.sh",
|
|
32
|
+
"start:mcp-http-test": "bash scripts/start-mcp-http-for-tests.sh",
|
|
33
|
+
"stop:mcp-http-test": "bash scripts/stop-mcp-http-for-tests.sh",
|
|
26
34
|
"test:watch": "vitest",
|
|
35
|
+
"lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
36
|
+
"lint:fix": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" --fix",
|
|
37
|
+
"format": "prettier --write .",
|
|
38
|
+
"format:check": "prettier --check .",
|
|
27
39
|
"check-tools": "node scripts/check-tools.js",
|
|
28
40
|
"validate-release": "node scripts/check-tools.js --validate-release",
|
|
29
41
|
"update-api-docs": "node scripts/fetch-grocy-api.js",
|
|
@@ -35,33 +47,38 @@
|
|
|
35
47
|
"prepare-dev-release:dryrun": "node scripts/prepare-dev-release.js --dry-run"
|
|
36
48
|
},
|
|
37
49
|
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
39
|
-
"axios": "^1.6
|
|
40
|
-
"cors": "^2.8.
|
|
41
|
-
"dotenv": "^
|
|
42
|
-
"express": "^5.1
|
|
43
|
-
"fuse.js": "^7.
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"zod": "^3.22.0"
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
51
|
+
"axios": "^1.13.6",
|
|
52
|
+
"cors": "^2.8.6",
|
|
53
|
+
"dotenv": "^17.3.1",
|
|
54
|
+
"express": "^5.2.1",
|
|
55
|
+
"fuse.js": "^7.1.0",
|
|
56
|
+
"yaml": "^2.8.3",
|
|
57
|
+
"zod": "^4.3.6"
|
|
47
58
|
},
|
|
48
59
|
"devDependencies": {
|
|
49
|
-
"@commitlint/cli": "^
|
|
50
|
-
"@commitlint/config-conventional": "^
|
|
51
|
-
"@commitlint/types": "^
|
|
60
|
+
"@commitlint/cli": "^20.5.0",
|
|
61
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
62
|
+
"@commitlint/types": "^20.5.0",
|
|
63
|
+
"@eslint/js": "^9.39.4",
|
|
52
64
|
"@semantic-release/changelog": "^6.0.3",
|
|
53
|
-
"@semantic-release/exec": "^
|
|
65
|
+
"@semantic-release/exec": "^7.1.0",
|
|
54
66
|
"@semantic-release/git": "^10.0.1",
|
|
55
|
-
"@types/cors": "^2.8.
|
|
56
|
-
"@types/express": "^5.0.
|
|
57
|
-
"@types/node": "^
|
|
58
|
-
"@types/uuid": "^10.0.0",
|
|
67
|
+
"@types/cors": "^2.8.19",
|
|
68
|
+
"@types/express": "^5.0.6",
|
|
69
|
+
"@types/node": "^22.15.0",
|
|
59
70
|
"@types/yaml": "^1.9.6",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
71
|
+
"eslint": "^9.39.4",
|
|
72
|
+
"eslint-config-prettier": "^10.1.8",
|
|
73
|
+
"fs-extra": "^11.3.4",
|
|
74
|
+
"husky": "^9.1.7",
|
|
75
|
+
"lint-staged": "^17.0.8",
|
|
76
|
+
"prettier": "^3.8.1",
|
|
77
|
+
"semantic-release": "^25.0.3",
|
|
62
78
|
"ts-node": "^10.9.1",
|
|
63
|
-
"typescript": "^5.
|
|
64
|
-
"
|
|
79
|
+
"typescript": "^5.9.3",
|
|
80
|
+
"typescript-eslint": "^8.57.2",
|
|
81
|
+
"vitest": "^4.1.2"
|
|
65
82
|
},
|
|
66
83
|
"keywords": [
|
|
67
84
|
"grocy",
|
|
@@ -84,9 +101,18 @@
|
|
|
84
101
|
},
|
|
85
102
|
"homepage": "https://github.com/miguelangel-nubla/mcp-grocy#readme",
|
|
86
103
|
"engines": {
|
|
87
|
-
"node": ">=
|
|
104
|
+
"node": ">=22.0.0"
|
|
88
105
|
},
|
|
89
106
|
"config": {
|
|
90
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
|
+
]
|
|
91
117
|
}
|
|
92
118
|
}
|
package/build/api/client.js
DELETED
|
@@ -1,121 +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
|
-
API_KEY_HEADER = 'GROCY-API-KEY';
|
|
12
|
-
constructor() {
|
|
13
|
-
this.axiosInstance = this.createAxiosInstance();
|
|
14
|
-
this.setupInterceptors();
|
|
15
|
-
}
|
|
16
|
-
createAxiosInstance() {
|
|
17
|
-
const instance = axios.create({
|
|
18
|
-
baseURL: config.grocy.base_url,
|
|
19
|
-
validateStatus: () => true, // Handle all status codes manually
|
|
20
|
-
timeout: 30000,
|
|
21
|
-
httpsAgent: config.grocy.enable_ssl_verify ? undefined : new https.Agent({
|
|
22
|
-
rejectUnauthorized: false
|
|
23
|
-
})
|
|
24
|
-
});
|
|
25
|
-
// Set default authentication
|
|
26
|
-
if (config.grocy.api_key) {
|
|
27
|
-
instance.defaults.headers.common[this.API_KEY_HEADER] = config.grocy.api_key;
|
|
28
|
-
}
|
|
29
|
-
return instance;
|
|
30
|
-
}
|
|
31
|
-
setupInterceptors() {
|
|
32
|
-
// Request logging
|
|
33
|
-
this.axiosInstance.interceptors.request.use((config) => {
|
|
34
|
-
logger.api(`${config.method?.toUpperCase()} ${config.url}`);
|
|
35
|
-
return config;
|
|
36
|
-
}, (error) => {
|
|
37
|
-
logger.error('Request error', 'API', { error: error.message });
|
|
38
|
-
return Promise.reject(error);
|
|
39
|
-
});
|
|
40
|
-
// Response logging
|
|
41
|
-
this.axiosInstance.interceptors.response.use((response) => {
|
|
42
|
-
if (response.status >= 400) {
|
|
43
|
-
logger.warn(`HTTP ${response.status}`, 'API', {
|
|
44
|
-
url: response.config?.url,
|
|
45
|
-
status: response.status
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
return response;
|
|
49
|
-
}, (error) => {
|
|
50
|
-
logger.error('Response error', 'API', { error: error.message });
|
|
51
|
-
return Promise.reject(error);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
normalizeEndpoint(endpoint) {
|
|
55
|
-
if (endpoint.startsWith('/api/'))
|
|
56
|
-
return endpoint;
|
|
57
|
-
if (endpoint.startsWith('api/'))
|
|
58
|
-
return `/${endpoint}`;
|
|
59
|
-
if (endpoint.startsWith('/'))
|
|
60
|
-
return `/api${endpoint}`;
|
|
61
|
-
return `/api/${endpoint}`;
|
|
62
|
-
}
|
|
63
|
-
buildQueryString(params) {
|
|
64
|
-
return Object.entries(params)
|
|
65
|
-
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
66
|
-
.join('&');
|
|
67
|
-
}
|
|
68
|
-
async request(endpoint, options = {}) {
|
|
69
|
-
const { method = 'GET', body = null, headers = {}, queryParams = {}, timeout } = options;
|
|
70
|
-
return ErrorHandler.handleAsync(async () => {
|
|
71
|
-
let url = this.normalizeEndpoint(endpoint);
|
|
72
|
-
if (Object.keys(queryParams).length > 0) {
|
|
73
|
-
url += `?${this.buildQueryString(queryParams)}`;
|
|
74
|
-
}
|
|
75
|
-
const requestConfig = {
|
|
76
|
-
method,
|
|
77
|
-
url,
|
|
78
|
-
headers: {
|
|
79
|
-
'Accept': 'application/json',
|
|
80
|
-
'Content-Type': 'application/json',
|
|
81
|
-
...config.getCustomHeaders(),
|
|
82
|
-
...headers
|
|
83
|
-
},
|
|
84
|
-
...(timeout && { timeout })
|
|
85
|
-
};
|
|
86
|
-
if (['POST', 'PUT', 'PATCH'].includes(method) && body !== null) {
|
|
87
|
-
requestConfig.data = body;
|
|
88
|
-
}
|
|
89
|
-
const response = await this.axiosInstance.request(requestConfig);
|
|
90
|
-
if (response.status >= 400) {
|
|
91
|
-
throw new ApiError(response.data?.message || `HTTP ${response.status} error`, response.status, `${method} ${url}`, { responseData: response.data });
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
data: response.data,
|
|
95
|
-
status: response.status,
|
|
96
|
-
headers: response.headers
|
|
97
|
-
};
|
|
98
|
-
}, `API ${method} ${endpoint}`);
|
|
99
|
-
}
|
|
100
|
-
// Convenience methods
|
|
101
|
-
async get(endpoint, options = {}) {
|
|
102
|
-
return this.request(endpoint, { ...options, method: 'GET' });
|
|
103
|
-
}
|
|
104
|
-
async post(endpoint, body, options = {}) {
|
|
105
|
-
return this.request(endpoint, { ...options, method: 'POST', body });
|
|
106
|
-
}
|
|
107
|
-
async put(endpoint, body, options = {}) {
|
|
108
|
-
return this.request(endpoint, { ...options, method: 'PUT', body });
|
|
109
|
-
}
|
|
110
|
-
async delete(endpoint, options = {}) {
|
|
111
|
-
return this.request(endpoint, { ...options, method: 'DELETE' });
|
|
112
|
-
}
|
|
113
|
-
async patch(endpoint, body, options = {}) {
|
|
114
|
-
return this.request(endpoint, { ...options, method: 'PATCH', body });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// Export singleton instance
|
|
118
|
-
export const apiClient = new GrocyApiClient();
|
|
119
|
-
export default apiClient;
|
|
120
|
-
// Re-export ApiError for convenience
|
|
121
|
-
export { ApiError };
|
package/build/config/index.js
DELETED
|
@@ -1,205 +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
|
-
// Environment schema
|
|
13
|
-
const EnvironmentSchema = z.object({
|
|
14
|
-
// Grocy Configuration
|
|
15
|
-
GROCY_BASE_URL: z.string().url().optional(),
|
|
16
|
-
GROCY_API_KEY: z.string().optional(),
|
|
17
|
-
GROCY_ENABLE_SSL_VERIFY: z.enum(['true', 'false']).optional(),
|
|
18
|
-
// Server Configuration
|
|
19
|
-
REST_RESPONSE_SIZE_LIMIT: z.string().regex(/^\d+$/).optional(),
|
|
20
|
-
ENABLE_HTTP_SERVER: z.enum(['true', 'false']).optional(),
|
|
21
|
-
HTTP_SERVER_PORT: z.string().regex(/^\d+$/).optional(),
|
|
22
|
-
// Logging Configuration
|
|
23
|
-
LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR']).optional(),
|
|
24
|
-
LOG_CATEGORIES: z.string().optional(),
|
|
25
|
-
// Build Configuration
|
|
26
|
-
RELEASE_VERSION: z.string().optional(),
|
|
27
|
-
NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
|
|
28
|
-
});
|
|
29
|
-
// YAML configuration schema
|
|
30
|
-
const YamlConfigSchema = z.object({
|
|
31
|
-
server: z.object({
|
|
32
|
-
enable_http_server: z.boolean().default(false),
|
|
33
|
-
http_server_port: z.number().min(1).max(65535).default(8080),
|
|
34
|
-
}).default({}),
|
|
35
|
-
grocy: z.object({
|
|
36
|
-
base_url: z.string().url().default('http://localhost:9283'),
|
|
37
|
-
api_key: z.string().optional(),
|
|
38
|
-
enable_ssl_verify: z.boolean().default(true),
|
|
39
|
-
response_size_limit: z.number().positive().default(10000),
|
|
40
|
-
}).default({}),
|
|
41
|
-
tools: z.record(z.string(), z.object({
|
|
42
|
-
enabled: z.boolean().default(false),
|
|
43
|
-
ack_token: z.string().optional(),
|
|
44
|
-
}).catchall(z.unknown())).default({}),
|
|
45
|
-
});
|
|
46
|
-
export class ConfigManager {
|
|
47
|
-
static instance;
|
|
48
|
-
config;
|
|
49
|
-
// Unified config properties - final resolved values
|
|
50
|
-
grocy;
|
|
51
|
-
server;
|
|
52
|
-
tools;
|
|
53
|
-
constructor(configPath) {
|
|
54
|
-
this.config = this.loadConfig(configPath);
|
|
55
|
-
// Expose final resolved values
|
|
56
|
-
this.grocy = {
|
|
57
|
-
base_url: this.config.yaml.grocy.base_url,
|
|
58
|
-
...(this.config.yaml.grocy.api_key !== undefined && { api_key: this.config.yaml.grocy.api_key }),
|
|
59
|
-
enable_ssl_verify: this.config.yaml.grocy.enable_ssl_verify,
|
|
60
|
-
response_size_limit: this.config.yaml.grocy.response_size_limit
|
|
61
|
-
};
|
|
62
|
-
this.server = {
|
|
63
|
-
enable_http_server: this.config.yaml.server.enable_http_server,
|
|
64
|
-
http_server_port: this.config.yaml.server.http_server_port
|
|
65
|
-
};
|
|
66
|
-
this.tools = this.config.yaml.tools;
|
|
67
|
-
}
|
|
68
|
-
static getInstance() {
|
|
69
|
-
if (!ConfigManager.instance) {
|
|
70
|
-
ConfigManager.instance = new ConfigManager();
|
|
71
|
-
}
|
|
72
|
-
return ConfigManager.instance;
|
|
73
|
-
}
|
|
74
|
-
loadConfig(configPath) {
|
|
75
|
-
// Load environment variables
|
|
76
|
-
const env = this.loadEnvironment();
|
|
77
|
-
// Load YAML configuration
|
|
78
|
-
const yaml = this.loadYamlConfig(configPath);
|
|
79
|
-
// Apply environment variable overrides
|
|
80
|
-
this.applyEnvironmentOverrides(yaml, env);
|
|
81
|
-
return { env, yaml };
|
|
82
|
-
}
|
|
83
|
-
loadEnvironment() {
|
|
84
|
-
try {
|
|
85
|
-
return EnvironmentSchema.parse(process.env);
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
if (error instanceof z.ZodError) {
|
|
89
|
-
logger.error('Invalid environment variables', 'CONFIG');
|
|
90
|
-
error.errors.forEach(err => {
|
|
91
|
-
logger.error(`${err.path.join('.')}: ${err.message}`, 'CONFIG');
|
|
92
|
-
});
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
loadYamlConfig(configPath) {
|
|
99
|
-
const yamlPath = this.findConfigFile(configPath);
|
|
100
|
-
try {
|
|
101
|
-
let configData = {};
|
|
102
|
-
if (existsSync(yamlPath)) {
|
|
103
|
-
const yamlContent = readFileSync(yamlPath, 'utf8');
|
|
104
|
-
configData = YAML.parse(yamlContent) || {};
|
|
105
|
-
logger.config(`Loaded YAML config from: ${yamlPath}`);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
logger.config('No YAML config found, using defaults');
|
|
109
|
-
}
|
|
110
|
-
return YamlConfigSchema.parse(configData);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
if (error instanceof z.ZodError) {
|
|
114
|
-
logger.error('Invalid YAML configuration', 'CONFIG');
|
|
115
|
-
error.errors.forEach(err => {
|
|
116
|
-
logger.error(`${err.path.join('.')}: ${err.message}`, 'CONFIG');
|
|
117
|
-
});
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
throw error;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
findConfigFile(configPath) {
|
|
124
|
-
if (configPath)
|
|
125
|
-
return configPath;
|
|
126
|
-
// Look for config files in the following order:
|
|
127
|
-
// 1. Current working directory (for development)
|
|
128
|
-
// 2. Project root (relative to the compiled main.js)
|
|
129
|
-
const projectRoot = resolve(__dirname, '../..');
|
|
130
|
-
const possiblePaths = [
|
|
131
|
-
resolve(process.cwd(), 'mcp-grocy.yaml'),
|
|
132
|
-
resolve(process.cwd(), 'mcp-grocy.yml'),
|
|
133
|
-
resolve(projectRoot, 'mcp-grocy.yaml'),
|
|
134
|
-
resolve(projectRoot, 'mcp-grocy.yml'),
|
|
135
|
-
];
|
|
136
|
-
return possiblePaths.find(path => existsSync(path)) ?? possiblePaths[0];
|
|
137
|
-
}
|
|
138
|
-
// Public getters
|
|
139
|
-
getConfig() {
|
|
140
|
-
return this.config;
|
|
141
|
-
}
|
|
142
|
-
getApiUrl() {
|
|
143
|
-
return this.grocy.base_url.endsWith('/') ? `${this.grocy.base_url}api` : `${this.grocy.base_url}/api`;
|
|
144
|
-
}
|
|
145
|
-
getCustomHeaders() {
|
|
146
|
-
const headers = {};
|
|
147
|
-
if (this.grocy.api_key) {
|
|
148
|
-
headers['GROCY-API-KEY'] = this.grocy.api_key;
|
|
149
|
-
}
|
|
150
|
-
return headers;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Apply environment variable overrides to YAML configuration
|
|
154
|
-
*/
|
|
155
|
-
applyEnvironmentOverrides(yaml, env) {
|
|
156
|
-
// Grocy configuration overrides
|
|
157
|
-
if (env.GROCY_BASE_URL) {
|
|
158
|
-
yaml.grocy.base_url = env.GROCY_BASE_URL;
|
|
159
|
-
}
|
|
160
|
-
if (env.GROCY_API_KEY) {
|
|
161
|
-
yaml.grocy.api_key = env.GROCY_API_KEY;
|
|
162
|
-
}
|
|
163
|
-
if (env.GROCY_ENABLE_SSL_VERIFY !== undefined) {
|
|
164
|
-
yaml.grocy.enable_ssl_verify = env.GROCY_ENABLE_SSL_VERIFY === 'true';
|
|
165
|
-
}
|
|
166
|
-
if (env.REST_RESPONSE_SIZE_LIMIT !== undefined) {
|
|
167
|
-
yaml.grocy.response_size_limit = parseInt(env.REST_RESPONSE_SIZE_LIMIT, 10);
|
|
168
|
-
}
|
|
169
|
-
// Server configuration overrides
|
|
170
|
-
if (env.ENABLE_HTTP_SERVER !== undefined) {
|
|
171
|
-
yaml.server.enable_http_server = env.ENABLE_HTTP_SERVER === 'true';
|
|
172
|
-
}
|
|
173
|
-
if (env.HTTP_SERVER_PORT !== undefined) {
|
|
174
|
-
yaml.server.http_server_port = parseInt(env.HTTP_SERVER_PORT, 10);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
parseToolConfiguration() {
|
|
178
|
-
const enabledTools = new Set();
|
|
179
|
-
const toolSubConfigs = new Map();
|
|
180
|
-
const toolAckTokens = new Map();
|
|
181
|
-
for (const [toolName, toolConfig] of Object.entries(this.config.yaml.tools)) {
|
|
182
|
-
if (toolConfig.enabled) {
|
|
183
|
-
enabledTools.add(toolName);
|
|
184
|
-
// Store ack_token separately if configured
|
|
185
|
-
if (toolConfig.ack_token && typeof toolConfig.ack_token === 'string') {
|
|
186
|
-
toolAckTokens.set(toolName, toolConfig.ack_token);
|
|
187
|
-
}
|
|
188
|
-
// Extract sub-configs (everything except standard fields)
|
|
189
|
-
const subConfigs = new Map();
|
|
190
|
-
for (const [key, value] of Object.entries(toolConfig)) {
|
|
191
|
-
if (!['enabled', 'ack_token'].includes(key)) {
|
|
192
|
-
subConfigs.set(key, value);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (subConfigs.size > 0) {
|
|
196
|
-
toolSubConfigs.set(toolName, subConfigs);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return { enabledTools, toolSubConfigs, toolAckTokens };
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Export singleton instance
|
|
204
|
-
export const config = ConfigManager.getInstance();
|
|
205
|
-
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
|
-
});
|