mcp-grocy 1.10.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.
Files changed (42) hide show
  1. package/README.md +4 -4
  2. package/build/api/client.js +62 -93
  3. package/build/config/environment.js +19 -99
  4. package/build/config/index.js +158 -0
  5. package/build/config/yaml-config.js +203 -0
  6. package/build/main.js +35 -18
  7. package/build/resources/README.md +4 -4
  8. package/build/resources/config.md +16 -152
  9. package/build/server/http-server.js +189 -185
  10. package/build/server/mcp-server.js +92 -103
  11. package/build/tools/base.js +122 -37
  12. package/build/tools/household/definitions.js +128 -0
  13. package/build/tools/household/handlers.js +92 -0
  14. package/build/tools/household/index.js +22 -0
  15. package/build/tools/index.js +2 -491
  16. package/build/tools/inventory/definitions.js +277 -0
  17. package/build/tools/inventory/handlers.js +278 -0
  18. package/build/tools/inventory/index.js +27 -0
  19. package/build/tools/module-loader.js +154 -0
  20. package/build/tools/recipes/definitions.js +106 -50
  21. package/build/tools/recipes/handlers.js +309 -163
  22. package/build/tools/recipes/index.js +21 -11
  23. package/build/tools/recipes/validations.js +23 -0
  24. package/build/tools/shopping/definitions.js +62 -0
  25. package/build/tools/shopping/handlers.js +37 -0
  26. package/build/tools/shopping/index.js +8 -102
  27. package/build/tools/system/definitions.js +86 -0
  28. package/build/tools/system/handlers.js +94 -0
  29. package/build/tools/system/index.js +11 -225
  30. package/build/tools/validation-helpers.js +110 -0
  31. package/build/types/index.js +62 -0
  32. package/build/utils/errors.js +138 -0
  33. package/build/utils/logger.js +141 -0
  34. package/build/version.js +1 -1
  35. package/package.json +3 -1
  36. package/build/resources/CHANGELOG.md +0 -352
  37. package/build/tools/products/definitions.js +0 -57
  38. package/build/tools/products/handlers.js +0 -78
  39. package/build/tools/products/index.js +0 -13
  40. package/build/tools/stock/definitions.js +0 -234
  41. package/build/tools/stock/handlers.js +0 -391
  42. 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 toggles** | Customize functionality | Edit `TOOL__*` variables in `.env` |
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) | Configuration template with ALL tools | Copy and customize for your setup |
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 `.env` file (tool toggles)
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 using tool toggles to disable unused functionality
198
+ - Consider disabling unused tools in `mcp-grocy.yaml`
199
199
 
200
200
  #### Debug Mode
201
201
 
@@ -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/environment.js';
4
- export class ApiError extends Error {
5
- status;
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
- const { GROCY_ENABLE_SSL_VERIFY, GROCY_APIKEY_VALUE } = config.get();
19
- this.axiosInstance = axios.create({
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, // Allow any status code
22
- timeout: 30000, // 30 seconds timeout
23
- httpsAgent: GROCY_ENABLE_SSL_VERIFY ? undefined : new https.Agent({
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 if available
28
- if (GROCY_APIKEY_VALUE) {
29
- this.axiosInstance.defaults.headers.common[this.API_KEY_HEADER] = GROCY_APIKEY_VALUE;
27
+ // Set default authentication
28
+ if (apiKey) {
29
+ instance.defaults.headers.common[this.API_KEY_HEADER] = apiKey;
30
30
  }
31
- // Add request interceptor for logging
31
+ return instance;
32
+ }
33
+ setupInterceptors() {
34
+ // Request logging
32
35
  this.axiosInstance.interceptors.request.use((config) => {
33
- console.error(`[API] ${config.method?.toUpperCase()} ${config.url}`);
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
- console.error('[API] Request error:', error);
39
+ logger.error('Request error', 'API', { error: error.message });
40
40
  return Promise.reject(error);
41
41
  });
42
- // Add response interceptor for error handling
42
+ // Response logging
43
43
  this.axiosInstance.interceptors.response.use((response) => {
44
44
  if (response.status >= 400) {
45
- console.error(`[API] Error response (${response.status}): ${JSON.stringify(response.data)}`);
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
- console.error('[API] Response error:', error);
52
+ logger.error('Response error', 'API', { error: error.message });
50
53
  return Promise.reject(error);
51
54
  });
52
55
  }
53
56
  normalizeEndpoint(endpoint) {
54
- // Standardize path handling
55
- let normalizedEndpoint = endpoint;
56
- // Check if endpoint explicitly starts with /api/ - use it as is
57
- if (endpoint.startsWith('/api/')) {
58
- normalizedEndpoint = endpoint;
59
- }
60
- // Handle endpoints that start with api/ without leading slash
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
- let url = this.normalizeEndpoint(endpoint);
84
- // Add query parameters
85
- if (Object.keys(queryParams).length > 0) {
86
- const queryString = this.buildQueryString(queryParams);
87
- url += `?${queryString}`;
88
- }
89
- const requestConfig = {
90
- method,
91
- url,
92
- headers: {
93
- 'Accept': 'application/json',
94
- 'Content-Type': 'application/json',
95
- ...config.getCustomHeaders(),
96
- ...headers
97
- },
98
- timeout
99
- };
100
- if (['POST', 'PUT', 'PATCH'].includes(method) && body !== null) {
101
- requestConfig.data = body;
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(`API error (${response.status}): ${JSON.stringify(response.data)}`, response.status, response.data);
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 a singleton instance
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
- config;
13
+ envConfig;
25
14
  constructor() {
26
15
  try {
27
- this.config = EnvironmentSchema.parse(process.env);
28
- this.validateConfiguration();
16
+ this.envConfig = EnvironmentSchema.parse(process.env);
29
17
  }
30
18
  catch (error) {
31
19
  if (error instanceof z.ZodError) {
32
- console.error('[CONFIG ERROR] Invalid environment variables:');
20
+ logger.error('Invalid environment variables', 'CONFIG');
33
21
  error.errors.forEach(err => {
34
- console.error(` - ${err.path.join('.')}: ${err.message}`);
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
- validateConfiguration() {
48
- // Custom validation logic
49
- if (this.config.HTTP_SERVER_PORT < 1 || this.config.HTTP_SERVER_PORT > 65535) {
50
- throw new Error('HTTP_SERVER_PORT must be between 1 and 65535');
51
- }
52
- // Log important configuration
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.config;
43
+ return this.envConfig;
60
44
  }
61
45
  getGrocyBaseUrl() {
62
- return this.config.GROCY_BASE_URL.replace(/\/+$/, '');
46
+ return yamlConfig.getGrocyBaseUrl();
63
47
  }
64
48
  getApiUrl() {
65
- return `${this.getGrocyBaseUrl()}/api`;
49
+ return yamlConfig.getApiUrl();
66
50
  }
67
51
  hasApiKeyAuth() {
68
- return !!this.config.GROCY_APIKEY_VALUE;
52
+ return !!this.envConfig.GROCY_APIKEY_VALUE;
69
53
  }
70
54
  getCustomHeaders() {
71
- const headers = {};
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
- const enabledTools = new Set();
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;