mcp-grocy 2.7.1 → 2.8.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 +20 -0
- package/README.md +16 -6
- package/build/api/client.js +119 -0
- package/build/config/index.js +283 -0
- package/build/main.js +44 -0
- package/build/resources/CHANGELOG.md +22 -3
- package/build/resources/README.md +16 -6
- package/build/resources/api-reference.md +2 -1
- package/build/resources/examples.md +13 -1
- package/build/resources/response-format.md +1 -3
- package/build/server/http-server.js +398 -0
- package/build/server/mcp-server.js +230 -0
- package/build/server/resources.js +73 -0
- package/build/server/tool-input-zod.js +118 -0
- package/build/tools/base.js +190 -0
- package/build/tools/base.test.js +64 -0
- package/build/tools/household/definitions.js +161 -0
- package/build/tools/household/handlers.js +109 -0
- package/build/tools/household/index.js +25 -0
- package/build/tools/index.js +2 -0
- package/build/tools/inventory/definitions.js +416 -0
- package/build/tools/inventory/handlers.js +443 -0
- package/build/tools/inventory/index.js +32 -0
- package/build/tools/module-loader.js +154 -0
- package/build/tools/recipes/definitions.js +300 -0
- package/build/tools/recipes/handlers.js +533 -0
- package/build/tools/recipes/index.js +34 -0
- package/build/tools/recipes/validations.js +29 -0
- package/build/tools/shopping/definitions.js +147 -0
- package/build/tools/shopping/handlers.js +198 -0
- package/build/tools/shopping/index.js +17 -0
- package/build/tools/system/definitions.js +89 -0
- package/build/tools/system/handlers.js +156 -0
- package/build/tools/system/index.js +16 -0
- package/build/tools/types.js +1 -0
- package/build/tools/validation-helpers.js +39 -0
- package/build/types/index.js +64 -0
- package/build/utils/errors.js +143 -0
- package/build/utils/logger.js +142 -0
- package/build/version.js +1 -1
- package/package.json +27 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# [2.8.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.2...v2.8.0) (2026-09-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* accept numbers and booleans encoded as strings in tool arguments ([5dfbbc7](https://github.com/miguelangel-nubla/mcp-grocy/commit/5dfbbc799fdfde36255f6fcc3665e0113f36cf14))
|
|
7
|
+
* advertise tool parameter descriptions in tools/list ([0418bf5](https://github.com/miguelangel-nubla/mcp-grocy/commit/0418bf5be83428f19e52c2952e2f3e5bdd078d73))
|
|
8
|
+
* **tools:** normalize created_object_id and id to numbers in responses ([5f613d6](https://github.com/miguelangel-nubla/mcp-grocy/commit/5f613d6b6c87f860ac3a79e6d8a473a91d19d3a7))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add recipes_mealplan_add_note tool for meal plan notes with sections ([e80aa65](https://github.com/miguelangel-nubla/mcp-grocy/commit/e80aa651e9653bf765d361a4ebc3d285d325a23e)), closes [#2](https://github.com/miguelangel-nubla/mcp-grocy/issues/2)
|
|
14
|
+
|
|
15
|
+
## [2.7.2](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.1...v2.7.2) (2026-09-10)
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
- add prepack script, update release configs, and fix docker workflows ([c464c05](https://github.com/miguelangel-nubla/mcp-grocy/commit/c464c054ab4f3f420647196ded7454c60f3702fc))
|
|
20
|
+
|
|
1
21
|
## [2.7.1](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.0...v2.7.1) (2026-09-10)
|
|
2
22
|
|
|
3
23
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
[](https://github.com/miguelangel-nubla/mcp-grocy/actions/workflows/validate-config.yml)
|
|
7
7
|
[](https://github.com/miguelangel-nubla/mcp-grocy/actions/workflows/pipeline.yml)
|
|
8
8
|
[](https://modelcontextprotocol.io)
|
|
9
|
+
[](https://github.com/miguelangel-nubla/grocy-next)
|
|
10
|
+
|
|
11
|
+
> ### 🌟 Recommended Companion: [grocy-next](https://github.com/miguelangel-nubla/grocy-next)
|
|
12
|
+
>
|
|
13
|
+
> While **mcp-grocy** works great with standard upstream [Grocy](https://grocy.info/) (v4.x+), it is primarily developed and daily-driven with **[grocy-next](https://github.com/miguelangel-nubla/grocy-next)**.
|
|
14
|
+
>
|
|
15
|
+
> **grocy-next** is an enhanced, 100% compatible drop-in Docker distribution of Grocy featuring:
|
|
16
|
+
>
|
|
17
|
+
> - 📦 **Container Inventory Management:** Barcode-driven container packing, location hierarchies, and rapid transfers.
|
|
18
|
+
> - ⚖️ **Hardware & Home Assistant Integration:** Native helper integration for smart scales and wireless barcode scanners.
|
|
19
|
+
> - 🖥️ **Live Screens & Pending Scans:** Dedicated live inventory dashboards and async scan queues.
|
|
20
|
+
> - 🥗 **Enhanced Meal Planning:** Calorie breakdown per serving and streamlined meal plan workflows.
|
|
21
|
+
>
|
|
22
|
+
> 👉 If you want the most seamless experience with this MCP, deploy **[miguelangel-nubla/grocy-next](https://github.com/miguelangel-nubla/grocy-next)**!
|
|
9
23
|
|
|
10
24
|
> **🍴 Opinionated Fork Notice**
|
|
11
25
|
>
|
|
@@ -26,10 +40,6 @@
|
|
|
26
40
|
>
|
|
27
41
|
> If you need complete API access, use the [original fork](https://github.com/saya6k/mcp-grocy-api). This version trades flexibility for focused, dependable grocery management workflows.
|
|
28
42
|
|
|
29
|
-
> **⚡ Supported Grocy Backends**
|
|
30
|
-
>
|
|
31
|
-
> This MCP server is compatible with standard [Grocy](https://grocy.info/) (v4.x+) as well as [grocy-next](https://github.com/miguelangel-nubla/grocy-next) (an enhanced fork with container inventory management and live workflows). The primary environment used by the author is `grocy-next`.
|
|
32
|
-
|
|
33
43
|
## 🎯 What This MCP Does
|
|
34
44
|
|
|
35
45
|
Transform your LLM into an intelligent household management assistant with focused tools for:
|
|
@@ -44,7 +54,7 @@ Transform your LLM into an intelligent household management assistant with focus
|
|
|
44
54
|
### 🛒 **Smart Shopping & Planning**
|
|
45
55
|
|
|
46
56
|
- Maintain shopping lists with intelligent quantity management
|
|
47
|
-
- Plan meals with recipe scheduling and fulfillment checking
|
|
57
|
+
- Plan meals with recipe scheduling, free-text notes per meal plan section, and fulfillment checking
|
|
48
58
|
- Automatically add missing ingredients to shopping lists
|
|
49
59
|
- Track shopping locations and optimize store visits
|
|
50
60
|
|
|
@@ -64,7 +74,7 @@ Transform your LLM into an intelligent household management assistant with focus
|
|
|
64
74
|
|
|
65
75
|
## ⚡ Quick Start
|
|
66
76
|
|
|
67
|
-
1. **Get your Grocy API key** from your Grocy instance (User Settings → API Keys)
|
|
77
|
+
1. **Get your Grocy API key** from your Grocy or [grocy-next](https://github.com/miguelangel-nubla/grocy-next) instance (User Settings → API Keys)
|
|
68
78
|
2. **Set up with Docker Compose:**
|
|
69
79
|
|
|
70
80
|
```bash
|
|
@@ -0,0 +1,119 @@
|
|
|
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 };
|
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
MCP_SESSION_IDLE_TIMEOUT_MS: z.string().regex(/^\d+$/).optional(),
|
|
28
|
+
MCP_SESSION_SWEEP_INTERVAL_MS: z.string().regex(/^\d+$/).optional(),
|
|
29
|
+
SERIALIZE_STRUCTURED_TO_CONTENT: z.enum(['true', 'false']).optional(),
|
|
30
|
+
// Logging Configuration
|
|
31
|
+
LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR']).optional(),
|
|
32
|
+
LOG_CATEGORIES: z.string().optional(),
|
|
33
|
+
// Build Configuration
|
|
34
|
+
RELEASE_VERSION: z.string().optional(),
|
|
35
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
|
|
36
|
+
});
|
|
37
|
+
// YAML configuration schema
|
|
38
|
+
const YamlConfigSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
server: z
|
|
41
|
+
.object({
|
|
42
|
+
enable_http_server: z.boolean().default(false),
|
|
43
|
+
http_server_port: z.number().min(1).max(65535).default(8080),
|
|
44
|
+
/** CORS `Access-Control-Allow-Origin` for HTTP MCP endpoints (`*` or a single origin URL) */
|
|
45
|
+
http_cors_origin: z.string().min(1).default('*'),
|
|
46
|
+
/** When set, MCP HTTP/SSE routes require `Authorization: Bearer <token>`, `X-MCP-Access-Token`, or `access_token` query (GET only). */
|
|
47
|
+
http_access_token: z.string().optional(),
|
|
48
|
+
/** Reap an idle streamable-HTTP MCP session after this many ms with no requests. Bounds memory when clients reconnect without sending `DELETE /mcp`. */
|
|
49
|
+
session_idle_timeout_ms: z.number().int().positive().default(300_000),
|
|
50
|
+
/** How often, in ms, to sweep for idle MCP sessions to reap. */
|
|
51
|
+
session_sweep_interval_ms: z.number().int().positive().default(60_000),
|
|
52
|
+
/** When true, tools also serialize structured JSON payload into the content text block for backwards-compatible MCP clients. Default: false. */
|
|
53
|
+
serialize_structured_to_content: z.boolean().default(false),
|
|
54
|
+
})
|
|
55
|
+
.strict()
|
|
56
|
+
.default({
|
|
57
|
+
enable_http_server: false,
|
|
58
|
+
http_server_port: 8080,
|
|
59
|
+
http_cors_origin: '*',
|
|
60
|
+
session_idle_timeout_ms: 300_000,
|
|
61
|
+
session_sweep_interval_ms: 60_000,
|
|
62
|
+
serialize_structured_to_content: false,
|
|
63
|
+
}),
|
|
64
|
+
grocy: z
|
|
65
|
+
.object({
|
|
66
|
+
base_url: z.string().url().default('http://localhost:9283'),
|
|
67
|
+
api_key: z.string().optional(),
|
|
68
|
+
enable_ssl_verify: z.boolean().default(true),
|
|
69
|
+
response_size_limit: z.number().positive().default(10000),
|
|
70
|
+
/** Max Grocy API response body size in bytes (all tools); larger responses fail fast */
|
|
71
|
+
max_response_bytes: z.number().positive().default(DEFAULT_MAX_RESPONSE_BYTES),
|
|
72
|
+
})
|
|
73
|
+
.strict()
|
|
74
|
+
.default({
|
|
75
|
+
base_url: 'http://localhost:9283',
|
|
76
|
+
enable_ssl_verify: true,
|
|
77
|
+
response_size_limit: 10000,
|
|
78
|
+
max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
|
|
79
|
+
}),
|
|
80
|
+
tools: z
|
|
81
|
+
.record(z.string(), z
|
|
82
|
+
.object({
|
|
83
|
+
enabled: z.boolean().default(false),
|
|
84
|
+
ack_token: z.string().optional(),
|
|
85
|
+
})
|
|
86
|
+
.catchall(z.unknown()))
|
|
87
|
+
.default({}),
|
|
88
|
+
})
|
|
89
|
+
.strict();
|
|
90
|
+
export class ConfigManager {
|
|
91
|
+
static instance;
|
|
92
|
+
config;
|
|
93
|
+
// Unified config properties - final resolved values
|
|
94
|
+
grocy;
|
|
95
|
+
server;
|
|
96
|
+
tools;
|
|
97
|
+
constructor(configPath) {
|
|
98
|
+
this.config = this.loadConfig(configPath);
|
|
99
|
+
// Expose final resolved values
|
|
100
|
+
this.grocy = {
|
|
101
|
+
base_url: this.config.yaml.grocy.base_url,
|
|
102
|
+
...(this.config.yaml.grocy.api_key !== undefined && {
|
|
103
|
+
api_key: this.config.yaml.grocy.api_key,
|
|
104
|
+
}),
|
|
105
|
+
enable_ssl_verify: this.config.yaml.grocy.enable_ssl_verify,
|
|
106
|
+
response_size_limit: this.config.yaml.grocy.response_size_limit,
|
|
107
|
+
max_response_bytes: this.config.yaml.grocy.max_response_bytes,
|
|
108
|
+
};
|
|
109
|
+
this.server = {
|
|
110
|
+
enable_http_server: this.config.yaml.server.enable_http_server,
|
|
111
|
+
http_server_port: this.config.yaml.server.http_server_port,
|
|
112
|
+
http_cors_origin: this.config.yaml.server.http_cors_origin,
|
|
113
|
+
session_idle_timeout_ms: this.config.yaml.server.session_idle_timeout_ms,
|
|
114
|
+
session_sweep_interval_ms: this.config.yaml.server.session_sweep_interval_ms,
|
|
115
|
+
serialize_structured_to_content: this.config.yaml.server.serialize_structured_to_content,
|
|
116
|
+
...(this.config.yaml.server.http_access_token !== undefined &&
|
|
117
|
+
this.config.yaml.server.http_access_token !== '' && {
|
|
118
|
+
http_access_token: this.config.yaml.server.http_access_token,
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
this.tools = this.config.yaml.tools;
|
|
122
|
+
}
|
|
123
|
+
static getInstance() {
|
|
124
|
+
if (!ConfigManager.instance) {
|
|
125
|
+
ConfigManager.instance = new ConfigManager();
|
|
126
|
+
}
|
|
127
|
+
return ConfigManager.instance;
|
|
128
|
+
}
|
|
129
|
+
loadConfig(configPath) {
|
|
130
|
+
// Load environment variables
|
|
131
|
+
const env = this.loadEnvironment();
|
|
132
|
+
// Load YAML configuration
|
|
133
|
+
const yaml = this.loadYamlConfig(configPath);
|
|
134
|
+
// Apply environment variable overrides
|
|
135
|
+
this.applyEnvironmentOverrides(yaml, env);
|
|
136
|
+
return { env, yaml };
|
|
137
|
+
}
|
|
138
|
+
loadEnvironment() {
|
|
139
|
+
try {
|
|
140
|
+
return EnvironmentSchema.parse(process.env);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error instanceof z.ZodError) {
|
|
144
|
+
logger.error('Invalid environment variables', 'CONFIG');
|
|
145
|
+
for (const issue of error.issues) {
|
|
146
|
+
const path = issue.path?.length ? issue.path.join('.') : '(root)';
|
|
147
|
+
logger.error(`${path}: ${issue.message}`, 'CONFIG');
|
|
148
|
+
}
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
loadYamlConfig(configPath) {
|
|
155
|
+
const yamlPath = this.findConfigFile(configPath);
|
|
156
|
+
try {
|
|
157
|
+
let configData = {};
|
|
158
|
+
if (existsSync(yamlPath)) {
|
|
159
|
+
const yamlContent = readFileSync(yamlPath, 'utf8');
|
|
160
|
+
configData = YAML.parse(yamlContent) || {};
|
|
161
|
+
logger.config(`Loaded YAML config from: ${yamlPath}`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
logger.config('No YAML config found, using defaults');
|
|
165
|
+
}
|
|
166
|
+
return YamlConfigSchema.parse(configData);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error instanceof z.ZodError) {
|
|
170
|
+
logger.error('Invalid YAML configuration', 'CONFIG');
|
|
171
|
+
for (const issue of error.issues) {
|
|
172
|
+
const path = issue.path?.length ? issue.path.join('.') : '(root)';
|
|
173
|
+
logger.error(`${path}: ${issue.message}`, 'CONFIG');
|
|
174
|
+
}
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
findConfigFile(configPath) {
|
|
181
|
+
if (configPath)
|
|
182
|
+
return configPath;
|
|
183
|
+
// Look for config files in the following order:
|
|
184
|
+
// 1. Current working directory (for development)
|
|
185
|
+
// 2. Project root (relative to the compiled main.js)
|
|
186
|
+
const projectRoot = resolve(__dirname, '../..');
|
|
187
|
+
const possiblePaths = [
|
|
188
|
+
resolve(process.cwd(), 'mcp-grocy.yaml'),
|
|
189
|
+
resolve(process.cwd(), 'mcp-grocy.yml'),
|
|
190
|
+
resolve(projectRoot, 'mcp-grocy.yaml'),
|
|
191
|
+
resolve(projectRoot, 'mcp-grocy.yml'),
|
|
192
|
+
];
|
|
193
|
+
return possiblePaths.find((path) => existsSync(path)) ?? possiblePaths[0];
|
|
194
|
+
}
|
|
195
|
+
// Public getters
|
|
196
|
+
getConfig() {
|
|
197
|
+
return this.config;
|
|
198
|
+
}
|
|
199
|
+
getApiUrl() {
|
|
200
|
+
return this.grocy.base_url.endsWith('/')
|
|
201
|
+
? `${this.grocy.base_url}api`
|
|
202
|
+
: `${this.grocy.base_url}/api`;
|
|
203
|
+
}
|
|
204
|
+
getCustomHeaders() {
|
|
205
|
+
const headers = {};
|
|
206
|
+
if (this.grocy.api_key) {
|
|
207
|
+
headers['GROCY-API-KEY'] = this.grocy.api_key;
|
|
208
|
+
}
|
|
209
|
+
return headers;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Apply environment variable overrides to YAML configuration
|
|
213
|
+
*/
|
|
214
|
+
applyEnvironmentOverrides(yaml, env) {
|
|
215
|
+
// Grocy configuration overrides
|
|
216
|
+
if (env.GROCY_BASE_URL) {
|
|
217
|
+
yaml.grocy.base_url = env.GROCY_BASE_URL;
|
|
218
|
+
}
|
|
219
|
+
if (env.GROCY_API_KEY) {
|
|
220
|
+
yaml.grocy.api_key = env.GROCY_API_KEY;
|
|
221
|
+
}
|
|
222
|
+
if (env.GROCY_ENABLE_SSL_VERIFY !== undefined) {
|
|
223
|
+
yaml.grocy.enable_ssl_verify = env.GROCY_ENABLE_SSL_VERIFY === 'true';
|
|
224
|
+
}
|
|
225
|
+
if (env.REST_RESPONSE_SIZE_LIMIT !== undefined) {
|
|
226
|
+
yaml.grocy.response_size_limit = parseInt(env.REST_RESPONSE_SIZE_LIMIT, 10);
|
|
227
|
+
}
|
|
228
|
+
if (env.GROCY_MAX_RESPONSE_BYTES !== undefined) {
|
|
229
|
+
yaml.grocy.max_response_bytes = parseInt(env.GROCY_MAX_RESPONSE_BYTES, 10);
|
|
230
|
+
}
|
|
231
|
+
// Server configuration overrides
|
|
232
|
+
if (env.ENABLE_HTTP_SERVER !== undefined) {
|
|
233
|
+
yaml.server.enable_http_server = env.ENABLE_HTTP_SERVER === 'true';
|
|
234
|
+
}
|
|
235
|
+
if (env.HTTP_SERVER_PORT !== undefined) {
|
|
236
|
+
yaml.server.http_server_port = parseInt(env.HTTP_SERVER_PORT, 10);
|
|
237
|
+
}
|
|
238
|
+
if (env.HTTP_CORS_ORIGIN !== undefined && env.HTTP_CORS_ORIGIN.length > 0) {
|
|
239
|
+
yaml.server.http_cors_origin = env.HTTP_CORS_ORIGIN;
|
|
240
|
+
}
|
|
241
|
+
if (env.MCP_HTTP_ACCESS_TOKEN !== undefined) {
|
|
242
|
+
yaml.server.http_access_token =
|
|
243
|
+
env.MCP_HTTP_ACCESS_TOKEN.length > 0 ? env.MCP_HTTP_ACCESS_TOKEN : undefined;
|
|
244
|
+
}
|
|
245
|
+
if (env.MCP_SESSION_IDLE_TIMEOUT_MS !== undefined) {
|
|
246
|
+
yaml.server.session_idle_timeout_ms = parseInt(env.MCP_SESSION_IDLE_TIMEOUT_MS, 10);
|
|
247
|
+
}
|
|
248
|
+
if (env.MCP_SESSION_SWEEP_INTERVAL_MS !== undefined) {
|
|
249
|
+
yaml.server.session_sweep_interval_ms = parseInt(env.MCP_SESSION_SWEEP_INTERVAL_MS, 10);
|
|
250
|
+
}
|
|
251
|
+
if (env.SERIALIZE_STRUCTURED_TO_CONTENT !== undefined) {
|
|
252
|
+
yaml.server.serialize_structured_to_content = env.SERIALIZE_STRUCTURED_TO_CONTENT === 'true';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
parseToolConfiguration() {
|
|
256
|
+
const enabledTools = new Set();
|
|
257
|
+
const toolSubConfigs = new Map();
|
|
258
|
+
const toolAckTokens = new Map();
|
|
259
|
+
for (const [toolName, toolConfig] of Object.entries(this.config.yaml.tools)) {
|
|
260
|
+
if (toolConfig.enabled) {
|
|
261
|
+
enabledTools.add(toolName);
|
|
262
|
+
// Store ack_token separately if configured
|
|
263
|
+
if (toolConfig.ack_token && typeof toolConfig.ack_token === 'string') {
|
|
264
|
+
toolAckTokens.set(toolName, toolConfig.ack_token);
|
|
265
|
+
}
|
|
266
|
+
// Extract sub-configs (everything except standard fields)
|
|
267
|
+
const subConfigs = new Map();
|
|
268
|
+
for (const [key, value] of Object.entries(toolConfig)) {
|
|
269
|
+
if (!['enabled', 'ack_token'].includes(key)) {
|
|
270
|
+
subConfigs.set(key, value);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (subConfigs.size > 0) {
|
|
274
|
+
toolSubConfigs.set(toolName, subConfigs);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { enabledTools, toolSubConfigs, toolAckTokens };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Export singleton instance
|
|
282
|
+
export const config = ConfigManager.getInstance();
|
|
283
|
+
export default config;
|
package/build/main.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
});
|
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
# [2.8.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.2...v2.8.0) (2026-09-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* accept numbers and booleans encoded as strings in tool arguments ([5dfbbc7](https://github.com/miguelangel-nubla/mcp-grocy/commit/5dfbbc799fdfde36255f6fcc3665e0113f36cf14))
|
|
7
|
+
* advertise tool parameter descriptions in tools/list ([0418bf5](https://github.com/miguelangel-nubla/mcp-grocy/commit/0418bf5be83428f19e52c2952e2f3e5bdd078d73))
|
|
8
|
+
* **tools:** normalize created_object_id and id to numbers in responses ([5f613d6](https://github.com/miguelangel-nubla/mcp-grocy/commit/5f613d6b6c87f860ac3a79e6d8a473a91d19d3a7))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add recipes_mealplan_add_note tool for meal plan notes with sections ([e80aa65](https://github.com/miguelangel-nubla/mcp-grocy/commit/e80aa651e9653bf765d361a4ebc3d285d325a23e)), closes [#2](https://github.com/miguelangel-nubla/mcp-grocy/issues/2)
|
|
2
14
|
|
|
15
|
+
## [2.7.2](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.1...v2.7.2) (2026-09-10)
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
- add prepack script, update release configs, and fix docker workflows ([c464c05](https://github.com/miguelangel-nubla/mcp-grocy/commit/c464c054ab4f3f420647196ded7454c60f3702fc))
|
|
20
|
+
|
|
21
|
+
## [2.7.1](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.0...v2.7.1) (2026-09-10)
|
|
3
22
|
|
|
4
23
|
### Bug Fixes
|
|
5
24
|
|
|
6
|
-
|
|
7
|
-
|
|
25
|
+
- refuse non-recipe meal plan entries in recipes_cooking_complete before writing ([172d1f0](https://github.com/miguelangel-nubla/mcp-grocy/commit/172d1f0089b1e401736b258cec652a30d90e0215)), closes [#2](https://github.com/miguelangel-nubla/mcp-grocy/issues/2)
|
|
26
|
+
- send correct meal plan payload in recipes_mealplan_add_recipe ([2b180b0](https://github.com/miguelangel-nubla/mcp-grocy/commit/2b180b02b58ab03db69efab727ee53ab88caa0d0)), closes [#2](https://github.com/miguelangel-nubla/mcp-grocy/issues/2)
|
|
8
27
|
|
|
9
28
|
# [2.7.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.6.0...v2.7.0) (2026-08-03)
|
|
10
29
|
|
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
[](https://github.com/miguelangel-nubla/mcp-grocy/actions/workflows/validate-config.yml)
|
|
7
7
|
[](https://github.com/miguelangel-nubla/mcp-grocy/actions/workflows/pipeline.yml)
|
|
8
8
|
[](https://modelcontextprotocol.io)
|
|
9
|
+
[](https://github.com/miguelangel-nubla/grocy-next)
|
|
10
|
+
|
|
11
|
+
> ### 🌟 Recommended Companion: [grocy-next](https://github.com/miguelangel-nubla/grocy-next)
|
|
12
|
+
>
|
|
13
|
+
> While **mcp-grocy** works great with standard upstream [Grocy](https://grocy.info/) (v4.x+), it is primarily developed and daily-driven with **[grocy-next](https://github.com/miguelangel-nubla/grocy-next)**.
|
|
14
|
+
>
|
|
15
|
+
> **grocy-next** is an enhanced, 100% compatible drop-in Docker distribution of Grocy featuring:
|
|
16
|
+
>
|
|
17
|
+
> - 📦 **Container Inventory Management:** Barcode-driven container packing, location hierarchies, and rapid transfers.
|
|
18
|
+
> - ⚖️ **Hardware & Home Assistant Integration:** Native helper integration for smart scales and wireless barcode scanners.
|
|
19
|
+
> - 🖥️ **Live Screens & Pending Scans:** Dedicated live inventory dashboards and async scan queues.
|
|
20
|
+
> - 🥗 **Enhanced Meal Planning:** Calorie breakdown per serving and streamlined meal plan workflows.
|
|
21
|
+
>
|
|
22
|
+
> 👉 If you want the most seamless experience with this MCP, deploy **[miguelangel-nubla/grocy-next](https://github.com/miguelangel-nubla/grocy-next)**!
|
|
9
23
|
|
|
10
24
|
> **🍴 Opinionated Fork Notice**
|
|
11
25
|
>
|
|
@@ -26,10 +40,6 @@
|
|
|
26
40
|
>
|
|
27
41
|
> If you need complete API access, use the [original fork](https://github.com/saya6k/mcp-grocy-api). This version trades flexibility for focused, dependable grocery management workflows.
|
|
28
42
|
|
|
29
|
-
> **⚡ Supported Grocy Backends**
|
|
30
|
-
>
|
|
31
|
-
> This MCP server is compatible with standard [Grocy](https://grocy.info/) (v4.x+) as well as [grocy-next](https://github.com/miguelangel-nubla/grocy-next) (an enhanced fork with container inventory management and live workflows). The primary environment used by the author is `grocy-next`.
|
|
32
|
-
|
|
33
43
|
## 🎯 What This MCP Does
|
|
34
44
|
|
|
35
45
|
Transform your LLM into an intelligent household management assistant with focused tools for:
|
|
@@ -44,7 +54,7 @@ Transform your LLM into an intelligent household management assistant with focus
|
|
|
44
54
|
### 🛒 **Smart Shopping & Planning**
|
|
45
55
|
|
|
46
56
|
- Maintain shopping lists with intelligent quantity management
|
|
47
|
-
- Plan meals with recipe scheduling and fulfillment checking
|
|
57
|
+
- Plan meals with recipe scheduling, free-text notes per meal plan section, and fulfillment checking
|
|
48
58
|
- Automatically add missing ingredients to shopping lists
|
|
49
59
|
- Track shopping locations and optimize store visits
|
|
50
60
|
|
|
@@ -64,7 +74,7 @@ Transform your LLM into an intelligent household management assistant with focus
|
|
|
64
74
|
|
|
65
75
|
## ⚡ Quick Start
|
|
66
76
|
|
|
67
|
-
1. **Get your Grocy API key** from your Grocy instance (User Settings → API Keys)
|
|
77
|
+
1. **Get your Grocy API key** from your Grocy or [grocy-next](https://github.com/miguelangel-nubla/grocy-next) instance (User Settings → API Keys)
|
|
68
78
|
2. **Set up with Docker Compose:**
|
|
69
79
|
|
|
70
80
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Grocy API Reference
|
|
2
2
|
|
|
3
|
-
> Auto-generated from https://demo.grocy.info/api/openapi/specification on
|
|
3
|
+
> Auto-generated from https://demo.grocy.info/api/openapi/specification on 2026-09-10T20:59:54.572Z
|
|
4
4
|
|
|
5
5
|
| Path | Method | Summary |
|
|
6
6
|
| -------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
| /stock/products/{productId} | GET | Returns details of the given product |
|
|
52
52
|
| /stock/products/{productId}/add | POST | Adds the given amount of the given product to stock |
|
|
53
53
|
| /stock/products/{productId}/consume | POST | Removes the given amount of the given product from stock |
|
|
54
|
+
| /stock/products/{productId}/copy | POST | Copies a product |
|
|
54
55
|
| /stock/products/{productId}/entries | GET | Returns all stock entries of the given product in order of next use (Opened first, then first due first, then first in first out) |
|
|
55
56
|
| /stock/products/{productId}/inventory | POST | Inventories the given product (adds/removes based on the given new amount) |
|
|
56
57
|
| /stock/products/{productId}/locations | GET | Returns all locations where the given product currently has stock |
|
|
@@ -122,6 +122,18 @@ use_mcp_tool('mcp-grocy', 'recipes_mealplan_add_recipe', {
|
|
|
122
122
|
|
|
123
123
|
Use **`recipes_mealplan_get_sections`** for valid `sectionId` values.
|
|
124
124
|
|
|
125
|
+
### Add a note to the meal plan (no recipe)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
use_mcp_tool('mcp-grocy', 'recipes_mealplan_add_note', {
|
|
129
|
+
day: '2024-07-01',
|
|
130
|
+
note: 'Leftovers from Sunday',
|
|
131
|
+
sectionId: 3,
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Same `sectionId` values as above (`-1` = Grocy's built-in "no section").
|
|
136
|
+
|
|
125
137
|
### Remove a meal plan entry
|
|
126
138
|
|
|
127
139
|
```typescript
|
|
@@ -130,7 +142,7 @@ use_mcp_tool('mcp-grocy', 'recipes_mealplan_delete_entry', {
|
|
|
130
142
|
});
|
|
131
143
|
```
|
|
132
144
|
|
|
133
|
-
Use **`recipes_mealplan_get`** to find `mealPlanEntryId`.
|
|
145
|
+
Use **`recipes_mealplan_get`** to find `mealPlanEntryId`. Works for recipe and note entries.
|
|
134
146
|
|
|
135
147
|
### Cook / consume recipe ingredients
|
|
136
148
|
|
|
@@ -159,9 +159,7 @@ If **`system_dev_test_request`** encounters an API error (e.g., authentication f
|
|
|
159
159
|
"statusCode": 401,
|
|
160
160
|
"statusText": "Unauthorized",
|
|
161
161
|
"timing": "50ms",
|
|
162
|
-
"headers": {
|
|
163
|
-
/* ... headers ... */
|
|
164
|
-
},
|
|
162
|
+
"headers": {/* ... headers ... */},
|
|
165
163
|
"body": {
|
|
166
164
|
"error_message": "API key is missing or invalid."
|
|
167
165
|
}
|