mcp-grocy 1.9.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/LICENSE +23 -0
- package/README.md +285 -0
- package/build/api/client.js +154 -0
- package/build/config/environment.js +142 -0
- package/build/main.js +29 -0
- package/build/resources/CHANGELOG.md +352 -0
- package/build/resources/DOCS.md +62 -0
- package/build/resources/README.md +285 -0
- package/build/resources/api-reference.md +92 -0
- package/build/resources/config.md +282 -0
- package/build/resources/examples.md +319 -0
- package/build/resources/installation.md +117 -0
- package/build/resources/response-format.md +165 -0
- package/build/server/http-server.js +214 -0
- package/build/server/mcp-server.js +173 -0
- package/build/server/resources.js +60 -0
- package/build/tools/base.js +56 -0
- package/build/tools/index.js +494 -0
- package/build/tools/products/definitions.js +57 -0
- package/build/tools/products/handlers.js +78 -0
- package/build/tools/products/index.js +13 -0
- package/build/tools/recipes/definitions.js +191 -0
- package/build/tools/recipes/handlers.js +258 -0
- package/build/tools/recipes/index.js +18 -0
- package/build/tools/shopping/index.js +107 -0
- package/build/tools/stock/definitions.js +234 -0
- package/build/tools/stock/handlers.js +391 -0
- package/build/tools/stock/index.js +19 -0
- package/build/tools/system/index.js +230 -0
- package/build/tools/types.js +1 -0
- package/build/version.js +4 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Christian Pedersen
|
|
4
|
+
Copyright (c) 2024 saya6k
|
|
5
|
+
Copyright (c) 2025 miguelangel-nubla
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# MCP Grocy
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/mcp-grocy)
|
|
4
|
+
[](https://github.com/miguelangel-nubla/mcp-grocy/pkgs/container/mcp-grocy)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/miguelangel-nubla/mcp-grocy/actions/workflows/validate-config.yml)
|
|
7
|
+
[](https://github.com/miguelangel-nubla/mcp-grocy/actions/workflows/pipeline.yml)
|
|
8
|
+
[](https://modelcontextprotocol.io)
|
|
9
|
+
|
|
10
|
+
> **🍴 Opinionated Fork Notice**
|
|
11
|
+
>
|
|
12
|
+
> This is a heavily opinionated fork of [saya6k/mcp-grocy-api](https://github.com/saya6k/mcp-grocy-api) that has diverged significantly to warrant a separate identity. This MCP prioritizes **usability over features**.
|
|
13
|
+
>
|
|
14
|
+
> **Why This Fork Exists:**
|
|
15
|
+
> - The original wrapper exposes the entire Grocy API unprocessed, leading to context overload and LLM confusion
|
|
16
|
+
> - Grocy's API design choices and limitations cause error-prone interactions
|
|
17
|
+
> - Generic API exposure increases hallucination and near-miss results
|
|
18
|
+
>
|
|
19
|
+
> **This Fork's Philosophy:**
|
|
20
|
+
> - **Filters and augments data** with relevant context for better LLM comprehension
|
|
21
|
+
> - **Reduces API calls** by combining common operations to minimize error chains
|
|
22
|
+
> - **Optimizes for reliability and repeatability** over feature completeness
|
|
23
|
+
> - **Opinionated workflows** that may not match everyone's preferences
|
|
24
|
+
>
|
|
25
|
+
> 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.
|
|
26
|
+
|
|
27
|
+
## 🎯 What This MCP Does
|
|
28
|
+
|
|
29
|
+
Transform your LLM into an intelligent household management assistant with focused tools for:
|
|
30
|
+
|
|
31
|
+
### 📦 **Stock Management**
|
|
32
|
+
- Track inventory across multiple locations with precision
|
|
33
|
+
- Record purchases and consumption with automatic stock updates
|
|
34
|
+
- Monitor expiry dates and get volatile stock alerts
|
|
35
|
+
- Transfer products between storage locations
|
|
36
|
+
|
|
37
|
+
### 🛒 **Smart Shopping & Planning**
|
|
38
|
+
- Maintain shopping lists with intelligent quantity management
|
|
39
|
+
- Plan meals with recipe scheduling and fulfillment checking
|
|
40
|
+
- Automatically add missing ingredients to shopping lists
|
|
41
|
+
- Track shopping locations and optimize store visits
|
|
42
|
+
|
|
43
|
+
### 🍽️ **Recipe & Meal Workflows**
|
|
44
|
+
- Find recipes with fuzzy search capabilities
|
|
45
|
+
- Check if recipes can be made with current stock
|
|
46
|
+
- Complete cooking workflows with portion control
|
|
47
|
+
- Integrate meal planning with inventory consumption
|
|
48
|
+
|
|
49
|
+
### 🏠 **Household Management**
|
|
50
|
+
- Manage chores, tasks, and battery tracking
|
|
51
|
+
- Get product price history for budgeting
|
|
52
|
+
- Organize products by groups and categories
|
|
53
|
+
- Print labels for stock entries
|
|
54
|
+
|
|
55
|
+
## ⚡ Quick Start
|
|
56
|
+
|
|
57
|
+
1. **Get your Grocy API key** from your Grocy instance (User Settings → API Keys)
|
|
58
|
+
2. **Set up with Docker Compose:**
|
|
59
|
+
```bash
|
|
60
|
+
# Get the project
|
|
61
|
+
git clone https://github.com/miguelangel-nubla/mcp-grocy.git
|
|
62
|
+
cd mcp-grocy
|
|
63
|
+
|
|
64
|
+
# Configure
|
|
65
|
+
cp .env.example .env
|
|
66
|
+
# Edit .env with your GROCY_BASE_URL and GROCY_APIKEY_VALUE
|
|
67
|
+
|
|
68
|
+
# Run
|
|
69
|
+
docker compose up -d
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Try Without Grocy
|
|
73
|
+
Test with mock data (no real Grocy instance needed):
|
|
74
|
+
```bash
|
|
75
|
+
# In .env file, any values work for mock mode
|
|
76
|
+
GROCY_BASE_URL=http://mock
|
|
77
|
+
GROCY_APIKEY_VALUE=mock
|
|
78
|
+
|
|
79
|
+
npm install && npm run dev
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
### NPM
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git clone -b main https://github.com/miguelangel-nubla/mcp-grocy.git
|
|
88
|
+
cd mcp-grocy
|
|
89
|
+
npm install
|
|
90
|
+
npm run build
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Docker
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
docker run -e GROCY_APIKEY_VALUE=your_api_key -e GROCY_BASE_URL=http://your-grocy-instance ghcr.io/miguelangel-nubla/mcp-grocy:latest
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Docker Compose (Recommended)
|
|
100
|
+
|
|
101
|
+
Create a `docker-compose.yml`:
|
|
102
|
+
```yaml
|
|
103
|
+
services:
|
|
104
|
+
mcp-grocy:
|
|
105
|
+
image: ghcr.io/miguelangel-nubla/mcp-grocy:latest
|
|
106
|
+
env_file:
|
|
107
|
+
- .env
|
|
108
|
+
restart: unless-stopped
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Then:
|
|
112
|
+
```bash
|
|
113
|
+
cp .env.example .env
|
|
114
|
+
# Edit .env with your configuration
|
|
115
|
+
docker compose up -d
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## ⚙️ Configuration
|
|
119
|
+
|
|
120
|
+
### Quick Setup
|
|
121
|
+
|
|
122
|
+
1. **Get your Grocy API key:**
|
|
123
|
+
- Open your Grocy instance → **User Settings** → **API Keys**
|
|
124
|
+
- Create a new API key and copy it
|
|
125
|
+
|
|
126
|
+
2. **Configure the server:**
|
|
127
|
+
```bash
|
|
128
|
+
cp .env.example .env
|
|
129
|
+
# Edit .env with your GROCY_BASE_URL and GROCY_APIKEY_VALUE
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
3. **Essential variables:**
|
|
133
|
+
- `GROCY_BASE_URL` - Your Grocy instance URL
|
|
134
|
+
- `GROCY_APIKEY_VALUE` - Your Grocy API key
|
|
135
|
+
|
|
136
|
+
### Configuration Options
|
|
137
|
+
|
|
138
|
+
| Method | Use Case | Command |
|
|
139
|
+
|--------|----------|---------|
|
|
140
|
+
| **`.env` file** | Recommended for most users | `cp .env.example .env` |
|
|
141
|
+
| **Environment variables** | CI/CD, containers | `GROCY_BASE_URL=... GROCY_APIKEY_VALUE=... mcp-grocy` |
|
|
142
|
+
| **Tool toggles** | Customize functionality | Edit `TOOL__*` variables in `.env` |
|
|
143
|
+
|
|
144
|
+
📖 **For complete configuration reference:** See [Configuration Guide](src/resources/config.md)
|
|
145
|
+
|
|
146
|
+
## 🚀 Usage Modes
|
|
147
|
+
|
|
148
|
+
### Production Mode
|
|
149
|
+
Start with your real Grocy instance:
|
|
150
|
+
```bash
|
|
151
|
+
npm start
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Development/Testing Mode
|
|
155
|
+
Use mock data (no Grocy instance required):
|
|
156
|
+
```bash
|
|
157
|
+
npm run dev
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### HTTP Server Mode
|
|
161
|
+
Enable web-based access via HTTP/SSE:
|
|
162
|
+
```bash
|
|
163
|
+
# In .env: ENABLE_HTTP_SERVER=true
|
|
164
|
+
npm start
|
|
165
|
+
# Access via http://localhost:8080/mcp
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
## 📚 Documentation & Resources
|
|
170
|
+
|
|
171
|
+
| Resource | Purpose | When to Use |
|
|
172
|
+
|----------|---------|-------------|
|
|
173
|
+
| [📖 API Reference](src/resources/api-reference.md) | Complete tool documentation | Tool usage and examples |
|
|
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 |
|
|
176
|
+
| [🧪 MCP Inspector](https://github.com/modelcontextprotocol/inspector) | Protocol debugging | Debug MCP interactions |
|
|
177
|
+
|
|
178
|
+
### 🆘 Troubleshooting
|
|
179
|
+
|
|
180
|
+
#### Common Issues
|
|
181
|
+
|
|
182
|
+
**"Connection refused" or "Cannot connect to Grocy"**
|
|
183
|
+
- Verify `GROCY_BASE_URL` is correct and accessible
|
|
184
|
+
- Check that your Grocy instance is running
|
|
185
|
+
- For HTTPS URLs, ensure SSL certificate is valid or disable verification with `GROCY_ENABLE_SSL_VERIFY=false`
|
|
186
|
+
|
|
187
|
+
**"Invalid API key" or "Authentication failed"**
|
|
188
|
+
- Verify your `GROCY_APIKEY_VALUE` is correct
|
|
189
|
+
- Check that the API key exists in your Grocy instance (User Settings → API Keys)
|
|
190
|
+
- Ensure the API key has proper permissions
|
|
191
|
+
|
|
192
|
+
**"Tool not found" errors**
|
|
193
|
+
- Check if the tool is enabled in your `.env` file (tool toggles)
|
|
194
|
+
- Verify you're using the correct tool names from the API reference
|
|
195
|
+
|
|
196
|
+
**Large response errors**
|
|
197
|
+
- Increase `REST_RESPONSE_SIZE_LIMIT` if you have many products/stock entries
|
|
198
|
+
- Consider using tool toggles to disable unused functionality
|
|
199
|
+
|
|
200
|
+
#### Debug Mode
|
|
201
|
+
|
|
202
|
+
Enable detailed logging and use the MCP inspector:
|
|
203
|
+
```bash
|
|
204
|
+
# Launch MCP inspector for protocol debugging
|
|
205
|
+
npm run inspector
|
|
206
|
+
|
|
207
|
+
# Run with mock data for testing
|
|
208
|
+
npm run dev
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## 🛠️ Development
|
|
212
|
+
|
|
213
|
+
### Prerequisites
|
|
214
|
+
|
|
215
|
+
- Node.js 18 or higher
|
|
216
|
+
- Grocy instance (optional with mock mode)
|
|
217
|
+
|
|
218
|
+
### Development Setup
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# Clone and install
|
|
222
|
+
git clone https://github.com/miguelangel-nubla/mcp-grocy.git
|
|
223
|
+
cd mcp-grocy
|
|
224
|
+
npm install
|
|
225
|
+
|
|
226
|
+
# Configure for development
|
|
227
|
+
cp .env.example .env
|
|
228
|
+
# Edit .env with your settings (or use mock values)
|
|
229
|
+
|
|
230
|
+
# Build and run
|
|
231
|
+
npm run build
|
|
232
|
+
npm start
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Development Commands
|
|
236
|
+
|
|
237
|
+
| Command | Description |
|
|
238
|
+
|---------|-------------|
|
|
239
|
+
| `npm run build` | Build TypeScript to JavaScript |
|
|
240
|
+
| `npm run watch` | Watch mode for development |
|
|
241
|
+
| `npm run dev` | Start with mock data (no Grocy needed) |
|
|
242
|
+
| `npm test` | Run test suite |
|
|
243
|
+
| `npm run test:watch` | Run tests in watch mode |
|
|
244
|
+
| `npm run inspector` | Launch MCP protocol inspector |
|
|
245
|
+
|
|
246
|
+
### Debugging
|
|
247
|
+
|
|
248
|
+
Use the MCP inspector to debug protocol interactions:
|
|
249
|
+
```bash
|
|
250
|
+
npm run inspector
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
This launches a web interface for testing MCP tools and viewing protocol messages.
|
|
254
|
+
|
|
255
|
+
## 🤝 Contributing
|
|
256
|
+
|
|
257
|
+
This is an **opinionated fork** focused on LLM usability and workflow reliability. Contributions are welcome but must align with the core philosophy:
|
|
258
|
+
|
|
259
|
+
### ✅ Welcome Contributions
|
|
260
|
+
- Bug fixes and reliability improvements
|
|
261
|
+
- Better error handling and validation
|
|
262
|
+
- Documentation improvements
|
|
263
|
+
- Test coverage enhancements
|
|
264
|
+
- Performance optimizations
|
|
265
|
+
|
|
266
|
+
### ❌ Contributions Requiring Discussion
|
|
267
|
+
- New tool additions (must demonstrate clear LLM workflow benefits)
|
|
268
|
+
- API design changes that increase complexity
|
|
269
|
+
- Features that expose raw Grocy API behavior
|
|
270
|
+
|
|
271
|
+
### Development Workflow
|
|
272
|
+
1. Fork the repository
|
|
273
|
+
2. Create a feature branch
|
|
274
|
+
3. Make your changes with tests
|
|
275
|
+
4. Run `npm test` and ensure all tests pass
|
|
276
|
+
5. Submit a pull request with clear description
|
|
277
|
+
|
|
278
|
+
## 📄 License
|
|
279
|
+
|
|
280
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
**🏠 Made for reliable household management with LLMs**
|
|
285
|
+
*Prioritizing workflow efficiency over feature completeness*
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
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
|
+
}
|
|
14
|
+
export class GrocyApiClient {
|
|
15
|
+
axiosInstance;
|
|
16
|
+
API_KEY_HEADER = 'GROCY-API-KEY';
|
|
17
|
+
constructor() {
|
|
18
|
+
const { GROCY_ENABLE_SSL_VERIFY, GROCY_APIKEY_VALUE } = config.get();
|
|
19
|
+
this.axiosInstance = axios.create({
|
|
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({
|
|
24
|
+
rejectUnauthorized: false
|
|
25
|
+
})
|
|
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;
|
|
30
|
+
}
|
|
31
|
+
// Add request interceptor for logging
|
|
32
|
+
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
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
}, (error) => {
|
|
39
|
+
console.error('[API] Request error:', error);
|
|
40
|
+
return Promise.reject(error);
|
|
41
|
+
});
|
|
42
|
+
// Add response interceptor for error handling
|
|
43
|
+
this.axiosInstance.interceptors.response.use((response) => {
|
|
44
|
+
if (response.status >= 400) {
|
|
45
|
+
console.error(`[API] Error response (${response.status}): ${JSON.stringify(response.data)}`);
|
|
46
|
+
}
|
|
47
|
+
return response;
|
|
48
|
+
}, (error) => {
|
|
49
|
+
console.error('[API] Response error:', error);
|
|
50
|
+
return Promise.reject(error);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
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;
|
|
75
|
+
}
|
|
76
|
+
buildQueryString(params) {
|
|
77
|
+
return Object.entries(params)
|
|
78
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
79
|
+
.join('&');
|
|
80
|
+
}
|
|
81
|
+
async request(endpoint, options = {}) {
|
|
82
|
+
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 {
|
|
104
|
+
const response = await this.axiosInstance.request(requestConfig);
|
|
105
|
+
if (response.status >= 400) {
|
|
106
|
+
throw new ApiError(`API error (${response.status}): ${JSON.stringify(response.data)}`, response.status, response.data);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
data: response.data,
|
|
110
|
+
status: response.status,
|
|
111
|
+
headers: response.headers
|
|
112
|
+
};
|
|
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
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Convenience methods
|
|
136
|
+
async get(endpoint, options = {}) {
|
|
137
|
+
return this.request(endpoint, { ...options, method: 'GET' });
|
|
138
|
+
}
|
|
139
|
+
async post(endpoint, body, options = {}) {
|
|
140
|
+
return this.request(endpoint, { ...options, method: 'POST', body });
|
|
141
|
+
}
|
|
142
|
+
async put(endpoint, body, options = {}) {
|
|
143
|
+
return this.request(endpoint, { ...options, method: 'PUT', body });
|
|
144
|
+
}
|
|
145
|
+
async delete(endpoint, options = {}) {
|
|
146
|
+
return this.request(endpoint, { ...options, method: 'DELETE' });
|
|
147
|
+
}
|
|
148
|
+
async patch(endpoint, body, options = {}) {
|
|
149
|
+
return this.request(endpoint, { ...options, method: 'PATCH', body });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Export a singleton instance
|
|
153
|
+
export const apiClient = new GrocyApiClient();
|
|
154
|
+
export default apiClient;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Environment variable schema with validation and defaults
|
|
3
|
+
const EnvironmentSchema = z.object({
|
|
4
|
+
// Grocy Configuration
|
|
5
|
+
GROCY_BASE_URL: z.string().url().default('http://localhost:9283'),
|
|
6
|
+
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
|
+
// Build Configuration
|
|
20
|
+
RELEASE_VERSION: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
export class ConfigManager {
|
|
23
|
+
static instance;
|
|
24
|
+
config;
|
|
25
|
+
constructor() {
|
|
26
|
+
try {
|
|
27
|
+
this.config = EnvironmentSchema.parse(process.env);
|
|
28
|
+
this.validateConfiguration();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof z.ZodError) {
|
|
32
|
+
console.error('[CONFIG ERROR] Invalid environment variables:');
|
|
33
|
+
error.errors.forEach(err => {
|
|
34
|
+
console.error(` - ${err.path.join('.')}: ${err.message}`);
|
|
35
|
+
});
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
static getInstance() {
|
|
42
|
+
if (!ConfigManager.instance) {
|
|
43
|
+
ConfigManager.instance = new ConfigManager();
|
|
44
|
+
}
|
|
45
|
+
return ConfigManager.instance;
|
|
46
|
+
}
|
|
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`);
|
|
57
|
+
}
|
|
58
|
+
get() {
|
|
59
|
+
return this.config;
|
|
60
|
+
}
|
|
61
|
+
getGrocyBaseUrl() {
|
|
62
|
+
return this.config.GROCY_BASE_URL.replace(/\/+$/, '');
|
|
63
|
+
}
|
|
64
|
+
getApiUrl() {
|
|
65
|
+
return `${this.getGrocyBaseUrl()}/api`;
|
|
66
|
+
}
|
|
67
|
+
hasApiKeyAuth() {
|
|
68
|
+
return !!this.config.GROCY_APIKEY_VALUE;
|
|
69
|
+
}
|
|
70
|
+
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;
|
|
80
|
+
}
|
|
81
|
+
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 };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export const config = ConfigManager.getInstance();
|
|
142
|
+
export default config;
|
package/build/main.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Load environment variables from .env file
|
|
3
|
+
import 'dotenv/config';
|
|
4
|
+
import { GrocyMcpServer } from './server/mcp-server.js';
|
|
5
|
+
import config from './config/environment.js';
|
|
6
|
+
import { VERSION, PACKAGE_NAME as SERVER_NAME } from './version.js';
|
|
7
|
+
// Debug output to help identify version and naming issues
|
|
8
|
+
console.error(`Starting ${SERVER_NAME} server version ${VERSION}`);
|
|
9
|
+
async function main() {
|
|
10
|
+
try {
|
|
11
|
+
// Validate configuration early
|
|
12
|
+
const envConfig = config.get();
|
|
13
|
+
// Ensure required environment variables
|
|
14
|
+
if (!config.hasApiKeyAuth()) {
|
|
15
|
+
console.error('[WARNING] No GROCY_APIKEY_VALUE configured. Some API calls may fail.');
|
|
16
|
+
}
|
|
17
|
+
// Create and start the server
|
|
18
|
+
const server = new GrocyMcpServer();
|
|
19
|
+
await server.start();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error('[ERROR] Failed to start server:', error);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
main().catch((error) => {
|
|
27
|
+
console.error('[FATAL] Unhandled error:', error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|