mcp-grocy 1.9.0 โ 1.11.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/README.md +4 -4
- package/build/api/client.js +62 -93
- package/build/config/environment.js +19 -99
- package/build/config/index.js +158 -0
- package/build/config/yaml-config.js +203 -0
- package/build/main.js +35 -18
- package/build/resources/README.md +4 -4
- package/build/resources/config.md +16 -152
- package/build/server/http-server.js +189 -185
- package/build/server/mcp-server.js +92 -103
- package/build/tools/base.js +122 -37
- package/build/tools/household/definitions.js +128 -0
- package/build/tools/household/handlers.js +92 -0
- package/build/tools/household/index.js +22 -0
- package/build/tools/index.js +2 -491
- package/build/tools/inventory/definitions.js +277 -0
- package/build/tools/inventory/handlers.js +278 -0
- package/build/tools/inventory/index.js +27 -0
- package/build/tools/module-loader.js +154 -0
- package/build/tools/recipes/definitions.js +106 -50
- package/build/tools/recipes/handlers.js +309 -163
- package/build/tools/recipes/index.js +21 -11
- package/build/tools/recipes/validations.js +23 -0
- package/build/tools/shopping/definitions.js +62 -0
- package/build/tools/shopping/handlers.js +37 -0
- package/build/tools/shopping/index.js +8 -102
- package/build/tools/system/definitions.js +86 -0
- package/build/tools/system/handlers.js +94 -0
- package/build/tools/system/index.js +11 -225
- package/build/tools/validation-helpers.js +110 -0
- package/build/types/index.js +62 -0
- package/build/utils/errors.js +138 -0
- package/build/utils/logger.js +141 -0
- package/build/version.js +1 -1
- package/package.json +3 -1
- package/build/resources/CHANGELOG.md +0 -352
- package/build/tools/products/definitions.js +0 -57
- package/build/tools/products/handlers.js +0 -78
- package/build/tools/products/index.js +0 -13
- package/build/tools/stock/definitions.js +0 -234
- package/build/tools/stock/handlers.js +0 -391
- package/build/tools/stock/index.js +0 -19
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ docker compose up -d
|
|
|
139
139
|
|--------|----------|---------|
|
|
140
140
|
| **`.env` file** | Recommended for most users | `cp .env.example .env` |
|
|
141
141
|
| **Environment variables** | CI/CD, containers | `GROCY_BASE_URL=... GROCY_APIKEY_VALUE=... mcp-grocy` |
|
|
142
|
-
| **Tool
|
|
142
|
+
| **Tool configuration** | Customize functionality | Edit `tools` section in `mcp-grocy.yaml` |
|
|
143
143
|
|
|
144
144
|
๐ **For complete configuration reference:** See [Configuration Guide](src/resources/config.md)
|
|
145
145
|
|
|
@@ -172,7 +172,7 @@ npm start
|
|
|
172
172
|
|----------|---------|-------------|
|
|
173
173
|
| [๐ API Reference](src/resources/api-reference.md) | Complete tool documentation | Tool usage and examples |
|
|
174
174
|
| [โ๏ธ Configuration Guide](src/resources/config.md) | Advanced configuration reference | Detailed setup, presets, troubleshooting |
|
|
175
|
-
| [๐ .env.example](.env.example) |
|
|
175
|
+
| [๐ .env.example](.env.example) | Environment configuration template | Copy and customize for your setup |
|
|
176
176
|
| [๐งช MCP Inspector](https://github.com/modelcontextprotocol/inspector) | Protocol debugging | Debug MCP interactions |
|
|
177
177
|
|
|
178
178
|
### ๐ Troubleshooting
|
|
@@ -190,12 +190,12 @@ npm start
|
|
|
190
190
|
- Ensure the API key has proper permissions
|
|
191
191
|
|
|
192
192
|
**"Tool not found" errors**
|
|
193
|
-
- Check if the tool is enabled in your
|
|
193
|
+
- Check if the tool is enabled in your `mcp-grocy.yaml` file
|
|
194
194
|
- Verify you're using the correct tool names from the API reference
|
|
195
195
|
|
|
196
196
|
**Large response errors**
|
|
197
197
|
- Increase `REST_RESPONSE_SIZE_LIMIT` if you have many products/stock entries
|
|
198
|
-
- Consider
|
|
198
|
+
- Consider disabling unused tools in `mcp-grocy.yaml`
|
|
199
199
|
|
|
200
200
|
#### Debug Mode
|
|
201
201
|
|
package/build/api/client.js
CHANGED
|
@@ -1,77 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified API client for Grocy
|
|
3
|
+
*/
|
|
1
4
|
import axios from 'axios';
|
|
2
5
|
import https from 'https';
|
|
3
|
-
import config from '../config/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
response;
|
|
7
|
-
constructor(message, status, response) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = 'ApiError';
|
|
10
|
-
this.status = status;
|
|
11
|
-
this.response = response;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
6
|
+
import { config } from '../config/index.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import { ApiError, ErrorHandler } from '../utils/errors.js';
|
|
14
9
|
export class GrocyApiClient {
|
|
15
10
|
axiosInstance;
|
|
16
11
|
API_KEY_HEADER = 'GROCY-API-KEY';
|
|
17
12
|
constructor() {
|
|
18
|
-
|
|
19
|
-
this.
|
|
13
|
+
this.axiosInstance = this.createAxiosInstance();
|
|
14
|
+
this.setupInterceptors();
|
|
15
|
+
}
|
|
16
|
+
createAxiosInstance() {
|
|
17
|
+
const { yaml } = config.getConfig();
|
|
18
|
+
const apiKey = config.getApiKey();
|
|
19
|
+
const instance = axios.create({
|
|
20
20
|
baseURL: config.getGrocyBaseUrl(),
|
|
21
|
-
validateStatus: () => true, //
|
|
22
|
-
timeout: 30000,
|
|
23
|
-
httpsAgent:
|
|
21
|
+
validateStatus: () => true, // Handle all status codes manually
|
|
22
|
+
timeout: 30000,
|
|
23
|
+
httpsAgent: yaml.grocy.enable_ssl_verify ? undefined : new https.Agent({
|
|
24
24
|
rejectUnauthorized: false
|
|
25
25
|
})
|
|
26
26
|
});
|
|
27
|
-
// Set default authentication
|
|
28
|
-
if (
|
|
29
|
-
|
|
27
|
+
// Set default authentication
|
|
28
|
+
if (apiKey) {
|
|
29
|
+
instance.defaults.headers.common[this.API_KEY_HEADER] = apiKey;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
return instance;
|
|
32
|
+
}
|
|
33
|
+
setupInterceptors() {
|
|
34
|
+
// Request logging
|
|
32
35
|
this.axiosInstance.interceptors.request.use((config) => {
|
|
33
|
-
|
|
34
|
-
if (config.data) {
|
|
35
|
-
console.error(`[API] Request body: ${JSON.stringify(config.data)}`);
|
|
36
|
-
}
|
|
36
|
+
logger.api(`${config.method?.toUpperCase()} ${config.url}`);
|
|
37
37
|
return config;
|
|
38
38
|
}, (error) => {
|
|
39
|
-
|
|
39
|
+
logger.error('Request error', 'API', { error: error.message });
|
|
40
40
|
return Promise.reject(error);
|
|
41
41
|
});
|
|
42
|
-
//
|
|
42
|
+
// Response logging
|
|
43
43
|
this.axiosInstance.interceptors.response.use((response) => {
|
|
44
44
|
if (response.status >= 400) {
|
|
45
|
-
|
|
45
|
+
logger.warn(`HTTP ${response.status}`, 'API', {
|
|
46
|
+
url: response.config?.url,
|
|
47
|
+
status: response.status
|
|
48
|
+
});
|
|
46
49
|
}
|
|
47
50
|
return response;
|
|
48
51
|
}, (error) => {
|
|
49
|
-
|
|
52
|
+
logger.error('Response error', 'API', { error: error.message });
|
|
50
53
|
return Promise.reject(error);
|
|
51
54
|
});
|
|
52
55
|
}
|
|
53
56
|
normalizeEndpoint(endpoint) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
else if (endpoint.startsWith('api/')) {
|
|
62
|
-
normalizedEndpoint = `/${endpoint}`;
|
|
63
|
-
}
|
|
64
|
-
// All other endpoints - ensure they start with /api/
|
|
65
|
-
else {
|
|
66
|
-
if (endpoint.startsWith('/')) {
|
|
67
|
-
normalizedEndpoint = `/api${endpoint}`;
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
normalizedEndpoint = `/api/${endpoint}`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
console.error(`[API] Normalized endpoint: ${normalizedEndpoint}`);
|
|
74
|
-
return normalizedEndpoint;
|
|
57
|
+
if (endpoint.startsWith('/api/'))
|
|
58
|
+
return endpoint;
|
|
59
|
+
if (endpoint.startsWith('api/'))
|
|
60
|
+
return `/${endpoint}`;
|
|
61
|
+
if (endpoint.startsWith('/'))
|
|
62
|
+
return `/api${endpoint}`;
|
|
63
|
+
return `/api/${endpoint}`;
|
|
75
64
|
}
|
|
76
65
|
buildQueryString(params) {
|
|
77
66
|
return Object.entries(params)
|
|
@@ -80,57 +69,35 @@ export class GrocyApiClient {
|
|
|
80
69
|
}
|
|
81
70
|
async request(endpoint, options = {}) {
|
|
82
71
|
const { method = 'GET', body = null, headers = {}, queryParams = {}, timeout } = options;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
72
|
+
return ErrorHandler.handleAsync(async () => {
|
|
73
|
+
let url = this.normalizeEndpoint(endpoint);
|
|
74
|
+
if (Object.keys(queryParams).length > 0) {
|
|
75
|
+
url += `?${this.buildQueryString(queryParams)}`;
|
|
76
|
+
}
|
|
77
|
+
const requestConfig = {
|
|
78
|
+
method,
|
|
79
|
+
url,
|
|
80
|
+
headers: {
|
|
81
|
+
'Accept': 'application/json',
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
...config.getCustomHeaders(),
|
|
84
|
+
...headers
|
|
85
|
+
},
|
|
86
|
+
...(timeout && { timeout })
|
|
87
|
+
};
|
|
88
|
+
if (['POST', 'PUT', 'PATCH'].includes(method) && body !== null) {
|
|
89
|
+
requestConfig.data = body;
|
|
90
|
+
}
|
|
104
91
|
const response = await this.axiosInstance.request(requestConfig);
|
|
105
92
|
if (response.status >= 400) {
|
|
106
|
-
throw new ApiError(`
|
|
93
|
+
throw new ApiError(response.data?.message || `HTTP ${response.status} error`, response.status, `${method} ${url}`, { responseData: response.data });
|
|
107
94
|
}
|
|
108
95
|
return {
|
|
109
96
|
data: response.data,
|
|
110
97
|
status: response.status,
|
|
111
98
|
headers: response.headers
|
|
112
99
|
};
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
if (axios.isAxiosError(error)) {
|
|
116
|
-
const axiosError = error;
|
|
117
|
-
if (axiosError.code === 'ECONNABORTED') {
|
|
118
|
-
throw new ApiError('Connection timeout: The server took too long to respond');
|
|
119
|
-
}
|
|
120
|
-
else if (axiosError.code === 'ECONNRESET' || axiosError.message.includes('socket hang up')) {
|
|
121
|
-
throw new ApiError('Connection reset: The server unexpectedly closed the connection');
|
|
122
|
-
}
|
|
123
|
-
else if (!axiosError.response) {
|
|
124
|
-
throw new ApiError('Network error: Unable to reach the Grocy server');
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// Re-throw ApiError instances
|
|
128
|
-
if (error instanceof ApiError) {
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
// Wrap other errors
|
|
132
|
-
throw new ApiError(`Request failed: ${error.message || error}`);
|
|
133
|
-
}
|
|
100
|
+
}, `API ${method} ${endpoint}`);
|
|
134
101
|
}
|
|
135
102
|
// Convenience methods
|
|
136
103
|
async get(endpoint, options = {}) {
|
|
@@ -149,6 +116,8 @@ export class GrocyApiClient {
|
|
|
149
116
|
return this.request(endpoint, { ...options, method: 'PATCH', body });
|
|
150
117
|
}
|
|
151
118
|
}
|
|
152
|
-
// Export
|
|
119
|
+
// Export singleton instance
|
|
153
120
|
export const apiClient = new GrocyApiClient();
|
|
154
121
|
export default apiClient;
|
|
122
|
+
// Re-export ApiError for convenience
|
|
123
|
+
export { ApiError };
|
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { yamlConfig } from './yaml-config.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
2
4
|
// Environment variable schema with validation and defaults
|
|
3
5
|
const EnvironmentSchema = z.object({
|
|
4
|
-
// Grocy Configuration
|
|
5
|
-
GROCY_BASE_URL: z.string().url().default('http://localhost:9283'),
|
|
6
|
+
// Grocy Configuration - API key must still come from env for security
|
|
6
7
|
GROCY_APIKEY_VALUE: z.string().optional(),
|
|
7
|
-
GROCY_ENABLE_SSL_VERIFY: z.string().default('true').transform(val => val !== 'false'),
|
|
8
|
-
// Server Configuration
|
|
9
|
-
ENABLE_HTTP_SERVER: z.string().default('false').transform(val => ['true', 'yes', '1', 'on', 'enabled'].includes(val.toLowerCase())),
|
|
10
|
-
HTTP_SERVER_PORT: z.string().default('8080').transform(val => parseInt(val, 10)),
|
|
11
|
-
// API Configuration
|
|
12
|
-
REST_RESPONSE_SIZE_LIMIT: z.string().default('10000').transform(val => {
|
|
13
|
-
const parsed = parseInt(val, 10);
|
|
14
|
-
if (isNaN(parsed) || parsed <= 0) {
|
|
15
|
-
throw new Error('REST_RESPONSE_SIZE_LIMIT must be a positive number');
|
|
16
|
-
}
|
|
17
|
-
return parsed;
|
|
18
|
-
}),
|
|
19
8
|
// Build Configuration
|
|
20
9
|
RELEASE_VERSION: z.string().optional(),
|
|
21
10
|
});
|
|
22
11
|
export class ConfigManager {
|
|
23
12
|
static instance;
|
|
24
|
-
|
|
13
|
+
envConfig;
|
|
25
14
|
constructor() {
|
|
26
15
|
try {
|
|
27
|
-
this.
|
|
28
|
-
this.validateConfiguration();
|
|
16
|
+
this.envConfig = EnvironmentSchema.parse(process.env);
|
|
29
17
|
}
|
|
30
18
|
catch (error) {
|
|
31
19
|
if (error instanceof z.ZodError) {
|
|
32
|
-
|
|
20
|
+
logger.error('Invalid environment variables', 'CONFIG');
|
|
33
21
|
error.errors.forEach(err => {
|
|
34
|
-
|
|
22
|
+
logger.error(`${err.path.join('.')}: ${err.message}`, 'CONFIG');
|
|
35
23
|
});
|
|
36
24
|
process.exit(1);
|
|
37
25
|
}
|
|
@@ -44,98 +32,30 @@ export class ConfigManager {
|
|
|
44
32
|
}
|
|
45
33
|
return ConfigManager.instance;
|
|
46
34
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.error(`[CONFIG] Grocy Base URL: ${this.config.GROCY_BASE_URL}`);
|
|
54
|
-
console.error(`[CONFIG] SSL Verification: ${this.config.GROCY_ENABLE_SSL_VERIFY ? 'enabled' : 'disabled'}`);
|
|
55
|
-
console.error(`[CONFIG] HTTP Server: ${this.config.ENABLE_HTTP_SERVER ? `enabled on port ${this.config.HTTP_SERVER_PORT}` : 'disabled'}`);
|
|
56
|
-
console.error(`[CONFIG] Response Size Limit: ${this.config.REST_RESPONSE_SIZE_LIMIT} bytes`);
|
|
35
|
+
/**
|
|
36
|
+
* Reset the singleton instance for testing purposes
|
|
37
|
+
* Note: This should only be used in tests
|
|
38
|
+
*/
|
|
39
|
+
static resetInstanceForTesting() {
|
|
40
|
+
ConfigManager.instance = undefined;
|
|
57
41
|
}
|
|
58
42
|
get() {
|
|
59
|
-
return this.
|
|
43
|
+
return this.envConfig;
|
|
60
44
|
}
|
|
61
45
|
getGrocyBaseUrl() {
|
|
62
|
-
return
|
|
46
|
+
return yamlConfig.getGrocyBaseUrl();
|
|
63
47
|
}
|
|
64
48
|
getApiUrl() {
|
|
65
|
-
return
|
|
49
|
+
return yamlConfig.getApiUrl();
|
|
66
50
|
}
|
|
67
51
|
hasApiKeyAuth() {
|
|
68
|
-
return !!this.
|
|
52
|
+
return !!this.envConfig.GROCY_APIKEY_VALUE;
|
|
69
53
|
}
|
|
70
54
|
getCustomHeaders() {
|
|
71
|
-
|
|
72
|
-
const headerPrefix = /^header_/i;
|
|
73
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
74
|
-
if (headerPrefix.test(key) && value !== undefined) {
|
|
75
|
-
const headerName = key.replace(headerPrefix, '');
|
|
76
|
-
headers[headerName] = value;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return headers;
|
|
55
|
+
return yamlConfig.getCustomHeaders();
|
|
80
56
|
}
|
|
81
57
|
parseToolConfiguration() {
|
|
82
|
-
|
|
83
|
-
const disabledTools = new Set();
|
|
84
|
-
const toolSubConfigs = new Map();
|
|
85
|
-
const errors = [];
|
|
86
|
-
// Scan all environment variables for TOOL__ patterns
|
|
87
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
88
|
-
if (!key.startsWith('TOOL__'))
|
|
89
|
-
continue;
|
|
90
|
-
const parts = key.split('__');
|
|
91
|
-
if (parts.length === 3) {
|
|
92
|
-
// TOOL__tool_name__sub_config
|
|
93
|
-
const toolName = parts[1];
|
|
94
|
-
const subConfig = parts[2];
|
|
95
|
-
if (!toolSubConfigs.has(toolName)) {
|
|
96
|
-
toolSubConfigs.set(toolName, new Map());
|
|
97
|
-
}
|
|
98
|
-
toolSubConfigs.get(toolName).set(subConfig, value === 'true');
|
|
99
|
-
}
|
|
100
|
-
else if (parts.length === 2) {
|
|
101
|
-
// TOOL__tool_name
|
|
102
|
-
const toolName = parts[1];
|
|
103
|
-
if (value === 'true') {
|
|
104
|
-
enabledTools.add(toolName);
|
|
105
|
-
}
|
|
106
|
-
else if (value === 'false') {
|
|
107
|
-
disabledTools.add(toolName);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
errors.push(`${key}=${value} (must be 'true' or 'false')`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// Error out if invalid values found
|
|
115
|
-
if (errors.length > 0) {
|
|
116
|
-
console.error('[CONFIG ERROR] Invalid TOOL_ configuration values:');
|
|
117
|
-
errors.forEach(error => console.error(` - ${error}`));
|
|
118
|
-
console.error('All TOOL_ variables must be set to either "true" or "false"');
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
if (enabledTools.size > 0) {
|
|
122
|
-
console.error(`[CONFIG] Enabled tools (${enabledTools.size}): ${Array.from(enabledTools).sort().join(', ')}`);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
console.error('[CONFIG] No tools enabled - all tools are disabled by default');
|
|
126
|
-
}
|
|
127
|
-
if (disabledTools.size > 0) {
|
|
128
|
-
console.error(`[CONFIG] Explicitly disabled tools (${disabledTools.size}): ${Array.from(disabledTools).sort().join(', ')}`);
|
|
129
|
-
}
|
|
130
|
-
// Log sub-configurations
|
|
131
|
-
if (toolSubConfigs.size > 0) {
|
|
132
|
-
console.error('[CONFIG] Tool sub-configurations:');
|
|
133
|
-
for (const [toolName, subConfigs] of toolSubConfigs) {
|
|
134
|
-
const subConfigList = Array.from(subConfigs.entries()).map(([key, value]) => `${key}=${value}`);
|
|
135
|
-
console.error(` - ${toolName}: ${subConfigList.join(', ')}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return { enabledTools, toolSubConfigs };
|
|
58
|
+
return yamlConfig.parseToolConfiguration();
|
|
139
59
|
}
|
|
140
60
|
}
|
|
141
61
|
export const config = ConfigManager.getInstance();
|
|
@@ -0,0 +1,158 @@
|
|
|
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_APIKEY_VALUE: z.string().optional(),
|
|
15
|
+
RELEASE_VERSION: z.string().optional(),
|
|
16
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
|
|
17
|
+
});
|
|
18
|
+
// YAML configuration schema
|
|
19
|
+
const YamlConfigSchema = z.object({
|
|
20
|
+
server: z.object({
|
|
21
|
+
enable_http_server: z.boolean().default(false),
|
|
22
|
+
http_server_port: z.number().min(1).max(65535).default(8080),
|
|
23
|
+
}).default({}),
|
|
24
|
+
grocy: z.object({
|
|
25
|
+
base_url: z.string().url().default('http://localhost:9283'),
|
|
26
|
+
enable_ssl_verify: z.boolean().default(true),
|
|
27
|
+
response_size_limit: z.number().positive().default(10000),
|
|
28
|
+
}).default({}),
|
|
29
|
+
tools: z.record(z.string(), z.object({
|
|
30
|
+
enabled: z.boolean().default(false),
|
|
31
|
+
ack_token: z.string().optional(),
|
|
32
|
+
}).catchall(z.unknown())).default({}),
|
|
33
|
+
});
|
|
34
|
+
export class ConfigManager {
|
|
35
|
+
static instance;
|
|
36
|
+
config;
|
|
37
|
+
constructor(configPath) {
|
|
38
|
+
this.config = this.loadConfig(configPath);
|
|
39
|
+
}
|
|
40
|
+
static getInstance() {
|
|
41
|
+
if (!ConfigManager.instance) {
|
|
42
|
+
ConfigManager.instance = new ConfigManager();
|
|
43
|
+
}
|
|
44
|
+
return ConfigManager.instance;
|
|
45
|
+
}
|
|
46
|
+
static createForTesting(configPath) {
|
|
47
|
+
return new ConfigManager(configPath);
|
|
48
|
+
}
|
|
49
|
+
loadConfig(configPath) {
|
|
50
|
+
// Load environment variables
|
|
51
|
+
const env = this.loadEnvironment();
|
|
52
|
+
// Load YAML configuration
|
|
53
|
+
const yaml = this.loadYamlConfig(configPath);
|
|
54
|
+
return { env, yaml };
|
|
55
|
+
}
|
|
56
|
+
loadEnvironment() {
|
|
57
|
+
try {
|
|
58
|
+
return EnvironmentSchema.parse(process.env);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof z.ZodError) {
|
|
62
|
+
logger.error('Invalid environment variables', 'CONFIG');
|
|
63
|
+
error.errors.forEach(err => {
|
|
64
|
+
logger.error(`${err.path.join('.')}: ${err.message}`, 'CONFIG');
|
|
65
|
+
});
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
loadYamlConfig(configPath) {
|
|
72
|
+
const yamlPath = this.findConfigFile(configPath);
|
|
73
|
+
try {
|
|
74
|
+
let configData = {};
|
|
75
|
+
if (existsSync(yamlPath)) {
|
|
76
|
+
const yamlContent = readFileSync(yamlPath, 'utf8');
|
|
77
|
+
configData = YAML.parse(yamlContent) || {};
|
|
78
|
+
logger.config(`Loaded YAML config from: ${yamlPath}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
logger.config('No YAML config found, using defaults');
|
|
82
|
+
}
|
|
83
|
+
return YamlConfigSchema.parse(configData);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof z.ZodError) {
|
|
87
|
+
logger.error('Invalid YAML configuration', 'CONFIG');
|
|
88
|
+
error.errors.forEach(err => {
|
|
89
|
+
logger.error(`${err.path.join('.')}: ${err.message}`, 'CONFIG');
|
|
90
|
+
});
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
findConfigFile(configPath) {
|
|
97
|
+
if (configPath)
|
|
98
|
+
return configPath;
|
|
99
|
+
// Look for config files in the following order:
|
|
100
|
+
// 1. Current working directory (for development)
|
|
101
|
+
// 2. Project root (relative to the compiled main.js)
|
|
102
|
+
const projectRoot = resolve(__dirname, '../..');
|
|
103
|
+
const possiblePaths = [
|
|
104
|
+
resolve(process.cwd(), 'mcp-grocy.yaml'),
|
|
105
|
+
resolve(process.cwd(), 'mcp-grocy.yml'),
|
|
106
|
+
resolve(projectRoot, 'mcp-grocy.yaml'),
|
|
107
|
+
resolve(projectRoot, 'mcp-grocy.yml'),
|
|
108
|
+
];
|
|
109
|
+
return possiblePaths.find(path => existsSync(path)) ?? possiblePaths[0];
|
|
110
|
+
}
|
|
111
|
+
// Public getters
|
|
112
|
+
getConfig() {
|
|
113
|
+
return this.config;
|
|
114
|
+
}
|
|
115
|
+
getGrocyBaseUrl() {
|
|
116
|
+
return this.config.yaml.grocy.base_url;
|
|
117
|
+
}
|
|
118
|
+
getApiUrl() {
|
|
119
|
+
const baseUrl = this.getGrocyBaseUrl();
|
|
120
|
+
return baseUrl.endsWith('/') ? `${baseUrl}api` : `${baseUrl}/api`;
|
|
121
|
+
}
|
|
122
|
+
hasApiKey() {
|
|
123
|
+
return !!this.config.env.GROCY_APIKEY_VALUE;
|
|
124
|
+
}
|
|
125
|
+
getApiKey() {
|
|
126
|
+
return this.config.env.GROCY_APIKEY_VALUE;
|
|
127
|
+
}
|
|
128
|
+
getCustomHeaders() {
|
|
129
|
+
const headers = {};
|
|
130
|
+
if (this.config.env.GROCY_APIKEY_VALUE) {
|
|
131
|
+
headers['GROCY-API-KEY'] = this.config.env.GROCY_APIKEY_VALUE;
|
|
132
|
+
}
|
|
133
|
+
return headers;
|
|
134
|
+
}
|
|
135
|
+
parseToolConfiguration() {
|
|
136
|
+
const enabledTools = new Set();
|
|
137
|
+
const toolSubConfigs = new Map();
|
|
138
|
+
for (const [toolName, toolConfig] of Object.entries(this.config.yaml.tools)) {
|
|
139
|
+
if (toolConfig.enabled) {
|
|
140
|
+
enabledTools.add(toolName);
|
|
141
|
+
// Extract sub-configs (everything except standard fields)
|
|
142
|
+
const subConfigs = new Map();
|
|
143
|
+
for (const [key, value] of Object.entries(toolConfig)) {
|
|
144
|
+
if (!['enabled', 'ack_token'].includes(key)) {
|
|
145
|
+
subConfigs.set(key, value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (subConfigs.size > 0) {
|
|
149
|
+
toolSubConfigs.set(toolName, subConfigs);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { enabledTools, toolSubConfigs };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Export singleton instance
|
|
157
|
+
export const config = ConfigManager.getInstance();
|
|
158
|
+
export default config;
|